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

11 KiB
Raw Blame History

D-4a — deploy 훅은 정말 듣는가

브랜치 feature/keycloak-d4a-deploy-hook · 증거 docs/evidence/d4a-deploy-hook/ · 2026-09-04 12:27 UTC (실제 시각)

D-4 는 결함을 찾고 처방을 적어두고 검증하지 않았다. 처방이 듣는지 모르는 채 "이렇게 고치면 된다"고 쓰는 것은, 이 실험대가 스물세 번 경계해 온 바로 그 실수다.


구조

D-4a — 훅 유무에 따른 갱신→서빙 타임라인

다이어그램 규약은 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. 주입 — 파일 하나

# /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 를 찍는다.

확인.

sudo ls -la /etc/letsencrypt/renewal-hooks/deploy/   # 비어 있으면 아무도 안 부른다

nginx -t && 를 앞에 두는가

설정이 깨진 상태에서 nginx -s reload 를 보내면 마스터가 새 워커를 못 띄운다. -t 로 먼저 검사하고 통과할 때만 reload 한다. 실패하면 옛 워커가 그대로 서비스를 계속한다 — 인증서는 안 바뀌지만 서비스는 죽지 않는다.

실행

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. ★ 얼마나 빨랐나 — 그리고 시계 문제

훅 로그와 워커 lstarttest-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. 재현 절차 (명령어)

# ── 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 터미널 판정 전문 · 시계 보정과 SCT 교차검증 · D-4 대조
02-certbot-with-hook.txt 터미널 certbot renew --force-renewal 원문
03-after-state.txt 터미널 실행 후 nginx 프로세스 · 서빙 인증서

파일별 상세는 evidence/d4a-deploy-hook/README.md.