pkmntrade.club/scripts/deploy-blue-green.sh
badbl0cks 30ce126a07
feat(deploy): implement blue-green deployment strategy
This commit replaces the previous deployment mechanism with a blue-green strategy to lay the groundwork for zero-downtime deployments.
Key changes:
Introduces a deploy-blue-green.sh script to manage "blue" and "green" container sets, creating versioned releases.
Updates the Anubis gatekeeper template to dynamically route traffic based on the active deployment color, allowing for seamless traffic switching.
Modifies Docker Compose files to include color-specific labels and environment variables.
Adapts the GitHub Actions workflow to execute the new blue-green deployment process.
Removes the old, now-obsolete deployment and health check scripts.
Note: Automated rollback on health check failure is not yet implemented. Downgrades can be performed manually by switching the active color.
2025-06-12 16:58:55 -07:00

207 lines
No EOL
8.6 KiB
Bash
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
set -euo pipefail
# Blue-Green deployment script with versioned releases
# Usage: ./deploy-blue-green.sh
# Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/common-lib.sh"
# Validate required environment variables
require_var "DOCKER_HOST"
require_var "REPO_PROJECT_PATH"
require_var "REPO_NAME_ONLY"
require_var "REPO"
require_var "IMAGE_TAR"
require_var "ENV_FILE_BASE64"
require_var "CF_PEM_CERT"
require_var "CF_PEM_CA"
require_var "PROD"
require_var "PRODUCTION_DOMAIN"
require_var "STAGING_DOMAIN"
require_var "REPLICA_COUNT"
validate_deployment_env
echo "⚙️ Docker host: $DOCKER_HOST"
# Generate deployment timestamp
DEPLOYMENT_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
NEW_RELEASE_PATH="${RELEASES_PATH}/${DEPLOYMENT_TIMESTAMP}"
# Use Git SHA for image tag (if available, otherwise use timestamp)
if [ -n "${GIT_SHA:-}" ]; then
IMAGE_TAG="sha-${GIT_SHA:0:7}"
else
# Fallback for local testing without GIT_SHA
IMAGE_TAG="local-${DEPLOYMENT_TIMESTAMP}"
fi
# Check for deployment in progress
if is_deployment_in_progress; then
echo "⚠️ ERROR: Deployment appears to be in progress (both colors are running)"
echo " This might indicate a previous deployment didn't complete properly."
echo " Please check the deployment status and clean up any old containers."
echo " If you are sure that the deployment is complete, you can run the following command to clean up the old containers:"
echo " ssh deploy 'docker compose -p pkmntrade-club-blue down && docker compose -p pkmntrade-club-green down'"
exit 1
fi
# Get current and new colors
CURRENT_COLOR=$(get_current_color)
NEW_COLOR=$(switch_color "$CURRENT_COLOR")
echo "📅 Deployment version: ${DEPLOYMENT_TIMESTAMP}"
echo "🏷️ Image tag: ${IMAGE_TAG}"
echo "🎨 Current: $CURRENT_COLOR → New: $NEW_COLOR"
echo "🚀 Enable and start docker service"
retry run_on_target "sudo systemctl enable --now docker.service"
echo "💾 Load the new docker image ($IMAGE_TAR)"
if [ ! -f "$IMAGE_TAR" ]; then
echo "Error: Docker image tar file not found: $IMAGE_TAR"
exit 1
fi
# Load the image - Docker handles the transfer via DOCKER_HOST
echo "📦 Loading Docker image..."
#retry docker load -i "$IMAGE_TAR"
# Verify the expected image exists
echo "🔍 Verifying image ${REPO}:${IMAGE_TAG} exists..."
if ! docker images -q "${REPO}:${IMAGE_TAG}" | grep -q .; then
echo "❌ Expected image tag ${IMAGE_TAG} not found!"
echo "Available tags:"
docker images "${REPO}" --format "{{.Tag}}"
exit 1
fi
echo "📁 Create versioned release directory"
run_on_target "mkdir -p '${NEW_RELEASE_PATH}'"
echo "💾 Copy new files to server"
if [ -d "./server" ]; then
retry scp -pr ./server/* "deploy:${NEW_RELEASE_PATH}/"
else
echo "⚠️ No server directory found, error"
exit 1
fi
echo "📝 Create new .env file with deployment configuration"
printf "%s" "${ENV_FILE_BASE64}" | base64 -d | run_on_target "cat > '${NEW_RELEASE_PATH}/.env' && chmod 600 '${NEW_RELEASE_PATH}/.env'"
# Add deployment color and image tag to .env
run_on_target "echo 'DEPLOYMENT_COLOR=${NEW_COLOR}' >> '${NEW_RELEASE_PATH}/.env'"
run_on_target "echo 'IMAGE_TAG=${IMAGE_TAG}' >> '${NEW_RELEASE_PATH}/.env'"
# Add domain name based on environment
if [ "${PROD}" = "true" ]; then
DOMAIN_NAME="${PRODUCTION_DOMAIN:-pkmntrade.club}"
else
DOMAIN_NAME="${STAGING_DOMAIN:-staging.pkmntrade.club}"
fi
# if there is a third part to the domain name, remove it
BASE_DOMAIN_NAME="${BASE_DOMAIN:-pkmntrade.club}"
run_on_target "echo 'DOMAIN_NAME=${DOMAIN_NAME}' >> '${NEW_RELEASE_PATH}/.env'"
run_on_target "echo 'BASE_DOMAIN_NAME=${BASE_DOMAIN_NAME}' >> '${NEW_RELEASE_PATH}/.env'"
run_on_target "echo 'REPLICA_COUNT=${REPLICA_COUNT}' >> '${NEW_RELEASE_PATH}/.env'"
echo "🔑 Set up certs"
run_on_target "mkdir -p '${NEW_RELEASE_PATH}/certs' && chmod 550 '${NEW_RELEASE_PATH}/certs' && chown 99:root '${NEW_RELEASE_PATH}/certs'"
printf "%s" "$CF_PEM_CERT" | run_on_target "cat > '${NEW_RELEASE_PATH}/certs/crt.pem' && chmod 440 '${NEW_RELEASE_PATH}/certs/crt.pem' && chown 99:root '${NEW_RELEASE_PATH}/certs/crt.pem'"
printf "%s" "$CF_PEM_CA" | run_on_target "cat > '${NEW_RELEASE_PATH}/certs/ca.pem' && chmod 440 '${NEW_RELEASE_PATH}/certs/ca.pem' && chown 99:root '${NEW_RELEASE_PATH}/certs/ca.pem'"
echo "📝 Save deployment metadata"
run_on_target "echo '${DEPLOYMENT_TIMESTAMP}' > '${NEW_RELEASE_PATH}/.deployment_version'"
run_on_target "echo '${PROD}' > '${NEW_RELEASE_PATH}/.deployment_is_prod'"
run_on_target "echo '${NEW_COLOR}' > '${NEW_RELEASE_PATH}/.deployment_color'"
run_on_target "echo '${IMAGE_TAG}' > '${NEW_RELEASE_PATH}/.image_tag'"
run_on_target "echo '${GIT_SHA:-unknown}' > '${NEW_RELEASE_PATH}/.git_sha'"
# Save previous version info for potential rollback
run_on_target "if [ -L '${CURRENT_LINK_PATH}' ]; then readlink -f '${CURRENT_LINK_PATH}' > '${NEW_RELEASE_PATH}/.previous_version'; fi"
# export PREVIOUS_RELEASE_PATH
if [ "$CURRENT_COLOR" != "none" ]; then
PREVIOUS_RELEASE_PATH=$(run_on_target "cat ${NEW_RELEASE_PATH}/.previous_version")
else
PREVIOUS_RELEASE_PATH=""
fi
run_on_target "export PREVIOUS_RELEASE_PATH='${PREVIOUS_RELEASE_PATH}'"
echo "🔗 Update current symlink to new release"
run_on_target "ln -sfn '${NEW_RELEASE_PATH}' '${CURRENT_LINK_PATH}'"
# Get deployment configuration
PROJECT_NAME=$(get_project_name "$NEW_COLOR")
COMPOSE_FILES=$(get_compose_files)
WEB_SERVICE=$(get_web_service_name)
# create network if it doesn't exist
echo "🔗 Creating network ${PROJECT_NAME}_network"
run_on_target "docker network create ${REPO_NAME_ONLY}_network >/dev/null 2>&1 || true"
# Handle core services
if [ "$CURRENT_COLOR" = "none" ]; then
echo "🚀 Starting core services (first deployment)"
retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} up -d"
sleep 10 # Give core services time to start
else
echo " Core services already running, checking for changes..."
PREVIOUS_SHA1=$(run_on_target "sha1sum '${PREVIOUS_RELEASE_PATH}/docker-compose_core.yml' | awk '{print \$1}'")
NEW_SHA1=$(run_on_target "sha1sum '${NEW_RELEASE_PATH}/docker-compose_core.yml' | awk '{print \$1}'")
echo "PREV_SHA1: ${PREVIOUS_SHA1}"
echo " NEW_SHA1: ${NEW_SHA1}"
if [ -n "$PREVIOUS_SHA1" ] && [ -n "$NEW_SHA1" ]; then
if [ "$PREVIOUS_SHA1" != "$NEW_SHA1" ]; then
echo "🚀 Core services have changed, restarting..."
retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} down"
retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} up -d"
else
echo " Core services have not changed, still restarting due to current folder change..."
retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} down"
retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} up -d"
fi
else
echo "❌ Current or previous core services not found, exiting..."
exit 1
fi
fi
echo "🚀 Start new ${NEW_COLOR} containers with image ${IMAGE_TAG}"
retry run_on_target "cd '${CURRENT_LINK_PATH}' && DEPLOYMENT_COLOR=${NEW_COLOR} IMAGE_TAG=${IMAGE_TAG} docker compose $COMPOSE_FILES -p ${PROJECT_NAME} up -d"
# Wait for new containers to be healthy
if ! wait_for_healthy_containers "$PROJECT_NAME" "$WEB_SERVICE" "$REPLICA_COUNT"; then
echo "❌ New containers failed health checks. Cancelling deployment..."
run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose $COMPOSE_FILES -p ${PROJECT_NAME} down"
#echo "🔄 Rolling back deployment..."
#TODO: implement rollback
exit 1
fi
echo "✅ New ${NEW_COLOR} deployment is healthy"
# Refresh gatekeepers
refresh_gatekeepers
# Wait for traffic to stabilize
wait_with_countdown 20 "⏳ Waiting for traffic to stabilize..."
# Clean up old containers if this isn't the first deployment
if [ "$CURRENT_COLOR" != "none" ]; then
# Get the old image tag before cleanup
OLD_IMAGE_TAG=$(get_deployment_image_tag "$CURRENT_COLOR")
echo "📷 Old deployment was using image: ${OLD_IMAGE_TAG}"
cleanup_color_containers "$CURRENT_COLOR"
echo "✅ Old containers removed"
fi
echo "🗑️ Clean up old releases (keep last 5)"
run_on_target "cd '${RELEASES_PATH}' && ls -dt */ 2>/dev/null | tail -n +6 | xargs -r rm -rf || true"
echo "✅ Blue-Green deployment completed"
echo " Active color: ${NEW_COLOR}"
echo " Image tag: ${IMAGE_TAG}"