Kyverno, Gatekeeper 다 좋다. 근데 정책 몇 개 걸겠다고 컨트롤러 하나 더 굴리는 게 부담스러울 때가 있다. 특히 규모가 작은 클러스터거나, 정책 자체가 단순해서 굳이 별도 프로세스가 필요없는 상황.
우리 팀도 최근에 개발 클러스터 몇 개를 정리하면서 Kyverno를 걷어냈다. 대신 K8s 1.30부터 GA된 ValidatingAdmissionPolicy(VAP)로 옮겼는데, 생각보다 부드러웠다. 이 글은 그 과정에서 정리한 실전 도입 가이드다.
왜 VAP를 고려하나
CEL 기반 in-tree 어드미션이라는 건데, 정리하면 이렇다.
- 웹훅 서버가 없다. apiserver 안에서 CEL을 평가한다. 별도 파드/서비스/인증서 관리 불필요.
- 네트워크 hop이 없으니 지연이 거의 없다. 웹훅 timeout 걸려서 잡 실패하는 새벽 알람이 사라진다.
- CEL은 상대적으로 학습곡선이 낮다. JSONPath 아는 사람이면 반나절이면 된다.
반대로, Kyverno의 mutate/generate 같은 건 못 한다. VAP는 이름 그대로 validate만 담당한다. mutate가 필요하면 MutatingAdmissionPolicy(1.32에서 알파, 1.34부터 베타)를 봐야 한다. 이 부분은 팀 내부에서도 논의가 있었는데, "mutate 정책은 어차피 CI에서 lint로 잡자"고 정리했다.
3분짜리 정책 예제
라벨 강제 정책을 하나 만들어보자. 모든 Deployment에 team 라벨이 있어야 한다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-team-label
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "has(object.metadata.labels) && 'team' in object.metadata.labels"
message: "Deployment must have 'team' label"
reason: Invalid
그리고 이걸 실제로 적용할 스코프를 정하는 바인딩.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-team-label-binding
spec:
policyName: require-team-label
validationActions: [Deny]
matchResources:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: ["kube-system", "kube-public"]
Policy와 Binding이 분리된 게 처음엔 좀 어색한데, 익숙해지면 오히려 편하다. 하나의 정책을 여러 스코프에 다른 액션(Deny/Warn/Audit)으로 재사용할 수 있다.
도입할 때 실제로 걸리는 것들
1. validationActions은 반드시 Audit부터
Deny로 바로 걸었다가 CI 파이프라인 다 죽는 경험, 한 번쯤 해봤을 거다. VAP도 예외가 아니다.
validationActions: [Audit, Warn] # 일단 이걸로 며칠 굴려본다
Audit은 apiserver audit log로 정책 위반이 남는다. Warn은 kubectl 응답에 경고만 표시한다. 며칠 관찰해서 위반 케이스가 없다는 걸 확인한 뒤에 Deny로 승격한다.
우리는 대략 3~5일 관찰하는 규칙을 세웠는데, 팀마다 다를 것이다.
2. CEL 표현식 테스트는 kubectl alpha resource-plugin로
CEL 문법 실수는 apiserver 로그에 뜨긴 하는데, 반복 개발할 때 답답하다. kubectl 1.30+에서는 dry-run 결과가 훨씬 친절해졌다.
kubectl apply --dry-run=server -f bad-deployment.yaml
정책 위반이면 Warning: ValidatingAdmissionPolicy 'require-team-label' ... 이런 식으로 즉시 응답이 온다. 로컬에서 CEL 표현식만 검증하고 싶으면 cel-go 기반 툴인 cel-playground가 편하다.
3. variables 활용해서 반복 줄이기
정책이 몇 개 늘어나면 같은 조건을 반복하게 된다. VAP는 변수를 지원한다.
spec:
variables:
- name: labels
expression: "object.metadata.labels"
- name: hasTeam
expression: "has(variables.labels) && 'team' in variables.labels"
validations:
- expression: "variables.hasTeam"
message: "Deployment must have 'team' label"
읽기도 편하고, 정책 여러 개에서 같은 조건을 참조할 때 실수가 줄어든다.
4. paramKind로 정책 파라미터화
정책 로직은 같은데 값만 팀별로 다르게 하고 싶을 때. 예를 들어 이미지 레지스트리 화이트리스트.
spec:
paramKind:
apiVersion: config.example.com/v1
kind: RegistryPolicy
validations:
- expression: "object.spec.template.spec.containers.all(c, params.allowedRegistries.exists(r, c.image.startsWith(r)))"
message: "Container image must be from allowed registries"
그리고 바인딩에서 실제 파라미터 CR을 참조한다.
spec:
policyName: image-registry-policy
paramRef:
name: team-a-registries
parameterNotFoundAction: Deny
CustomResourceDefinition 하나 만들어야 하는 게 오버헤드긴 한데, 다중 팀 환경에서 정책 자체를 손대지 않고 팀별 튜닝이 가능해진다.
Kyverno에서 넘어올 때 체크리스트
우리가 실제로 옮기면서 정리한 목록이다.
- mutate/generate 룰이 있나? 있으면 못 옮긴다. 그 룰만 Kyverno에 남기거나 CI 단계로 옮기는 결정 필요.
- background scan에 의존하는가? VAP도 매치 시점의 리소스에 대해서만 평가한다. 이미 클러스터에 있는 위반 리소스는 안 잡힌다. 별도 스캔 툴(Trivy operator, Kubescape)로 보와.
- 정책 위반 리포팅. Kyverno의 PolicyReport 같은 표준 리포트가 없다. Audit 로그를 Loki든 CloudWatch든으로 보내서 그라파나에서 대시보드 만드는 게 현실적이다.
- 이미지 검증(imagepullpolicy, 서명 확인 등) 은 VAP만으로는 부족하다. 이건 Kyverno 남기거나 Sigstore policy-controller 같은 걸 쓴다.
마무리
정리하면, VAP는 "간단한 validate 정책만 있으면 Kyverno 안 써도 된다"는 옵션이 하나 생긴 셈이다. 완전한 대체는 아니고, 하이브리드(Kyverno + VAP)로 가는 팀도 있을 것 같다.
우리 팀 개발 클러스터에서는 지난 6주간 문제없이 돌고 있다. 프로덕션 쪽은 아직 Kyverno를 유지 중인데, 다음 분기에 하나씩 옮기는 걸 검토 중이다. 옮기고 나면 후속 글로 정리해보겠다.
혹시 VAP 프로덕션에서 이미 굴리시는 분 있으면 어떤 함정 만났는지 궁금하다.
태그: Kubernetes, CEL, admission-controller, 어드미션, DevOps, Kyverno
'IT > Kubernets' 카테고리의 다른 글
| Kubernetes 1.34 PreferSameNode 내부 동작 — EndpointSlice 힌트가 하는 일 (0) | 2026.09.11 |
|---|---|
| Karpenter가 새벽마다 노드를 뽑아버린 이유 (0) | 2026.09.10 |
| kubectl debug의 --profile 옵션, 이거 모르는 분 꽤 많더라 (0) | 2026.09.10 |
| KEDA HTTP add-on으로 워커 아닌 웹서비스를 scale-to-zero 하는 법 (0) | 2026.09.09 |
| Pod가 idle인데도 throttling 되는 이유, 사실 내부적으로는 (0) | 2026.09.08 |