LinuxBash Scripting

Real-World Bash Automation Projects for Sysadmins and Developers

Build four production-grade automation projects in Bash: database backup with retention, system health monitor with alerts, zero-downtime deployment, and log analysis pipeline. Lesson 10 of the Linux & Bash Scripting course.

TT
Daniel Brooks
7 min read
Real-World Bash Automation Projects for Sysadmins and Developers

The fastest way to consolidate scripting skills is to build things you would actually use. This lesson presents four complete, production-ready automation projects that apply every concept from this course: error handling, traps, functions, arrays, process management, scheduling, and API integration.

Previous: Lesson 9 — Bash in CI/CD Pipelines


Project 1: Database Backup with Retention

A complete PostgreSQL backup script with compression, remote transfer, retention policy, and alerting.

bash
#!/usr/bin/env bash
# pg_backup.sh — PostgreSQL backup with S3 upload and retention
set -euo pipefail

# ---------- Configuration ----------
readonly DB_NAME="${1:?'Usage: pg_backup.sh <database_name>'}"
readonly BACKUP_DIR="/var/backups/postgresql"
readonly S3_BUCKET="${S3_BUCKET:?'S3_BUCKET must be set'}"
readonly RETENTION_DAYS=30
readonly TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
readonly BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"
readonly LOG_FILE="/var/log/pg_backup.log"
readonly SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"

# ---------- Logging ----------
log()   { echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO  $*" | tee -a "$LOG_FILE"; }
error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR $*" | tee -a "$LOG_FILE" >&2; }

# ---------- Alerting ----------
alert() {
    local message="$1"
    log "ALERT: $message"
    if [[ -n "$SLACK_WEBHOOK" ]]; then
        curl -sf -X POST "$SLACK_WEBHOOK" \
            -H 'Content-type: application/json' \
            -d "{\"text\":\":rotating_light: Backup alert: ${message}\"}" \
            || true
    fi
}

# ---------- Cleanup ----------
cleanup() {
    local exit_code=$?
    if [[ -f "${BACKUP_FILE}.tmp" ]]; then
        rm -f "${BACKUP_FILE}.tmp"
    fi
    if [[ $exit_code -ne 0 ]]; then
        alert "Backup of ${DB_NAME} failed (exit code: ${exit_code})"
    fi
}
trap cleanup EXIT

# ---------- Main ----------
main() {
    log "Starting backup: ${DB_NAME}"
    mkdir -p "$BACKUP_DIR"

    # Dump and compress atomically
    pg_dump "$DB_NAME" | gzip > "${BACKUP_FILE}.tmp"
    mv "${BACKUP_FILE}.tmp" "$BACKUP_FILE"

    local size
    size="$(du -sh "$BACKUP_FILE" | cut -f1)"
    log "Backup created: ${BACKUP_FILE} (${size})"

    # Upload to S3
    aws s3 cp "$BACKUP_FILE" "s3://${S3_BUCKET}/postgresql/${DB_NAME}/"
    log "Uploaded to s3://${S3_BUCKET}/postgresql/${DB_NAME}/"

    # Remove local file after successful upload
    rm -f "$BACKUP_FILE"

    # Apply retention: remove backups older than RETENTION_DAYS
    local deleted
    deleted=$(aws s3 ls "s3://${S3_BUCKET}/postgresql/${DB_NAME}/" \
        | awk '{print $4}' \
        | while read -r key; do
            local ts="${key%%_*}"
            local age=$(( ( $(date +%s) - $(date -d "${ts:0:8}" +%s) ) / 86400 ))
            if (( age > RETENTION_DAYS )); then
                aws s3 rm "s3://${S3_BUCKET}/postgresql/${DB_NAME}/${key}"
                echo "$key"
            fi
          done | wc -l)
    log "Retention policy applied: ${deleted} old backup(s) removed"
    log "Backup complete"
}

main

Project 2: System Health Monitor with Webhook Alerts

A script that checks CPU, memory, disk, and service status — and sends a Slack alert when thresholds are breached.

bash
#!/usr/bin/env bash
# health_monitor.sh — System health check with Slack alerts
set -euo pipefail

readonly SLACK_WEBHOOK="${SLACK_WEBHOOK:?'SLACK_WEBHOOK must be set'}"
readonly CPU_THRESHOLD=80
readonly MEM_THRESHOLD=90
readonly DISK_THRESHOLD=85
readonly SERVICES=("nginx" "postgresql" "redis")

alerts=()

# ---------- Checks ----------
check_cpu() {
    local cpu_idle
    cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | cut -d. -f1)
    local cpu_used=$(( 100 - cpu_idle ))
    if (( cpu_used > CPU_THRESHOLD )); then
        alerts+=("CPU usage is ${cpu_used}% (threshold: ${CPU_THRESHOLD}%)")
    fi
}

