Files
keycloak-pattern/docs/experiment-d4a-deploy-hook.md
T
DongHyeonkaandClaude Opus 5 a2f749292a docs: add architecture diagrams for the three follow-up experiments
Each of A-7a, B-7a and D-4a had the concepts, procedure, commands and
terminal evidence but no diagram, which the rest of the series carries.

  a7a-cache-temperature.svg  the three cache states and the SQL each issues
  b7a-orphan-lifecycle.svg   ticket structure, accumulation across two
                             rotations, and why TTL is the only signal
  d4a-hook-timeline.svg      the 38m25s gap against the 1-2s one, on one axis

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:37:00 +09:00

275 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# D-4a — deploy 훅은 정말 듣는가
브랜치 `feature/keycloak-d4a-deploy-hook` ·
증거 [`docs/evidence/d4a-deploy-hook/`](evidence/d4a-deploy-hook/) ·
2026-09-04 12:27 UTC (실제 시각)
D-4 는 결함을 찾고 **처방을 적어두고 검증하지 않았다.**
처방이 듣는지 모르는 채 "이렇게 고치면 된다"고 쓰는 것은,
이 실험대가 스물세 번 경계해 온 바로 그 실수다.
---
## 구조
![D-4a — 훅 유무에 따른 갱신→서빙 타임라인](diagrams/d4a-hook-timeline.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
---
## 0. 결론부터
| 확인 | 결과 |
|---|---|
| 훅이 실행되는가 | **된다.** certbot 이 `Hook 'deploy-hook' ran` 을 찍는다 |
| nginx 가 reload 되는가 | **된다.** 마스터 585 유지, 워커 28829 → **37252** |
| **얼마나 빠른가** | **발급 → 서빙 1~2초** (D-4 는 38분 25초) |
| 함정 | **`ran with error output` 은 실패가 아니다** |
| **★ 부수 발견** | **test-server 시계가 106초 빠르다.** D-4 의 공백 수치를 정정했다 |
---
## 1. D-4 가 무엇을 남겼나
| 항목 | 상태 |
|---|---|
| deploy 훅을 넣으면 자동 반영되는가 | **미측정. 훅은 아직 넣지 않았다** |
D-4 는 원인을 셋으로 특정했고(유닛에 `ExecStartPost` 없음 · 훅 3경로 비었음 ·
nginx 플러그인 없음) 처방을 적었다. **그 처방을 실행해 본 적이 없다.**
---
## 2. 주입 — 파일 하나
```bash
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/sh
nginx -t && nginx -s reload
```
### 개념 — `deploy/` 와 `post/` 는 다르다
| 디렉터리 | 언제 실행되나 |
|---|---|
| `pre/` | 갱신 **시도** 전 |
| **`deploy/`** | **실제로 갱신된 인증서가 있을 때만** |
| `post/` | 갱신 여부와 **무관하게** 매번 |
**왜 `deploy/` 인가.** 타이머는 하루 두 번 돈다. `post/` 에 넣으면 갱신이
없는 날에도 하루 두 번 nginx 를 reload 하게 된다 — 아무 이득 없이 워커만
갈아치우는 셈이다. `deploy/`**`RENEWED_LINEAGE` 가 있을 때만** 돈다.
**없거나 틀리면.** D-4 가 측정한 그대로다 — 갱신은 성공하고 서빙은 안 바뀐다.
그리고 그 상태로 타이머는 `SUCCESS` 를 찍는다.
**확인.**
```bash
sudo ls -la /etc/letsencrypt/renewal-hooks/deploy/ # 비어 있으면 아무도 안 부른다
```
### 왜 `nginx -t &&` 를 앞에 두는가
설정이 깨진 상태에서 `nginx -s reload` 를 보내면 마스터가 새 워커를 못 띄운다.
`-t` 로 먼저 검사하고 통과할 때만 reload 한다. 실패하면 **옛 워커가 그대로
서비스를 계속한다** — 인증서는 안 바뀌지만 서비스는 죽지 않는다.
### 실행
```bash
install -m755 /tmp/reload-nginx.sh /etc/letsencrypt/renewal-hooks/deploy/
certbot renew --force-renewal
```
> **이 스크립트는 실험 자동화가 아니라 시험 대상 자체다.**
> certbot 은 훅을 파일로만 받는다. 절차는 전부 명령어로 되어 있다.
---
## 3. 결과 — certbot 이 훅을 실행했다
```
Processing /etc/letsencrypt/renewal/auth.hyeonworks.com.conf
Renewing an existing certificate for auth.hyeonworks.com and 2 more
Hook 'deploy-hook' ran with error output:
2026/09/04 21:29:36 [warn] 37250#37250: could not build optimal types_hash, …
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
2026/09/04 21:29:37 [warn] 37251#37251: could not build optimal types_hash, …
2026/09/04 21:29:37 [notice] 37251#37251: signal process started
Congratulations, all renewals succeeded:
/etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem (success)
```
### ★ 함정 — "ran with error output" 은 실패가 아니다
certbot 은 훅이 **stderr 에 무엇이라도 쓰면** 이 문구를 붙인다.
여기 stderr 로 나간 것은 nginx 의 `types_hash` 경고뿐이고, 내용은 전부 성공이다.
| 줄 | 실제 의미 |
|---|---|
| `[warn] could not build optimal types_hash` | nginx 의 일반 경고. 갱신과 무관 |
| `nginx: … test is successful` | **`nginx -t` 통과** |
| `[notice] signal process started` | **`nginx -s reload` 가 신호를 보냄** |
> 로그에서 `error` 를 grep 하는 감시를 걸어두면 **성공한 훅을 실패로 오독한다.**
> 실패를 확인하려면 문구가 아니라 **워커 PID 를 봐야 한다.**
---
## 4. 검증 — 워커가 교체됐다
| | PID | 기동 시각 (test-server 시계) |
|---|---|---|
| 마스터 (전·후 동일) | **585** | Thu Sep 3 19:00:39 |
| 워커 (전) | 28829 | Fri Sep 4 18:00:35 ← D-4 에서 **사람이** reload |
| **워커 (후)** | **37252** | Fri Sep 4 21:29:36 ← **훅이 자동으로** |
**마스터 PID 는 유지되고 워커만 바뀌었다.** D-4 에서 "reload 되었는가"를
판정하려고 세운 방법이 그대로 작동한다.
서빙 인증서도 바뀌었다.
```
serial=06F3E0EF4D1BB03DE58130EAAD1176101373
notBefore=Sep 4 11:29:18 2026 GMT
notAfter=Dec 3 11:29:17 2026 GMT
SAN: app1 / app2 / auth .hyeonworks.com
```
---
## 5. ★ 얼마나 빨랐나 — 그리고 시계 문제
훅 로그와 워커 `lstart` 는 **test-server 시계**다. 그런데 그 시계는
NTP 동기가 꺼져 있다.
```
dev → Google +0초
dev → Let's Encrypt ACME +0초
test-server → Google 105초 (test-server 가 빠르다)
ssh 왕복 3회 측정 +106.1 / +106.1 / +106.1초
```
**dev 가 정확하고 test-server 가 106초 빠르다.**
### 독립 검증 — SCT
새 인증서에 박힌 Certificate Transparency 서명 시각은 **CT 로그의 자체
시계**로 찍힌다. 양쪽 어느 것과도 무관한 제3의 기준이다.
```
Signed Certificate Timestamp: Sep 4 12:27:49.054 2026 GMT
Signed Certificate Timestamp: Sep 4 12:27:49.048 2026 GMT
```
보정한 타임라인 (전부 실제 UTC):
```
12:27:49.05 인증서 발급 ← SCT (외부 권위 기준)
12:27:50 훅 nginx -t ← 로그 21:29:36 KST 106초
12:27:50 새 워커 37252 기동 ← lstart 21:29:36 KST 106초
12:27:51 훅 nginx -s reload ← 로그 21:29:37 KST 106초
```
**발급에서 서빙까지 1~2초.**
그리고 **보정이 자기 검증된다** — 독립 시계인 SCT 가 보정한 훅 시각의 1초
앞에 정확히 놓인다. 보정하지 않으면 훅이 발급보다 **104초 먼저** 실행된 것이
되어 물리적으로 불가능해진다.
### 부수 정정 — D-4 의 2199초는 틀렸다
D-4 에서 적은 **2199초(36분 39초)**`archive/cert2.pem` 의 mtime
(test-server 시계)과 일련번호 관측(dev 시계)을 **그대로 뺀** 값이었다.
| | 시각 (실제 UTC) |
|---|---|
| 새 인증서 디스크 기록 | **08:20:27** ← mtime 17:22:13 KST 106초 |
| 실제 서빙 시작 | 08:58:52 ← dev 관측, 보정 불필요 |
| **공백** | **2305초 = 38분 25초** |
관련 문서를 전부 정정했다.
> **두 시계에서 온 값을 빼면서 그 사실을 적지 않으면, 자릿수가 아니라
> 방향까지 틀릴 수 있다.** D-4 에서는 오차가 106초여서 결론이 안 바뀌었지만,
> 1~2초를 재는 D-4a 에서는 결과를 완전히 뒤집었다.
---
## 6. 대조
| | 훅 없음 (D-4) | **훅 있음 (D-4a)** |
|---|---|---|
| 갱신 → 서빙 | **2305초 = 38분 25초** | **1~2초** |
| 무엇이 reload 했나 | 사람이 친 `nginx -s reload` | certbot deploy 훅 |
| 아무도 안 했다면 | 다음 nginx 재시작까지 = **사실상 무기한** | 해당 없음 |
| 차이 | | **약 1150배** |
---
## 7. 남는 것
| 항목 | 상태 |
|---|---|
| `certbot-renew.timer`**실제 갱신**을 하는가 | **미측정.** 만료 30일 전(약 89일 뒤)에야 조건이 성립한다 |
훅은 `--force-renewal` 로 검증했다. **타이머가 스스로 갱신하는 경로**는
시간이 지나야 시험할 수 있다. 다만 그 경로도 같은 `certbot renew` 를 부르고
같은 `deploy/` 훅을 실행하므로, 남은 미지수는 "타이머가 뜨는가" 하나다 —
그리고 그것은 D-4 에서 이미 확인했다(오늘 두 번 `status=0/SUCCESS`).
---
## 8. 재현 절차 (명령어)
```bash
# ── 0. 기준선 — 이 워커 PID 가 바뀌는지가 판정이다
ssh test-server "ps -eo pid,ppid,etimes,lstart,args | grep 'nginx:' | grep -v grep"
echo | openssl s_client -connect auth.hyeonworks.com:443 \
-servername auth.hyeonworks.com 2>/dev/null | openssl x509 -noout -serial -dates
# ── 1. ★ 시계 왜곡을 먼저 잰다. 나중에 재면 값을 해석할 수 없다
for i in 1 2 3; do
A=$(date -u +%s.%N); B=$(ssh test-server 'date -u +%s.%N'); C=$(date -u +%s.%N)
python3 -c "print(f'왜곡 {$B-($A+$C)/2:+.1f}초')"
done
# 어느 쪽이 맞는지는 외부 기준으로 가른다
for H in https://www.google.com https://acme-v02.api.letsencrypt.org/directory; do
A=$(date -u +%s)
D=$(curl -sI --max-time 10 "$H" | grep -i '^date:' | sed 's/^[Dd]ate: *//' | tr -d '\r')
C=$(date -u +%s)
python3 -c "print(f'$H 차이 {$(date -u -d \"$D\" +%s)-($A+$C)//2:+d}초')"
done
# ── 2. 훅을 sudo 없는 곳에 미리 만들어 둔다 (사람이 칠 명령을 짧게)
ssh test-server "printf '#!/bin/sh\nnginx -t && nginx -s reload\n' > /tmp/reload-nginx.sh"
# ── 3. 주입 — 여기만 sudo 가 필요하다
ssh -t test-server 'sudo sh -c "install -m755 /tmp/reload-nginx.sh /etc/letsencrypt/renewal-hooks/deploy/ && certbot renew --force-renewal > /tmp/d4a-renew.txt 2>&1; chmod 644 /tmp/d4a-renew.txt; tail -25 /tmp/d4a-renew.txt"'
# ── 4. 판정 — 문구가 아니라 워커 PID 로 본다
ssh test-server "ps -eo pid,ppid,etimes,lstart,args | grep 'nginx:' | grep -v grep"
# 마스터 PID 그대로 + 워커 PID 바뀜 = reload 됨
# ── 5. 얼마나 빨랐나 — SCT 가 발급 시각의 외부 기준이다
echo | openssl s_client -connect auth.hyeonworks.com:443 \
-servername auth.hyeonworks.com 2>/dev/null \
| openssl x509 -noout -ext ct_precert_scts | grep Timestamp
# 훅 로그 시각에서 왜곡을 빼고 SCT 와 비교한다
```
---
## 증거 파일
| 파일 | 종류 | 무엇을 보여주는가 |
|---|---|---|
| [`01-hook-verified.txt`](evidence/d4a-deploy-hook/01-hook-verified.txt) | 터미널 | 판정 전문 · 시계 보정과 SCT 교차검증 · D-4 대조 |
| [`02-certbot-with-hook.txt`](evidence/d4a-deploy-hook/02-certbot-with-hook.txt) | 터미널 | `certbot renew --force-renewal` 원문 |
| [`03-after-state.txt`](evidence/d4a-deploy-hook/03-after-state.txt) | 터미널 | 실행 후 nginx 프로세스 · 서빙 인증서 |
파일별 상세는 [`evidence/d4a-deploy-hook/README.md`](evidence/d4a-deploy-hook/README.md).