Bash Scripting for AWS Deployments
What is Bash?
Bash is a command-line shell and scripting language. When engineers write scripts to automate tasks — building code, deploying apps, checking server health — bash is almost always what they’re using. It’s the default shell on most Linux servers, and therefore on most AWS EC2 instances.
Script Structure
Every bash script starts with a shebang — a line that tells the OS which interpreter to use.
#!/bin/bash
echo "Hello from bash"
Making a script executable and running it
chmod +x script.sh # make it executable (only needed once)
./script.sh # run it
Core Bash Concepts
Variables
NAME="acebook"
VERSION="1.0.3"
ENVIRONMENT="staging"
echo "Deploying $NAME version $VERSION to $ENVIRONMENT"
Always quote variables (
"$VAR") to avoid unexpected behaviour when values contain spaces.
Arguments
When you run a script, you can pass arguments to it:
./deploy.sh production
Inside the script:
$0 # the script name itself
$1 # first argument → "production"
$2 # second argument
$@ # all arguments as a list
Command substitution
Run a command and capture its output into a variable using $():
STATE=$(aws ec2 describe-instances \
--instance-ids "$INSTANCE_ID" \
--query 'Reservations[0].Instances[0].State.Name' \
--output text)
echo "Instance state is: $STATE"
Conditionals
if [ "$STATE" == "running" ]; then
echo "Ready to deploy"
elif [ "$STATE" == "stopped" ]; then
echo "Instance is stopped"
else
echo "Unknown state: $STATE"
fi
Common test flags:
| Flag | Meaning |
|---|---|
-z "$VAR" | True if variable is empty |
-n "$VAR" | True if variable is not empty |
-f "$FILE" | True if file exists |
-d "$DIR" | True if directory exists |
== | String equality |
!= | String inequality |
-eq | Numeric equality |
-ne | Numeric not equal |
Loops
STEPS=("Pull code" "Install dependencies" "Run migrations" "Restart app")
for STEP in "${STEPS[@]}"; do
echo "Running: $STEP"
done
Functions
log() {
echo "[$(date '+%H:%M:%S')] $1"
}
log "Starting deployment"
log "Done"
Exit codes
Every command returns an exit code. 0 means success, anything else means failure.
exit 0 # success — CI pipeline continues
exit 1 # failure — CI pipeline stops
Check the exit code of the last command with $?:
aws s3 cp app.zip s3://my-bucket/
if [ $? -ne 0 ]; then
echo "Upload failed"
exit 1
fi
Or use set -e at the top of your script to automatically exit on any error:
#!/bin/bash
set -e # exit immediately if any command fails
User input
read -p "Are you sure you want to deploy to production? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Deployment cancelled."
exit 0
fi
AWS CLI Basics
The AWS CLI lets you interact with AWS services from a bash script.
Check instance state
aws ec2 describe-instances \
--instance-ids "$INSTANCE_ID" \
--query 'Reservations[0].Instances[0].State.Name' \
--output text
--queryfilters the JSON response using JMESPath — without it you get the full JSON output--output textgives plain text instead of JSON, making it easy to use in scripts
Copy a file to S3
aws s3 cp app.zip s3://my-bucket/releases/app.zip
Trigger a CodeDeploy deployment
aws deploy create-deployment \
--application-name my-app \
--deployment-group-name production \
--s3-location bucket=my-bucket,key=releases/app.zip,bundleType=zip
Script 1 — check_environment.sh
This script checks that an EC2 instance is running before a deployment starts.
#!/bin/bash
INSTANCE_ID=$1
# Validate argument
if [ -z "$INSTANCE_ID" ]; then
echo "Usage: $0 <instance-id>"
exit 1
fi
echo "Checking EC2 instance: $INSTANCE_ID"
# Get instance state from AWS CLI
STATE=$(aws ec2 describe-instances \
--instance-ids "$INSTANCE_ID" \
--query 'Reservations[0].Instances[0].State.Name' \
--output text)
# Evaluate state
if [ "$STATE" == "running" ]; then
echo "✓ Instance $INSTANCE_ID is running. Ready to deploy."
exit 0
else
echo "✗ Instance $INSTANCE_ID is not running. Current state: $STATE"
exit 1
fi
Run it:
chmod +x check_environment.sh
./check_environment.sh i-0abc1234def567890
How it works step by step:
$1receives the instance ID passed as an argument-zchecks if the argument is empty — if so, print usage and exit with an error$()runs the AWS CLI command and stores the result (e.g.running,stopped,terminated) in$STATE- The conditional checks the state and exits with
0(success) or1(failure) - The exit code can be read by GitHub Actions or any CI tool — a failing check will stop the pipeline
Script 2 — deploy.sh
This script simulates deploying a new version of the application, handling staging and production differently.
#!/bin/bash
ENVIRONMENT=$1
# Validate argument
if [ -z "$ENVIRONMENT" ]; then
echo "Usage: $0 <staging|production>"
exit 1
fi
if [ "$ENVIRONMENT" != "staging" ] && [ "$ENVIRONMENT" != "production" ]; then
echo "Error: environment must be 'staging' or 'production'"
exit 1
fi
# Production safety gate — require explicit confirmation
if [ "$ENVIRONMENT" == "production" ]; then
echo "⚠️ WARNING: You are deploying to PRODUCTION"
read -p "Are you sure? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Deployment cancelled."
exit 0
fi
fi
# Deployment steps
STEPS=(
"Pull latest code from S3"
"Install dependencies"
"Run database migrations"
"Restart application"
)
echo ""
echo "Starting deployment to $ENVIRONMENT..."
echo "---"
for STEP in "${STEPS[@]}"; do
echo "→ $STEP"
sleep 1
# Check if the last command failed
if [ $? -ne 0 ]; then
echo "✗ FAILED at step: $STEP"
exit 1
fi
echo " ✓ Done"
done
echo "---"
echo "✓ Deployment to $ENVIRONMENT complete."
exit 0
Run it:
chmod +x deploy.sh
./deploy.sh staging # deploys to staging, no confirmation needed
./deploy.sh production # prompts for confirmation before proceeding
How it works step by step:
$1receivesstagingorproductionas an argument- The script validates the argument is one of the two expected values
- If deploying to production,
read -pprompts the user — anything other thanyescancels safely - The
STEPSarray defines the deployment sequence - The
forloop iterates through each step, printing progress $?checks the exit code of each step — if anything fails the script exits immediately with1- A final
exit 0tells the calling process (e.g. GitHub Actions) the deployment succeeded
Connecting the Two Scripts
In a real CI/CD pipeline you would chain these together — only deploy if the environment check passes:
#!/bin/bash
INSTANCE_ID=$1
ENVIRONMENT=$2
# Step 1: check environment
./check_environment.sh "$INSTANCE_ID"
if [ $? -ne 0 ]; then
echo "Environment check failed. Aborting deployment."
exit 1
fi
# Step 2: deploy
./deploy.sh "$ENVIRONMENT"
Or in a GitHub Actions workflow:
- name: Check environment
run: ./scripts/check_environment.sh ${{ secrets.INSTANCE_ID }}
- name: Deploy
run: ./scripts/deploy.sh production
If check_environment.sh exits with 1, GitHub Actions stops and the deploy step never runs.
Tips and Gotchas
- Always quote variables —
"$VAR"not$VAR. Unquoted variables break when they contain spaces. - Use
set -eat the top of scripts that should fail fast on any error. - Never hardcode credentials — use IAM roles on EC2 or GitHub Secrets in Actions. The AWS CLI picks up credentials automatically from the instance role.
- Test with
echofirst — before a script actually runs AWS commands or restarts services, comment out the real commands andechowhat would happen instead. - Exit codes matter — CI/CD tools read them. A script that always exits
0even on failure will silently pass checks. --dry-runflag — many AWS CLI commands support--dry-runto validate the command without executing it. Useful for testing.