ci + ansible

This commit is contained in:
sangnn
2026-06-20 11:06:27 +07:00
parent a6d74d9316
commit 966578ed58
15 changed files with 637 additions and 30 deletions
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# Rolling deploy: drains each api container from nginx before replacing it,
# so users see zero downtime. Job workers are restarted last without draining.
#
# Usage:
# ./deploy.sh <image-tag> e.g. ./deploy.sh a1b2c3d
#
# Overridable via env vars (defaults below are gatehouse-api):
# IMAGE_NAME docker image name
# SVC1/SVC2 compose service names
# SVC1_PORT host port for SVC1
# SVC2_PORT host port for SVC2
# HEALTH_PATH HTTP path for health check
# NGINX_CONF path to nginx site config
# COMPOSE_DIR directory with docker-compose.yml and .env
#
# The runner user needs passwordless sudo for nginx:
# echo "runner ALL=(ALL) NOPASSWD: /usr/sbin/nginx, /bin/systemctl reload nginx" \
# | sudo tee /etc/sudoers.d/runner-nginx
set -euo pipefail
TAG="${1:?Usage: deploy.sh <image-tag> (e.g. deploy.sh a1b2c3d)}"
IMAGE_NAME="${IMAGE_NAME:-gatehouse-api}"
NGINX_CONF="${NGINX_CONF:-/etc/nginx/conf.d/gatehouse-api.conf}"
COMPOSE_DIR="${COMPOSE_DIR:-/opt/gatehouse-api}"
SVC1="${SVC1:-api1}"
SVC2="${SVC2:-api2}"
SVC1_PORT="${SVC1_PORT:-5000}"
SVC2_PORT="${SVC2_PORT:-5001}"
HEALTH_PATH="${HEALTH_PATH:-/api/health}"
HEALTH_RETRIES=18 # 18 × 5 s = 90 s max
HEALTH_INTERVAL=5
# ── helpers ───────────────────────────────────────────────────────────────────
log() { echo "[$(date '+%H:%M:%S')] $*"; }
die() { log "ERROR: $*" >&2; exit 1; }
step() { log ""; log "── $* ──"; }
health_check() {
local port=$1 label=$2 attempt=0
log "Waiting for ${label} on :${port}${HEALTH_PATH} ..."
while (( attempt < HEALTH_RETRIES )); do
if curl -sf "http://127.0.0.1:${port}${HEALTH_PATH}" -o /dev/null; then
log "${label} healthy"
return 0
fi
attempt=$(( attempt + 1 ))
log " [${attempt}/${HEALTH_RETRIES}] not ready, retrying in ${HEALTH_INTERVAL}s"
sleep "${HEALTH_INTERVAL}"
done
log "ERROR: ${label} failed health check after $((HEALTH_RETRIES * HEALTH_INTERVAL))s"
return 1
}
get_service_tag() {
docker compose ps -q "$1" 2>/dev/null \
| xargs -r docker inspect --format '{{.Config.Image}}' 2>/dev/null \
| cut -d: -f2
}
rollback() {
local service=$1 port=$2 old_tag=$3
if [[ -z "${old_tag}" ]]; then
nginx_restore "${port}"
die "Deploy aborted — ${service} failed health check, no previous tag to roll back to"
fi
log "Rolling back ${service} to ${old_tag}..."
IMAGE_TAG="${old_tag}" docker compose up -d --no-deps --force-recreate "${service}"
nginx_restore "${port}"
die "Deploy aborted — ${service} rolled back to ${old_tag}"
}
nginx_drain() {
local port=$1
sudo sed -i "s|server 127.0.0.1:${port};|server 127.0.0.1:${port} down;|" "$NGINX_CONF"
sudo nginx -t 2>&1 | tail -2
sudo nginx -s reload
log "nginx: drained :${port}"
}
nginx_restore() {
local port=$1
sudo sed -i "s|server 127.0.0.1:${port} down;|server 127.0.0.1:${port};|" "$NGINX_CONF"
sudo nginx -t 2>&1 | tail -2
sudo nginx -s reload
log "nginx: restored :${port}"
}
# ── pre-flight ────────────────────────────────────────────────────────────────
cd "${COMPOSE_DIR}"
pwd; ls -la
log "Deploying ${IMAGE_NAME}:${TAG}"
docker image inspect "${IMAGE_NAME}:${TAG}" > /dev/null 2>&1 \
|| die "Image ${IMAGE_NAME}:${TAG} not found locally — build it first."
# ── roll SVC1 ─────────────────────────────────────────────────────────────────
step "${SVC1}${TAG} (traffic: ${SVC2} only)"
old_svc1=$(get_service_tag "${SVC1}")
nginx_drain "${SVC1_PORT}"
log "Waiting 15s for in-flight requests to drain..."
sleep 15
IMAGE_TAG="${TAG}" docker compose up -d --no-deps --force-recreate "${SVC1}"
health_check "${SVC1_PORT}" "${SVC1}" || rollback "${SVC1}" "${SVC1_PORT}" "${old_svc1}"
nginx_restore "${SVC1_PORT}"
# ── roll SVC2 ─────────────────────────────────────────────────────────────────
step "${SVC2}${TAG} (traffic: ${SVC1} only)"
old_svc2=$(get_service_tag "${SVC2}")
nginx_drain "${SVC2_PORT}"
log "Waiting 15s for in-flight requests to drain..."
sleep 15
IMAGE_TAG="${TAG}" docker compose up -d --no-deps --force-recreate "${SVC2}"
health_check "${SVC2_PORT}" "${SVC2}" || rollback "${SVC2}" "${SVC2_PORT}" "${old_svc2}"
nginx_restore "${SVC2_PORT}"
# ── job workers ───────────────────────────────────────────────────────────────
step "job workers → ${TAG}"
IMAGE_TAG="${TAG}" docker compose up -d --no-deps --force-recreate zerotier-reconciler mfa-compliance
# ── done ──────────────────────────────────────────────────────────────────────
log ""
log "Deploy complete ✓ ${IMAGE_NAME}:${TAG}"