{ "version": "1.1", "id": "renewal-to-serving-gap", "title": "인증서 갱신이 서빙에 닿기까지", "question": "certbot 이 갱신에 성공한 뒤 nginx 가 새 인증서를 서빙하기까지 무엇이 필요한가", "type": "architecture", "direction": "LR", "audience": [ "TLS 종단을 직접 운영하는 인프라 엔지니어" ], "summary": "certbot 은 live 심볼릭 링크를 갈아끼우지만 nginx 는 기동 시점에 읽은 인증서를 메모리에 들고 있다. 둘을 잇는 것은 reload 하나뿐이고 이 실험대에는 그것을 부르는 경로가 셋 다 비어 있었다.", "alt": "certbot 이 archive 에 새 인증서를 쓰고 live 링크를 옮기지만, nginx 워커가 교체되지 않아 옛 인증서를 계속 서빙하는 구성.", "long_description": "certbot renew 는 archive 디렉터리에 새 인증서를 쓰고 live 심볼릭 링크가 그것을 가리키게 한다. nginx 는 ssl_certificate 가 가리키는 파일을 기동 시점에 한 번 읽어 메모리에 보관하므로, 경로가 그대로여도 reload 없이는 옛 인증서를 계속 서빙한다. 이 실험대에서는 certbot-renew.service 의 ExecStartPost, renewal-hooks 의 세 디렉터리, certbot 의 nginx 플러그인이 모두 비어 있어 2305초 동안 옛 인증서가 서빙됐다. deploy 훅 하나를 넣자 같은 구간이 1~2초가 됐다.", "source_context": { "document": "docs/keycloak-session-store/final/document.md", "document_sha256": "609353e10bfd37a9bbb6a79ecf2a32f3d3c02d5d161879a14ad4713e49e7e5e8", "anchor": { "kind": "heading", "value": "D-4 · D-4a — 인증서, 그리고 이 실험대 최대의 발견", "line": 497 } }, "composition": { "profile": "component-flow", "diagram_only": true, "reference_ids": [ "payment-event-flow" ], "rationale": "이 절의 지배적 질문은 시간 순서가 아니라 '디스크의 새 인증서가 네트워크에 닿는 경로가 무엇인가' 이다. 경로의 유무가 핵심이므로 component-flow 를 골랐다." }, "groups": [], "nodes": [ { "id": "certbot", "label": "certbot renew", "kind": "process", "role": "source", "emphasis": "primary", "description": "ACME 로 새 인증서를 받아 archive 에 쓰고 live 링크를 옮긴다.", "details": [ "--force-renewal" ], "evidence": [ { "start_line": 499, "end_line": 504 } ], "assumption": false }, { "id": "live-link", "label": "live/fullchain.pem", "kind": "datastore", "role": "store", "emphasis": "primary", "description": "심볼릭 링크. 경로는 그대로이고 가리키는 대상만 바뀐다.", "details": [ "→ archive/cert2.pem" ], "evidence": [ { "start_line": 536, "end_line": 540 } ], "assumption": false }, { "id": "deploy-hook", "label": "renewal-hooks/deploy", "kind": "process", "role": "control", "emphasis": "warning", "description": "갱신이 실제로 일어났을 때만 실행된다. 이 실험대에서는 비어 있었다.", "details": [ "nginx -t && nginx -s reload" ], "evidence": [ { "start_line": 527, "end_line": 533 } ], "assumption": false }, { "id": "nginx", "label": "nginx 워커", "kind": "service", "role": "target", "emphasis": "primary", "description": "기동 시점에 읽은 인증서를 메모리에 들고 있다. reload 해야 새 워커가 새 인증서를 읽는다.", "details": [ "마스터 유지 · 워커 교체" ], "evidence": [ { "start_line": 541, "end_line": 548 } ], "assumption": false } ], "edges": [ { "id": "write", "from": "certbot", "to": "live-link", "label": "새 인증서 기록", "kind": "write", "evidence": [ { "start_line": 504, "end_line": 510 } ], "assumption": false }, { "id": "needs-reload", "from": "live-link", "to": "nginx", "label": "reload 필요", "kind": "blocked", "evidence": [ { "start_line": 536, "end_line": 540 } ], "assumption": false }, { "id": "trigger", "from": "certbot", "to": "deploy-hook", "label": "갱신 성공 시 호출", "kind": "request", "evidence": [ { "start_line": 527, "end_line": 533 } ], "assumption": false }, { "id": "reload", "from": "deploy-hook", "to": "nginx", "label": "reload 신호", "kind": "request", "evidence": [ { "start_line": 556, "end_line": 562 } ], "assumption": false } ], "legend": [], "metadata": { "rationale": "갱신과 서빙을 두 사건으로 분리하고 그 사이에 reload 를 놓았다. 이 그림의 요지는 그 자리가 비어 있을 수 있다는 것이다." } }