Day 15 - Bash Scripting Day
Write a practical bash script to automate common DevOps tasks like deployments, backups, and health checks.
Description
You're tired of running the same deployment commands manually. It's time to automate repetitive tasks with bash scripts. Today, you'll create a production-ready deployment script with proper error handling, logging, and validation.
Task
Write a bash script to automate application deployment.
Requirements:
- Check prerequisites (Docker, kubectl, etc.)
- Validate inputs and configuration
- Build and tag Docker image
- Push to registry
- Deploy to Kubernetes
- Verify deployment success
- Include error handling and logging
Target
- ✅ Script runs without errors
- ✅ Proper error handling
- ✅ Colored output for readability
- ✅ Logging to file
- ✅ Rollback on failure
- ✅ Help documentation
Sample App
Deployment Script
deploy.sh
#!/bin/bash
#######################################
# Application Deployment Script
# Description: Build, push, and deploy application
# Usage: ./deploy.sh [environment] [version]
#######################################
set -euo pipefail # Exit on error, undefined vars, pipe failures
#######################################
# Configuration
#######################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="${SCRIPT_DIR}/logs"
LOG_FILE="${LOG_DIR}/deploy-$(date +%Y%m%d-%H%M%S).log"
# Docker configuration
DOCKER_REGISTRY="${DOCKER_REGISTRY:-docker.io}"
IMAGE_NAME="${IMAGE_NAME:-myapp}"
# Kubernetes configuration
KUBECTL_TIMEOUT=300
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
#######################################
# Functions
#######################################
# Print colored message
log() {
local level=$1
shift
local message="$@"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case $level in
INFO)
echo -e "${BLUE}[INFO]${NC} $message" | tee -a "$LOG_FILE"
;;
SUCCESS)
echo -e "${GREEN}[SUCCESS]${NC} $message" | tee -a "$LOG_FILE"
;;
WARN)
echo -e "${YELLOW}[WARN]${NC} $message" | tee -a "$LOG_FILE"
;;
ERROR)
echo -e "${RED}[ERROR]${NC} $message" | tee -a "$LOG_FILE"
;;
esac
}
# Error handler
error_exit() {
log ERROR "$1"
exit 1
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Show usage
usage() {
cat << EOF
Usage: $0 [OPTIONS] ENVIRONMENT VERSION
Deploy application to Kubernetes cluster.
Arguments:
ENVIRONMENT Target environment (dev|staging|prod)
VERSION Version tag to deploy
Options:
-h, --help Show this help message
-d, --dry-run Perform dry run without actual deployment
-s, --skip-build Skip Docker build step
-v, --verbose Enable verbose output
Examples:
$0 dev 1.0.0
$0 --dry-run prod 2.1.0
$0 --skip-build staging 1.5.2
EOF
exit 0
}
# Validate prerequisites
check_prerequisites() {
log INFO "Checking prerequisites..."
local missing_tools=()
# Required tools
local required_tools=("docker" "kubectl" "git")
for tool in "${required_tools[@]}"; do
if ! command_exists "$tool"; then
missing_tools+=("$tool")
fi
done
if [ ${#missing_tools[@]} -ne 0 ]; then
error_exit "Missing required tools: ${missing_tools[*]}"
fi
# Check Docker daemon
if ! docker info >/dev/null 2>&1; then
error_exit "Docker daemon is not running"
fi
# Check kubectl connection
if ! kubectl cluster-info >/dev/null 2>&1; then
error_exit "Cannot connect to Kubernetes cluster"
fi
log SUCCESS "All prerequisites met"
}
# Validate environment
validate_environment() {
local env=$1
case $env in
dev|staging|prod)
log INFO "Environment: $env"
;;
*)
error_exit "Invalid environment: $env. Must be dev, staging, or prod"
;;
esac
}
# Validate version format
validate_version() {
local version=$1
if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
error_exit "Invalid version format: $version. Expected format: X.Y.Z"
fi
log INFO "Version: $version"
}
# Build Docker image
build_image() {
local version=$1
local image_tag="${DOCKER_REGISTRY}/${IMAGE_NAME}:${version}"
log INFO "Building Docker image: $image_tag"
if [ "$DRY_RUN" = true ]; then
log INFO "[DRY-RUN] Would build: $image_tag"
return 0
fi
if ! docker build -t "$image_tag" .; then
error_exit "Docker build failed"
fi
# Also tag as latest for environment
docker tag "$image_tag" "${DOCKER_REGISTRY}/${IMAGE_NAME}:${ENVIRONMENT}-latest"
log SUCCESS "Image built successfully"
}
# Push Docker image
push_image() {
local version=$1
local image_tag="${DOCKER_REGISTRY}/${IMAGE_NAME}:${version}"
log INFO "Pushing Docker image: $image_tag"
if [ "$DRY_RUN" = true ]; then
log INFO "[DRY-RUN] Would push: $image_tag"
return 0
fi
if ! docker push "$image_tag"; then
error_exit "Docker push failed"
fi
# Push environment-specific latest tag
docker push "${DOCKER_REGISTRY}/${IMAGE_NAME}:${ENVIRONMENT}-latest"
log SUCCESS "Image pushed successfully"
}
# Deploy to Kubernetes
deploy_to_k8s() {
local environment=$1
local version=$2
local namespace="$environment"
log INFO "Deploying to Kubernetes namespace: $namespace"
if [ "$DRY_RUN" = true ]; then
log INFO "[DRY-RUN] Would deploy version $version to $namespace"
return 0
fi
# Create namespace if it doesn't exist
kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply -f -
# Update deployment with new image
kubectl set image deployment/myapp \
myapp="${DOCKER_REGISTRY}/${IMAGE_NAME}:${version}" \
-n "$namespace" \
--record
# Wait for rollout
log INFO "Waiting for rollout to complete..."
if ! kubectl rollout status deployment/myapp -n "$namespace" --timeout="${KUBECTL_TIMEOUT}s"; then
error_exit "Deployment rollout failed"
fi
log SUCCESS "Deployment successful"
}
# Verify deployment
verify_deployment() {
local namespace=$1
log INFO "Verifying deployment..."
# Check pod status
local ready_pods=$(kubectl get pods -n "$namespace" -l app=myapp -o json | \
jq '.items | map(select(.status.phase == "Running")) | length')
local total_pods=$(kubectl get pods -n "$namespace" -l app=myapp --no-headers | wc -l)
log INFO "Ready pods: $ready_pods/$total_pods"
if [ "$ready_pods" -eq 0 ]; then
error_exit "No pods are running"
fi
# Test health endpoint
local pod_name=$(kubectl get pod -n "$namespace" -l app=myapp -o jsonpath='{.items[0].metadata.name}')
if kubectl exec -n "$namespace" "$pod_name" -- wget -q -O- http://localhost:3000/health >/dev/null 2>&1; then
log SUCCESS "Health check passed"
else
log WARN "Health check failed"
fi
log SUCCESS "Deployment verified"
}
# Rollback deployment
rollback() {
local namespace=$1
log WARN "Rolling back deployment..."
if [ "$DRY_RUN" = true ]; then
log INFO "[DRY-RUN] Would rollback in namespace $namespace"
return 0
fi
kubectl rollout undo deployment/myapp -n "$namespace"
kubectl rollout status deployment/myapp -n "$namespace"
log SUCCESS "Rollback completed"
}
# Cleanup function
cleanup() {
log INFO "Cleaning up..."
# Add cleanup tasks here
}
# Signal handler
trap cleanup EXIT
trap 'error_exit "Script interrupted"' INT TERM
#######################################
# Main Script
#######################################
main() {
# Create log directory
mkdir -p "$LOG_DIR"
# Parse options
DRY_RUN=false
SKIP_BUILD=false
VERBOSE=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
;;
-d|--dry-run)
DRY_RUN=true
shift
;;
-s|--skip-build)
SKIP_BUILD=true
shift
;;
-v|--verbose)
VERBOSE=true
set -x
shift
;;
-*)
error_exit "Unknown option: $1"
;;
*)
break
;;
esac
done
# Validate arguments
if [ $# -ne 2 ]; then
error_exit "Missing required arguments. Use -h for help."
fi
ENVIRONMENT=$1
VERSION=$2
log INFO "=== Starting Deployment ==="
log INFO "Environment: $ENVIRONMENT"
log INFO "Version: $VERSION"
log INFO "Dry Run: $DRY_RUN"
log INFO "Log file: $LOG_FILE"
echo ""
# Run deployment steps
check_prerequisites
validate_environment "$ENVIRONMENT"
validate_version "$VERSION"
if [ "$SKIP_BUILD" = false ]; then
build_image "$VERSION"
push_image "$VERSION"
else
log INFO "Skipping build step"
fi
deploy_to_k8s "$ENVIRONMENT" "$VERSION"
verify_deployment "$ENVIRONMENT"
log SUCCESS "=== Deployment Complete ==="
log INFO "Application version $VERSION deployed to $ENVIRONMENT"
}
# Run main function
main "$@"
Helper Scripts
check-health.sh
#!/bin/bash
# Health check script
set -euo pipefail
NAMESPACE="${1:-default}"
APP_LABEL="${2:-app=myapp}"
echo "Checking health of $APP_LABEL in namespace $NAMESPACE..."
# Get all pods
PODS=$(kubectl get pods -n "$NAMESPACE" -l "$APP_LABEL" -o json)
# Check each pod
echo "$PODS" | jq -r '.items[] |
"\(.metadata.name): \(.status.phase) - Ready: \(
.status.containerStatuses[0].ready
)"'
# Count healthy pods
READY=$(echo "$PODS" | jq '[.items[].status.containerStatuses[0].ready] | map(select(. == true)) | length')
TOTAL=$(echo "$PODS" | jq '.items | length')
echo "Ready: $READY/$TOTAL"
if [ "$READY" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then
echo "✓ All pods healthy"
exit 0
else
echo "✗ Some pods unhealthy"
exit 1
fi
backup.sh
#!/bin/bash
# Backup script
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-./backups}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/backup-${TIMESTAMP}.tar.gz"
mkdir -p "$BACKUP_DIR"
echo "Creating backup: $BACKUP_FILE"
# Backup Kubernetes resources
kubectl get all --all-namespaces -o yaml > "${BACKUP_DIR}/k8s-resources-${TIMESTAMP}.yaml"
# Backup configs
kubectl get configmap --all-namespaces -o yaml > "${BACKUP_DIR}/configmaps-${TIMESTAMP}.yaml"
kubectl get secret --all-namespaces -o yaml > "${BACKUP_DIR}/secrets-${TIMESTAMP}.yaml"
# Create archive
tar -czf "$BACKUP_FILE" -C "$BACKUP_DIR" \
"k8s-resources-${TIMESTAMP}.yaml" \
"configmaps-${TIMESTAMP}.yaml" \
"secrets-${TIMESTAMP}.yaml"
# Cleanup individual files
rm -f "${BACKUP_DIR}"/*.yaml
echo "Backup complete: $BACKUP_FILE"
# Cleanup old backups (keep last 7 days)
find "$BACKUP_DIR" -name "backup-*.tar.gz" -mtime +7 -delete
echo "Old backups cleaned up"
Explanation
Bash Best Practices
1. Strict Mode
set -euo pipefail
-e: Exit on error-u: Exit on undefined variable-o pipefail: Pipeline fails if any command fails
2. Error Handling
error_exit() {
echo "ERROR: $1" >&2
exit 1
}
command || error_exit "Command failed"
3. Functions
function_name() {
local param=$1 # Local variable
# Function body
echo "Result"
return 0 # Success
}
4. Logging
log() {
local level=$1
local message=$2
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $message" | tee -a "$LOG_FILE"
}
5. Input Validation
if [ $# -lt 2 ]; then
echo "Usage: $0 <arg1> <arg2>"
exit 1
fi
# Validate format
if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
error_exit "Invalid version format"
fi
Common Patterns
Checking Command Existence
if ! command -v docker &> /dev/null; then
echo "Docker not found"
exit 1
fi
Reading Configuration
# From file
CONFIG_FILE="config.env"
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
# From environment with default
DATABASE_URL="${DATABASE_URL:-postgresql://localhost:5432/mydb}"
Loops
# Array iteration
ENVIRONMENTS=("dev" "staging" "prod")
for env in "${ENVIRONMENTS[@]}"; do
echo "Deploying to $env"
done
# File iteration
while IFS= read -r line; do
echo "Processing: $line"
done < input.txt
Result
Run the Script
# Make executable
chmod +x deploy.sh check-health.sh backup.sh
# Show help
./deploy.sh --help
# Dry run
./deploy.sh --dry-run dev 1.0.0
# Actual deployment
./deploy.sh dev 1.0.0
# Output:
# [INFO] === Starting Deployment ===
# [INFO] Environment: dev
# [INFO] Version: 1.0.0
# [INFO] Dry Run: false
# [INFO] Log file: ./logs/deploy-20251215-120000.log
#
# [INFO] Checking prerequisites...
# [SUCCESS] All prerequisites met
# [INFO] Environment: dev
# [INFO] Version: 1.0.0
# [INFO] Building Docker image: docker.io/myapp:1.0.0
# [SUCCESS] Image built successfully
# [INFO] Pushing Docker image: docker.io/myapp:1.0.0
# [SUCCESS] Image pushed successfully
# [INFO] Deploying to Kubernetes namespace: dev
# [INFO] Waiting for rollout to complete...
# [SUCCESS] Deployment successful
# [INFO] Verifying deployment...
# [SUCCESS] Health check passed
# [SUCCESS] Deployment verified
# [SUCCESS] === Deployment Complete ===
Check Deployment Health
./check-health.sh dev app=myapp
# Output:
# myapp-5d7f8c9b4d-abc12: Running - Ready: true
# myapp-5d7f8c9b4d-def34: Running - Ready: true
# Ready: 2/2
# ✓ All pods healthy
Create Backup
./backup.sh
# Output:
# Creating backup: ./backups/backup-20251215-120000.tar.gz
# Backup complete: ./backups/backup-20251215-120000.tar.gz
# Old backups cleaned up
Validation
Testing Checklist
# 1. Script is executable
[ -x deploy.sh ]
echo "Executable: $?"
# 2. Help works
./deploy.sh --help
# 3. Validates inputs
./deploy.sh invalid 1.0.0
# Should exit with error
# 4. Dry run works
./deploy.sh --dry-run dev 1.0.0
# Should not make changes
# 5. Logging works
[ -f logs/deploy-*.log ]
echo "Log file exists: $?"
# 6. Error handling works
# Introduce error and verify script exits
Advanced Techniques
Parallel Execution
# Run commands in parallel
deploy_service() {
local service=$1
echo "Deploying $service..."
kubectl apply -f "$service.yaml"
}
for service in api web worker; do
deploy_service "$service" &
done
wait # Wait for all background jobs
Progress Indicators
spinner() {
local pid=$1
local delay=0.1
local spinstr='|/-\'
while ps -p $pid > /dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
# Usage
long_running_command &
spinner $!
Interactive Prompts
confirm() {
read -p "$1 [y/N]: " -n 1 -r
echo
[[ $REPLY =~ ^[Yy]$ ]]
}
if confirm "Deploy to production?"; then
deploy_to_prod
fi
Best Practices
✅ Do's
- Use strict mode:
set -euo pipefail - Quote variables:
"$VAR"not$VAR - Check exit codes:
if command; then - Use functions: Organize code
- Log everything: Debug later
- Handle signals: Cleanup with
trap
❌ Don'ts
- Don't ignore errors: Check return codes
- Don't use
eval: Security risk - Don't parse
ls: Use glob patterns - Don't forget quotes: Word splitting issues
- Don't use
catunnecessarily: Use redirection
Links
- Bash Manual
- ShellCheck - Script linter
- Google Shell Style Guide
- Advanced Bash-Scripting Guide
- Bash Pitfalls
Share Your Success
Created your automation script? Share it!
Tag @thedevopsdaily on X with:
- What task you automated
- Time saved per run
- Lines of code
- Coolest feature
Use hashtags: #AdventOfDevOps #Bash #Automation #Day15
Ready to complete this challenge?
Mark this challenge as complete once you've finished the task. We'll track your progress!
Completed this challenge? Share your success!
Tag @thedevopsdaily on X (Twitter) and share your learning journey with the community!
These amazing companies help us create free, high-quality DevOps content for the community
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorFound an issue?