diff --git a/.gitignore b/.gitignore index 7d690374..72b65d10 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ out/ ### VS Code ### .vscode/ +### deploy +/letsencrypt/acme.json + ### Rest Docs /src/main/resources/static/docs/openapi3.yaml diff --git a/docker/application/docker-compose.application.yml b/docker/application/docker-compose.application.yml new file mode 100644 index 00000000..2e9ec098 --- /dev/null +++ b/docker/application/docker-compose.application.yml @@ -0,0 +1,54 @@ +services: + application: + image: debatetimer/debate_timer:${ENV:-dev} + container_name: application + environment: + - SERVER_FORWARD_HEADERS_STRATEGY=framework + - SPRING_PROFILES_ACTIVE=${ENV:-dev},monitor + - TZ=Asia/Seoul + + - DD_AGENT_HOST=datadog-agent + - DD_SERVICE=debate-timer + - DD_ENV=${ENV:-dev} + - DD_VERSION=${APP_VERSION:-1.0.0} + - DD_LOGS_INJECTION=true + - DD_PROFILING_ENABLED=true + - DD_PROFILING_ALLOCATION_ENABLED=true + - DD_PROFILING_HEAP_ENABLED=true + + - MANAGEMENT_STATSD_METRICS_EXPORT_ENABLED=true + - MANAGEMENT_STATSD_METRICS_EXPORT_FLAVOR=datadog + - MANAGEMENT_STATSD_METRICS_EXPORT_HOST=datadog-agent + - MANAGEMENT_STATSD_METRICS_EXPORT_PORT=8125 + - MANAGEMENT_STATSD_METRICS_EXPORT_PROTOCOL=UDP + + networks: + - debate-timer-net + + depends_on: + traefik: + condition: service_healthy + + healthcheck: + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8083/monitoring/health" ] + interval: 60s + retries: 10 + start_period: 450s + + labels: + - "traefik.enable=true" + - "traefik.http.routers.application.rule=Host(`api.${ENV:-dev}.debate-timer.com`)" + - "traefik.http.routers.application.entrypoints=websecure" + - "traefik.http.routers.application.tls=true" + - "traefik.http.routers.application.tls.certresolver=myresolver" + - "traefik.http.routers.application.service=application" + - "traefik.http.services.application.loadbalancer.server.port=8080" + + - "traefik.http.routers.application-monitor.rule=PathPrefix(`/`)" + - "traefik.http.routers.application-monitor.entrypoints=monitoring" + - "traefik.http.routers.application-monitor.service=application-monitor-svc" + - "traefik.http.services.application-monitor-svc.loadbalancer.server.port=8083" + + - "com.datadoghq.ad.logs=[{\"source\": \"java\", \"service\": \"debate-timer\"}]" + + restart: unless-stopped diff --git a/docker/docker-compose.datadog.yml b/docker/datadog/docker-compose.datadog.yml similarity index 92% rename from docker/docker-compose.datadog.yml rename to docker/datadog/docker-compose.datadog.yml index cfa62de8..a3823718 100644 --- a/docker/docker-compose.datadog.yml +++ b/docker/datadog/docker-compose.datadog.yml @@ -5,6 +5,7 @@ services: environment: - DD_API_KEY=${DD_API_KEY} - DD_SITE=us5.datadoghq.com + - DD_HOSTNAME=debate-timer-${ENV:-dev}-server - DD_APM_ENABLED=true - DD_APM_NON_LOCAL_TRAFFIC=true - DD_APM_IGNORE_RESOURCES=/monitoring/health @@ -25,6 +26,6 @@ services: healthcheck: test: ["CMD", "agent", "health"] interval: 60s - timeout: 10s + timeout: 30s retries: 5 start_period: 120s diff --git a/docker/docker-compose.spring.yml b/docker/docker-compose.spring.yml deleted file mode 100644 index a678bbe3..00000000 --- a/docker/docker-compose.spring.yml +++ /dev/null @@ -1,75 +0,0 @@ -x-app-template: &app-template - image: debatetimer/debate_timer:${ENV:-dev} - environment: - - SERVER_FORWARD_HEADERS_STRATEGY=framework - - SPRING_PROFILES_ACTIVE=${PROFILE:-dev,monitor} - - TZ=Asia/Seoul - - - DD_AGENT_HOST=datadog-agent - - DD_SERVICE=debate-timer - - DD_ENV=${ENV:-dev} - - DD_VERSION=1.0.0 - - DD_LOGS_INJECTION=true - - DD_PROFILING_ENABLED=true - - DD_PROFILING_ALLOCATION_ENABLED=true - - DD_PROFILING_HEAP_ENABLED=true - - - MANAGEMENT_STATSD_METRICS_EXPORT_ENABLED=true - - MANAGEMENT_STATSD_METRICS_EXPORT_FLAVOR=datadog - - MANAGEMENT_STATSD_METRICS_EXPORT_HOST=datadog-agent - - MANAGEMENT_STATSD_METRICS_EXPORT_PORT=8125 - - MANAGEMENT_STATSD_METRICS_EXPORT_PROTOCOL=UDP - - networks: - - debate-timer-net - - depends_on: - traefik: - condition: service_healthy - datadog-agent: - condition: service_healthy - - healthcheck: - test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8083/monitoring/health" ] - interval: 60s - retries: 5 - start_period: 300s - -services: - app-blue: - <<: *app-template - container_name: app-blue - labels: - - "traefik.enable=true" - - "traefik.http.routers.app-blue.rule=Host(`api.${ENV:-dev}.debate-timer.com`)" - - "traefik.http.routers.app-blue.entrypoints=websecure" - - "traefik.http.routers.app-blue.tls=true" - - "traefik.http.routers.app-blue.tls.certresolver=myresolver" - - "traefik.http.services.app-blue.loadbalancer.server.port=8080" - - "traefik.http.routers.app-blue.service=app-blue" - - - "traefik.http.routers.app-blue-monitor.rule=PathPrefix(`/`)" - - "traefik.http.routers.app-blue-monitor.entrypoints=monitoring" - - "traefik.http.routers.app-blue-monitor.service=app-blue-monitor-svc" - - "traefik.http.services.app-blue-monitor-svc.loadbalancer.server.port=8083" - - - "com.datadoghq.ad.logs=[{\"source\": \"java\", \"service\": \"debate-timer\"}]" - - app-green: - <<: *app-template - container_name: app-green - labels: - - "traefik.enable=true" - - "traefik.http.routers.app-green.rule=Host(`api.${ENV:-dev}.debate-timer.com`)" - - "traefik.http.routers.app-green.entrypoints=websecure" - - "traefik.http.routers.app-green.tls=true" - - "traefik.http.routers.app-green.tls.certresolver=myresolver" - - "traefik.http.services.app-green.loadbalancer.server.port=8080" - - "traefik.http.routers.app-green.service=app-green" - - - "traefik.http.routers.app-green-monitor.rule=PathPrefix(`/`)" - - "traefik.http.routers.app-green-monitor.entrypoints=monitoring" - - "traefik.http.routers.app-green-monitor.service=app-green-monitor-svc" - - "traefik.http.services.app-green-monitor-svc.loadbalancer.server.port=8083" - - - "com.datadoghq.ad.logs=[{\"source\": \"java\", \"service\": \"debate-timer\"}]" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9bb209d9..7fc5b5b7 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,9 +1,10 @@ name: debate-timer-server include: - - docker-compose.traefik.yml - - docker-compose.spring.yml - - docker-compose.datadog.yml + - ./application/docker-compose.application.yml + - ./datadog/docker-compose.datadog.yml + - ./traefik/docker-compose.traefik.yml + - ./webhook/docker-compose.webhook.yml networks: debate-timer-net: diff --git a/docker/docker-compose.traefik.yml b/docker/traefik/docker-compose.traefik.yml similarity index 96% rename from docker/docker-compose.traefik.yml rename to docker/traefik/docker-compose.traefik.yml index 54e2a2cc..3fe979cb 100644 --- a/docker/docker-compose.traefik.yml +++ b/docker/traefik/docker-compose.traefik.yml @@ -34,6 +34,6 @@ services: start_period: 120s volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - - ../letsencrypt:/letsencrypt + - ../../letsencrypt:/letsencrypt networks: - debate-timer-net diff --git a/docker/webhook/docker-compose.webhook.yml b/docker/webhook/docker-compose.webhook.yml new file mode 100644 index 00000000..c9e4e0a3 --- /dev/null +++ b/docker/webhook/docker-compose.webhook.yml @@ -0,0 +1,35 @@ +services: + webhook: + build: + context: .. + dockerfile: ./webhook/webhook.Dockerfile + container_name: webhook + user: root + command: ["-verbose", "-hooks=/etc/webhook/hooks.json", "-hotreload"] + environment: + - ENV=${ENV:-dev} + - SECRET_TOKEN=${WEBHOOK_KEY} + labels: + - "traefik.enable=true" + - "traefik.http.routers.webhook.rule=Host(`webhook.${ENV:-dev}.debate-timer.com`)" + - "traefik.http.routers.webhook.entrypoints=websecure" + - "traefik.http.routers.webhook.tls=true" + - "traefik.http.routers.webhook.tls.certresolver=myresolver" + - "traefik.http.services.webhook.loadbalancer.server.port=9000" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /home/ubuntu/.docker:/root/.docker:ro + - ./hooks.json:/etc/webhook/hooks.json + - ../..:/home/ubuntu/debate-timer + networks: + - debate-timer-net + depends_on: + traefik: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 9000 || exit 1"] + interval: 120s + timeout: 30s + retries: 5 + start_period: 30s + restart: always diff --git a/docker/webhook/hooks.json b/docker/webhook/hooks.json new file mode 100644 index 00000000..db3e8f9d --- /dev/null +++ b/docker/webhook/hooks.json @@ -0,0 +1,38 @@ +[ + { + "id": "deploy", + "http-methods": ["POST"], + "execute-command": "/home/ubuntu/debate-timer/scripts/deploy/deploy-docker.sh", + "command-working-directory": "/home/ubuntu/debate-timer", + "pass-environment-to-command": [ + { + "source": "header", + "name": "X-Deploy-Token", + "envname": "REQUEST_TOKEN" + } + ], + "pass-arguments-to-command": [ + { "source": "entire-payload" } + ] + }, + { + "id": "status", + "http-methods": ["GET"], + "execute-command": "/home/ubuntu/debate-timer/scripts/deploy/check-deploy-status.sh", + "command-working-directory": "/home/ubuntu/debate-timer", + "include-command-output-in-response": true, + "response-headers": [ + { + "name": "Content-Type", + "value": "application/json" + } + ], + "pass-environment-to-command": [ + { + "source": "header", + "name": "X-Deploy-Token", + "envname": "REQUEST_TOKEN" + } + ] + } +] diff --git a/docker/webhook/webhook.Dockerfile b/docker/webhook/webhook.Dockerfile new file mode 100644 index 00000000..0453da68 --- /dev/null +++ b/docker/webhook/webhook.Dockerfile @@ -0,0 +1,7 @@ +FROM almir/webhook:2.8.3 +USER root +RUN apk add --no-cache docker-cli-compose curl bash util-linux git + +WORKDIR /etc/webhook + +RUN git config --global --add safe.directory /home/ubuntu/debate-timer diff --git a/scripts/deploy/check-deploy-status.sh b/scripts/deploy/check-deploy-status.sh new file mode 100755 index 00000000..98dc2fa0 --- /dev/null +++ b/scripts/deploy/check-deploy-status.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +if [ -z "$SECRET_TOKEN" ] || [ "$REQUEST_TOKEN" != "$SECRET_TOKEN" ]; then + echo '{"status": "error", "message": "Unauthorized request. Invalid Token."}' + exit 0 +fi + +LOCK_FILE="/tmp/deploy-application.lock" + +exec 200>"$LOCK_FILE" + +if ! flock -n 200; then + echo '{"status": "running", "message": "배포 스크립트가 현재 실행 중입니다."}' +else + flock -u 200 + echo '{"status": "idle", "message": "현재 진행 중인 배포가 없습니다. 대기 중입니다."}' +fi + +exec 200>&- diff --git a/scripts/deploy/deploy-docker.sh b/scripts/deploy/deploy-docker.sh new file mode 100755 index 00000000..746ecf3a --- /dev/null +++ b/scripts/deploy/deploy-docker.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +if [ -z "$SECRET_TOKEN" ] || [ "$REQUEST_TOKEN" != "$SECRET_TOKEN" ]; then + echo "[$(date)] 🚨 보안 경고: 유효하지 않은 배포 요청입니다. (Unauthorized)" + exit 1 +fi + +echo "스크립트 실행 여부를 확인합니다." +LOCK_FILE="/tmp/deploy-application.lock" +exec 200>"$LOCK_FILE" +flock -n 200 || { echo "⚠️ 이미 배포 스크립트가 실행 중입니다. 중복 실행을 차단합니다."; exit 1; } +echo "스크립트를 실행하고 있지 않습니다. 배포를 시작합니다." + +TARGET_ENV=${ENV:-dev} +PROJECT_DIR="/home/ubuntu/debate-timer" +DOCKER_DIR="/home/ubuntu/debate-timer/docker" +TARGET_SERVICE="application" + +echo "--- Git 저장소 최신화 시작 ---" +cd $PROJECT_DIR || { echo "프로젝트 디렉토리 이동 실패"; exit 1; } + +if [ "$TARGET_ENV" = "prod" ]; then + echo "▶ [ENV: prod] main 브랜치로 이동하여 최신 코드를 가져옵니다." + git fetch origin main || { echo "❌ git fetch(main) 실패"; exit 1; } + git switch main || { echo "❌ git switch(main) 실패"; exit 1; } + git reset --hard origin/main || { echo "❌ git reset(main) 실패"; exit 1; } +elif [ "$TARGET_ENV" = "dev" ]; then + echo "▶ [ENV: dev] develop 브랜치로 이동하여 최신 코드를 가져옵니다." + git fetch origin develop || { echo "❌ git fetch(develop) 실패"; exit 1; } + git switch develop || { echo "❌ git switch(develop) 실패"; exit 1; } + git reset --hard origin/develop || { echo "❌ git reset(develop) 실패"; exit 1; } +else + CURRENT_BRANCH=$(git branch --show-current) + if [ -z "$CURRENT_BRANCH" ]; then + echo "❌ 현재 브랜치를 확인할 수 없습니다 (detached HEAD 상태일 수 있음)" + exit 1 + fi + echo "▶ [ENV: $TARGET_ENV] 현재 브랜치($CURRENT_BRANCH)에서 최신 코드를 가져옵니다." + git fetch origin "$CURRENT_BRANCH" || { echo "❌ git fetch($CURRENT_BRANCH) 실패"; exit 1; } + git reset --hard origin/"$CURRENT_BRANCH" || { echo "❌ git reset($CURRENT_BRANCH) 실패"; exit 1; } +fi + +cd $DOCKER_DIR || { echo "디렉토리 이동 실패"; exit 1; } +echo "--- 중단 배포 시작 ENV: $TARGET_ENV at $(date) ---" +export ENV=$TARGET_ENV + +echo "최근 이미지 가져오는 중 (ENV: $TARGET_ENV)" +docker compose pull $TARGET_SERVICE || { echo "❌ 이미지 풀 실패"; exit 1; } +if docker ps --format '{{.Names}}' | grep -q "^${TARGET_SERVICE}$"; then + echo "기존 $TARGET_SERVICE 중지 및 제거 중..." + docker compose stop $TARGET_SERVICE +fi + +echo "새 컨테이너 실행 중 - $TARGET_SERVICE..." +docker compose up -d --no-deps $TARGET_SERVICE || { echo "❌ 컨테이너 실행 실패"; exit 1; } + +echo "헬스 체크 진행 중 - $TARGET_SERVICE" +MAX_RETRIES=50 +SLEEP_SECOND=10 +COUNT=0 + +while [ $COUNT -lt $MAX_RETRIES ]; do + HEALTH_STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$TARGET_SERVICE" 2>/dev/null) + if [ "$HEALTH_STATUS" = "healthy" ]; then + echo "헬스 체크 완료 - $TARGET_SERVICE" + break + fi + + echo "헬스체크 진행 중 ($COUNT/$MAX_RETRIES) - 현재 상태: $HEALTH_STATUS" + sleep $SLEEP_SECOND + COUNT=$((COUNT + 1)) +done + +if [ $COUNT -eq $MAX_RETRIES ]; then + echo "배포 실패: $TARGET_SERVICE가 healthy 상태가 되지 않았습니다." + docker logs --tail 50 $TARGET_SERVICE + exit 1 +fi + +docker image prune -f + +echo "--- $TARGET_ENV 배포 완료 at $(date) ---" diff --git a/scripts/init/auto-swap.sh b/scripts/init/auto-swap.sh old mode 100644 new mode 100755 diff --git a/scripts/init/init-docker.sh b/scripts/init/init-docker.sh old mode 100644 new mode 100755 diff --git a/scripts/init/init-letsencrypt.sh b/scripts/init/init-letsencrypt.sh old mode 100644 new mode 100755 index 5223ca63..f8820a66 --- a/scripts/init/init-letsencrypt.sh +++ b/scripts/init/init-letsencrypt.sh @@ -6,4 +6,4 @@ mkdir -p ../../letsencrypt touch ../../letsencrypt/acme.json chmod 600 ../../letsencrypt/acme.json -echo "✅ 스크립트 위치($(pwd))에 letsencrypt 폴더를 생성했습니다." +echo "letsencrypt 폴더를 생성했습니다."