feat(infra): blue-green deployment via HAProxy

- HAProxy: api/stream/web backends with blue+green servers (backup)
- docker-compose.prod: backend-api-blue/green, stream-server-blue/green, web-blue/green
- haproxy-blue.cfg, haproxy-green.cfg: config variants for active stack
- scripts/deploy-blue-green.sh: switch traffic via config copy + HUP reload
This commit is contained in:
senke 2026-02-23 19:52:19 +01:00
parent cdc4bd82e6
commit 0ff8a85684
5 changed files with 324 additions and 26 deletions

View file

@ -0,0 +1,77 @@
# Blue stack active - copy to haproxy.cfg for blue deployment
# Generated from haproxy.cfg - blue servers main, green backup
global
log stdout format raw local0
maxconn 4096
daemon
stats socket /var/run/haproxy.sock level admin
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
option http-server-close
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
timeout http-request 10000ms
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
acl from_internal src 127.0.0.1 172.20.0.0/16
stats admin if from_internal
frontend http_frontend
bind *:80
mode http
redirect scheme https code 301 if !{ ssl_fc }
acl is_api path_beg /api/v1
acl is_stream path_beg /stream
acl is_hls path_beg /hls
acl is_web path_beg /
use_backend backend_api if is_api
use_backend stream_ws if is_stream
use_backend stream_ws if is_hls
use_backend web_frontend if is_web
frontend https_frontend
bind *:443 ssl crt /etc/ssl/veza/veza.pem
mode http
acl is_api path_beg /api/v1
acl is_stream path_beg /stream
acl is_hls path_beg /hls
acl is_web path_beg /
use_backend backend_api if is_api
use_backend stream_ws if is_stream
use_backend stream_ws if is_hls
use_backend web_frontend if is_web
backend backend_api
mode http
balance roundrobin
option httpchk GET /api/v1/health
http-check expect status 200
server api_blue backend-api-blue:8080 check inter 5s fall 3 rise 2
server api_green backend-api-green:8080 check inter 5s fall 3 rise 2 backup
backend stream_ws
mode http
balance roundrobin
option httpchk GET /health
http-check expect status 200
timeout tunnel 3600s
server stream_blue stream-server-blue:3001 check inter 5s fall 3 rise 2
server stream_green stream-server-green:3001 check inter 5s fall 3 rise 2 backup
backend web_frontend
mode http
balance roundrobin
option httpchk GET /
http-check expect status 200
server web_blue web-blue:5173 check inter 5s fall 3 rise 2
server web_green web-green:5173 check inter 5s fall 3 rise 2 backup

View file

@ -0,0 +1,77 @@
# Green stack active - copy to haproxy.cfg for green deployment
# Generated from haproxy.cfg - green servers main, blue backup
global
log stdout format raw local0
maxconn 4096
daemon
stats socket /var/run/haproxy.sock level admin
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
option http-server-close
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
timeout http-request 10000ms
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
acl from_internal src 127.0.0.1 172.20.0.0/16
stats admin if from_internal
frontend http_frontend
bind *:80
mode http
redirect scheme https code 301 if !{ ssl_fc }
acl is_api path_beg /api/v1
acl is_stream path_beg /stream
acl is_hls path_beg /hls
acl is_web path_beg /
use_backend backend_api if is_api
use_backend stream_ws if is_stream
use_backend stream_ws if is_hls
use_backend web_frontend if is_web
frontend https_frontend
bind *:443 ssl crt /etc/ssl/veza/veza.pem
mode http
acl is_api path_beg /api/v1
acl is_stream path_beg /stream
acl is_hls path_beg /hls
acl is_web path_beg /
use_backend backend_api if is_api
use_backend stream_ws if is_stream
use_backend stream_ws if is_hls
use_backend web_frontend if is_web
backend backend_api
mode http
balance roundrobin
option httpchk GET /api/v1/health
http-check expect status 200
server api_blue backend-api-blue:8080 check inter 5s fall 3 rise 2 backup
server api_green backend-api-green:8080 check inter 5s fall 3 rise 2
backend stream_ws
mode http
balance roundrobin
option httpchk GET /health
http-check expect status 200
timeout tunnel 3600s
server stream_blue stream-server-blue:3001 check inter 5s fall 3 rise 2 backup
server stream_green stream-server-green:3001 check inter 5s fall 3 rise 2
backend web_frontend
mode http
balance roundrobin
option httpchk GET /
http-check expect status 200
server web_blue web-blue:5173 check inter 5s fall 3 rise 2 backup
server web_green web-green:5173 check inter 5s fall 3 rise 2

View file

