

작년쯤 Kubernetes 1.30이 풀리고 ValidatingAdmissionPolicy(이하 VAP)가 정식으로 GA되었을 때, 솔직히 의심이 좀 있었다. CEL로 정책을 쓴다는 발상 자체는 깔끔한데, Kyverno 같은 PaC(Policy-as-Code) 엔진이 이미 잘 돌아가는 클러스터에서 굳이 또 한 겹을 더 얹을 필요가 있나 싶었다. 그러다 작년 말쯤 우리 팀에서도 일부 정책을 VAP로 옮기는 실험을 시작했고, 6개월쯤 지난 지금 시점에 정리해두면 좋겠다는 생각이 들었다.
결론부터 적자면, 모든 걸 VAP로 옮길 일은 없다. 그럼에도 옮길 가치가 있는 정책이 분명히 있다. 그 경계가 어디에 있는지가 이 글의 본론이다.
왜 VAP를 썼나
원래 우리 클러스터에서는 Kyverno 하나로 모든 admission 정책을 관리했다. 노드 18대, 워크로드 약 1500개 파드. Kyverno가 webhook 기반이다 보니, API 서버 → admission webhook 왕복 비용이 늘 신경 쓰였다. P99로 보면 일반적인 정책 평가는 5~15ms 정도. 큰 숫자는 아니다. 근데 CI/CD 파이프라인에서 한꺼번에 수십 개 리소스를 apply할 때, 또는 컨트롤러가 spec 변경으로 다량의 update를 쏠 때 누적 지연이 눈에 띄게 보였다. 특히 ArgoCD가 sync wave 끝에서 한꺼번에 200~300개 리소스를 던질 때 webhook 지연이 sync 시간의 30%까지 차지했다.
VAP는 이런 상황에 답이 된다. API 서버 안에서 in-process로 평가되니까 네트워크 왕복이 없고, CEL은 단순한 표현식 평가만 하기 때문에 꽤 빠르다. 평균적으로 1ms 미만. 우리 측정으로는 동일 정책을 VAP로 바꿨을 때 admission 지연이 80~90% 줄었다.
추가로, Kyverno 컨트롤러 자체가 죽거나 webhook 인증서가 만료되면 admission 자체가 막히는 위험이 있다. 작년에 한 번 Kyverno 파드 OOM으로 30분 동안 모든 배포가 막힌 사건이 있었다. (그날 새벽 2시에 깼다.) VAP는 이 단일 장애점을 일부 우회할 수 있다.
어떤 정책을 옮겼나
전부 옮긴 게 아니다. 우리가 옮긴 건 다음 세 가지 부류다.
첫째, 단순 필드 검증. 예를 들어 "모든 Deployment는 resource.requests.cpu가 있어야 한다", "image tag에 latest가 들어가면 안 된다" 같이 입력 리소스 안에서 닫히는(self-contained) 검증.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-resource-requests
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: >
object.spec.template.spec.containers.all(c,
has(c.resources) && has(c.resources.requests) &&
has(c.resources.requests.cpu)
)
message: "모든 컨테이너는 cpu request를 명시해야 한다"
둘째, 레이블/어노테이션 정합성 체크. "owner 레이블이 없는 네임스페이스는 안 된다", "PodDisruptionBudget이 없는 Deployment는 prod 네임스페이스에서 거부한다" 같은 류.
셋째, 변경 차단(immutable field). 예전 Kyverno에서 정의했던 "한 번 설정된 storageClass는 못 바꾼다" 같은 정책. CEL의 oldObject 비교로 깔끔하게 표현된다.
expression: "object.spec.storageClassName == oldObject.spec.storageClassName"
옮기지 않은 것
반대로, 옮길 수 없거나 옮길 가치가 없다고 본 정책도 분명하다.
1) 다른 리소스 참조가 필요한 정책. "이 PVC가 가리키는 StorageClass의 reclaim policy를 보고 판단" 같은 케이스. VAP에 paramKind가 있긴 하지만 임의의 클러스터 객체를 lookup하는 용도는 아니다. Kyverno의 context.apiCall이나 mutate 같은 기능이 훨씬 풍부하다.
2) Mutation이 필요한 정책. VAP는 이름 그대로 "Validating"만 한다. MutatingAdmissionPolicy가 1.32에서 알파로 들어왔지만 아직 운영에서 쓸 만한 단계는 아니다. 사이드카 주입이나 기본값 채우기 같은 mutation은 그대로 Kyverno가 맡는다.
3) 복잡한 정책 라이프사이클이 필요한 것. 정책 위반을 모니터링 대시보드로 보내거나, 리포트를 자동 생성하거나, 예외 정책을 시간 단위로 적용하는 등의 운영적 요구사항이 있는 정책. Kyverno의 Policy Reports가 이 부분에서 훨씬 강하다.
CEL이라는 이름의 함정
CEL은 처음에 쉬워 보인다. 단순한 표현식이라 입문 장벽이 낮다. 근데 막상 쓰다 보면 읽기 어려운 표현식이 금방 생긴다. 특히 nested array를 다룰 때.
object.spec.template.spec.containers.all(c,
c.securityContext.runAsNonRoot == true ||
(has(object.spec.template.spec.securityContext) &&
object.spec.template.spec.securityContext.runAsNonRoot == true)
)
이게 그냥 "컨테이너가 non-root로 실행되어야 한다"인데, pod-level securityContext와 container-level의 inheritance까지 고려하면 표현식이 길어진다. Kyverno의 YAML 정책이 verbose하다고 욕먹지만, 사실 이런 케이스에서는 가독성이 더 낫다.
추가로 CEL은 일반 프로그래밍 언어가 아니다. early return이 없고, 변수 선언은 가능해도 복잡한 분기는 한 표현식 안에서 처리해야 한다. 우리 팀에서 한 명이 농담 삼아 "CEL은 정규식의 사촌 같다"고 했는데 어느 정도 공감했다.
운영 중 만난 함정
옮긴 직후 며칠 동안 두 가지 이슈가 있었다.
failurePolicy: Fail의 의미가 다르다. Kyverno에서 failurePolicy: Fail은 "정책 평가 자체에 실패하면 거부"라는 의미였다. VAP에서도 이름은 같지만, CEL 컴파일 에러가 나면 거부 동작이 우리가 기대한 것과 살짝 달랐다. 정확히 말하면 컴파일 에러는 PolicyBinding 단계에서 잡혀야 한다는 점을 미리 알아두는 게 좋다. 그래서 VAP는 정책 작성 단계에서 kubectl alpha validate (혹은 운영 중에는 dry-run 클러스터)에서 충분히 검증한 뒤에만 적용해야 한다.
validationActions의 Audit이 생각보다 좋다. 처음에 정책 적용할 때 무작정 Deny부터 걸지 말고, Audit + Warn으로 한 주 정도 모니터링하는 워크플로우를 추천한다. Audit 결과는 API 서버 audit log에 들어가니까, 우리 팀은 이걸 Loki로 흘려서 어떤 워크로드가 정책에 걸릴지를 미리 확인했다. 이 단계 없이 바로 Deny로 갔다가 한 번 prod 배포 막힌 적이 있다.
spec:
validationActions: ["Audit", "Warn"] # 도입기에는 이걸로 시작
그래서 어떻게 선택하나
지금 우리 팀의 기준은 대략 이렇다. 새 정책을 만들 때 우선 VAP로 표현 가능한가를 본다. 가능하면 VAP. 표현이 너무 꼬이거나, 외부 lookup, mutation, 정책 리포트가 필요하면 Kyverno. 그래도 애매하면 Kyverno쪽 ValidatingPolicy로 작성한 뒤 Kyverno가 자동으로 VAP를 생성하게 하는 옵션도 있다. 이건 작년 초부터 Kyverno에서 지원하는 기능인데, "둘 다 쓴다"는 결정의 부담을 줄여주긴 한다. 다만 실제로 우리 팀은 이 자동 생성 기능을 쓰진 않았다. 정책의 출처가 둘로 갈리는 게 운영상 더 헷갈렸다.
요약하면 VAP는 "성능과 가용성에 민감한 단순한 검증"에 강하고, Kyverno는 "복잡한 정책 라이프사이클과 mutation까지 포함하는 풀스택 PaC"에 강하다. 양쪽 다 쓰는 hybrid가 우리에겐 가장 현실적이었다.
마무리
VAP가 나온 지 1년 좀 넘었는데, 처음 GA됐을 때의 "이게 Kyverno와 OPA를 대체할 것이다" 같은 글들이 다소 과장이었다는 인상을 받는다. 대체보다는 분담이다. 그리고 그 분담이 의외로 깔끔하다. 새로 클러스터를 세팅한다면 처음부터 두 도구의 역할을 명확히 갈라놓고 시작하는 게 좋겠다.
다음에는 VAP를 GitOps로 관리하면서 겪은 정책 버저닝 문제를 정리해볼까 한다. 정책 변경이 곧 인증/거부의 기준 변경이라, 일반 manifest 배포보다 신경 쓸 게 좀 많다.
혹시 다른 PaC 엔진(예: jsPolicy, kubewarden)과 VAP를 같이 쓰는 분 계시면 어떻게 역할 분담했는지 댓글로 알려주시면 좋겠다.
'IT > Kubernets' 카테고리의 다른 글
| ingress-nginx EOL 이후, ingress2gateway로 Gateway API 옮기기 (0) | 2026.05.01 |
|---|---|
| Cilium Hubble로 Network Policy 디버깅하기 (0) | 2026.04.30 |
| Pod이 OOMKill 되기 직전, kernel과 kubelet이 보는 것 (0) | 2026.04.30 |
| KEDA SQS scaler 도입했다가 thrashing에 한참 데인 이야기 (0) | 2026.04.29 |
| Karpenter consolidationPolicy, 이거 한 번은 짚고 가자 (0) | 2026.04.28 |