
새벽 1시쯤 알람이 울렸다. 정확히는 PagerDuty가 두 번 울렸고 그 다음엔 슬랙이 폭주했다. "kubectl이 다 timeout 난다"는 메시지가 #infra 채널에 5명한테서 동시에 올라왔다. 그날 저녁에 마지막으로 한 일이 Kyverno에 ClusterPolicy 하나를 새로 올린 거였다. 그게 거의 확정이라는 느낌이 왔다.
이 글은 그날 새벽 한 시간 반 동안 무엇을 잘못했고 무엇을 배웠는지에 대한 회고다. 결론부터 말하면, failurePolicy: Fail을 너무 가볍게 봤다.
무슨 정책을 올렸냐
별 거 아닌 정책이었다. 워크로드에 app.kubernetes.io/name 레이블이 없으면 deployment를 막는 정책. 보안팀이 자산 인벤토리 정리하면서 요구한 거였고, 우리는 "이거 그냥 라벨 체크인데 뭐"라는 마음으로 PR을 머지했다. ArgoCD가 sync했고, 그게 22시 47분.
대략 이런 모양이었다:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-app-name-label
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-app-name
match:
any:
- resources:
kinds:
- Deployment
- StatefulSet
- DaemonSet
validate:
message: "Workloads must have app.kubernetes.io/name label"
pattern:
metadata:
labels:
app.kubernetes.io/name: "?*"
문제는 이게 단일 정책이 아니라는 거였다. 우리 클러스터엔 이미 Kyverno 정책이 18개 깔려있었고, 그 중 일부는 wildcard match를 쓰고 있었다. 새벽까지는 어떻게든 굴러갔지만 노드 한 대가 자정 무렵에 evict되면서 워크로드 재배치가 시작됐고, 거기서부터 도미노가 시작됐다.
무슨 일이 벌어졌나
상황을 시간순으로 정리하면 이렇다.
22:47 — 정책 sync. 알람 없음.
23:30 — 백그라운드 스캔이 시작됨. background: true였기 때문에 기존 워크로드도 다 검사 대상이었다. 우리 클러스터에 deployment, statefulset, daemonset 합쳐서 1,400개 정도 있는데 이게 한꺼번에 reconcile 큐에 들어갔다.
00:50 — Karpenter consolidation이 노드 하나를 빼면서 거기 있던 파드들이 다른 노드로 옮겨갔다. 옮겨가는 과정에서 admission webhook이 호출됨. 평소엔 별 일 없었을 텐데, 이때 Kyverno 파드가 백그라운드 작업으로 CPU를 거의 풀로 쓰고 있었다.
01:02 — admission webhook timeout (10초 기본값)이 터지기 시작. failurePolicy가 Fail이라서 timeout 나면 API 서버는 그 요청을 거절한다. 즉 새 파드가 안 뜬다.
01:08 — kube-system 안의 워크로드도 거절당하기 시작. CoreDNS 파드 하나가 재시작되어야 했는데 이것도 막혔다. 이때부터 클러스터 내부 DNS가 일부 흔들렸다.
01:14 — kubectl 명령어들이 다 timeout. API 서버가 살아있긴 한데 admission webhook 응답을 기다리느라 모든 mutating/validating 요청이 줄을 서있는 상태.
01:17 — 내가 일어났다.
처음에 한 삽질
처음엔 Kyverno를 재시작하면 될 줄 알았다. 근데 kubectl rollout이 그 자체로 deployment 변경이라 admission webhook을 타고, webhook 응답이 안 와서 rollout도 막혔다. 멘탈이 살짝 나갔다.
kubectl delete pod -n kyverno --all도 비슷한 이유로 막혔다. delete가 mutating webhook을 안 타긴 하지만, 새 파드가 뜨려면 또 webhook을 타야 한다.
그래서 결국 한 일은:
1. kubeconfig를 admin context로 바꾸고 (이건 운이 좋았다, EKS 클러스터 IRSA가 아닌 root 권한 kubeconfig가 따로 있었다)
2. validating/mutating webhook configuration을 직접 삭제:
kubectl delete validatingwebhookconfiguration kyverno-policy-validating-webhook-cfg
kubectl delete mutatingwebhookconfiguration kyverno-resource-mutating-webhook-cfg
kubectl delete validatingwebhookconfiguration kyverno-resource-validating-webhook-cfg
webhook configuration이 사라지자 API 서버는 더 이상 Kyverno를 호출하지 않게 됐고, 막혀있던 요청들이 한꺼번에 풀렸다. 그제서야 kubectl이 다시 응답하기 시작했다. 01:32.
이 방법은 Kyverno 공식 트러블슈팅 가이드에도 나와있다. 다행이라고 해야 할지, 같은 함정에 빠진 사람이 많다는 뜻이라고 해야 할지.
무엇이 잘못이었나
복기해보면 잘못은 한두 개가 아니었다.
failurePolicy를 기본값으로 둔 것. Kyverno는 기본적으로 webhook을 Fail로 설정한다. 정책이 강제력을 가지려면 그래야 하긴 하는데, 우리처럼 비핵심 라벨 검사 같은 정책까지 Fail로 두면 webhook 한 번 삐끗할 때 클러스터 전체가 굳는다. 적어도 라벨 체크 정도는 Ignore로 두는 게 맞았다.
background: true를 별 생각 없이 켠 것. 기존 워크로드에도 정책을 적용하고 싶어서 켰는데, 1,400개를 한꺼번에 스캔하는 동안 Kyverno가 admission 요청에 응답할 여력이 줄어든다. 큰 클러스터에서는 background scan과 admission 처리가 같은 컨트롤러를 쓰는 게 부담이다. 1.14부터는 ValidatingPolicy로 빼서 background는 별도 처리할 수도 있는데, 우리는 아직 1.12였다.
webhookTimeoutSeconds를 안 만진 것. 기본 10초. 10초가 길어보이지만 정책이 18개고 그 중에 wildcard가 있으면 10초 안에 처리 못 하는 경우가 종종 생긴다. 작년에 KubeCon에서도 이 timeout 튜닝 얘기가 한 세션 통째로 있었다.
ClusterPolicy를 머지하면서 카나리 클러스터에 먼저 안 올린 것. 우리는 staging 클러스터가 있는데, 보안 정책은 "이게 위험할 게 뭐 있어"하면서 prod에 바로 올렸다. 어처구니없는 실수다.
그래서 지금은 어떻게 운영하나
사고 이후에 우리 팀이 바꾼 것들:
첫째, 비핵심 정책(라벨 체크, naming 컨벤션 등)은 다 failureAction: Audit로 시작한다. Audit으로 며칠 돌려보고 policy report에서 위반이 어느 정도 잡히는지 본 다음에 Enforce로 올린다. validationFailureAction을 namespace별로 다르게 줄 수도 있어서, 신규 ns부터 Enforce 적용하고 기존은 Audit으로 두는 식으로 점진 전환한다.
둘째, webhook configuration을 보면 우리는 이제 라벨 체크나 인벤토리성 정책에 대해서는 failurePolicy: Ignore를 명시적으로 지정한다. Kyverno 자체 옵션은 아니고 webhook configuration을 직접 패치하는 방식. 정책이 죽었을 때 클러스터까지 같이 죽으면 안 되는 종류의 정책이기 때문.
셋째, webhookTimeoutSeconds를 5초로 줄였다. 줄였다는 게 좀 역설적인데, 한 정책이 10초를 다 잡아먹는 것보다 차라리 빨리 실패해서 다음 정책에 자원을 넘기는 게 낫다고 판단했다. Kyverno 측 처리 시간은 metric으로 모니터링하고 있다.
넷째, Kyverno 1.14로 올리는 작업을 우선순위 위로 올렸다. 1.14의 ValidatingPolicy는 native ValidatingAdmissionPolicy를 생성할 수 있어서 일부 정책은 아예 API 서버가 처리하게 된다. webhook을 안 타는 정책이 늘면 이런 류의 사고가 줄어들 거라고 본다. 검증 중이지만 방향성은 맞아 보인다.
다섯째, prod 변경은 카나리 클러스터를 무조건 거친다. 정책 변경도 코드 변경처럼 본다. 이건 사실 너무 당연한 건데 그동안 우리가 정책을 너무 가볍게 본 게 문제였다.
마치며
가장 뼈아팠던 건, 이 모든 게 공식 문서에 다 적혀있었다는 거다. Kyverno 트러블슈팅 페이지에 "webhook configuration을 지우고 재시작하라"는 가이드가 있고, GKE/EKS 환경의 webhook 통신 이슈도 적혀있고, failurePolicy: Ignore 사용에 대한 권장사항도 있다. 우리는 정책 작성법만 읽고 운영 가이드는 안 읽은 거다.
DevOps 일을 하다 보면 "기본값"이라는 단어가 항상 무섭다. 누군가 안전하다고 정해놓은 게 우리 환경에서도 안전한 건 아니다. 특히 admission controller처럼 클러스터 전체 트래픽을 가로채는 컴포넌트에서는 기본값을 한 번 더 의심해야 한다.
혹시 비슷한 사고 겪으신 분 있으면 어떻게 복구하셨는지 댓글 남겨주세요. 우리는 지금도 더 나은 운영 방식을 찾는 중입니다.
'IT > DevSecOps' 카테고리의 다른 글
| Trivy Operator vs Kubescape, 6개월 굴려보고 내린 결정 (0) | 2026.05.30 |
|---|---|
| Falco vs Tetragon, 둘 다 6개월 써본 결정 (0) | 2026.05.28 |
| cert-manager로 Wildcard 인증서 자동화하기 (운영하며 만난 함정들) (0) | 2026.05.24 |
| ClusterSecretStore 쓸 거면 namespaceSelector는 꼭 걸어두자 (0) | 2026.05.23 |
| Tetragon vs Falco, 런타임 보안 뭘 쓸까 (0) | 2026.05.21 |