dotlinux guide

Shell Scripting for DevOps: Simplifying Deployment Processes

Table of Contents

  1. Fundamentals of Shell Scripting
    • What is a Shell Script?
    • Shebang Line & Execution
    • Variables, Commands, and Control Structures
  2. Key Concepts for DevOps Workflows
    • Automation in CI/CD
    • Infrastructure as Code (IaC) Primer
    • Environment Management
  3. Usage Methods in Deployment
    • Automated Application Deployment
    • Rollback Mechanisms
    • Database Migrations
    • Service Management
  4. Common Practices
    • Parameterization with Arguments
    • Error Handling & Exit Codes
    • Logging & Debugging
    • Idempotency
  5. Best Practices
    • Security: Secrets & Permissions
    • Readability & Maintainability
    • Testing & Validation
    • Performance Optimization
  6. Advanced Use Cases
    • Cloud CLI Integration (AWS, GCP, Azure)
    • Docker/Kubernetes Orchestration
    • CI/CD Pipeline Integration (GitHub Actions, GitLab CI)
  7. Conclusion
  8. 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:

  1. Make it executable with chmod +x script.sh.
  2. Run it with ./script.sh (or bash script.sh if 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 sudo unless 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_DIR instead of d).
  • 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 shellcheck to 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 or while read).
  • Parallelize tasks where possible (e.g., xargs -P 4 for 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.

References