deploy.sh

Download
bash 436 lines 11.1 KB
  1#!/usr/bin/env bash
  2set -euo pipefail
  3
  4# Deployment Automation Script
  5# Supports rolling deployments, rollback, and health checks
  6# Can deploy to staging or production environments
  7
  8# ============================================================================
  9# Configuration
 10# ============================================================================
 11
 12SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 13
 14# Default configuration (can be overridden by deploy.conf or environment)
 15APP_NAME="${APP_NAME:-myapp}"
 16DEPLOY_USER="${DEPLOY_USER:-deploy}"
 17DEPLOY_PATH="${DEPLOY_PATH:-/var/www/apps}"
 18KEEP_RELEASES="${KEEP_RELEASES:-5}"
 19HEALTH_CHECK_TIMEOUT="${HEALTH_CHECK_TIMEOUT:-30}"
 20HEALTH_CHECK_PATH="${HEALTH_CHECK_PATH:-/health}"
 21
 22# Environment-specific server lists
 23STAGING_SERVERS="${STAGING_SERVERS:-staging1.example.com}"
 24PRODUCTION_SERVERS="${PRODUCTION_SERVERS:-prod1.example.com,prod2.example.com,prod3.example.com}"
 25
 26# Colors
 27RED='\033[0;31m'
 28GREEN='\033[0;32m'
 29YELLOW='\033[1;33m'
 30BLUE='\033[0;34m'
 31CYAN='\033[0;36m'
 32BOLD='\033[1m'
 33NC='\033[0m'
 34
 35# ============================================================================
 36# Utility Functions
 37# ============================================================================
 38
 39log_info() {
 40    echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $*"
 41}
 42
 43log_success() {
 44    echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC}$*"
 45}
 46
 47log_error() {
 48    echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')]${NC}$*" >&2
 49}
 50
 51log_warning() {
 52    echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')]${NC}$*"
 53}
 54
 55log_section() {
 56    echo
 57    echo -e "${CYAN}${BOLD}==> $*${NC}"
 58}
 59
 60# ============================================================================
 61# Configuration Loading
 62# ============================================================================
 63
 64load_config() {
 65    local config_file="$SCRIPT_DIR/deploy.conf"
 66
 67    if [[ -f "$config_file" ]]; then
 68        log_info "Loading configuration from $config_file"
 69        # shellcheck disable=SC1090
 70        source "$config_file"
 71    fi
 72}
 73
 74get_servers() {
 75    local env=$1
 76
 77    case "$env" in
 78        staging)
 79            echo "$STAGING_SERVERS"
 80            ;;
 81        production)
 82            echo "$PRODUCTION_SERVERS"
 83            ;;
 84        *)
 85            log_error "Invalid environment: $env"
 86            exit 1
 87            ;;
 88    esac
 89}
 90
 91# ============================================================================
 92# Remote Execution Functions
 93# ============================================================================
 94
 95remote_exec() {
 96    local server=$1
 97    shift
 98    local command="$*"
 99
100    ssh -o StrictHostKeyChecking=no \
101        -o ConnectTimeout=10 \
102        "${DEPLOY_USER}@${server}" \
103        "$command"
104}
105
106sync_files() {
107    local server=$1
108    local source=$2
109    local dest=$3
110
111    rsync -avz --delete \
112        -e "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10" \
113        "$source" \
114        "${DEPLOY_USER}@${server}:${dest}"
115}
116
117# ============================================================================
118# Deployment Functions
119# ============================================================================
120
121prepare_release() {
122    local release_name
123    release_name=$(date +%Y%m%d-%H%M%S)
124    echo "$release_name"
125}
126
127create_release_structure() {
128    local server=$1
129    local release=$2
130
131    log_info "Creating release structure on $server"
132
133    remote_exec "$server" "
134        mkdir -p ${DEPLOY_PATH}/${APP_NAME}/{releases,shared}
135        mkdir -p ${DEPLOY_PATH}/${APP_NAME}/releases/${release}
136    "
137}
138
139upload_release() {
140    local server=$1
141    local release=$2
142    local source_dir=$3
143
144    log_info "Uploading release to $server"
145
146    # In real scenario, this would upload actual built artifacts
147    # For demo, we'll create a simple structure
148    remote_exec "$server" "
149        cat > ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/app.sh << 'EOF'
150#!/bin/bash
151echo 'Application version: ${release}'
152echo 'Server: \$(hostname)'
153echo 'Status: Running'
154EOF
155        chmod +x ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/app.sh
156    "
157}
158
159link_shared_resources() {
160    local server=$1
161    local release=$2
162
163    log_info "Linking shared resources on $server"
164
165    remote_exec "$server" "
166        # Create shared directories if they don't exist
167        mkdir -p ${DEPLOY_PATH}/${APP_NAME}/shared/{config,logs,data}
168
169        # Link shared resources into release
170        ln -sf ${DEPLOY_PATH}/${APP_NAME}/shared/config \
171               ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/config
172        ln -sf ${DEPLOY_PATH}/${APP_NAME}/shared/logs \
173               ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/logs
174        ln -sf ${DEPLOY_PATH}/${APP_NAME}/shared/data \
175               ${DEPLOY_PATH}/${APP_NAME}/releases/${release}/data
176    "
177}
178
179switch_current_release() {
180    local server=$1
181    local release=$2
182
183    log_info "Switching current release on $server"
184
185    remote_exec "$server" "
186        ln -sfn ${DEPLOY_PATH}/${APP_NAME}/releases/${release} \
187                ${DEPLOY_PATH}/${APP_NAME}/current
188    "
189}
190
191health_check() {
192    local server=$1
193
194    log_info "Running health check on $server"
195
196    # In real scenario, this would check HTTP endpoint
197    # For demo, we'll check if the symlink exists and app runs
198    if remote_exec "$server" "
199        [[ -L ${DEPLOY_PATH}/${APP_NAME}/current ]] && \
200        ${DEPLOY_PATH}/${APP_NAME}/current/app.sh > /dev/null 2>&1
201    "; then
202        log_success "Health check passed on $server"
203        return 0
204    else
205        log_error "Health check failed on $server"
206        return 1
207    fi
208}
209
210cleanup_old_releases() {
211    local server=$1
212
213    log_info "Cleaning up old releases on $server"
214
215    remote_exec "$server" "
216        cd ${DEPLOY_PATH}/${APP_NAME}/releases
217        ls -t | tail -n +$((KEEP_RELEASES + 1)) | xargs -r rm -rf
218    "
219}
220
221# ============================================================================
222# Deployment Commands
223# ============================================================================
224
225deploy() {
226    local env=$1
227    local servers
228
229    IFS=',' read -ra servers <<< "$(get_servers "$env")"
230
231    log_section "Starting deployment to $env environment"
232    log_info "Servers: ${servers[*]}"
233
234    local release
235    release=$(prepare_release)
236    log_info "Release: $release"
237
238    # Deploy to each server in rolling fashion
239    for server in "${servers[@]}"; do
240        log_section "Deploying to $server"
241
242        # Create structure
243        create_release_structure "$server" "$release"
244
245        # Upload files
246        upload_release "$server" "$release" "."
247
248        # Link shared resources
249        link_shared_resources "$server" "$release"
250
251        # Switch to new release
252        switch_current_release "$server" "$release"
253
254        # Health check
255        if ! health_check "$server"; then
256            log_error "Deployment failed on $server"
257            log_warning "Consider rolling back"
258            exit 1
259        fi
260
261        # Cleanup old releases
262        cleanup_old_releases "$server"
263
264        log_success "Deployment successful on $server"
265
266        # Wait between servers in production for rolling deployment
267        if [[ "$env" == "production" ]] && [[ "$server" != "${servers[-1]}" ]]; then
268            log_info "Waiting 10 seconds before next server..."
269            sleep 10
270        fi
271    done
272
273    log_section "Deployment Complete"
274    log_success "Successfully deployed $release to $env"
275}
276
277rollback() {
278    local env=$1
279    local servers
280
281    IFS=',' read -ra servers <<< "$(get_servers "$env")"
282
283    log_section "Starting rollback on $env environment"
284
285    for server in "${servers[@]}"; do
286        log_info "Rolling back on $server"
287
288        # Get previous release
289        local previous_release
290        previous_release=$(remote_exec "$server" "
291            cd ${DEPLOY_PATH}/${APP_NAME}/releases
292            current=\$(readlink ${DEPLOY_PATH}/${APP_NAME}/current | xargs basename)
293            ls -t | grep -v \"\$current\" | head -n1
294        ")
295
296        if [[ -z "$previous_release" ]]; then
297            log_error "No previous release found on $server"
298            continue
299        fi
300
301        log_info "Previous release: $previous_release"
302
303        # Switch to previous release
304        switch_current_release "$server" "$previous_release"
305
306        # Health check
307        if health_check "$server"; then
308            log_success "Rollback successful on $server"
309        else
310            log_error "Rollback failed on $server"
311        fi
312    done
313
314    log_section "Rollback Complete"
315}
316
317status() {
318    local env=$1
319    local servers
320
321    IFS=',' read -ra servers <<< "$(get_servers "$env")"
322
323    log_section "Status for $env environment"
324
325    for server in "${servers[@]}"; do
326        echo
327        echo -e "${BOLD}Server: $server${NC}"
328
329        # Get current release
330        local current
331        current=$(remote_exec "$server" "
332            if [[ -L ${DEPLOY_PATH}/${APP_NAME}/current ]]; then
333                readlink ${DEPLOY_PATH}/${APP_NAME}/current | xargs basename
334            else
335                echo 'not deployed'
336            fi
337        ")
338
339        echo "  Current release: $current"
340
341        # List available releases
342        echo "  Available releases:"
343        remote_exec "$server" "
344            cd ${DEPLOY_PATH}/${APP_NAME}/releases 2>/dev/null && ls -t | head -n5 || echo '    (none)'
345        " | sed 's/^/    /'
346
347        # Health check
348        if health_check "$server" > /dev/null 2>&1; then
349            echo -e "  Health: ${GREEN}OK${NC}"
350        else
351            echo -e "  Health: ${RED}FAILED${NC}"
352        fi
353    done
354
355    echo
356}
357
358# ============================================================================
359# Usage
360# ============================================================================
361
362show_usage() {
363    cat << EOF
364${BOLD}Deployment Automation Script${NC}
365
366${BOLD}Usage:${NC}
367  $0 <command> [options]
368
369${BOLD}Commands:${NC}
370  deploy   --env <staging|production>    Deploy new release
371  rollback --env <staging|production>    Rollback to previous release
372  status   --env <staging|production>    Show deployment status
373
374${BOLD}Examples:${NC}
375  $0 deploy --env staging
376  $0 deploy --env production
377  $0 rollback --env production
378  $0 status --env staging
379
380EOF
381}
382
383# ============================================================================
384# Main Entry Point
385# ============================================================================
386
387main() {
388    load_config
389
390    local command="${1:-}"
391    local env="staging"
392
393    # Parse arguments
394    shift || true
395    while [[ $# -gt 0 ]]; do
396        case "$1" in
397            --env)
398                env="$2"
399                shift 2
400                ;;
401            *)
402                log_error "Unknown option: $1"
403                show_usage
404                exit 1
405                ;;
406        esac
407    done
408
409    # Validate environment
410    if [[ "$env" != "staging" ]] && [[ "$env" != "production" ]]; then
411        log_error "Invalid environment: $env"
412        show_usage
413        exit 1
414    fi
415
416    # Execute command
417    case "$command" in
418        deploy)
419            deploy "$env"
420            ;;
421        rollback)
422            rollback "$env"
423            ;;
424        status)
425            status "$env"
426            ;;
427        *)
428            log_error "Unknown command: $command"
429            show_usage
430            exit 1
431            ;;
432    esac
433}
434
435main "$@"