check_memory() {
    local mem_used_pct
    mem_used_pct=$(free | awk '/^Mem:/{printf "%.0f", $3/$2*100}')
    if (( mem_used_pct > MEM_THRESHOLD )); then
        alerts+=("Memory usage is ${mem_used_pct}% (threshold: ${MEM_THRESHOLD}%)")
    fi
}

check_disk() {
    while IFS= read -r line; do
        local use_pct mount
        use_pct=$(echo "$line" | awk '{print $5}' | tr -d '%')
        mount=$(echo "$line" | awk '{print $6}')
        if (( use_pct > DISK_THRESHOLD )); then
            alerts+=("Disk ${mount} is ${use_pct}% full (threshold: ${DISK_THRESHOLD}%)")
        fi
    done < <(df -h | tail -n +2 | grep -v tmpfs)
}

check_services() {
    for service in "${SERVICES[@]}"; do
        if ! systemctl is-active --quiet "$service"; then
            alerts+=("Service '${service}' is not running")
        fi
    done
}

# ---------- Alert ----------
send_slack_alert() {
    local hostname
    hostname="$(hostname -f)"
    local message="*Health alert on ${hostname}*\n"
    for alert in "${alerts[@]}"; do
        message+="• ${alert}\n"
    done

    curl -sf -X POST "$SLACK_WEBHOOK" \
        -H 'Content-type: application/json' \
        -d "{\"text\":\"${message}\"}"
}

# ---------- Main ----------
check_cpu
check_memory
check_disk
check_services

