ValidatingAdmissionPolicy vs Kyverno, 우리 팀은 이렇게 선을 그었다
정책 엔진 이야기다. 두 달 전쯤 팀 내부에서 Kyverno로 다 밀어붙이던 policy를 재정비하기로 했다. 계기는 별거 아니다. 신규 클러스터를 하나 더 세팅하는데 Kyverno controller 파드가 죽었다는 알람이 새벽에 두 번 울렸고, 새벽에 눈 뜬 내가 생각했다. "이거, 정말 다 Kyverno여야 되나."
Kubernetes 1.30에서 ValidatingAdmissionPolicy(이하 VAP)가 GA된 게 2024년 4월이다. 지금은 2026년 중반이니 GA된 지 2년이 넘었고, EKS든 GKE든 안정 채널에서 이미 쓸 수 있는 상태다. CEL 기반이라 API 서버 안에서 바로 평가되고, 웹훅이 없다. 이 두 가지가 결국 우리 결정의 핵심이 됐다.
Kyverno 하나로 밀어붙였을 때의 문제
Kyverno는 훌륭한 도구다. YAML 기반이라 진입장벽이 낮고, mutate/validate/generate/verifyImages까지 커버한다. 그래서 우리는 자연스럽게 라벨 강제, 리소스 요청/제한, PodSecurityStandard 관련 룰, 이미지 서명 검증까지 전부 Kyverno에 몰아넣었다.
문제는 웹훅이다. Kyverno는 결국 웹훅 서버다. 웹훅 서버가 죽으면 어떻게 되냐. failurePolicy 설정에 따라 다르지만, 우리 팀은 대부분 Ignore였다. 그 말은 controller가 죽어 있는 순간 정책이 없는 클러스터가 된다는 뜻이다. failurePolicy를 Fail로 두면? 그러면 이번엔 정책 위반이 아니라 클러스터 자체가 admission을 못 해서 배포가 다 막힌다. 어느 쪽이든 즐겁지 않다.
또 하나는 성능. 지난 분기 노드 60대 정도 되는 클러스터에서 대량 롤아웃 도중 Kyverno 웹훅 응답이 200ms 이상 튀는 걸 몇 번 봤다. 정책 수가 늘어나면 자연스레 웹훅 부담도 올라간다. 그리고 이 웹훅 응답은 admission chain의 P99를 그대로 올린다.
VAP는 뭐가 다른가
VAP는 kube-apiserver 안에서 CEL로 평가된다. 웹훅이 없다. TLS 인증서도, controller 파드도, 재시작 스토리도 없다. 그냥 CR 하나만 있으면 된다. 예를 들어 "namespace label에 team이 반드시 있어야 한다"는 룰이면 이렇게 쓴다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-team-label
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["namespaces"]
validations:
- expression: "has(object.metadata.labels) && 'team' in object.metadata.labels"
message: "namespace에 team 라벨이 필요합니다"
문법은 CEL이다. Rego처럼 낯설지도 않고, YAML도 아닌 표현식이라 오히려 짧다. 그리고 이건 웹훅이 아니라서 apiserver가 살아 있는 한 항상 동작한다.
우리가 그은 경계선
결국 이렇게 나눴다.
VAP가 담당하는 것: 라벨/어노테이션 강제, 필드 형식 검증, 이미지 레지스트리 allowlist(단순 prefix 매칭), replicas 상한, resource requests/limits 강제, hostPath 금지 같은 securityContext 룰. 요컨대 "리소스 자체 필드만 보고 판단 가능한 룰"은 전부 VAP다. 이게 우리 룰의 대략 70% 정도였다.
Kyverno가 계속 담당하는 것: cosign 이미지 서명 검증(외부 호출 필요), NetworkPolicy 자동 생성(mutate/generate), 다른 리소스 상태를 참조해야 하는 룰(예: 이 파드의 서비스어카운트가 존재하는지 크로스 룩업), 이미지 태그가 latest면 warning 남기고 특정 팀 예외 처리 같은 복잡한 조건 분기.
이 경계는 CEL의 근본적인 한계에서 나온다. CEL은 외부 호출을 할 수 없고 상태를 유지할 수 없다. 이미지 서명 검증은 registry로 나가서 signature bundle을 검증해야 하니 CEL로는 불가능하다. 크로스 리소스 룩업도 마찬가지다. 그래서 "웹훅이 꼭 필요한 것만 Kyverno"라는 원칙으로 갔다.
이관하면서 겪은 것들
CEL 문법은 한 번만 익히면 별거 없는데, 처음엔 자꾸 실수한다. has()로 감싸지 않고 바로 필드 접근해서 nil 참조 에러 내는 실수, object와 oldObject를 헷갈리는 실수. UPDATE에서 특정 필드 변경 금지 룰을 짤 땐 oldObject를 써야 한다.
그리고 하나 알아둘 것. VAP는 GA지만, validationActions에 Warn과 Audit이 있어서 배포 초기엔 Deny 대신 Audit으로 두고 관찰하는 게 훨씬 낫다. 우리도 2주 정도 Audit으로 돌리면서 예외 케이스를 다 잡아낸 뒤 Deny로 승격했다. 이 소프트 롤아웃 경로가 있는 게 Kyverno보다 훨씬 편했다.
한 가지 아쉬웠던 건 정책 테스트다. Kyverno는 kyverno test CLI가 있어서 CI에서 fixture 기반 테스트가 편하다. VAP는 아직 이 부분이 약하다. 결국 우리는 kind 클러스터에 정책 적용하고 시나리오별 CR 던져보는 방식으로 테스트를 짰다. Gatekeeper 3.20에서 VAP 관리 기능이 beta로 들어왔고 3.22에서 sync-vap-enforcement-scope가 기본 활성화됐다는 얘기가 있던데, 이쪽 흐름도 계속 지켜봐야 할 것 같다.
결론이라기보다는
세 달 지나고 나서 돌아보니 웹훅 관련 새벽 알람이 눈에 띄게 줄었다. 이게 VAP 덕분인지, 그냥 룰 개수가 줄어서인지 정확히는 모르겠다. 근데 확실한 건, "정책 엔진 하나로 다 해결하겠다"는 발상 자체가 좀 게을렀다는 것.
Kyverno가 나쁜 게 아니다. 웹훅이 필요한 상황에서 여전히 최고의 선택 중 하나다. 다만 웹훅이 필요없는 룰까지 웹훅에 태우는 건 지금 시점엔 낭비다. 아직 VAP를 안 써본 팀이라면 리소스 필드 검증 룰 하나만이라도 옮겨보길 추천한다. 웹훅 하나 걷어냈다는 감각이 생각보다 홀가분하다.
혹시 다른 방식으로 경계 그은 팀 있으면 댓글로 공유 부탁드립니다.