새벽 3시에 터지는 Kubernetes 클러스터, 인증서 때문이었다

|Platform Decision|10분 읽기

새벽 3시의 악몽

며칠 전 동료로부터 들은 이야기입니다. 새벽 3시, PagerDuty 알림이 끊임없이 울리더니 Kubernetes API 서버에 아예 접근이 안 되더라는 거예요. SSH로 급히 들어가보니 Pod들이 줄줄이 실패하고, 노드들이 하나씩 NotReady 상태가 되어가고 있었다고 합니다.

로그를 확인해보니 답이 나왔습니다:

x509: certificate has expired

잊혀진 인증서 하나가 전체 클러스터를 마비시킨 것이죠. GitHub(2025년), Microsoft Azure(2019년)에서도 비슷한 사고가 있었는데, 모두 단순한 이유였습니다. 아무도 인증서 만료일을 챙기지 못했던 것이죠.

복잡한 버그도 아니고, 예방할 수 없는 장애도 아닙니다. 그냥 관리가 안 됐을 뿐이에요.

Kubernetes의 인증서들

먼저 Kubernetes에서 사용되는 인증서들을 정리해보겠습니다:

인증서 유형 용도 만료 시 영향
API 서버 인증서 제어 평면과의 보안 통신 클러스터 전체 접근 불가
Kubelet 인증서 노드의 API 서버 인증 노드 NotReady, Pod 스케줄링 실패
Ingress 인증서 외부 HTTPS 트래픽 서비스 접근 불가
etcd 인증서 etcd 멤버 간 통신 데이터 저장소 장애

각각 만료일이 있고, 만료되면 해당 통신이 실패합니다. 예상 외로 단순한 구조죠.

실수 1: 만료일을 모르고 있기

대부분의 장애는 "언제 만료되는지 몰랐다"에서 시작됩니다. 볼 수 없는 건 고칠 수도 없으니까요.

해결책: 자동 모니터링 구축

1단계: x509-certificate-exporter 설치

Prometheus 메트릭으로 모든 인증서를 감시할 수 있게 해줍니다:

helm repo add enix https://charts.enix.io
helm repo update

helm upgrade --install x509-certificate-exporter enix/x509-certificate-exporter \
  --set prometheusServiceMonitor.enabled=true \
  --set prometheusServiceMonitor.labels.release=prometheus \
  --set service.port=9793

2단계: cert-manager 메트릭 활성화

helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
  --version v1.19.4 \
  --namespace cert-manager \
  --create-namespace \
  --set prometheus.enabled=true \
  --set prometheus.servicemonitor.enabled=true

3단계: Prometheus 알림 규칙 설정

30일, 7일 전에 미리 알려주는 규칙을 만듭니다:

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-cert-rules
  namespace: monitoring