@ -2,6 +2,8 @@ global
log stdout format raw local0 log stdout format raw local0
maxconn 4096 maxconn 4096
daemon daemon
# Blue-green: runtime API for server enable/disable
stats socket /var/run/haproxy.sock level admin
defaults defaults
log global log global
@ -67,35 +69,34 @@ frontend https_frontend
use_backend web_frontend if is_web use_backend web_frontend if is_web
# ============================================================================ # ============================================================================
# BACKENDS # BACKENDS - Blue-Green Deployment
# Use scripts/deploy-blue-green.sh to switch active stack
# ============================================================================ # ============================================================================
# Backend API (Go) # Backend API (Go) - blue/green
backend backend_api backend backend_api
mode http mode http
balance roundrobin balance roundrobin
option httpchk GET /api/v1/health option httpchk GET /api/v1/health
http-check expect status 200 http-check expect status 200
server backend1 backend-api:8080 check inter 5s fall 3 rise 2 server api_blue backend-api-blue:8080 check inter 5s fall 3 rise 2
# Add more servers for load balancing: server api_green backend-api-green:8080 check inter 5s fall 3 rise 2 backup
# server backend2 backend-api-2:8080 check inter 5s fall 3 rise 2
# Stream WebSocket (Rust) # Stream WebSocket (Rust) - blue/green
backend stream_ws backend stream_ws
mode http mode http
balance roundrobin balance roundrobin
option httpchk GET /health option httpchk GET /health
http-check expect status 200 http-check expect status 200
server stream1 stream-server:3001 check inter 5s fall 3 rise 2
# WebSocket specific options
timeout tunnel 3600s timeout tunnel 3600s
server stream_blue stream-server-blue:3001 check inter 5s fall 3 rise 2
server stream_green stream-server-green:3001 check inter 5s fall 3 rise 2 backup
# Web Frontend (React/Vite) # Web Frontend (React/Vite) - blue/green
backend web_frontend backend web_frontend
mode http mode http
balance roundrobin balance roundrobin
option httpchk GET / option httpchk GET /
http-check expect status 200 http-check expect status 200
server web1 web:5173 check inter 5s fall 3 rise 2 server web_blue web-blue:5173 check inter 5s fall 3 rise 2
# Add more servers for load balancing: server web_green web-green:5173 check inter 5s fall 3 rise 2 backup
# server web2 web-2:5173 check inter 5s fall 3 rise 2

View file

