Project: Deployment Automation
Project: Deployment Automation¶
Difficulty: ⭐⭐⭐⭐
Previous: 14_Project_Task_Runner.md | Next: 16_Project_Monitor.md
1. Overview¶
What is Deployment Automation?¶
Deployment automation is the process of automatically moving code from source control to production servers. It eliminates manual steps, reduces errors, and enables faster, more reliable releases.
Key components include:
- Remote execution: Running commands on target servers via SSH
- File synchronization: Copying code and assets to servers
- Health checks: Verifying the deployment succeeded
- Rollback capability: Reverting to the previous version on failure
- Multi-server orchestration: Deploying to multiple hosts sequentially or in parallel
Why Pure Bash?¶
While tools like Ansible, Terraform, and Kubernetes exist, bash deployment scripts offer:
- Zero dependencies: No agents or orchestration tools required
- Transparency: Exactly what runs on each server is visible
- Simplicity: Perfect for small-to-medium deployments
- SSH-native: Leverage existing SSH infrastructure
- Customization: Easy to adapt for specific needs
What We're Building¶
This lesson covers three deployment tools:
- SSH-based deployment: Deploy to remote servers using rsync and SSH
- Rolling deployment: Gradually deploy to a fleet of servers with health checks
- Docker entrypoint scripts: Proper container initialization with signal handling
2. Design¶
Architecture Overview¶
Deployment System
├── Configuration Management
│ ├── Environment variables (.env files)
│ ├── Target hosts (inventory)
│ └── Deployment strategy (rolling, blue-green)
│
├── Remote Execution
│ ├── SSH connection management
│ ├── Command execution on remote hosts
│ └── File synchronization (rsync)
│
├── Health Checks
│ ├── Application health endpoints
│ ├── Process verification
│ └── Log inspection
│
└── Rollback Strategy
├── Version tracking
├── Symlink-based releases
└── Automatic rollback on failure
Deployment Strategies¶
| Strategy | Description | Use Case | Risk |
|---|---|---|---|
| All-at-once | Deploy to all servers simultaneously | Low-traffic apps, staging | High |
| Rolling | Deploy to servers one-by-one or in batches | Production, gradual rollout | Medium |
| Blue-Green | Maintain two environments, switch traffic | Zero-downtime deploys | Low |
| Canary | Deploy to a subset, monitor, then full rollout | High-risk changes | Low |
We'll implement rolling deployment in this lesson.
Target Directory Structure¶
On remote servers:
/opt/myapp/
├── current -> releases/20240215_143022/ # Symlink to active version
├── releases/
│ ├── 20240215_143022/ # Current deployment
│ ├── 20240215_120000/ # Previous deployment
│ └── 20240214_093000/ # Older deployment
└── shared/
├── logs/
├── uploads/
└── .env # Shared environment config
This structure enables: - Quick rollback: Just change the symlink - Keep old versions: Retain N previous releases - Shared state: Logs and uploads persist across deploys
3. SSH-Based Deployment¶
SSH Connection Basics¶
Establish passwordless SSH with key-based authentication:
# Generate SSH key (if not already done)
ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -N ""
# Copy public key to remote server
ssh-copy-id -i ~/.ssh/deploy_key.pub user@server.example.com
# Test connection
ssh -i ~/.ssh/deploy_key user@server.example.com "echo 'Connected successfully'"
Reusable SSH Functions¶
#!/usr/bin/env bash
set -euo pipefail
# SSH configuration
SSH_KEY="${SSH_KEY:-$HOME/.ssh/deploy_key}"
SSH_USER="${SSH_USER:-deploy}"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
# Execute command on remote host
remote_exec() {
local host="$1"
shift
local cmd="$*"
ssh ${SSH_OPTS} -i "${SSH_KEY}" "${SSH_USER}@${host}" "${cmd}"
}
# Example usage
remote_exec "web01.example.com" "uptime"
remote_exec "web01.example.com" "df -h"
File Synchronization with rsync¶
rsync is more efficient than scp for repeated deployments:
# Sync local directory to remote server
sync_files() {
local host="$1"
local source="$2"
local destination="$3"
rsync -avz --delete \
-e "ssh ${SSH_OPTS} -i ${SSH_KEY}" \
"${source}/" \
"${SSH_USER}@${host}:${destination}/"
}
# Example: Deploy application code
sync_files "web01.example.com" "./build" "/opt/myapp/releases/$(date +%Y%m%d_%H%M%S)"
rsync flags explained:
- -a: Archive mode (preserves permissions, timestamps, etc.)
- -v: Verbose
- -z: Compress during transfer
- --delete: Remove files on destination that don't exist in source
- -e: Specify remote shell command
Advanced: Excluding Files¶
sync_files_with_exclusions() {
local host="$1"
local source="$2"
local destination="$3"
rsync -avz --delete \
--exclude='.git' \
--exclude='node_modules' \
--exclude='*.log' \
--exclude='.env' \
-e "ssh ${SSH_OPTS} -i ${SSH_KEY}" \
"${source}/" \
"${SSH_USER}@${host}:${destination}/"
}
SSH Connection Pooling (ControlMaster)¶
Reuse SSH connections for faster repeated commands:
# Enable connection multiplexing
SSH_CONTROL_PATH="/tmp/ssh-control-%r@%h:%p"
SSH_OPTS+=" -o ControlMaster=auto -o ControlPath=${SSH_CONTROL_PATH} -o ControlPersist=10m"
# First command opens connection and keeps it alive for 10 minutes
remote_exec "web01.example.com" "echo 'First command'"
# Subsequent commands reuse the connection (much faster)
remote_exec "web01.example.com" "echo 'Second command'"
remote_exec "web01.example.com" "echo 'Third command'"
# Cleanup control socket when done
cleanup_ssh() {
local host="$1"
ssh ${SSH_OPTS} -O exit "${SSH_USER}@${host}" 2>/dev/null || true
}
4. Rolling Deployment¶
Rolling Deployment Strategy¶
Deploy to servers sequentially, verifying each succeeds before proceeding:
- Deploy to server 1
- Run health check on server 1
- If healthy, continue to server 2; otherwise rollback and abort
- Repeat for all servers
Implementation¶
#!/usr/bin/env bash
set -euo pipefail
# ============================================================================
# Rolling Deployment Script
# ============================================================================
# Configuration
APP_NAME="${APP_NAME:-myapp}"
APP_DIR="/opt/${APP_NAME}"
RELEASES_DIR="${APP_DIR}/releases"
CURRENT_LINK="${APP_DIR}/current"
SHARED_DIR="${APP_DIR}/shared"
KEEP_RELEASES=5
# Health check settings
HEALTH_CHECK_URL="${HEALTH_CHECK_URL:-http://localhost:8080/health}"
HEALTH_CHECK_RETRIES=5
HEALTH_CHECK_DELAY=2
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RESET='\033[0m'
log_info() {
echo -e "${BLUE}[$(date +'%H:%M:%S')]${RESET} $*"
}
log_success() {
echo -e "${GREEN}[$(date +'%H:%M:%S')] ✓${RESET} $*"
}
log_error() {
echo -e "${RED}[$(date +'%H:%M:%S')] ✗${RESET} $*" >&2
}
log_warn() {
echo -e "${YELLOW}[$(date +'%H:%M:%S')] !${RESET} $*"
}
# ============================================================================
# Remote Operations
# ============================================================================
remote_exec() {
local host="$1"
shift
ssh ${SSH_OPTS} -i "${SSH_KEY}" "${SSH_USER}@${host}" "$@"
}
# ============================================================================
# Deployment Functions
# ============================================================================
# Create directory structure on remote server
setup_remote_directories() {
local host="$1"
log_info "Setting up directories on ${host}..."
remote_exec "${host}" "mkdir -p ${RELEASES_DIR} ${SHARED_DIR}/logs"
log_success "Directories ready on ${host}"
}
# Deploy application code to remote server
deploy_release() {
local host="$1"
local release_name="$(date +%Y%m%d_%H%M%S)"
local release_path="${RELEASES_DIR}/${release_name}"
log_info "Deploying release ${release_name} to ${host}..."
# Create release directory
remote_exec "${host}" "mkdir -p ${release_path}"
# Sync application code
rsync -avz --delete \
--exclude='.git' \
--exclude='*.log' \
--exclude='.env' \
-e "ssh ${SSH_OPTS} -i ${SSH_KEY}" \
./build/ \
"${SSH_USER}@${host}:${release_path}/"
# Link shared resources
remote_exec "${host}" "ln -snf ${SHARED_DIR}/logs ${release_path}/logs"
remote_exec "${host}" "ln -snf ${SHARED_DIR}/.env ${release_path}/.env"
echo "${release_name}"
}
# Activate a release by updating the 'current' symlink
activate_release() {
local host="$1"
local release_name="$2"
local release_path="${RELEASES_DIR}/${release_name}"
log_info "Activating release ${release_name} on ${host}..."
# Update symlink atomically
remote_exec "${host}" "ln -snf ${release_path} ${CURRENT_LINK}"
# Restart application
restart_application "${host}"
log_success "Activated ${release_name} on ${host}"
}
# Restart the application
restart_application() {
local host="$1"
log_info "Restarting application on ${host}..."
# Systemd service restart
if remote_exec "${host}" "systemctl is-active --quiet ${APP_NAME}"; then
remote_exec "${host}" "sudo systemctl restart ${APP_NAME}"
else
log_warn "Service ${APP_NAME} not running on ${host}, starting..."
remote_exec "${host}" "sudo systemctl start ${APP_NAME}"
fi
sleep 2
}
# ============================================================================
# Health Checks
# ============================================================================
check_health() {
local host="$1"
local retries="${HEALTH_CHECK_RETRIES}"
log_info "Running health check on ${host}..."
while [ "${retries}" -gt 0 ]; do
if remote_exec "${host}" "curl -sf ${HEALTH_CHECK_URL} > /dev/null"; then
log_success "Health check passed on ${host}"
return 0
fi
retries=$((retries - 1))
if [ "${retries}" -gt 0 ]; then
log_warn "Health check failed, retrying in ${HEALTH_CHECK_DELAY}s... (${retries} attempts left)"
sleep "${HEALTH_CHECK_DELAY}"
fi
done
log_error "Health check failed on ${host} after ${HEALTH_CHECK_RETRIES} attempts"
return 1
}
# ============================================================================
# Rollback
# ============================================================================
rollback() {
local host="$1"
log_warn "Rolling back deployment on ${host}..."
# Get previous release
local previous_release=$(remote_exec "${host}" \
"ls -t ${RELEASES_DIR} | sed -n '2p'")
if [ -z "${previous_release}" ]; then
log_error "No previous release found for rollback on ${host}"
return 1
fi
log_info "Rolling back to ${previous_release} on ${host}"
# Reactivate previous release
activate_release "${host}" "${previous_release}"
if check_health "${host}"; then
log_success "Rollback successful on ${host}"
return 0
else
log_error "Rollback failed on ${host}"
return 1
fi
}
# ============================================================================
# Cleanup
# ============================================================================
cleanup_old_releases() {
local host="$1"
log_info "Cleaning up old releases on ${host}..."
remote_exec "${host}" \
"ls -t ${RELEASES_DIR} | tail -n +$((KEEP_RELEASES + 1)) | xargs -I {} rm -rf ${RELEASES_DIR}/{}"
log_success "Cleanup complete on ${host}"
}
# ============================================================================
# Rolling Deploy to Fleet
# ============================================================================
rolling_deploy() {
local hosts=("$@")
log_info "Starting rolling deployment to ${#hosts[@]} servers..."
local start_time=$(date +%s)
local failed_hosts=()
for host in "${hosts[@]}"; do
log_info "Deploying to ${host}..."
# Setup
setup_remote_directories "${host}"
# Deploy
local release_name=$(deploy_release "${host}")
# Activate
activate_release "${host}" "${release_name}"
# Health check
if check_health "${host}"; then
log_success "Deployment to ${host} successful"
cleanup_old_releases "${host}"
else
log_error "Deployment to ${host} failed health check"
# Rollback
if rollback "${host}"; then
log_warn "Rolled back ${host} successfully"
fi
failed_hosts+=("${host}")
# Abort on first failure
log_error "Aborting rolling deployment due to failure on ${host}"
break
fi
log_info "Waiting 5 seconds before next deployment..."
sleep 5
done
local end_time=$(date +%s)
local duration=$((end_time - start_time))
echo ""
if [ ${#failed_hosts[@]} -eq 0 ]; then
log_success "Rolling deployment completed successfully in ${duration}s"
return 0
else
log_error "Rolling deployment failed on: ${failed_hosts[*]}"
return 1
fi
}
# ============================================================================
# Main
# ============================================================================
main() {
# Load environment
if [ -f .env.deploy ]; then
source .env.deploy
fi
# Validate configuration
if [ -z "${SSH_KEY:-}" ]; then
log_error "SSH_KEY not set"
exit 1
fi
if [ ! -f "${SSH_KEY}" ]; then
log_error "SSH key not found: ${SSH_KEY}"
exit 1
fi
# Read hosts from file or arguments
local hosts=()
if [ -f "hosts.txt" ]; then
mapfile -t hosts < hosts.txt
elif [ $# -gt 0 ]; then
hosts=("$@")
else
log_error "No hosts specified. Provide hosts.txt or pass as arguments."
exit 1
fi
# Run rolling deployment
rolling_deploy "${hosts[@]}"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
5. Docker Entrypoint Scripts¶
Why Proper Entrypoint Scripts Matter¶
Docker containers should: - Handle signals gracefully (SIGTERM for shutdown) - Wait for dependencies (database, Redis, etc.) - Configure themselves from environment variables - Execute as the correct user (not root)
A proper entrypoint script orchestrates initialization.
Basic Entrypoint Structure¶
#!/usr/bin/env bash
set -euo pipefail
# ============================================================================
# Docker Entrypoint Script
# ============================================================================
# Signal handling for graceful shutdown
shutdown() {
echo "Received SIGTERM, shutting down gracefully..."
# Kill child processes
kill -TERM "$APP_PID" 2>/dev/null || true
wait "$APP_PID"
exit 0
}
trap shutdown SIGTERM SIGINT
# ============================================================================
# Wait for Dependencies
# ============================================================================
wait_for_service() {
local host="$1"
local port="$2"
local max_attempts=30
local attempt=1
echo "Waiting for ${host}:${port}..."
while [ $attempt -le $max_attempts ]; do
if nc -z "${host}" "${port}" 2>/dev/null; then
echo "Service ${host}:${port} is ready!"
return 0
fi
echo "Attempt ${attempt}/${max_attempts}: ${host}:${port} not ready, waiting..."
sleep 2
attempt=$((attempt + 1))
done
echo "Service ${host}:${port} failed to become ready after ${max_attempts} attempts"
return 1
}
# Wait for PostgreSQL
if [ -n "${POSTGRES_HOST:-}" ]; then
wait_for_service "${POSTGRES_HOST}" "${POSTGRES_PORT:-5432}"
fi
# Wait for Redis
if [ -n "${REDIS_HOST:-}" ]; then
wait_for_service "${REDIS_HOST}" "${REDIS_PORT:-6379}"
fi
# ============================================================================
# Configuration from Environment
# ============================================================================
# Substitute environment variables in config template
if [ -f /app/config.template.yml ]; then
envsubst < /app/config.template.yml > /app/config.yml
echo "Generated config.yml from environment variables"
fi
# ============================================================================
# Run Migrations (if needed)
# ============================================================================
if [ "${RUN_MIGRATIONS:-false}" = "true" ]; then
echo "Running database migrations..."
python manage.py migrate
fi
# ============================================================================
# Start Application
# ============================================================================
echo "Starting application..."
# Start app in background so we can handle signals
exec "$@" &
APP_PID=$!
# Wait for application process
wait "$APP_PID"
Advanced: Template Processing with envsubst¶
Use envsubst to generate config files from environment variables:
# config.template.yml
database:
host: ${DB_HOST}
port: ${DB_PORT}
user: ${DB_USER}
password: ${DB_PASSWORD}
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
app:
debug: ${DEBUG}
secret_key: ${SECRET_KEY}
In the entrypoint:
export DB_HOST="${DB_HOST:-localhost}"
export DB_PORT="${DB_PORT:-5432}"
export REDIS_HOST="${REDIS_HOST:-localhost}"
export DEBUG="${DEBUG:-false}"
envsubst < config.template.yml > config.yml
Wait-for-it Pattern¶
Reusable function for dependency waiting:
wait_for_it() {
local service="$1"
local timeout="${2:-30}"
if [[ "${service}" =~ ^([^:]+):([0-9]+)$ ]]; then
local host="${BASH_REMATCH[1]}"
local port="${BASH_REMATCH[2]}"
else
echo "Invalid service format: ${service} (expected host:port)"
return 1
fi
local start=$(date +%s)
while true; do
if nc -z "${host}" "${port}" 2>/dev/null; then
echo "✓ ${service} is ready"
return 0
fi
local now=$(date +%s)
local elapsed=$((now - start))
if [ $elapsed -ge $timeout ]; then
echo "✗ ${service} not ready after ${timeout}s"
return 1
fi
echo "Waiting for ${service}... (${elapsed}s)"
sleep 1
done
}
# Usage
wait_for_it "${DATABASE_HOST}:${DATABASE_PORT}" 60
wait_for_it "${REDIS_HOST}:${REDIS_PORT}" 30
Running as Non-Root User¶
Security best practice: run application as non-root:
# In Dockerfile
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# Or in entrypoint (if you need root for initialization)
#!/bin/bash
# Do root-level setup
chown -R appuser:appuser /app/logs
# Drop privileges and execute app
exec gosu appuser "$@"
6. Environment Configuration¶
Loading .env Files¶
load_env() {
local env_file="${1:-.env}"
if [ ! -f "${env_file}" ]; then
echo "Warning: ${env_file} not found" >&2
return 1
fi
# Export all variables from .env
set -a
source "${env_file}"
set +a
echo "Loaded environment from ${env_file}"
}
# Usage
load_env ".env.production"
Environment Variable Validation¶
Ensure required variables are set:
validate_env() {
local required_vars=(
"DATABASE_URL"
"REDIS_URL"
"SECRET_KEY"
"API_KEY"
)
local missing_vars=()
for var in "${required_vars[@]}"; do
if [ -z "${!var:-}" ]; then
missing_vars+=("${var}")
fi
done
if [ ${#missing_vars[@]} -gt 0 ]; then
echo "Error: Missing required environment variables:" >&2
printf ' - %s\n' "${missing_vars[@]}" >&2
return 1
fi
echo "✓ All required environment variables are set"
}
# Usage in entrypoint
load_env ".env"
validate_env || exit 1
Secrets Management¶
Never hardcode secrets. Use environment variables or secret managers:
# Bad: Hardcoded secrets
DB_PASSWORD="super_secret_123"
# Good: From environment
DB_PASSWORD="${DB_PASSWORD}"
# Better: From secret file (Docker secrets, Kubernetes secrets)
if [ -f /run/secrets/db_password ]; then
DB_PASSWORD="$(cat /run/secrets/db_password)"
fi
# Best: From secret manager (AWS Secrets Manager, Vault)
DB_PASSWORD="$(aws secretsmanager get-secret-value --secret-id prod/db/password --query SecretString --output text)"
7. Complete Deploy Script¶
Here's a full-featured deployment script combining all concepts:
#!/usr/bin/env bash
set -euo pipefail
# ============================================================================
# Complete Deployment Automation Script
# ============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")"
# Configuration
APP_NAME="${APP_NAME:-myapp}"
DEPLOY_ENV="${DEPLOY_ENV:-production}"
SSH_KEY="${SSH_KEY:-${HOME}/.ssh/deploy_key}"
SSH_USER="${SSH_USER:-deploy}"
BUILD_DIR="${PROJECT_ROOT}/build"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RESET='\033[0m'
log() { echo -e "${BLUE}[$(date +'%H:%M:%S')]${RESET} $*"; }
success() { echo -e "${GREEN}[$(date +'%H:%M:%S')] ✓${RESET} $*"; }
error() { echo -e "${RED}[$(date +'%H:%M:%S')] ✗${RESET} $*" >&2; }
warn() { echo -e "${YELLOW}[$(date +'%H:%M:%S')] !${RESET} $*"; }
# ============================================================================
# Preflight Checks
# ============================================================================
preflight_checks() {
log "Running preflight checks..."
# Check SSH key
if [ ! -f "${SSH_KEY}" ]; then
error "SSH key not found: ${SSH_KEY}"
return 1
fi
# Check build directory
if [ ! -d "${BUILD_DIR}" ]; then
error "Build directory not found: ${BUILD_DIR}"
return 1
fi
# Check hosts file
if [ ! -f "${SCRIPT_DIR}/hosts.${DEPLOY_ENV}.txt" ]; then
error "Hosts file not found: hosts.${DEPLOY_ENV}.txt"
return 1
fi
# Verify build
if [ ! -f "${BUILD_DIR}/index.html" ]; then
warn "index.html not found in build directory"
fi
success "Preflight checks passed"
}
# ============================================================================
# Build
# ============================================================================
build_application() {
log "Building application..."
cd "${PROJECT_ROOT}"
# Clean previous build
rm -rf "${BUILD_DIR}"
mkdir -p "${BUILD_DIR}"
# Run build command (customize for your project)
if [ -f "package.json" ]; then
npm run build
elif [ -f "Makefile" ]; then
make build
else
error "No build system detected"
return 1
fi
success "Build completed"
}
# ============================================================================
# Deploy to Single Host
# ============================================================================
deploy_to_host() {
local host="$1"
local release_name="$(date +%Y%m%d_%H%M%S)"
log "Deploying ${release_name} to ${host}..."
# Setup directories
ssh -i "${SSH_KEY}" "${SSH_USER}@${host}" \
"mkdir -p /opt/${APP_NAME}/{releases,shared/logs}"
# Sync files
rsync -avz --delete \
--exclude='.git' \
--exclude='node_modules' \
--exclude='*.log' \
-e "ssh -i ${SSH_KEY}" \
"${BUILD_DIR}/" \
"${SSH_USER}@${host}:/opt/${APP_NAME}/releases/${release_name}/"
# Activate release
ssh -i "${SSH_KEY}" "${SSH_USER}@${host}" \
"ln -snf /opt/${APP_NAME}/releases/${release_name} /opt/${APP_NAME}/current"
# Restart service
ssh -i "${SSH_KEY}" "${SSH_USER}@${host}" \
"sudo systemctl restart ${APP_NAME}"
sleep 3
# Health check
if ssh -i "${SSH_KEY}" "${SSH_USER}@${host}" \
"curl -sf http://localhost:8080/health >/dev/null"; then
success "Deployment to ${host} successful"
# Cleanup old releases
ssh -i "${SSH_KEY}" "${SSH_USER}@${host}" \
"ls -t /opt/${APP_NAME}/releases | tail -n +6 | xargs -I {} rm -rf /opt/${APP_NAME}/releases/{}"
return 0
else
error "Health check failed on ${host}"
return 1
fi
}
# ============================================================================
# Rolling Deploy
# ============================================================================
rolling_deploy() {
local hosts_file="${SCRIPT_DIR}/hosts.${DEPLOY_ENV}.txt"
mapfile -t hosts < "${hosts_file}"
log "Deploying to ${#hosts[@]} hosts in ${DEPLOY_ENV}..."
for host in "${hosts[@]}"; do
# Skip empty lines and comments
[[ -z "${host}" || "${host}" =~ ^# ]] && continue
if deploy_to_host "${host}"; then
log "Waiting 10 seconds before next deployment..."
sleep 10
else
error "Deployment failed on ${host}, aborting rollout"
return 1
fi
done
success "Rolling deployment completed successfully"
}
# ============================================================================
# Main
# ============================================================================
show_help() {
cat <<EOF
Usage: $0 [OPTIONS] COMMAND
Commands:
build Build the application
deploy Build and deploy to ${DEPLOY_ENV}
rollback HOST Rollback to previous release on HOST
Options:
-e, --env ENV Deployment environment (default: production)
-h, --help Show this help message
Environment Variables:
APP_NAME Application name (default: myapp)
DEPLOY_ENV Deployment environment (default: production)
SSH_KEY Path to SSH private key
SSH_USER SSH username (default: deploy)
Examples:
$0 build
$0 deploy
$0 -e staging deploy
$0 rollback web01.example.com
EOF
}
main() {
local command=""
# Parse arguments
while [ $# -gt 0 ]; do
case "$1" in
-e|--env)
DEPLOY_ENV="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
build|deploy|rollback)
command="$1"
shift
break
;;
*)
error "Unknown option: $1"
show_help
exit 1
;;
esac
done
# Execute command
case "${command}" in
build)
build_application
;;
deploy)
preflight_checks
build_application
rolling_deploy
;;
rollback)
if [ $# -eq 0 ]; then
error "Rollback requires a host argument"
exit 1
fi
# Rollback logic here
;;
*)
show_help
exit 1
;;
esac
}
main "$@"
8. Usage Examples¶
Deploy to Production¶
# Build and deploy
./deploy.sh deploy
# Deploy to staging environment
./deploy.sh -e staging deploy
Hosts File¶
Create hosts.production.txt:
web01.example.com
web02.example.com
web03.example.com
Environment File¶
Create .env.deploy:
APP_NAME=myapp
DEPLOY_ENV=production
SSH_KEY=/home/user/.ssh/deploy_key
SSH_USER=deploy
HEALTH_CHECK_URL=http://localhost:8080/health
KEEP_RELEASES=5
Docker Entrypoint¶
In Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
CMD ["node", "server.js"]
Run the container:
docker run \
-e DB_HOST=postgres \
-e DB_PORT=5432 \
-e REDIS_HOST=redis \
myapp:latest
9. Extensions¶
1. Blue-Green Deployment¶
Maintain two identical environments (blue and green):
blue_green_deploy() {
local current_env=$(get_active_environment)
local target_env=$( [ "${current_env}" = "blue" ] && echo "green" || echo "blue" )
log "Current: ${current_env}, deploying to: ${target_env}"
# Deploy to inactive environment
deploy_to_environment "${target_env}"
# Switch traffic
switch_load_balancer_target "${target_env}"
success "Traffic switched to ${target_env}"
}
2. Canary Deployment¶
Deploy to a subset of servers first:
canary_deploy() {
local canary_hosts=("web01.example.com")
local production_hosts=("web02.example.com" "web03.example.com")
# Deploy to canary
for host in "${canary_hosts[@]}"; do
deploy_to_host "${host}"
done
# Monitor metrics
log "Canary deployed. Monitor metrics for 10 minutes..."
sleep 600
# If metrics are good, deploy to production
read -p "Proceed with full rollout? (y/n) " -n 1 -r
if [[ $REPLY =~ ^[Yy]$ ]]; then
rolling_deploy "${production_hosts[@]}"
fi
}
3. Slack Notifications¶
Send deployment notifications:
send_slack_notification() {
local message="$1"
local webhook_url="${SLACK_WEBHOOK_URL}"
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"${message}\"}" \
"${webhook_url}"
}
# Usage
send_slack_notification "Deployment started to production"
rolling_deploy
send_slack_notification "Deployment to production completed successfully"
4. Deployment Locking¶
Prevent concurrent deployments:
acquire_lock() {
local lock_file="/tmp/deploy.lock"
if [ -f "${lock_file}" ]; then
error "Another deployment is in progress"
return 1
fi
echo $$ > "${lock_file}"
trap "rm -f ${lock_file}" EXIT
}
# Usage
acquire_lock || exit 1
rolling_deploy
Previous: 14_Project_Task_Runner.md | Next: 16_Project_Monitor.md