IT/Kubernets

ValidatingAdmissionPolicy 도입 가이드, Kyverno 없이 CEL로 처리하기

gfrog 2026. 9. 13. 18:16

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에서 넘어올 때 체크리스트

우리가 실제로 옮기면서 정리한 목록이다.

  1. mutate/generate 룰이 있나? 있으면 못 옮긴다. 그 룰만 Kyverno에 남기거나 CI 단계로 옮기는 결정 필요.
  2. background scan에 의존하는가? VAP도 매치 시점의 리소스에 대해서만 평가한다. 이미 클러스터에 있는 위반 리소스는 안 잡힌다. 별도 스캔 툴(Trivy operator, Kubescape)로 보와.
  3. 정책 위반 리포팅. Kyverno의 PolicyReport 같은 표준 리포트가 없다. Audit 로그를 Loki든 CloudWatch든으로 보내서 그라파나에서 대시보드 만드는 게 현실적이다.
  4. 이미지 검증(imagepullpolicy, 서명 확인 등) 은 VAP만으로는 부족하다. 이건 Kyverno 남기거나 Sigstore policy-controller 같은 걸 쓴다.

마무리

정리하면, VAP는 "간단한 validate 정책만 있으면 Kyverno 안 써도 된다"는 옵션이 하나 생긴 셈이다. 완전한 대체는 아니고, 하이브리드(Kyverno + VAP)로 가는 팀도 있을 것 같다.

우리 팀 개발 클러스터에서는 지난 6주간 문제없이 돌고 있다. 프로덕션 쪽은 아직 Kyverno를 유지 중인데, 다음 분기에 하나씩 옮기는 걸 검토 중이다. 옮기고 나면 후속 글로 정리해보겠다.

혹시 VAP 프로덕션에서 이미 굴리시는 분 있으면 어떤 함정 만났는지 궁금하다.


태그: Kubernetes, CEL, admission-controller, 어드미션, DevOps, Kyverno