@ -139,17 +139,19 @@ services:
memory: 256M memory: 256M
# ============================================================================ # ============================================================================
# APPLICATION SERVICES # APPLICATION SERVICES - Blue-Green Deployment
# STACK_COLOR=blue|green. Use scripts/deploy-blue-green.sh to switch.
# ============================================================================ # ============================================================================
backend-api: backend-api-blue:
build: build:
context: ./veza-backend-api context: ./veza-backend-api
dockerfile: Dockerfile.production dockerfile: Dockerfile.production
image: veza-backend-api:latest image: veza-backend-api:latest
container_name: veza_backend_api container_name: veza_backend_api_blue
restart: unless-stopped restart: unless-stopped
environment: environment:
- APP_ENV=production - APP_ENV=production
- STACK_COLOR=blue
- DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require - DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379 - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379
- AMQP_URL=amqp://${DB_USER:-veza}:${RABBITMQ_PASS:?RABBITMQ_PASS must be set}@rabbitmq:5672 - AMQP_URL=amqp://${DB_USER:-veza}:${RABBITMQ_PASS:?RABBITMQ_PASS must be set}@rabbitmq:5672
@ -192,14 +194,92 @@ services:
timeout: 5s timeout: 5s
retries: 3 retries: 3
# Chat Server removed in v0.502 -- chat is now handled by backend-api WebSocket at /api/v1/ws backend-api-green:
build:
context: ./veza-backend-api
dockerfile: Dockerfile.production
image: veza-backend-api:latest
container_name: veza_backend_api_green
restart: unless-stopped
environment:
- APP_ENV=production
- STACK_COLOR=green
- DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379
- AMQP_URL=amqp://${DB_USER:-veza}:${RABBITMQ_PASS:?RABBITMQ_PASS must be set}@rabbitmq:5672
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET must be set for production}
- COOKIE_SECURE=true
- COOKIE_SAME_SITE=strict
- COOKIE_HTTP_ONLY=true
- CORS_ALLOWED_ORIGINS=${CORS_ORIGINS:-http://veza.fr}
- HYPERSWITCH_URL=http://hyperswitch:8080
- HYPERSWITCH_API_KEY=${HYPERSWITCH_API_KEY:-}
- HYPERSWITCH_WEBHOOK_SECRET=${HYPERSWITCH_WEBHOOK_SECRET:-}
- HYPERSWITCH_ENABLED=${HYPERSWITCH_ENABLED:-false}
- CHECKOUT_SUCCESS_URL=${CHECKOUT_SUCCESS_URL:-https://veza.fr/purchases}
- ENABLE_CLAMAV=true
- CLAMAV_REQUIRED=true
- CLAMAV_ADDRESS=clamav:3310
- AWS_S3_ENDPOINT=http://minio:9000
- AWS_S3_BUCKET=veza-files
- AWS_ACCESS_KEY_ID=${S3_ACCESS_KEY:?S3_ACCESS_KEY must be set}
- AWS_SECRET_ACCESS_KEY=${S3_SECRET_KEY:?S3_SECRET_KEY must be set}
- AWS_REGION=${AWS_REGION:-us-east-1}
- HLS_STREAMING=true
- HLS_STORAGE_DIR=/data/hls
volumes:
- hls_prod_data:/data/hls
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
clamav:
condition: service_started
networks:
- veza-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/api/v1/health"]
interval: 10s
timeout: 5s
retries: 3
stream-server: stream-server-blue:
build: build:
context: ./veza-stream-server context: ./veza-stream-server
dockerfile: Dockerfile.production dockerfile: Dockerfile.production
image: veza-stream-server:latest image: veza-stream-server:latest
container_name: veza_stream_server container_name: veza_stream_server_blue
restart: unless-stopped
environment:
- DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET must be set}
- PORT=3001
- HLS_OUTPUT_DIR=/data/hls
volumes:
- hls_prod_data:/data/hls
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- veza-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health"]
interval: 10s
timeout: 5s
retries: 3
stream-server-green:
build:
context: ./veza-stream-server
dockerfile: Dockerfile.production
image: veza-stream-server:latest
container_name: veza_stream_server_green
restart: unless-stopped restart: unless-stopped
environment: environment:
- DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require - DATABASE_URL=postgres://${DB_USER:-veza}:${DB_PASS:?DB_PASS must be set}@postgres:5432/${DB_NAME:-veza}?sslmode=require
@ -257,20 +337,42 @@ services:
networks: networks:
- veza-network - veza-network
web: web-blue:
build: build:
context: ./apps/web context: ./apps/web
dockerfile: Dockerfile.production dockerfile: Dockerfile.production
image: veza-web:latest image: veza-web:latest
container_name: veza_web container_name: veza_web_blue
restart: unless-stopped restart: unless-stopped
environment: environment:
- VITE_API_URL=http://haproxy/api/v1 - VITE_API_URL=http://haproxy/api/v1
- VITE_STREAM_URL=ws://haproxy/stream - VITE_STREAM_URL=ws://haproxy/stream
- VITE_UPLOAD_URL=http://haproxy/api/v1/uploads - VITE_UPLOAD_URL=http://haproxy/api/v1/uploads
depends_on: depends_on:
- backend-api - backend-api-blue
- stream-server - stream-server-blue
networks:
- veza-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5173"]
interval: 10s
timeout: 5s
retries: 3
web-green:
build:
context: ./apps/web
dockerfile: Dockerfile.production
image: veza-web:latest
container_name: veza_web_green
restart: unless-stopped
environment:
- VITE_API_URL=http://haproxy/api/v1
- VITE_STREAM_URL=ws://haproxy/stream
- VITE_UPLOAD_URL=http://haproxy/api/v1/uploads
depends_on:
- backend-api-green
- stream-server-green
networks: networks:
- veza-network - veza-network
healthcheck: healthcheck:
@ -280,7 +382,7 @@ services:
retries: 3 retries: 3
# ============================================================================ # ============================================================================
# REVERSE PROXY - HAProxy # REVERSE PROXY - HAProxy (Blue-Green)
# ============================================================================ # ============================================================================
haproxy: haproxy:
image: haproxy:2.8-alpine image: haproxy:2.8-alpine
@ -298,9 +400,12 @@ services:
- ./config/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro - ./config/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- ./config/ssl:/etc/ssl/veza:ro - ./config/ssl:/etc/ssl/veza:ro
depends_on: depends_on:
- backend-api - backend-api-blue
- stream-server - backend-api-green
- web - stream-server-blue
- stream-server-green
- web-blue
- web-green
networks: networks:
- veza-network - veza-network
healthcheck: healthcheck:

38
scripts/deploy-blue-green.sh Executable file
View file

@ -0,0 +1,38 @@
#!/bin/bash
# Blue-Green deployment switch for Veza production
# Usage: ./scripts/deploy-blue-green.sh [blue|green]
# Requires: docker, config/haproxy/ directory, veza_haproxy container running
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
HAPROXY_DIR="$REPO_ROOT/config/haproxy"
TARGET="${1:-}"
if [[ "$TARGET" != "blue" && "$TARGET" != "green" ]]; then
echo "Usage: $0 [blue|green]"
echo " blue - Route traffic to blue stack (default)"
echo " green - Route traffic to green stack"
exit 1
fi
CONFIG_SOURCE="$HAPROXY_DIR/haproxy-$TARGET.cfg"
CONFIG_DEST="$HAPROXY_DIR/haproxy.cfg"
if [[ ! -f "$CONFIG_SOURCE" ]]; then
echo "Error: Config file not found: $CONFIG_SOURCE"
exit 1
fi
echo "Switching to $TARGET stack..."
cp "$CONFIG_SOURCE" "$CONFIG_DEST"
# Reload HAProxy (graceful, no connection drop)
if docker ps --format '{{.Names}}' | grep -q '^veza_haproxy$'; then
docker kill -s HUP veza_haproxy 2>/dev/null || {
echo "Warning: Could not send HUP to HAProxy. Try: docker restart veza_haproxy"
}
echo "HAProxy reloaded. Active stack: $TARGET"
else
echo "Warning: veza_haproxy container not running. Config updated; start stack to apply."
fi