From a84f842490713dd48f253c00236fd21ca32ec0db Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Tue, 30 Jun 2026 22:11:09 +0900 Subject: [PATCH] feat: implement zero-downtime Green/Blue deployment using local TCP proxy --- .gitea/workflows/deploy.yml | 9 ++- TaxBaik.Proxy/Program.cs | 93 +++++++++++++++++++++++++++++ TaxBaik.Proxy/TaxBaik.Proxy.csproj | 10 ++++ deploy_gb.sh | 96 ++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 TaxBaik.Proxy/Program.cs create mode 100644 TaxBaik.Proxy/TaxBaik.Proxy.csproj create mode 100644 deploy_gb.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 27cfd3d..9e6d593 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -100,6 +100,7 @@ jobs: - name: Package artifact run: | + cp deploy_gb.sh ./publish/deploy_gb.sh tar -czf taxbaik_deploy.tgz -C ./publish . echo "✓ Package: $(du -sh taxbaik_deploy.tgz | cut -f1)" @@ -163,11 +164,9 @@ jobs: test -s "\$DEPLOY_DIR/appsettings.Production.json" \ || { echo "FATAL: appsettings.Production.json 없음" >&2; exit 1; } - echo "--- [3/5] 심볼릭 링크 전환 ---" - ln -sfn "\$DEPLOY_DIR" "\$DEPLOY_HOME/taxbaik_active" - - echo "--- [4/5] 서비스 재시작 ---" - sudo /usr/bin/systemctl restart taxbaik + echo "--- [3/4] Green-Blue 배포 실행 ---" + chmod +x "\$DEPLOY_DIR/deploy_gb.sh" + "\$DEPLOY_DIR/deploy_gb.sh" "\$DEPLOY_DIR" echo "--- [5/5] 헬스 체크 (최대 60초) ---" ATTEMPTS=20 diff --git a/TaxBaik.Proxy/Program.cs b/TaxBaik.Proxy/Program.cs new file mode 100644 index 0000000..ddb1839 --- /dev/null +++ b/TaxBaik.Proxy/Program.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +class Program +{ + private const string PortFile = "/home/kjh2064/taxbaik_port"; + private static int _fallbackPort = 5003; + + static async Task Main(string[] args) + { + // Allow setting fallback port via args + if (args.Length > 0 && int.TryParse(args[0], out var port)) + { + _fallbackPort = port; + } + + var listener = new TcpListener(IPAddress.Loopback, 5001); + listener.Start(); + Console.WriteLine($"[TaxBaik Proxy] Listening on 127.0.0.1:5001 (Forwarding to target in {PortFile})"); + + while (true) + { + try + { + var client = await listener.AcceptTcpClientAsync(); + _ = HandleClientAsync(client); + } + catch (Exception ex) + { + Console.WriteLine($"[TaxBaik Proxy] Accept error: {ex.Message}"); + await Task.Delay(100); + } + } + } + + private static int GetTargetPort() + { + try + { + if (File.Exists(PortFile)) + { + var content = File.ReadAllText(PortFile).Trim(); + if (int.TryParse(content, out var port) && port > 1024 && port < 65535) + { + return port; + } + } + } + catch { } + return _fallbackPort; + } + + private static async Task HandleClientAsync(TcpClient client) + { + client.NoDelay = true; + int targetPort = GetTargetPort(); + using var backend = new TcpClient(); + backend.NoDelay = true; + + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await backend.ConnectAsync(IPAddress.Loopback, targetPort, cts.Token); + } + catch (Exception ex) + { + Console.WriteLine($"[TaxBaik Proxy] Failed to connect to backend on port {targetPort}: {ex.Message}"); + client.Close(); + return; + } + + try + { + using var clientStream = client.GetStream(); + using var backendStream = backend.GetStream(); + + var toBackend = clientStream.CopyToAsync(backendStream); + var toClient = backendStream.CopyToAsync(clientStream); + + await Task.WhenAny(toBackend, toClient); + } + catch { } + finally + { + client.Close(); + backend.Close(); + } + } +} diff --git a/TaxBaik.Proxy/TaxBaik.Proxy.csproj b/TaxBaik.Proxy/TaxBaik.Proxy.csproj new file mode 100644 index 0000000..ed9781c --- /dev/null +++ b/TaxBaik.Proxy/TaxBaik.Proxy.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/deploy_gb.sh b/deploy_gb.sh new file mode 100644 index 0000000..0869a84 --- /dev/null +++ b/deploy_gb.sh @@ -0,0 +1,96 @@ +#!/bin/bash +set -e + +DEPLOY_HOME="/home/kjh2064" +PORT_FILE="$DEPLOY_HOME/taxbaik_port" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) + +echo "===== 🚀 TaxBaik Green/Blue Deployment Script =====" + +# 1. Determine active port +ACTIVE_PORT=5003 +if [ -f "$PORT_FILE" ]; then + ACTIVE_PORT=$(cat "$PORT_FILE" | tr -d '[:space:]') +fi + +# 2. Determine target port +TARGET_PORT=5003 +if [ "$ACTIVE_PORT" -eq 5003 ]; then + TARGET_PORT=5004 +else + TARGET_PORT=5003 +fi + +echo "Active Port: $ACTIVE_PORT" +echo "Target Port: $TARGET_PORT" + +# 3. New deploy dir is passed as first argument +DEPLOY_DIR="$1" +if [ -z "$DEPLOY_DIR" ]; then + echo "Error: Deployment directory argument required" + exit 1 +fi + +echo "Deploy Directory: $DEPLOY_DIR" + +# 4. Start the new app on the target port +echo "=== Starting New App on Port $TARGET_PORT ===" +cd "$DEPLOY_DIR" +export ASPNETCORE_ENVIRONMENT=Production +export ASPNETCORE_URLS="http://127.0.0.1:$TARGET_PORT" + +# Run dotnet process +nohup /usr/bin/dotnet TaxBaik.Web.dll > "web_${TARGET_PORT}.log" 2>&1 & +NEW_PID=$! +sleep 2 + +# Verify process is running +if ! ps -p $NEW_PID > /dev/null; then + echo "❌ Failed to start dotnet process on port $TARGET_PORT" + exit 1 +fi + +# 5. Health Check Loop +echo "=== Health Checking Port $TARGET_PORT ===" +ATTEMPTS=20 +SUCCESS=false +for i in $(seq 1 $ATTEMPTS); do + STATUS=$(curl -sf -o /dev/null -w '%{http_code}' "http://127.0.0.1:${TARGET_PORT}/taxbaik/healthz" 2>/dev/null || echo "000") + if [ "$STATUS" = "200" ]; then + echo "✓ Health check passed on port $TARGET_PORT (Attempt $i/$ATTEMPTS)" + SUCCESS=true + break + fi + echo " Waiting for health check... ($i/$ATTEMPTS, Status: $STATUS)" + sleep 2 +done + +if [ "$SUCCESS" = "false" ]; then + echo "❌ Health check failed. Rolling back..." + kill -9 $NEW_PID || true + exit 1 +fi + +# 6. Switch Traffic +echo "=== Switching Traffic to Port $TARGET_PORT ===" +echo "$TARGET_PORT" > "$PORT_FILE" +echo "✓ Traffic routed to $TARGET_PORT" + +# 7. Terminate Old App +echo "=== Stopping Old App on Port $ACTIVE_PORT ===" +# Find PID listening on ACTIVE_PORT +OLD_PID=$(ss -tlnp | grep ":$ACTIVE_PORT " | grep -oP 'pid=\K\d+' | head -n1) +if [ -n "$OLD_PID" ]; then + echo "Killing old process PID: $OLD_PID" + kill -15 $OLD_PID || kill -9 $OLD_PID + echo "✓ Old process terminated" +else + echo "No old process found on port $ACTIVE_PORT" +fi + +# 8. Cleanup old deployment directories (Keep last 5) +echo "=== Cleaning Up Old Deployments ===" +ls -1dt $DEPLOY_HOME/deployments/taxbaik_* 2>/dev/null | tail -n +6 | xargs rm -rf 2>/dev/null || true +echo "✓ Cleanup completed" + +echo "===== ✅ Green/Blue Deployment Completed Successfully ====="