#!/bin/bash # Traffic switching script for blue-green deployment # Updates Traefik weighted routing configuration # # Usage: ./switch-traffic.sh [mode] # target_stack: blue or green (the stack to switch TO) # mode: instant (default) or gradual # # Gradual mode: 25% -> 50% -> 75% -> 100% with 3s intervals # # Exit codes: # 0 - Traffic switch successful # 1 - Switch failed set -euo pipefail # Use DEPLOY_PATH if set (CI environment), otherwise calculate from script location # This is critical: CI workflows must pass DEPLOY_PATH to ensure we modify # the actual deployment config, not the checkout directory PROJECT_ROOT="${DEPLOY_PATH:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" echo "Using PROJECT_ROOT: $PROJECT_ROOT" TARGET_STACK="${1:-}" MODE="${2:-instant}" if [[ -z "$TARGET_STACK" ]] || [[ ! "$TARGET_STACK" =~ ^(blue|green)$ ]]; then echo "Usage: $0 [instant|gradual]" exit 1 fi TRAEFIK_CONFIG="$PROJECT_ROOT/config/traefik/dynamic/blue-green.yml" STATE_FILE="$PROJECT_ROOT/config/deployment/state.json" if [[ ! -f "$TRAEFIK_CONFIG" ]]; then echo "ERROR: Traefik config not found: $TRAEFIK_CONFIG" exit 1 fi echo "========================================" echo "Traffic Switch" echo "Target Stack: $TARGET_STACK" echo "Mode: $MODE" echo "========================================" # Determine source and target weights if [[ "$TARGET_STACK" == "blue" ]]; then SOURCE_STACK="green" else SOURCE_STACK="blue" fi # Function to update weights in Traefik config update_weights() { local blue_weight="$1" local green_weight="$2" echo " Setting weights: blue=$blue_weight, green=$green_weight" # YAML structure has name and weight on separate lines: # - name: mvp-frontend-blue-svc # weight: 100 # Use awk for reliable multi-line pattern matching (more portable than sed) # Match "- name: service-name" to avoid matching the loadBalancer section awk -v blue="$blue_weight" -v green="$green_weight" ' /- name: mvp-frontend-blue-svc$/ {print; getline; sub(/weight: [0-9]+/, "weight: " blue); print; next} /- name: mvp-frontend-green-svc$/ {print; getline; sub(/weight: [0-9]+/, "weight: " green); print; next} /- name: mvp-backend-blue-svc$/ {print; getline; sub(/weight: [0-9]+/, "weight: " blue); print; next} /- name: mvp-backend-green-svc$/ {print; getline; sub(/weight: [0-9]+/, "weight: " green); print; next} {print} ' "$TRAEFIK_CONFIG" > "${TRAEFIK_CONFIG}.tmp" && mv "${TRAEFIK_CONFIG}.tmp" "$TRAEFIK_CONFIG" # Traefik watches the file and reloads automatically # Give it a moment to pick up changes sleep 1 } # Verify weights were actually written to config file verify_weights_applied() { local expected_blue="$1" local expected_green="$2" # Extract actual weights from the config file local actual_blue=$(grep -A1 "mvp-frontend-blue-svc" "$TRAEFIK_CONFIG" | grep weight | grep -oE '[0-9]+' | head -1) local actual_green=$(grep -A1 "mvp-frontend-green-svc" "$TRAEFIK_CONFIG" | grep weight | grep -oE '[0-9]+' | head -1) if [[ "$actual_blue" != "$expected_blue" ]] || [[ "$actual_green" != "$expected_green" ]]; then echo " ERROR: Weight verification failed!" echo " Expected: blue=$expected_blue, green=$expected_green" echo " Actual: blue=$actual_blue, green=$actual_green" echo " Config file: $TRAEFIK_CONFIG" return 1 fi echo " OK: Weights verified (blue=$actual_blue, green=$actual_green)" return 0 } # Verify Traefik has picked up the changes verify_traefik_reload() { # Give Traefik time to reload config sleep 2 # Check if Traefik is still healthy if docker exec mvp-traefik traefik healthcheck > /dev/null 2>&1; then echo " OK: Traefik config reloaded" return 0 else echo " WARNING: Could not verify Traefik health" return 0 # Don't fail on this, file watcher is reliable fi } if [[ "$MODE" == "gradual" ]]; then echo "" echo "Gradual traffic switch starting..." echo "----------------------------------------" if [[ "$TARGET_STACK" == "blue" ]]; then STEPS=("25 75" "50 50" "75 25" "100 0") else STEPS=("75 25" "50 50" "25 75" "0 100") fi step=1 for weights in "${STEPS[@]}"; do blue_w=$(echo "$weights" | cut -d' ' -f1) green_w=$(echo "$weights" | cut -d' ' -f2) echo "" echo "Step $step/4: blue=$blue_w%, green=$green_w%" update_weights "$blue_w" "$green_w" verify_traefik_reload if [[ $step -lt 4 ]]; then echo " Waiting 3 seconds before next step..." sleep 3 fi step=$((step + 1)) done else # Instant switch echo "" echo "Instant traffic switch..." echo "----------------------------------------" if [[ "$TARGET_STACK" == "blue" ]]; then update_weights 100 0 verify_weights_applied 100 0 || exit 1 else update_weights 0 100 verify_weights_applied 0 100 || exit 1 fi verify_traefik_reload fi # Update state file echo "" echo "Updating deployment state..." echo "----------------------------------------" if [[ -f "$STATE_FILE" ]] && command -v jq &> /dev/null; then TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") jq --arg active "$TARGET_STACK" \ --arg inactive "$SOURCE_STACK" \ --arg ts "$TIMESTAMP" \ '.active_stack = $active | .inactive_stack = $inactive | .last_switch = $ts' \ "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" echo " Active stack: $TARGET_STACK" echo " Inactive stack: $SOURCE_STACK" fi echo "" echo "========================================" echo "Traffic Switch COMPLETE" echo "All traffic now routed to: $TARGET_STACK" echo "========================================" exit 0