The AWS CLI is powerful on its own, but combined with bash scripting, it becomes a complete automation platform. You can launch and configure EC2 instances programmatically, automate backups to S3, deploy applications, send alerts on failures, and build entire infrastructure workflows that run on schedule.
# Get only running instances
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query "Reservations[*].Instances[*].[InstanceId,PublicIpAddress,Tags[?Key==Name].Value | [0]]" \
--output table
# Get instance IDs only
aws ec2 describe-instances \
--query "Reservations[*].Instances[*].InstanceId" \
--output text
# Get latest AMI ID for Ubuntu 22.04
aws ec2 describe-images \
--owners 099720109477 \
--filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*" \
--query "sort_by(Images, &CreationDate)[-1].ImageId" \
--output text
#!/bin/bash
set -euo pipefail
AMI_ID="ami-0287a05f0ef0e9d9a" # Ubuntu 22.04 ap-south-1
INSTANCE_TYPE="t3.micro"
KEY_NAME="my-key"
SECURITY_GROUP="sg-0123456789abcdef"
SUBNET_ID="subnet-0123456789abcdef"
echo "Launching EC2 instance..."
INSTANCE_ID=$(aws ec2 run-instances \
--image-id $AMI_ID \
--instance-type $INSTANCE_TYPE \
--key-name $KEY_NAME \
--security-group-ids $SECURITY_GROUP \
--subnet-id $SUBNET_ID \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=auto-server}]" \
--query "Instances[0].InstanceId" \
--output text)
echo "Instance: $INSTANCE_ID"
echo "Waiting for running state..."
aws ec2 wait instance-running --instance-ids $INSTANCE_ID
PUBLIC_IP=$(aws ec2 describe-instances \
--instance-ids $INSTANCE_ID \
--query "Reservations[0].Instances[0].PublicIpAddress" \
--output text)
echo "Ready! SSH: ssh -i my-key.pem ubuntu@$PUBLIC_IP"
#!/bin/bash
# deploy.sh -- Download and deploy app from S3
set -e
APP_NAME="myapp"
S3_BUCKET="my-deployments"
VERSION=${1:-latest}
log() { echo "[$(date +%H:%M:%S)] $1"; }
log "Deploying $APP_NAME version $VERSION"
# Download from S3
aws s3 cp "s3://${S3_BUCKET}/${APP_NAME}-${VERSION}.tar.gz" /tmp/
# Extract
tar -xzf "/tmp/${APP_NAME}-${VERSION}.tar.gz" -C /opt/
# Install dependencies
cd /opt/$APP_NAME
pip3 install -r requirements.txt
# Restart service
sudo systemctl restart $APP_NAME
# Health check
for i in $(seq 1 5); do
curl -sf http://localhost:8000/health && { log "Healthy!"; exit 0; }
log "Attempt $i failed, waiting..."
sleep 10
done
log "ERROR: Health check failed after deployment"
exit 1
Preferred: Use IAM roles for EC2 -- no credentials in scripts. Alternative: aws configure sets credentials in ~/.aws/credentials. For CI/CD: use OIDC to assume roles, not stored keys. Never hardcode credentials in scripts or commit them to Git.
set -e exits on any error. set -u exits on undefined variables. set -o pipefail makes pipes fail properly. Use if aws command; then ... else ... fi for expected failures. Log errors to CloudWatch Logs for visibility.
aws ec2 wait instance-running --instance-ids $ID waits until running state. aws ec2 wait instance-status-ok waits until all status checks pass. For SSH readiness: loop with ssh -o ConnectTimeout=5 until it succeeds.
Pass --tag-specifications at creation time. Or use: aws ec2 create-tags --resources $ID --tags Key=Environment,Value=production Key=Project,Value=myapp. Always tag resources with environment, project, and owner for cost allocation and cleanup.
Use AWS SNS: aws sns publish --topic-arn arn:aws:sns:... --message "Deploy done". Or Slack webhook: curl -X POST -d {"text":"Deploy complete"} $SLACK_WEBHOOK. Set up CloudWatch Alarms for automated monitoring alerts.
In Part 9, we master the AWS CLI with advanced queries, output formats, and profiles.
#!/bin/bash
# Create EBS snapshots for all production instances
set -euo pipefail
RETENTION_DAYS=7
ENVIRONMENT="production"
log() { echo "[$(date +%Y-%m-%dT%H:%M:%S)] $1"; }
# Get all production instance IDs
INSTANCES=$(aws ec2 describe-instances --filters "Name=tag:Environment,Values=${ENVIRONMENT}" "Name=instance-state-name,Values=running" --query "Reservations[*].Instances[*].InstanceId" --output text)
for INSTANCE_ID in $INSTANCES; do
INSTANCE_NAME=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID --query "Reservations[0].Instances[0].Tags[?Key=='Name'].Value | [0]" --output text)
# Get volumes attached to instance
VOLUMES=$(aws ec2 describe-volumes --filters "Name=attachment.instance-id,Values=${INSTANCE_ID}" --query "Volumes[*].VolumeId" --output text)
for VOLUME_ID in $VOLUMES; do
SNAP_ID=$(aws ec2 create-snapshot --volume-id $VOLUME_ID --description "Automated backup of ${INSTANCE_NAME} ${INSTANCE_ID}" --tag-specifications "ResourceType=snapshot,Tags=[
{Key=Name,Value=auto-${INSTANCE_NAME}-$(date +%Y%m%d)},
{Key=CreatedBy,Value=automation},
{Key=RetentionDays,Value=${RETENTION_DAYS}}
]" --query "SnapshotId" --output text)
log "Created snapshot ${SNAP_ID} for volume ${VOLUME_ID} (${INSTANCE_NAME})"
done
done
# Clean up old snapshots older than RETENTION_DAYS
OLD_SNAPS=$(aws ec2 describe-snapshots --filters "Name=tag:CreatedBy,Values=automation" --query "Snapshots[?StartTime<='$(date -d "-${RETENTION_DAYS} days" +%Y-%m-%dT%H:%M:%S)'].SnapshotId" --output text)
for SNAP_ID in $OLD_SNAPS; do
aws ec2 delete-snapshot --snapshot-id $SNAP_ID
log "Deleted old snapshot: ${SNAP_ID}"
done
log "Backup automation complete"
#!/bin/bash
# Send daily AWS cost summary
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
TODAY=$(date +%Y-%m-%d)
COST=$(aws ce get-cost-and-usage --time-period Start=$YESTERDAY,End=$TODAY --granularity DAILY --metrics BlendedCost --query "ResultsByTime[0].Total.BlendedCost.Amount" --output text)
COST_ROUNDED=$(printf "%.2f" $COST)
# Send via SES
aws ses send-email --from "alerts@mycompany.com" --to "team@mycompany.com" --subject "AWS Daily Cost: \$${COST_ROUNDED} for $YESTERDAY" --text "AWS spend for $YESTERDAY: \$${COST_ROUNDED} USD" --region us-east-1
#!/bin/bash
# Process ALL EC2 instances across all regions
REGIONS=$(aws ec2 describe-regions --query "Regions[*].RegionName" --output text)
for REGION in $REGIONS; do
echo "=== Region: $REGION ==="
# --paginate handles multiple pages automatically
aws ec2 describe-instances --region $REGION --query "Reservations[*].Instances[*].[InstanceId,State.Name,InstanceType,Tags[?Key=='Name'].Value|[0]]" --output text 2>/dev/null
done
#!/bin/bash
# Tag 100 instances in parallel (much faster than sequential)
INSTANCES=$(aws ec2 describe-instances --filters "Name=tag:Environment,Values=untagged" --query "Reservations[*].Instances[*].InstanceId" --output text)
# Run 10 tagging operations in parallel using GNU parallel
echo $INSTANCES | tr ' ' '
' | parallel -j10 aws ec2 create-tags --resources {} --tags Key=Project,Value=myapp
# Alternative: xargs with -P for parallel
echo $INSTANCES | tr ' ' '
' | xargs -P10 -I{} aws ec2 create-tags --resources {} --tags Key=Project,Value=myapp