diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 506f702..78dce64 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -86,7 +86,7 @@ jobs: host: ${{ secrets.EC2_HOST }} username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} - source: "shared-infra/blocks/run_container.sh,shared-infra/blocks/healthcheck.sh,infra/nginx/extractor.conf" + source: "shared-infra/blocks/run_container.sh,shared-infra/blocks/healthcheck.sh,shared-infra/blocks/slot_decide.sh,shared-infra/blocks/slot_switch.sh,infra/nginx/extractor.conf" target: "/tmp/piki-blocks" strip_components: 2 @@ -146,11 +146,11 @@ jobs: echo "LOGGING_STRUCTURED_FORMAT_CONSOLE=ecs" >> "$ENV_FILE" echo "SSM env $(grep -c '' "$ENV_FILE") 라인 로드 (/piki-extractor/app/)" - chmod +x /tmp/piki-blocks/run_container.sh /tmp/piki-blocks/healthcheck.sh + chmod +x /tmp/piki-blocks/run_container.sh /tmp/piki-blocks/healthcheck.sh \ + /tmp/piki-blocks/slot_decide.sh /tmp/piki-blocks/slot_switch.sh # ── nginx 프로비저닝(멱등) - 무중단 전환의 고정 프론트(:8090) ── - # 설치·conf 반영은 매 배포 수행하되, (재)시작·reload 는 아래 전환 단계가 수행한다 - # (레거시 단일 컨테이너가 :8090 을 점유한 이행 1회차엔 그 전까지 nginx 가 8090 을 못 문다). + # 설치·conf 반영은 매 배포 수행하고, (재)시작·reload 는 아래 전환 블록(slot_switch)이 수행한다. command -v nginx >/dev/null || { sudo DEBIAN_FRONTEND=noninteractive apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nginx @@ -159,20 +159,13 @@ jobs: sudo cp /tmp/piki-blocks/extractor.conf /etc/nginx/sites-available/piki-extractor sudo ln -sf /etc/nginx/sites-available/piki-extractor /etc/nginx/sites-enabled/piki-extractor - # ── 슬롯 결정: upstream 상태 파일(server 127.0.0.1:1809X;)이 source of truth ── + # ── 슬롯 결정: 공용 블록(slot_decide)에 위임 - 상태 파일이 source of truth, 값(슬롯 이름·포트)은 + # 이 호출부가 소유한다. 할당 후 eval 2단계는 블록 헤더가 정한 소비 계약(치환 실패 은폐 방지). UPSTREAM_CONF=/etc/nginx/piki-extractor-upstream.conf - ACTIVE_PORT="" - if [ -f "$UPSTREAM_CONF" ]; then - # 콜론 뒤 숫자만 추출한다 - 라인이 127.0.0.1 을 담고 있어 무차별 숫자 grep 은 "127" 을 먼저 문다. - ACTIVE_PORT=$(grep -oE ':[0-9]+' "$UPSTREAM_CONF" | head -1 | tr -d ':' || true) - fi - if [ "$ACTIVE_PORT" = "18090" ]; then - ACTIVE=blue INACTIVE=green INACTIVE_PORT=18091 - else - # 18091 활성이거나, 상태 파일이 없는 부트스트랩(레거시 단일 컨테이너가 8090 서빙 중) - blue 로 간다. - ACTIVE=green INACTIVE=blue INACTIVE_PORT=18090 - fi - echo "DEPLOY slot=$INACTIVE port=$INACTIVE_PORT (active=${ACTIVE_PORT:-legacy/none})" + DECIDED=$(bash /tmp/piki-blocks/slot_decide.sh --state-file "$UPSTREAM_CONF" \ + --slot-a blue:18090 --slot-b green:18091) + eval "$DECIDED" + echo "DEPLOY slot=$INACTIVE port=$INACTIVE_PORT (active=${ACTIVE_PORT:-none})" # 직전 배포의 teardown 이 끊겼으면 이 슬롯 컨테이너가 남아있을 수 있다 - run 의 이름·포트 충돌 전에 # 정리하고, 데몬 지연에 대비해 timeout 상한을 건다 (core deploy 와 동일 가드). @@ -203,51 +196,27 @@ jobs: exit 1 fi - # ── 전환: 이전 upstream 을 보존한 채 갱신 → 검증·reload·최종 헬스 중 무엇이 실패해도 원복 ── - # 원복이 없으면 상태 파일(=슬롯 판정의 source of truth)만 새 슬롯을 가리킨 채 남아, - # 다음 배포가 "실제 서빙 중인 슬롯"을 비활성으로 오판해 제거한다 (상태·현실 괴리 사고). - PREV_UPSTREAM="" - [ -f "$UPSTREAM_CONF" ] && PREV_UPSTREAM=$(cat "$UPSTREAM_CONF") - restore_upstream() { - # 부트스트랩(이전 상태 없음)은 원복 대상도 없다 - 레거시는 이미 제거됐을 수 있어 수동 개입 영역. - [ -n "$PREV_UPSTREAM" ] || return 0 - printf '%s\n' "$PREV_UPSTREAM" | sudo tee "$UPSTREAM_CONF" >/dev/null - sudo systemctl reload nginx || true - } - echo "server 127.0.0.1:$INACTIVE_PORT;" | sudo tee "$UPSTREAM_CONF" >/dev/null - if ! sudo nginx -t; then - echo "::error::nginx -t 실패 - upstream 원복 후 중단 (트래픽은 기존 경로 유지)" - restore_upstream - exit 1 - fi - if docker ps -a --format '{{.Names}}' | grep -qx piki-extractor; then - # 블루그린 이행 1회차: 레거시 단일 컨테이너가 :8090 을 점유해 nginx 가 그 포트를 못 문다. - # 제거→nginx 기동 사이 수초 공백은 소비자(core 워커) 재시도가 흡수한다. 이행 후엔 이 분기가 no-op. - timeout 40 docker stop -t 30 piki-extractor 2>/dev/null || true - timeout 30 docker rm -f piki-extractor 2>/dev/null || true - fi + # ── 전환: 공용 블록(slot_switch)에 위임 ── + # 갱신 → nginx -t → reload(정지 시 restart 폴백) → 프론트 경유 최종 검증까지 블록의 원복 경계 + # 안에서 돈다: 어느 단계가 실패해도 이전 upstream 이 원복되고 exit 1 이 온다. 검증 실패 시 + # 새 슬롯 정리는 호출자 몫(블록은 컨테이너를 모른다). 이행 1회차의 레거시 단일 컨테이너 분기는 + # 이행 완료(2026-08-10)로 제거됐다. sudo systemctl enable nginx >/dev/null 2>&1 || true - if ! (sudo systemctl reload nginx || sudo systemctl restart nginx); then - echo "::error::nginx reload/restart 실패 - upstream 원복 후 중단" - restore_upstream - exit 1 - fi - - # 전환 검증: 소비자와 같은 경로(:8090 → nginx → 새 슬롯)로 최종 확인. 실패 시 이전 슬롯으로 원복하고 - # 새 슬롯을 정리한다 - 구 슬롯 제거는 이 검증을 통과한 뒤에만 일어난다. - if ! bash /tmp/piki-blocks/healthcheck.sh \ - --url http://localhost:8090/actuator/health --interval 2 --attempts 10 \ - --expect-body '"status":"UP"'; then - echo "::error::전환 후 최종 헬스체크 실패 - 이전 슬롯으로 원복" - restore_upstream + if ! bash /tmp/piki-blocks/slot_switch.sh \ + --state-file "$UPSTREAM_CONF" --server "127.0.0.1:$INACTIVE_PORT" \ + --verify-cmd "bash /tmp/piki-blocks/healthcheck.sh --url http://localhost:8090/actuator/health --interval 2 --attempts 10 --expect-body '\"status\":\"UP\"'"; then + echo "::error::슬롯 전환 실패(이전 upstream 원복됨) - 새 슬롯 정리 후 중단" timeout 40 docker stop -t 30 "piki-extractor-$INACTIVE" 2>/dev/null || true timeout 30 docker rm -f "piki-extractor-$INACTIVE" 2>/dev/null || true exit 1 fi - # 구 슬롯 종료 - 전환은 이미 끝났으므로 best-effort (데몬 지연이 성공한 배포를 못 막게 timeout 상한) - timeout 60 docker stop -t 30 "piki-extractor-$ACTIVE" 2>/dev/null || true - timeout 30 docker rm -f "piki-extractor-$ACTIVE" 2>/dev/null || true + # 구 슬롯 종료 - 전환은 이미 끝났으므로 best-effort (데몬 지연이 성공한 배포를 못 막게 timeout 상한). + # 부트스트랩(ACTIVE 빈 값)이면 종료할 구 슬롯이 없다. + if [ -n "$ACTIVE" ]; then + timeout 60 docker stop -t 30 "piki-extractor-$ACTIVE" 2>/dev/null || true + timeout 30 docker rm -f "piki-extractor-$ACTIVE" 2>/dev/null || true + fi # 이전 이미지 정리 (dangling 만 - 직전 sha 태그는 남겨 수동 롤백 여지 유지) docker image prune -f >/dev/null