IT/Kubernets

cert-manager가 조용히 renewal에 실패하고 있었다

gfrog 2026. 8. 7. 06:16

지난 주말 새벽 3시, 알림이 울려서 눈을 떴다. 사내 스테이징 API 게이트웨이가 TLS handshake failure를 뱉고 있었다. 그때만 해도 "그냥 인증서 문제겠지, 아침에 보자" 하고 다시 누웠는데, 아침이 되니 프로덕션까지 같은 증상이 번지고 있었다.

결론부터 말하면 cert-manager는 인증서를 잘 발급하고 있었다. Kubernetes Secret도 잘 갱신됐다. 그런데 워크로드가 새 인증서를 안 읽고 있었다. 이 삽질 이야기를 정리해둔다.

처음엔 cert-manager를 의심했다

먼저 한 게 이거다.

kubectl describe certificate api-gateway-tls -n gateway
kubectl get certificaterequest -n gateway --sort-by=.metadata.creationTimestamp

Certificate 리소스의 status는 Ready=True. CertificateRequest도 최근에 잘 통과했다. Secret을 까보니 실제로 새 인증서가 들어가 있었고 만료일도 90일 뒤로 갱신돼 있었다.

여기서 좀 당황했다. cert-manager는 자기 할 일을 다 했다는 얘기다. 그런데 왜 클라이언트는 옛날 인증서를 받고 있지?

실마리는 Pod 로그가 아니라 파일 mtime에서 나왔다

Ingress 컨트롤러 Pod에 들어가서 마운트된 Secret 볼륨의 파일 시간을 봤다.

kubectl exec -it nginx-ingress-xxx -- ls -la /etc/nginx/tls/

파일 mtime은 하루 전으로 최신이었다. 여기서 확신했다. 파일은 갱신됐는데 프로세스가 다시 안 읽고 있는 거였다.

이건 사실 cert-manager 문서 FAQ에도 나와 있는 유명한 함정이다. Kubernetes는 Secret이 바뀌면 볼륨 파일을 in-place로 갈아치우는데(대략 60초 내), 앱이 시작 시점에만 파일을 읽고 이후엔 안 본다면 새 인증서는 프로세스 입장에서 존재하지 않는다. Nginx도 SIGHUP을 받아야 reload한다. 우리 팀 setup에서는 그 트리거가 어딘가 끊겨 있었다.

reloader가 붙어있었지만 그게 문제였다

여기서 진짜 원인이 나왔다. Ingress Deployment에는 이미 stakater/reloader annotation이 붙어 있었다.

metadata:
  annotations:
    secret.reloader.stakater.com/reload: "api-gateway-tls"

원래는 Secret이 바뀌면 reloader가 Deployment를 rollout 시켜서 Pod를 재기동해야 한다. 근데 최근에 팀에서 argo-rollouts로 이 Deployment를 감싸는 작업을 했다. Rollout 리소스로 바뀌면서 reloader의 annotation이 위쪽 Rollout에 붙지 않고 내부 template 쪽에만 남은 상태였다. reloader는 Rollout kind를 인식 못 해서 아무 액션도 안 했다.

한마디로, "인증서 자동 갱신"이라는 파이프라인의 마지막 한 칸이 조용히 빠져 있었던 거다. 알람도 없이.

임시 조치와 근본 대응

일단 급한 불부터 껐다.

kubectl rollout restart deployment/nginx-ingress -n ingress-nginx

이걸로 프로덕션은 회복. 그다음 근본 대응은 세 갈래로 나눴다.

첫째, reloader를 Rollout 리소스도 감지하도록 설정을 바꿨다. --resources-to-ignore 반대편에 있는 감시 대상 리스트에 rollouts.argoproj.io를 명시적으로 추가했다. 이 부분은 stakater/reloader 최근 릴리스에서 Argo Rollout 지원이 좀 개선되기는 했지만, 우리 클러스터의 버전이 낮아서 업그레이드도 같이 잡았다.

둘째, 감시가 필요 없는 앱도 있으니 sidecar 형태로 재시도했다. 우리 서비스 중에 gRPC 서버는 인증서 파일을 fs watch하도록 코드가 이미 짜여 있었다. 그런 앱은 pod 재기동이 필요 없다. 문제는 어떤 앱이 fs watch를 지원하는지 우리 팀이 정확히 알고 있지 않았다는 거다. 서비스 오너별로 확인 요청을 돌렸다.

셋째 — 이게 가장 아팠던 부분인데 — 인증서 만료 임박 알림을 실제 트래픽 관점에서 다시 잡았다. 기존 알림은 Certificate 리소스의 NotAfter만 보고 있었다. 그런데 이번 사고에서는 리소스는 갱신됐고 프로세스만 옛 인증서를 쓰는 상황이었다. Prometheus에서 실제 서빙되는 인증서를 blackbox_exporter로 찍고, 그 만료일을 기준으로 알람을 재설정했다.

- alert: ServedCertExpiringSoon
  expr: probe_ssl_earliest_cert_expiry - time() < 7 * 24 * 3600
  for: 30m

리소스 상의 인증서가 아니라 클라이언트가 실제로 받는 인증서를 보는 알람이다. 이번 사고가 아니었으면 이런 게 필요하다는 생각도 못 했을 것 같다.

그리고 하나 더 — 1.19.0 이슈

수습이 어느 정도 되고 나서 이번 사건을 계기로 cert-manager 버전도 훑어봤다. 최근 v1.19.0에 CRD 기반 API 기본값 처리에서 예상치 못하게 renewal이 발생하는 이슈가 있었고 v1.19.1에서 수정됐다는 얘기가 있다. 우리는 다행히 이 버전 대역이 아니었지만, 만약 이걸 맞았다면 renewal이 폭주하면서 rate limit에 걸렸을 수도 있다. 업그레이드 계획 짤 때는 minor 릴리스도 한 번 체크해두는 게 좋겠다.

남는 교훈

이번 사고를 요약하면 이렇다. 인증서 자동화는 발급 → Secret 반영 → 프로세스 재적용, 이 세 단계가 다 이어져야 한다. 그런데 대부분의 모니터링은 첫 단계만 본다. 두 번째와 세 번째는 조용히 끊어져도 아무도 모른다.

우리는 이번에 세 번째 단계에서 넘어졌다. 다음엔 두 번째에서 넘어질 수도 있다. 그때도 알람이 울리게 만들어놓는 것, 그게 이번에 얻은 진짜 교훈이다.

혹시 비슷한 구조 쓰시는 분들 중에 다른 접근 있으시면 댓글 부탁드립니다. 특히 Rollout 리소스 + reloader 조합 어떻게들 쓰고 계신지 궁금하다.