data:
  cert-expiry.rules: |
    groups:
    - name: certificate_expiry
      interval: 1h
      rules:
      - alert: CertManagerCertExpiring30Days
        expr: (certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 30
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "인증서 {{ $labels.name }}가 30일 내 만료됩니다"
          
      - alert: ControlPlaneCertExpiring7Days
        expr: (x509_cert_not_after - time()) / 86400 < 7
        for: 1h
        labels:
          severity: critical
        annotations:
          summary: "긴급: 제어플레인 인증서가 7일 내 만료됩니다"

수동으로도 확인할 수 있습니다:

# kubelet 인증서 확인
openssl x509 -in /var/lib/kubelet/pki/kubelet-client-current.pem -noout -enddate

# API 서버 인증서 확인
openssl s_client -connect localhost:6443 -showcerts 2>/dev/null | \
openssl x509 -noout -enddate

실수 2: 수동으로 인증서 관리하기

수동 관리는 시간도 오래 걸리고, 실수하기 쉽고, 스케일되지도 않습니다. 더 중요한 건 깜빡하기 쉽다는 거예요.

해결책: cert-manager로 자동화

ClusterIssuer 생성

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your-email@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          class: nginx

자동 갱신되는 Ingress 인증서

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    cert-manager.io/renew-before: "720h"  # 30일 전 갱신
spec:
  tls:
  - hosts:
    - example.com
    secretName: example-com-tls
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app-service
            port:
              number: 80

cert-manager가 알아서 인증서를 발급하고, 만료 30일 전에 자동으로 갱신합니다. 손댈 필요가 없어요.

실수 3: Kubelet 인증서 회전 설정 안 하기

Kubelet 인증서가 만료되면 노드가 API 서버와 통신할 수 없어서 NotReady 상태가 됩니다. 의외로 많은 분들이 놓치는 부분이에요.

해결책: Kubelet 자동 회전 활성화

# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
rotateCertificates: true
serverTLSBootstrap: true

kubeadm으로 구성된 클러스터라면:

apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
  RotateKubeletServerCertificate: true
rotateCertificates: true
serverTLSBootstrap: true

설정 후 kubelet을 재시작하면 자동 회전이 시작됩니다.

실수 4: 회전이 잘 되는지 테스트 안 하기

자동화를 구현했지만 실제로 작동하는지 확인하지 않는 경우가 많습니다. 그러다 실제 상황에서 실패하죠.

해결책: 정기적인 회전 훈련

짧은 TTL로 테스트 인증서를 만들어 회전을 확인해보세요:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: test-rotation-cert
spec:
  secretName: test-rotation-tls
  duration: 2h      # 2시간만 유효
  renewBefore: 1h   # 1시간 전 갱신
  issuerRef:
    name: letsencrypt-staging  # 반드시 staging 사용!
    kind: ClusterIssuer
  dnsNames:
  - test.example.com

⚠️ 중요: 테스트에는 반드시 Let's Encrypt staging 환경을 사용하세요. production 환경은 요청 제한이 있어서 테스트로 사용하면 실제 인증서 발급이 막힐 수 있습니다.

실수 5: 인증서 백업 안 하기

인증서가 실수로 삭제되거나 회전 중 문제가 생겼을 때, 백업이 없으면 복구가 어려워집니다.

해결책: 자동 백업 시스템

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cert-backup
  namespace: kube-system
spec:
  schedule: "0 2 * * *"  # 매일 새벽 2시
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: bitnami/kubectl:latest
            command:
            - /bin/sh
            - -c
            - |
              kubectl get secret --all-namespaces -o yaml > /backup/certs-$(date +%Y%m%d).yaml
            volumeMounts:
            - name: backup-volume
              mountPath: /backup
          volumes:
          - name: backup-volume
            persistentVolumeClaim:
              claimName: cert-backup-pvc
          restartPolicy: OnFailure

실수 6: 프로덕션에서 자체 서명 인증서 사용

자체 서명 인증서는 브라우저 경고를 발생시키고, 자동 회전이 까다로우며, 문제 해결을 복잡하게 만듭니다.

해결책: 신뢰할 수 있는 CA 사용

  • 외부 서비스: Let's Encrypt (무료, 자동화, 신뢰됨)
  • 내부 서비스: cert-manager와 private CA 조합

내부 CA 설정 예시:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: internal-ca-key-pair

실수 7: 사고 대응 계획 없이 운영하기

예상치 못한 인증서 만료가 발생했을 때 명확한 절차가 없으면 복구 시간이 몇 배로 늘어납니다.

해결책: 인증서 사고 대응 플레이북

1단계: 즉시 평가 (5분)

# 영향받은 인증서 식별
kubectl get certificate --all-namespaces

# 만료일 확인
openssl x509 -in /path/to/cert.pem -noout -enddate

2단계: 즉각적인 완화 (10분)

# Ingress 인증서 강제 갱신
kubectl delete certificate <cert-name> -n <namespace>
kubectl apply -f ingress-with-cert.yaml

# Kubelet 인증서 갱신
sudo kubeadm certs renew all
sudo systemctl restart kubelet

# API 서버 인증서 갱신
sudo kubeadm certs renew apiserver

3단계: 검증 (5분)

# 새 인증서 확인
kubectl get nodes
kubectl get pods --all-namespaces
curl -k https://your-api-endpoint/healthz

경험에서 배운 것들

인증서 관리는 생각보다 단순합니다. 복잡한 로직이 필요한 게 아니라, 그냥 미리 알고, 자동으로 갱신하고, 잘 되는지 확인하는 것뿐이에요.

중요한 건 이 세 가지입니다:

  1. 30일 전에 알림 - 여유롭게 대응할 시간 확보
  2. cert-manager로 자동화 - 사람이 깜빡하는 실수 방지
  3. 정기적인 테스트 - 자동화가 실제로 작동하는지 확인

새벽 3시에 깨는 일은 정말 피하고 싶잖아요. 미리미리 준비해두면 충분히 예방할 수 있는 문제라서, 한 번 설정해두고 나면 마음이 편해집니다.

#Kubernetes#TLS인증서#cert-manager#DevOps#장애대응