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.
This commit is contained in:
parent
a58a0e642a
commit
30ce126a07
19 changed files with 1166 additions and 591 deletions
377
server/scripts/manage.sh
Executable file
377
server/scripts/manage.sh
Executable file
|
|
@ -0,0 +1,377 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Manage deployment releases
|
||||
# Usage: ./manage.sh [--dry-run] COMMAND [ARGS]
|
||||
|
||||
# Source common functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common-lib.sh"
|
||||
|
||||
# Global variables
|
||||
DRY_RUN=false
|
||||
COMMAND=""
|
||||
ARGS=()
|
||||
|
||||
# Parse global options
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo "Error: Unknown option: $1"
|
||||
echo "Usage: $0 [--dry-run] COMMAND [ARGS]"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
# First non-option argument is the command
|
||||
if [ -z "$COMMAND" ]; then
|
||||
COMMAND="$1"
|
||||
else
|
||||
# Rest are command arguments
|
||||
ARGS+=("$1")
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$COMMAND" ]; then
|
||||
echo "Error: No command specified"
|
||||
echo "Usage: $0 [--dry-run] COMMAND [ARGS]"
|
||||
echo "Commands:"
|
||||
indent_output echo "status - Show deployment status"
|
||||
indent_output echo "list - List all releases"
|
||||
indent_output echo "version - Show current release"
|
||||
indent_output echo "switch VERSION - Switch to a specific release version"
|
||||
indent_output echo "cleanup [KEEP] - Clean up old releases (default: keep 5)"
|
||||
echo ""
|
||||
echo "Global options:"
|
||||
indent_output echo "--dry-run - Show what would happen without making changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_PROJECT_PATH="$(realpath "${SCRIPT_DIR}/../../../")"
|
||||
CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current"
|
||||
RELEASES_PATH="${REPO_PROJECT_PATH}/releases"
|
||||
|
||||
# Announce dry-run mode if active
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "🔍 DRY RUN MODE - No changes will be made"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
case "$COMMAND" in
|
||||
status)
|
||||
echo "🔍 Deployment Status"
|
||||
echo "===================="
|
||||
|
||||
# Check if deployment is downgraded
|
||||
if [ -d "$RELEASES_PATH" ] && [ -L "$CURRENT_LINK_PATH" ]; then
|
||||
CURRENT_RELEASE_DIR_NAME=$(basename "$(readlink -f "$CURRENT_LINK_PATH")")
|
||||
|
||||
# Find the latest release by modification time.
|
||||
LATEST_RELEASE_DIR_NAME=$(find "$RELEASES_PATH" -maxdepth 1 -mindepth 1 -type d -printf '%T@ %f\n' | sort -nr | head -n 1 | cut -d' ' -f2-)
|
||||
|
||||
if [ -n "$LATEST_RELEASE_DIR_NAME" ]; then
|
||||
if [ "$CURRENT_RELEASE_DIR_NAME" == "$LATEST_RELEASE_DIR_NAME" ]; then
|
||||
echo "✅ Deployment is on the latest release (${LATEST_RELEASE_DIR_NAME})."
|
||||
else
|
||||
echo "⚠️ Deployment is downgraded."
|
||||
indent_output echo "Current: ${CURRENT_RELEASE_DIR_NAME}"
|
||||
indent_output echo "Latest: ${LATEST_RELEASE_DIR_NAME}"
|
||||
fi
|
||||
else
|
||||
# This case happens if RELEASES_PATH is empty
|
||||
echo "ℹ️ No releases found in ${RELEASES_PATH}."
|
||||
fi
|
||||
elif [ ! -L "$CURRENT_LINK_PATH" ]; then
|
||||
echo "ℹ️ No current deployment symlink found."
|
||||
else # RELEASES_PATH does not exist
|
||||
echo "ℹ️ Releases directory not found at ${RELEASES_PATH}."
|
||||
fi
|
||||
echo "" # Add a newline for spacing
|
||||
|
||||
# Get current state
|
||||
CURRENT_STATE=$(get_deployment_state)
|
||||
|
||||
if [ "$CURRENT_STATE" = "both" ]; then
|
||||
echo "🟡 Deployment State: both"
|
||||
elif [ "$CURRENT_STATE" = "blue" ]; then
|
||||
echo "🔵 Deployment State: blue"
|
||||
elif [ "$CURRENT_STATE" = "green" ]; then
|
||||
echo "🟢 Deployment State: green"
|
||||
else
|
||||
indent_output echo "Deployment State: none"
|
||||
fi
|
||||
|
||||
echo "⚙️ Core Containers:"
|
||||
indent_output docker ps --filter 'label=deployment.core=true' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}'
|
||||
|
||||
# Show containers by color with image info
|
||||
echo "🔵 Blue Containers:"
|
||||
BLUE_COUNT=$(count_containers "--filter label=deployment.color=blue")
|
||||
# make sure BLUE_COUNT is a number
|
||||
BLUE_COUNT=$(echo "$BLUE_COUNT" | tr -d '\n')
|
||||
if [ "$BLUE_COUNT" -gt 0 ]; then
|
||||
BLUE_IMAGE=$(get_deployment_image_tag "blue")
|
||||
indent_output echo "Image: ${BLUE_IMAGE}"
|
||||
indent_output docker ps --filter 'label=deployment.color=blue' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}'
|
||||
else
|
||||
indent_output echo "No blue containers running"
|
||||
fi
|
||||
|
||||
echo "🟢 Green Containers:"
|
||||
GREEN_COUNT=$(count_containers "--filter label=deployment.color=green")
|
||||
if [ "$GREEN_COUNT" -gt 0 ]; then
|
||||
GREEN_IMAGE=$(get_deployment_image_tag "green")
|
||||
indent_output echo "Image: ${GREEN_IMAGE}"
|
||||
indent_output docker ps --filter 'label=deployment.color=green' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}'
|
||||
else
|
||||
indent_output echo "No green containers running"
|
||||
fi
|
||||
|
||||
list_releases "${REPO_PROJECT_PATH}"
|
||||
|
||||
# Health check summary
|
||||
echo "❤️ Health Check Summary:"
|
||||
case "$CURRENT_STATE" in
|
||||
"both")
|
||||
indent_output echo "⚠️ WARNING: Both blue and green containers are running!"
|
||||
indent_output echo "This might indicate an incomplete deployment."
|
||||
;;
|
||||
"none")
|
||||
indent_output echo "⚠️ WARNING: No web containers are running!"
|
||||
;;
|
||||
*)
|
||||
if [ "$CURRENT_STATE" = "blue" ]; then
|
||||
indent_output echo "🔵 System is running on blue deployment"
|
||||
else
|
||||
indent_output echo "🟢 System is running on green deployment"
|
||||
fi
|
||||
indent_output echo "❤️ Overall Healthcheck:"
|
||||
indent_output " " get_health_check_status
|
||||
;;
|
||||
esac
|
||||
|
||||
# Show resource usage
|
||||
echo "📊 Resource Usage:"
|
||||
indent_output docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}\t{{.NetIO}}'
|
||||
|
||||
# Show deployment images
|
||||
echo "📦 Deployment Images:"
|
||||
indent_output docker images 'badbl0cks/pkmntrade-club' --format 'table {{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}' | grep -E '^TAG|sha-.{7} ' || indent_output echo "No deployment images found"
|
||||
;;
|
||||
|
||||
list)
|
||||
list_releases "${REPO_PROJECT_PATH}"
|
||||
;;
|
||||
|
||||
version)
|
||||
if [ -L "$CURRENT_LINK_PATH" ]; then
|
||||
current_version=$(basename "$(readlink -f "$CURRENT_LINK_PATH")")
|
||||
echo "📌 Current version: ${current_version}"
|
||||
else
|
||||
echo "❌ No current deployment found"
|
||||
fi
|
||||
;;
|
||||
|
||||
switch)
|
||||
if [ ${#ARGS[@]} -lt 1 ]; then
|
||||
echo "Error: VERSION required for switch"
|
||||
echo "Usage: $0 [--dry-run] switch VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_VERSION="${ARGS[0]}"
|
||||
TARGET_PATH="${RELEASES_PATH}/${TARGET_VERSION}"
|
||||
|
||||
# Validate target version exists
|
||||
if [ ! -d "$TARGET_PATH" ]; then
|
||||
echo "❌ Error: Version ${TARGET_VERSION} not found"
|
||||
echo "Available releases:"
|
||||
list_releases "${REPO_PROJECT_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current version if exists
|
||||
CURRENT_VERSION="none"
|
||||
CURRENT_VERSION_PATH=""
|
||||
if [ -L "$CURRENT_LINK_PATH" ]; then
|
||||
CURRENT_VERSION_PATH=$(readlink -f "$CURRENT_LINK_PATH")
|
||||
CURRENT_VERSION=$(basename "$CURRENT_VERSION_PATH")
|
||||
fi
|
||||
|
||||
# Edge case: trying to switch to the same version
|
||||
if [ "$CURRENT_VERSION" == "$TARGET_VERSION" ]; then
|
||||
echo "✅ Already on version ${TARGET_VERSION}. No action taken."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CURRENT_COLOR=$(get_current_color)
|
||||
NEW_COLOR=$(switch_color "$CURRENT_COLOR")
|
||||
|
||||
echo "🔄 Switch Plan:"
|
||||
indent_output echo "Current version: ${CURRENT_VERSION}"
|
||||
if [ "$CURRENT_VERSION" != "none" ]; then
|
||||
indent_output echo "Current path: ${CURRENT_VERSION_PATH}"
|
||||
indent_output echo "Current color: ${CURRENT_COLOR}"
|
||||
fi
|
||||
indent_output echo "Target version: ${TARGET_VERSION}"
|
||||
indent_output echo "Target path: ${TARGET_PATH}"
|
||||
indent_output echo "Target color: ${NEW_COLOR}"
|
||||
|
||||
# Verify target release has necessary files
|
||||
echo "📋 Checking target release integrity..."
|
||||
MISSING_FILES=()
|
||||
for file in "docker-compose_web.yml" "docker-compose_core.yml" ".env"; do
|
||||
if [ ! -f "${TARGET_PATH}/${file}" ]; then
|
||||
MISSING_FILES+=("$file")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MISSING_FILES[@]} -gt 0 ]; then
|
||||
echo "❌ Error: Target release is missing required files:"
|
||||
printf " - %s\n" "${MISSING_FILES[@]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get compose files based on environment
|
||||
COMPOSE_FILES=$(get_compose_files)
|
||||
|
||||
echo "🛑 Stopping current containers..."
|
||||
if [ -d "$CURRENT_VERSION_PATH" ]; then
|
||||
(
|
||||
cd "$CURRENT_VERSION_PATH" || exit 1
|
||||
WEB_PROJECT_NAME=$(get_project_name "$CURRENT_COLOR")
|
||||
|
||||
indent_output echo "Stopping web containers for project: ${WEB_PROJECT_NAME}..."
|
||||
execute_or_warn "stop web containers" docker compose ${COMPOSE_FILES} -p "${WEB_PROJECT_NAME}" down
|
||||
|
||||
indent_output echo "Stopping core services for project: ${CORE_PROJECT_NAME}..."
|
||||
execute_or_warn "stop core services" docker compose -f "docker-compose_core.yml" -p "${CORE_PROJECT_NAME}" down
|
||||
)
|
||||
else
|
||||
indent_output echo "No current deployment to stop"
|
||||
fi
|
||||
|
||||
echo "📝 Updating deployment metadata for ${TARGET_VERSION}..."
|
||||
execute_or_fail "update .deployment_color to ${NEW_COLOR}" \
|
||||
bash -c "echo '${NEW_COLOR}' > '${TARGET_PATH}/.deployment_color'"
|
||||
|
||||
execute_or_fail "update DEPLOYMENT_COLOR in .env" \
|
||||
bash -c "sed -i 's/^DEPLOYMENT_COLOR=.*/DEPLOYMENT_COLOR=${NEW_COLOR}/' '${TARGET_PATH}/.env'"
|
||||
|
||||
# Update symlink
|
||||
echo "🔗 Updating deployment symlink..."
|
||||
execute_or_fail "update symlink from $CURRENT_LINK_PATH to $TARGET_PATH" \
|
||||
ln -sfn "$TARGET_PATH" "$CURRENT_LINK_PATH"
|
||||
|
||||
# Start containers
|
||||
echo "🚀 Starting containers from ${TARGET_VERSION}..."
|
||||
(
|
||||
cd "$TARGET_PATH" || exit 1
|
||||
|
||||
TARGET_WEB_PROJECT_NAME=$(get_project_name "$NEW_COLOR")
|
||||
|
||||
indent_output echo "Starting core services for project: ${CORE_PROJECT_NAME}..."
|
||||
execute_or_fail "start core services" \
|
||||
docker compose -f "docker-compose_core.yml" -p "${CORE_PROJECT_NAME}" up -d
|
||||
|
||||
indent_output echo "Starting web containers for project: ${TARGET_WEB_PROJECT_NAME}..."
|
||||
execute_or_fail "start web containers" \
|
||||
docker compose ${COMPOSE_FILES} -p "${TARGET_WEB_PROJECT_NAME}" up -d
|
||||
)
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo ""
|
||||
echo "✅ Dry run completed - no changes made"
|
||||
else
|
||||
echo "✅ Switch completed to version: ${TARGET_VERSION}"
|
||||
echo "Run '$0 status' to verify deployment health"
|
||||
fi
|
||||
;;
|
||||
|
||||
cleanup)
|
||||
# Parse cleanup arguments
|
||||
KEEP_COUNT=5
|
||||
for arg in "${ARGS[@]}"; do
|
||||
if [[ "$arg" =~ ^[0-9]+$ ]]; then
|
||||
KEEP_COUNT="$arg"
|
||||
else
|
||||
echo "Error: Invalid argument for cleanup: $arg"
|
||||
echo "Usage: $0 [--dry-run] cleanup [KEEP_COUNT]"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "🗑️ Cleaning up old releases (keeping last ${KEEP_COUNT} and current)"
|
||||
|
||||
if [ ! -L "$CURRENT_LINK_PATH" ]; then
|
||||
echo "❌ No current deployment symlink found. Aborting cleanup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_RELEASE_DIR_NAME=$(basename "$(readlink -f "$CURRENT_LINK_PATH")")
|
||||
echo "📌 Current release: ${CURRENT_RELEASE_DIR_NAME}"
|
||||
|
||||
if [ -d "$RELEASES_PATH" ]; then
|
||||
cd "$RELEASES_PATH"
|
||||
|
||||
# Get a list of inactive release directories, sorted by modification time (newest first).
|
||||
INACTIVE_RELEASES=$(find . -maxdepth 1 -mindepth 1 -type d \
|
||||
-not -name "$CURRENT_RELEASE_DIR_NAME" \
|
||||
-printf '%T@ %f\n' | sort -nr | cut -d' ' -f2-)
|
||||
|
||||
if [ -z "$INACTIVE_RELEASES" ]; then
|
||||
echo "No inactive releases found to clean up."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count total inactive releases
|
||||
TOTAL_INACTIVE=$(echo "$INACTIVE_RELEASES" | wc -l | xargs)
|
||||
echo "📊 Found ${TOTAL_INACTIVE} inactive release(s)"
|
||||
|
||||
# Identify releases to delete by skipping the KEEP_COUNT newest ones.
|
||||
RELEASES_TO_DELETE=$(echo "$INACTIVE_RELEASES" | tail -n +$((KEEP_COUNT + 1)))
|
||||
|
||||
if [ -n "$RELEASES_TO_DELETE" ]; then
|
||||
DELETE_COUNT=$(echo "$RELEASES_TO_DELETE" | wc -l | xargs)
|
||||
echo "🗑️ The following ${DELETE_COUNT} old release(s) will be deleted:"
|
||||
|
||||
# Show releases with their sizes
|
||||
while IFS= read -r release; do
|
||||
if [ -d "$release" ]; then
|
||||
SIZE=$(du -sh "$release" 2>/dev/null | cut -f1)
|
||||
indent_output echo "- $release (Size: $SIZE)"
|
||||
fi
|
||||
done <<< "$RELEASES_TO_DELETE"
|
||||
|
||||
# Delete the releases
|
||||
echo ""
|
||||
while IFS= read -r release; do
|
||||
execute_if_not_dry "delete release $release" rm -rf "$release"
|
||||
done <<< "$RELEASES_TO_DELETE"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo ""
|
||||
echo "✅ Dry run completed - no releases were deleted"
|
||||
else
|
||||
echo "✅ Cleanup completed - deleted ${DELETE_COUNT} release(s)"
|
||||
fi
|
||||
else
|
||||
KEPT_COUNT=$(echo "$INACTIVE_RELEASES" | wc -l | tr -d '\n')
|
||||
echo "No old releases to delete. Found ${KEPT_COUNT} inactive release(s), which is within the retention count of ${KEEP_COUNT}."
|
||||
fi
|
||||
else
|
||||
echo "No releases directory found"
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Error: Unknown command: $COMMAND"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Loading…
Add table
Add a link
Reference in a new issue