diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3f79ebf0..fd4939fb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -566,10 +566,13 @@ jobs: - name: Health check and switch id: health uses: appleboy/ssh-action@v1 + env: + DOMAIN: ${{ needs.resolve.outputs.domain }} with: host: ${{ secrets.EC2_HOST }} username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} + envs: DOMAIN script: | # 슬롯 재판정 - 배포 스텝과 같은 공용 블록·같은 값으로 읽는다 (상태 파일이 source of truth) DECIDED=$(bash /tmp/piki-deploy/slot_decide.sh --state-file /etc/nginx/team3-upstream.conf \ @@ -590,8 +593,14 @@ jobs: # 실패 시 이전 upstream 원복까지 블록의 원복 경계 안에서 돈다. 종전 인라인(tee + reload)에는 # 원복이 없어 전환 실패 시 상태 파일과 실제 서빙이 어긋난 채 남았고, 그 괴리는 다음 배포가 # 서빙 중인 슬롯을 비활성으로 오판·제거하게 만들었다 (infra#37 에서 실사고로 실증된 결함의 해소). + # verify-cmd: 전환 직후 소비자 경로(프론트 443 → nginx → 새 슬롯)를 통째로 검증한다 - 앱만 + # 살아있고 배달 경로가 끊긴 부류(extractor 이행 배포의 Host/444 사고가 그 실례)를 원복 경계 + # 안에서 잡는다. /health 는 conf 가 127.0.0.1 에만 열어 둔 내부 검증 전용 경로이고, 검증 배선 + # (폴링·재시도·TLS 우회 -k·Host 헤더)은 extractor 와 같은 공용 블록(healthcheck.sh)이 진다 - + # 값(URL·간격·도메인)만 여기서 준다. 짧은 재시도(2s x 5)는 reload 직후 워커 교체 흔들림 흡수. if ! bash /tmp/piki-deploy/slot_switch.sh \ - --state-file /etc/nginx/team3-upstream.conf --server "localhost:$INACTIVE_PORT"; then + --state-file /etc/nginx/team3-upstream.conf --server "localhost:$INACTIVE_PORT" \ + --verify-cmd "bash /tmp/piki-deploy/healthcheck.sh --url https://127.0.0.1/health --interval 2 --attempts 5 --insecure --host-header $DOMAIN"; then echo "슬롯 전환 실패(이전 upstream 원복됨) - 새 슬롯 정리 후 중단" timeout 40 docker stop -t 30 "piki-core-$INACTIVE" 2>/dev/null || true timeout 30 docker rm -f "piki-core-$INACTIVE" 2>/dev/null || true diff --git a/infra/nginx/api.piki.day.conf b/infra/nginx/api.piki.day.conf index 8e53acf9..8da838ba 100644 --- a/infra/nginx/api.piki.day.conf +++ b/infra/nginx/api.piki.day.conf @@ -141,8 +141,14 @@ server { proxy_pass http://team3; } - # /health 는 화이트리스트에 없다(#833) - 배포 헬스체크는 localhost 직결이라 nginx 를 안 타고, - # 외부 호출 실적은 봇 스캔뿐이었다(7일 실측). 외부 uptime 모니터를 도입하면 그때 재개방한다. + # /health 는 박스 자신(127.0.0.1)에게만 연다 - 배포(slot_switch)가 전환 직후 "프론트 → nginx → 새 슬롯" + # 경로를 검증하는 용도다. 외부는 종전(#833)대로 봇 스캔에 상태코드조차 안 주는 444 를 유지한다 + # ("외부 uptime 모니터 도입 시 재개방" 조건과 별개 - 이건 외부 개방이 아니다). + # if+return 은 rewrite phase 라 limit_req 를 안 타지만, 내부 1회성 검증이라 레이트리밋이 불필요하다. + location = /health { + if ($remote_addr != "127.0.0.1") { return 444; } + proxy_pass http://team3; + } # 어드민 페이지가 참조하는 유일한 정적 리소스. location = /favicon.ico { diff --git a/infra/nginx/dev.api.piki.day.conf b/infra/nginx/dev.api.piki.day.conf index 95c1a241..8a335f9f 100644 --- a/infra/nginx/dev.api.piki.day.conf +++ b/infra/nginx/dev.api.piki.day.conf @@ -144,8 +144,14 @@ server { proxy_pass http://team3; } - # /health 는 화이트리스트에 없다(#833) - 배포 헬스체크는 localhost 직결이라 nginx 를 안 타고, - # 외부 호출 실적은 봇 스캔뿐이었다(7일 실측). 외부 uptime 모니터를 도입하면 그때 재개방한다. + # /health 는 박스 자신(127.0.0.1)에게만 연다 - 배포(slot_switch)가 전환 직후 "프론트 → nginx → 새 슬롯" + # 경로를 검증하는 용도다. 외부는 종전(#833)대로 봇 스캔에 상태코드조차 안 주는 444 를 유지한다 + # ("외부 uptime 모니터 도입 시 재개방" 조건과 별개 - 이건 외부 개방이 아니다). + # if+return 은 rewrite phase 라 limit_req 를 안 타지만, 내부 1회성 검증이라 레이트리밋이 불필요하다. + location = /health { + if ($remote_addr != "127.0.0.1") { return 444; } + proxy_pass http://team3; + } # 어드민 페이지가 참조하는 유일한 정적 리소스. location = /favicon.ico {