From f252e6306592ecc71e0825e13341dccc55c3d9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:17:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?infra:=20=EC=A0=84=ED=99=98=20=ED=9B=84=20?= =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=20=EA=B2=BD=EC=9C=A0=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80=20-=20/health=20=EB=A5=BC=20127.?= =?UTF-8?q?0.0.1=20=ED=95=9C=EC=A0=95=20=EA=B0=9C=EB=B0=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - extractor 배포에는 있고 core 에는 없던 전환 후 검증(프론트 → nginx → 새 슬롯)을 통일한다. 이 검증이 잡는 것은 앱은 살아있는데 배달 경로가 끊긴 부류(extractor 이행 배포의 Host/444 사고가 실례)로, 중간 장치(TLS·레이트리밋·화이트리스트)가 더 많은 core 쪽이 오히려 검증 없이 배포되고 있었다 - nginx conf(dev·prod 동일): /health 를 박스 자신(127.0.0.1)에게만 열고 외부는 종전(#833)대로 444 유지. 외부 개방이 아니라 #833 의 재개방 조건과 별개다. 두 conf 에 함께 넣어 promote 때 prod 검증도 같이 성립한다 - deploy: slot_switch 에 verify-cmd 추가 - 블록이 이미 verify 를 원복 경계 안에서 지원(infra#39)하므로 인자 하나로 끝난다. curl -k 는 cert 가 도메인 앞이라 IP 접속의 hostname 검증만 끄는 것, --retry 는 reload 직후 워커 교체 흔들림 흡수 - 검증: 스텝 스크립트 bash -n·shellcheck 클린. conf 는 CI nginx-validate(동일 더미 우회 스크립트)가 게이트 --- .github/workflows/deploy.yml | 11 ++++++++++- infra/nginx/api.piki.day.conf | 10 ++++++++-- infra/nginx/dev.api.piki.day.conf | 10 ++++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3f79ebf0..f5d2ecd1 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 에만 열어 둔 내부 검증 전용 경로. -k 는 cert 가 + # 도메인 앞으로라 IP 접속에서 hostname 검증만 끄는 것이고, SNI 미전송이어도 박스당 서버 블록이 + # 하나뿐이라 default 매칭으로 닿는다. --retry 는 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 "curl -skf -o /dev/null -m 5 --retry 4 --retry-delay 2 --retry-all-errors -H 'Host: $DOMAIN' https://127.0.0.1/health"; 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 { From 022e6b08d3d46f92f538a336392b5699102bd87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:29:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=A0=ED=8A=B8=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20raw=20curl=20=EB=8C=80=EC=8B=A0?= =?UTF-8?q?=20healthcheck=20=EB=B8=94=EB=A1=9D=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - infra#40 이 --insecure·--host-header 를 지원해, 검증 배선(폴링·재시도·종료코드)을 블록 한 곳에 두고 호출부는 값만 준다 - extractor 의 verify 와 같은 모양 --- .github/workflows/deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f5d2ecd1..fd4939fb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -595,12 +595,12 @@ jobs: # 서빙 중인 슬롯을 비활성으로 오판·제거하게 만들었다 (infra#37 에서 실사고로 실증된 결함의 해소). # verify-cmd: 전환 직후 소비자 경로(프론트 443 → nginx → 새 슬롯)를 통째로 검증한다 - 앱만 # 살아있고 배달 경로가 끊긴 부류(extractor 이행 배포의 Host/444 사고가 그 실례)를 원복 경계 - # 안에서 잡는다. /health 는 conf 가 127.0.0.1 에만 열어 둔 내부 검증 전용 경로. -k 는 cert 가 - # 도메인 앞으로라 IP 접속에서 hostname 검증만 끄는 것이고, SNI 미전송이어도 박스당 서버 블록이 - # 하나뿐이라 default 매칭으로 닿는다. --retry 는 reload 직후 워커 교체의 순간 흔들림 흡수. + # 안에서 잡는다. /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" \ - --verify-cmd "curl -skf -o /dev/null -m 5 --retry 4 --retry-delay 2 --retry-all-errors -H 'Host: $DOMAIN' https://127.0.0.1/health"; then + --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