if (( ${#alerts[@]} > 0 )); then
    send_slack_alert
    echo "ALERT: ${#alerts[@]} issue(s) found"
    exit 1
else
    echo "OK: All checks passed"
fi

Project 3: Zero-Downtime Application Deployment

A deployment script that performs health-checked rolling updates with automatic rollback on failure.

bash
#!/usr/bin/env bash
# deploy_app.sh — Zero-downtime deploy with rollback
set -euo pipefail

readonly APP="${1:?'Usage: deploy_app.sh <app_name> <image_tag>'}"
readonly IMAGE_TAG="${2:?'Usage: deploy_app.sh <app_name> <image_tag>'}"
readonly NAMESPACE="${NAMESPACE:-production}"
readonly HEALTH_URL="${HEALTH_URL:-http://localhost:8080/health}"
readonly ROLLOUT_TIMEOUT=300

log()   { echo "[$(date '+%H:%M:%S')] $*"; }
error() { echo "[$(date '+%H:%M:%S')] ERROR: $*" >&2; }

get_current_tag() {
    kubectl get deployment "$APP" -n "$NAMESPACE" \
        -o jsonpath='{.spec.template.spec.containers[0].image}' \
        2>/dev/null | awk -F: '{print $NF}' || echo "none"
}

rollback() {
    local previous_tag="$1"
    error "Deployment failed — rolling back to ${previous_tag}"
    kubectl set image "deployment/${APP}" \
        "${APP}=${DOCKER_REGISTRY}/${APP}:${previous_tag}" \
        -n "$NAMESPACE"
    kubectl rollout status "deployment/${APP}" -n "$NAMESPACE" --timeout=120s
    error "Rollback complete"
}

wait_for_health() {
    local attempts=0
    local max=30
    log "Waiting for health check at ${HEALTH_URL}..."
    until curl -sf "$HEALTH_URL" &>/dev/null; do
        (( attempts++ ))
        if (( attempts >= max )); then
            error "Health check failed after ${max} attempts"
            return 1
        fi
        sleep 10
    done
    log "Health check passed"
}

main() {
    local previous_tag
    previous_tag="$(get_current_tag)"

    if [[ "$previous_tag" == "$IMAGE_TAG" ]]; then
        log "Already at ${IMAGE_TAG} — nothing to do"
        exit 0
    fi

    log "Deploying ${APP}: ${previous_tag} → ${IMAGE_TAG}"

    # Register rollback in case anything fails from here
    trap "rollback '${previous_tag}'" ERR

    kubectl set image "deployment/${APP}" \
        "${APP}=${DOCKER_REGISTRY}/${APP}:${IMAGE_TAG}" \
        -n "$NAMESPACE"

    kubectl rollout status "deployment/${APP}" \
        -n "$NAMESPACE" --timeout="${ROLLOUT_TIMEOUT}s"

    wait_for_health

    # Remove the rollback trap — deployment succeeded
    trap - ERR
    log "Deployment complete: ${APP} is running ${IMAGE_TAG}"
}

main

Project 4: Log Analysis Pipeline

A daily log analysis script that parses nginx access logs, extracts key metrics, and writes a summary report.

bash
#!/usr/bin/env bash
# log_analysis.sh — Daily nginx access log analysis
set -euo pipefail

readonly LOG_FILE="${1:-/var/log/nginx/access.log}"
readonly REPORT_DIR="/var/reports/nginx"
readonly DATE="$(date +%Y-%m-%d)"
readonly REPORT="${REPORT_DIR}/report_${DATE}.txt"

mkdir -p "$REPORT_DIR"

{
    echo "======================================"
    echo "  Nginx Access Log Report: ${DATE}"
    echo "======================================"
    echo

    echo "## Total Requests"
    wc -l < "$LOG_FILE"
    echo

    echo "## HTTP Status Code Breakdown"
    awk '{print $9}' "$LOG_FILE" \
        | sort | uniq -c | sort -rn \
        | awk '{printf "  %-6s %d\n", $2, $1}'
    echo

    echo "## Top 10 Requested URLs"
    awk '{print $7}' "$LOG_FILE" \
        | cut -d'?' -f1 \
        | sort | uniq -c | sort -rn | head -10 \
        | awk '{printf "  %6d  %s\n", $1, $2}'
    echo

    echo "## Top 10 Client IPs"
    awk '{print $1}' "$LOG_FILE" \
        | sort | uniq -c | sort -rn | head -10 \
        | awk '{printf "  %6d  %s\n", $1, $2}'
    echo

    echo "## Top 10 Slowest Requests (request time field)"
    awk '{print $NF, $7}' "$LOG_FILE" \
        | sort -rn | head -10 \
        | awk '{printf "  %8ss  %s\n", $1, $2}'
    echo

    echo "## 5xx Errors"
    grep '" 5[0-9][0-9] ' "$LOG_FILE" \
        | awk '{print $9, $7}' \
        | sort | uniq -c | sort -rn | head -20 \
        | awk '{printf "  %4d  %s %s\n", $1, $2, $3}'

    echo
    echo "Report generated: $(date)"
} > "$REPORT"

echo "Report written to: $REPORT"
cat "$REPORT"

Schedule it daily:

text
# /etc/cron.d/nginx-log-analysis
0 1 * * * root /usr/local/bin/log_analysis.sh >> /var/log/log_analysis.log 2>&1

What to Build Next

These four projects cover the core patterns. Here are natural extensions to deepen your skills:

  • Add bats-core unit tests to the backup and health monitor scripts
  • Port the health monitor to post metrics to Prometheus Pushgateway instead of Slack
  • Extend the deploy script to support blue-green deployment (two live deployments, traffic switch via load balancer)
  • Build a self-service script that new engineers run to bootstrap their local dev environment — databases, config files, dependencies — in a single idempotent run