#!/usr/bin/env bash # blue-green-deploy.sh — swap the active soul slot on GKE # # Usage: # blue-green-deploy.sh --image --slot # # What it does: # 1. Sets the new image on the target slot deployment # 2. Scales the target slot to 1 replica and waits for it to become ready # 3. Patches the neuron-mcp Service selector to point at the new slot # 4. Scales the old slot down to 0 replicas # # This script uses kubectl imperatively for the live traffic swap. # After a successful swap, Argo CD manifests should be updated to reflect # the new active slot (so Argo CD doesn't revert the replica counts on next sync). # # Prerequisites: # - kubectl configured with access to neuron-platform cluster # - Sufficient RBAC: get/patch/scale Deployments and Services in neuron-prod # # Examples: # blue-green-deploy.sh --image us-central1-docker.pkg.dev/neuron-785695/neuron-api/neuron-soul:abc12345 --slot green # blue-green-deploy.sh --image us-central1-docker.pkg.dev/neuron-785695/neuron-api/neuron-soul:latest --slot blue set -euo pipefail NAMESPACE="neuron-prod" APP="neuron-mcp" SERVICE="neuron-mcp" IMAGE="" NEW_SLOT="" usage() { echo "Usage: $0 --image --slot " echo "" echo " --image Full image URI including tag" echo " --slot Target slot to deploy to: blue or green" exit 1 } while [[ $# -gt 0 ]]; do case $1 in --image) IMAGE="$2"; shift 2 ;; --slot) NEW_SLOT="$2"; shift 2 ;; -h|--help) usage ;; *) echo "Unknown argument: $1" usage ;; esac done [[ -z "$IMAGE" ]] && { echo "ERROR: --image is required"; usage; } [[ -z "$NEW_SLOT" ]] && { echo "ERROR: --slot is required"; usage; } [[ "$NEW_SLOT" != "blue" && "$NEW_SLOT" != "green" ]] && { echo "ERROR: --slot must be 'blue' or 'green'"; exit 1 } # Determine the old (currently active) slot if [[ "$NEW_SLOT" == "blue" ]]; then OLD_SLOT="green" else OLD_SLOT="blue" fi NEW_DEPLOYMENT="${APP}-${NEW_SLOT}" OLD_DEPLOYMENT="${APP}-${OLD_SLOT}" echo "==> Deploying to slot: ${NEW_SLOT}" echo " Image: ${IMAGE}" echo " New deploy: ${NEW_DEPLOYMENT}" echo " Old deploy: ${OLD_DEPLOYMENT}" echo " Namespace: ${NAMESPACE}" echo "" # Step 1: Update the image on the new slot echo "[1/4] Setting image on ${NEW_DEPLOYMENT}..." kubectl set image deployment/"${NEW_DEPLOYMENT}" \ soul="${IMAGE}" \ --namespace="${NAMESPACE}" # Step 2: Scale the new slot up (it may already be at 0) echo "[2/4] Scaling ${NEW_DEPLOYMENT} to 1 replica..." kubectl scale deployment/"${NEW_DEPLOYMENT}" \ --replicas=1 \ --namespace="${NAMESPACE}" # Wait for rollout to complete (pod ready) echo " Waiting for rollout to complete (timeout: 5m)..." kubectl rollout status deployment/"${NEW_DEPLOYMENT}" \ --namespace="${NAMESPACE}" \ --timeout=5m echo " ${NEW_DEPLOYMENT} is ready." # Step 3: Patch the service selector to point at the new slot echo "[3/4] Flipping service selector to slot=${NEW_SLOT}..." kubectl patch service "${SERVICE}" \ --namespace="${NAMESPACE}" \ --type=merge \ --patch="{\"spec\":{\"selector\":{\"app\":\"${APP}\",\"slot\":\"${NEW_SLOT}\"}}}" echo " Traffic now routing to ${NEW_SLOT}." # Step 4: Scale down the old slot echo "[4/4] Scaling ${OLD_DEPLOYMENT} down to 0 replicas..." kubectl scale deployment/"${OLD_DEPLOYMENT}" \ --replicas=0 \ --namespace="${NAMESPACE}" echo "" echo "==> Blue-green swap complete." echo " Active slot: ${NEW_SLOT} (${IMAGE})" echo " Idle slot: ${OLD_SLOT} (scaled to 0)" echo "" echo "NOTE: Update Argo CD manifests to reflect new state:" echo " - deployment-${NEW_SLOT}.yaml: replicas: 1, image: ${IMAGE}" echo " - deployment-${OLD_SLOT}.yaml: replicas: 0"