Table of Contents
- Fundamentals of Shell Scripting
- What is a Shell Script?
- Shebang Line & Execution
- Variables, Commands, and Control Structures
- Key Concepts for DevOps Workflows
- Automation in CI/CD
- Infrastructure as Code (IaC) Primer
- Environment Management
- Usage Methods in Deployment
- Automated Application Deployment
- Rollback Mechanisms
- Database Migrations
- Service Management
- Common Practices
- Parameterization with Arguments
- Error Handling & Exit Codes
- Logging & Debugging
- Idempotency
- Best Practices
- Security: Secrets & Permissions
- Readability & Maintainability
- Testing & Validation
- Performance Optimization
- Advanced Use Cases
- Cloud CLI Integration (AWS, GCP, Azure)
- Docker/Kubernetes Orchestration
- CI/CD Pipeline Integration (GitHub Actions, GitLab CI)
- Conclusion
- References
Fundamentals of Shell Scripting
What is a Shell Script?
A shell script is a text file containing a sequence of commands executed by a Unix/Linux shell (e.g., Bash, Zsh). It enables automation of tasks like file manipulation, process control, and system administration—critical for DevOps workflows.
Shebang Line & Execution
Every shell script starts with a shebang line (#!), which specifies the interpreter to use. For Bash (the most common shell), this is:
#!/bin/bash
To execute a script:
- Make it executable with
chmod +x script.sh. - Run it with
./script.sh(orbash script.shif not executable).
Variables, Commands, and Control Structures
Variables
Store and manipulate data. Use VAR=value to define, and $VAR to reference:
APP_NAME="my-devops-app"
echo "Deploying $APP_NAME..."
Basic Commands
Leverage Unix tools like echo, cd, mkdir, curl, and grep to build logic:
# Create a deployment directory
DEPLOY_DIR="/opt/$APP_NAME"
mkdir -p "$DEPLOY_DIR" # -p creates parent dirs if missing
Control Structures
Add logic with if-else, loops, and functions:
Example: Check if a directory exists
if [ -d "$DEPLOY_DIR" ]; then
echo "Directory $DEPLOY_DIR already exists."
else
echo "Creating $DEPLOY_DIR..."
mkdir -p "$DEPLOY_DIR"
fi
Example: For loop to process files
LOG_FILES="/var/log/*.log"
for file in $LOG_FILES; do
echo "Processing $file..."
gzip "$file" # Compress log files
done
Key Concepts for DevOps Workflows
Shell scripts are the glue that connects DevOps tools and processes. Here’s how they fit into critical workflows:
Automation in CI/CD
Shell scripts automate steps in CI/CD pipelines (e.g., building code, running tests, deploying artifacts). For example, a script to run unit tests and package an application:
#!/bin/bash
set -e # Exit on any error
# Run tests
echo "Running unit tests..."
npm test
# Package application
echo "Packaging app..."
tar -czf app.tar.gz dist/
Infrastructure as Code (IaC) Primer
While tools like Terraform and Ansible handle large-scale IaC, shell scripts complement them for ad-hoc tasks (e.g., validating Terraform outputs or post-provisioning setup):
#!/bin/bash
TERRAFORM_DIR="./terraform"
# Validate Terraform configs
cd "$TERRAFORM_DIR" && terraform validate
# Apply and capture output
INSTANCE_IP=$(terraform output -raw instance_ip)
echo "Provisioned instance at $INSTANCE_IP"
# Post-provision: Install dependencies via SSH
ssh "admin@$INSTANCE_IP" "sudo apt update && sudo apt install -y docker.io"
Environment Management
Scripts enforce consistency across environments (dev, staging, prod) by standardizing configurations. Use environment variables to toggle behavior:
#!/bin/bash
ENV="${ENV:-dev}" # Default to "dev" if ENV not set
if [ "$ENV" = "prod" ]; then
DB_HOST="prod-db.example.com"
else
DB_HOST="dev-db.example.com"
fi
echo "Connecting to $DB_HOST..."
Usage Methods in Deployment
Let’s explore practical scripts for common deployment scenarios.
Automated Application Deployment
A typical deployment script might pull code from Git, install dependencies, and restart services.
Example: Deploy a Node.js App
#!/bin/bash
set -euo pipefail # Strict error checking
APP_REPO="https://github.com/your-org/my-app.git"
APP_DIR="/opt/my-app"
BRANCH="main"
# Step 1: Pull latest code
echo "Pulling latest code from $BRANCH..."
git -C "$APP_DIR" pull origin "$BRANCH" || git clone "$APP_REPO" "$APP_DIR"
# Step 2: Install dependencies
cd "$APP_DIR"
npm install --production
# Step 3: Restart the service (systemd example)
echo "Restarting app service..."
sudo systemctl restart my-app.service
# Step 4: Verify deployment
if systemctl is-active --quiet my-app.service; then
echo "Deployment successful!"
else
echo "Deployment failed. Check logs with: journalctl -u my-app.service"
exit 1
fi
Rollback Mechanisms
Mistakes happen—scripts can automate rollbacks by reverting to a previous version.
Example: Rollback to Last Known Good Version
#!/bin/bash
set -euo pipefail
APP_DIR="/opt/my-app"
BACKUP_DIR="/opt/my-app-backup"
# Create backup before deployment (run this pre-deployment)
create_backup() {
echo "Creating backup of $APP_DIR..."
rsync -av --delete "$APP_DIR/" "$BACKUP_DIR/"
}
# Rollback function
rollback() {
echo "Rolling back to backup..."
rsync -av --delete "$BACKUP_DIR/" "$APP_DIR/"
sudo systemctl restart my-app.service
echo "Rollback complete."
}
# Trigger rollback if deployment fails (call this on error)
# rollback
Database Migrations
Scripts can automate database schema updates, ensuring consistency with application code.
Example: Run SQL Migrations with psql
#!/bin/bash
set -euo pipefail
DB_USER="app-user"
DB_NAME="app-db"
MIGRATION_DIR="./migrations"
# Apply all pending migrations
for migration in "$MIGRATION_DIR"/*.sql; do
echo "Applying $migration..."
psql -U "$DB_USER" -d "$DB_NAME" -f "$migration"
done
Common Practices
Parameterization with Arguments
Make scripts reusable by accepting command-line arguments. Use $1, $2, etc., for positional parameters, or getopts for flags.
Example: Script with Positional Arguments
#!/bin/bash
# Usage: ./deploy.sh <environment> <version>
ENV="$1"
VERSION="$2"
if [ -z "$ENV" ] || [ -z "$VERSION" ]; then
echo "Error: Usage - ./deploy.sh <environment> <version>"
exit 1
fi
echo "Deploying version $VERSION to $ENV environment..."
Error Handling & Exit Codes
Use set -e to exit on errors, set -u to catch undefined variables, and set -o pipefail to propagate errors in pipelines. Combine with explicit exit codes for clarity.
Example: Strict Error Checking
#!/bin/bash
set -euo pipefail # Exit on error, undefined var, or pipeline failure
# Check if curl succeeds
echo "Downloading artifact..."
curl -fSL "https://example.com/app-v1.tar.gz" -o app.tar.gz || {
echo "Error: Failed to download artifact"
exit 1 # Explicit non-zero exit code
}
Logging & Debugging
Redirect output to log files and enable debug mode for troubleshooting.
Example: Logging to a File
#!/bin/bash
LOG_FILE="/var/log/deploy-$(date +%Y%m%d).log"
# Redirect stdout/stderr to log file and console
exec > >(tee -a "$LOG_FILE") 2>&1
echo "Starting deployment at $(date)"
# ... rest of script ...
For debugging, add a --debug flag to enable verbose output:
if [ "$1" = "--debug" ]; then
set -x # Print commands as they execute
fi
Idempotency
Ensure scripts can run multiple times without side effects (e.g., avoid creating duplicate files or re-running migrations).
Example: Idempotent Directory Creation
# Safe: mkdir -p will not error if dir exists
mkdir -p "/opt/my-app"
# Idempotent migration check (track applied migrations)
APPLIED_MIGRATIONS="/var/log/applied-migrations.txt"
for migration in "$MIGRATION_DIR"/*.sql; do
if ! grep -q "$migration" "$APPLIED_MIGRATIONS"; then
psql -f "$migration" # Run only if not applied
echo "$migration" >> "$APPLIED_MIGRATIONS"
fi
done
Best Practices
Security: Secrets & Permissions
- Avoid hardcoding secrets: Use environment variables (e.g.,
$DB_PASSWORD) or secure vaults (HashiCorp Vault). - Restrict permissions: Run scripts with least privilege (avoid
sudounless necessary). - Sanitize inputs: Validate user/command-line inputs to prevent injection attacks.
Example: Secure Secret Handling
#!/bin/bash
# Load secrets from environment variables (never commit to Git!)
DB_PASSWORD="$DB_PASSWORD" # Set via CI/CD or vault
psql -U "app-user" -d "app-db" -h "db-host" -c "SELECT 1" << EOF
$DB_PASSWORD
EOF
Readability & Maintainability
- Use descriptive variable names (e.g.,
DEPLOY_DIRinstead ofd). - Add comments for complex logic.
- Format with consistent indentation (2–4 spaces).
Example: Readable Script
#!/bin/bash
set -euo pipefail
# Configuration
APP_NAME="my-app"
APP_DIR="/opt/$APP_NAME"
LOG_DIR="/var/log/$APP_NAME"
# Initialize directories
init_directories() {
echo "Creating app and log directories..."
mkdir -p "$APP_DIR" "$LOG_DIR"
chown -R app-user:app-group "$APP_DIR" "$LOG_DIR"
}
# Main workflow
init_directories
deploy_code # Assume this function is defined elsewhere
restart_service
Testing & Validation
- Use
shellcheckto lint scripts for syntax errors and bad practices:shellcheck my-script.sh # Install with: sudo apt install shellcheck - Write unit tests with frameworks like shunit2.
Performance Optimization
- Avoid subshells (
$(...)) in loops (use process substitution orwhile read). - Parallelize tasks where possible (e.g.,
xargs -P 4for parallel execution).
Advanced Use Cases
Cloud CLI Integration
Shell scripts integrate seamlessly with cloud providers (AWS, GCP, Azure) to automate infrastructure tasks.
Example: Deploy to AWS S3 with aws cli
#!/bin/bash
set -euo pipefail
S3_BUCKET="my-app-bucket"
BUILD_DIR="./dist"
echo "Syncing $BUILD_DIR to S3..."
aws s3 sync "$BUILD_DIR" "s3://$S3_BUCKET/" --delete
echo "Invalidating CloudFront cache..."
aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_ID" --paths "/*"
Docker/Kubernetes Orchestration
Automate container lifecycle management with docker or kubectl commands.
Example: Deploy Docker Container
#!/bin/bash
set -euo pipefail
IMAGE="my-app:v1.2.3"
CONTAINER_NAME="my-app-container"
# Pull image and restart container
docker pull "$IMAGE"
docker stop "$CONTAINER_NAME" || true # Ignore if not running
docker rm "$CONTAINER_NAME" || true
docker run -d --name "$CONTAINER_NAME" -p 8080:8080 "$IMAGE"
CI/CD Pipeline Integration
Embed shell scripts directly into CI/CD pipelines (GitHub Actions, GitLab CI) for custom workflows.
Example: GitHub Actions Step with Shell Script
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run deployment script
run: |
#!/bin/bash
set -euo pipefail
./scripts/deploy.sh prod v1.2.3
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
Conclusion
Shell scripting is an indispensable tool for DevOps engineers, enabling the automation of deployment workflows, infrastructure management, and repetitive tasks. By mastering fundamentals like variables, control structures, and error handling, and adopting best practices around security, readability, and idempotency, you can write scripts that are robust, maintainable, and scalable.
Whether you’re deploying microservices, managing cloud infrastructure, or orchestrating containers, shell scripts provide the flexibility to tailor solutions to your team’s needs. Start small, iterate, and leverage tools like shellcheck and CI/CD platforms to elevate your scripting game—your future self (and your team) will thank you.