
올해 2월에 Kyverno 1.17이 나오면서 재밌는 변화가 하나 있었다. ClusterPolicy와 CleanupPolicy가 공식적으로 Deprecated로 마킹된 것. 아직 지워지진 않았지만, Kyverno 팀이 CEL(Common Expression Language) 중심 아키텍처로 방향을 확 틀었다는 신호다. 우리 팀은 지난달부터 조금씩 이 전환을 시작했는데, 처음 시작하는 팀들을 위해 몇 가지 정리해두려고 한다.
왜 갑자기 CEL인가
간단히 말하면, Kubernetes 자체가 1.30부터 ValidatingAdmissionPolicy(VAP)를 GA로 넣으면서 admission control 계층에 CEL 기반 표현식을 표준으로 밀기 시작했다. Kyverno가 오랫동안 자체 DSL로 정책을 표현해 왔는데, 이걸 계속 유지하면 결국 Kubernetes 표준과 파편화된다. 그래서 Kyverno 1.17에서는 CEL을 1급 시민으로 승격시키고, 기존 ClusterPolicy는 sunset 단계에 진입한 상태다.
실무자 입장에서 얻는 이점은 두 가지다. 하나는 정책 문법이 K8s API 다른 곳(예: CustomResourceDefinition의 x-kubernetes-validations)과 통일된다는 것. 다른 하나는 CEL이 컴파일 타임에 문법 검증이 되기 때문에 오탈자로 인한 사고가 줄어든다.
기존 ClusterPolicy를 CEL로 옮겨보기
우리 팀이 가장 먼저 옮긴 정책은 "모든 Pod은 resources.requests를 반드시 지정해야 한다"였다. 예전 Kyverno DSL로는 이렇게 썼다.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-requests
spec:
validationFailureAction: Enforce
rules:
- name: check-requests
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "resources.requests는 필수입니다"
pattern:
spec:
containers:
- resources:
requests:
memory: "?*"
cpu: "?*"
이걸 Kubernetes 표준 VAP로 옮기면 이렇게 된다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-requests
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: >
object.spec.containers.all(c,
has(c.resources) &&
has(c.resources.requests) &&
has(c.resources.requests.memory) &&
has(c.resources.requests.cpu))
message: "resources.requests.cpu/memory는 필수입니다"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-requests-binding
spec:
policyName: require-requests
validationActions: [Deny]
matchResources:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: ["kube-system", "kube-public"]
VAP는 두 리소스가 세트다. 정책 자체(ValidatingAdmissionPolicy)와 어디에 적용할지 붙이는 바인딩(ValidatingAdmissionPolicyBinding). 처음엔 좀 번거로워 보이지만, 정책을 재사용하기 쉽다는 장점이 있다. 같은 정책을 stage와 prod에 서로 다른 namespace selector로 바인딩할 수 있다.
Kyverno의 CEL 지원은 어디까지 왔나
Kyverno 1.17 기준으로 새 정책 타입인 ValidatingPolicy와 MutatingPolicy가 도입됐다. VAP와 거의 같지만 Kyverno 특유의 기능(exceptions, image verification, generate 등)이 통합된 상위 집합이라고 보면 된다.
apiVersion: policies.kyverno.io/v1alpha1
kind: ValidatingPolicy
metadata:
name: disallow-latest-tag
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: >
object.spec.containers.all(c,
!c.image.endsWith(":latest") &&
c.image.contains(":"))
message: "이미지에 :latest 태그는 금지, 명시적 태그를 쓰세요"
validationActions: [Deny, Audit]
우리 팀 사례로는, 순수 K8s VAP만 쓸지 Kyverno의 ValidatingPolicy를 쓸지 정할 때 판단 기준을 이렇게 잡았다. 정책이 단순 validation이면 VAP, 이미지 서명 검증이나 리소스 자동 생성이 필요하면 Kyverno. 도구를 하나로 통일하고 싶어도 현실적으로 VAP만으로 커버 안 되는 영역이 아직 꽤 있다.
마이그레이션 순서 잡기
정책 수가 많으면 한 번에 다 옮기려다가 실패한다. 우리는 이렇게 진행 중이다.
- 기존 ClusterPolicy 목록 뽑고 카테고리 분류 (security, resource-limits, naming-convention, image-policy 등)
validationFailureAction: Audit으로 되어 있는 순수 검증 정책부터 VAP로 이관Enforce로 되어 있는 정책은 새 CEL 정책과 기존 정책을 병행 배포 후 로그 비교- 이미지 검증, generate, mutation 정책은 Kyverno 유지
- 마지막에 기존 ClusterPolicy 제거
3번이 좀 귀찮다. Kyverno의 policy report와 VAP의 audit annotation을 크로스 체크해야 하는데, 우리는 임시로 Grafana에 두 개 카운트를 나란히 띄워두고 며칠 관찰한 다음 넘어갔다.
놓치기 쉬운 점
CEL은 표현력이 강하지만 함수가 K8s DSL이랑 다르다. 특히 has() 함수를 안 쓰고 바로 필드에 접근하면 존재하지 않는 경우 에러가 난다. 위 예제에서 has(c.resources)를 반드시 앞에 넣은 이유다. 처음 몇 번은 Deny가 아니라신 error로 튀어서 당황할 수 있다.
또 하나, VAP는 dry-run이 좀 제한적이다. Kyverno의 CLI(kyverno test)만큼 편한 도구가 아직 표준으로 없어서, 정책 리뷰 프로세스를 어떻게 잡을지 팀에서 고민이 필요하다. 우리는 GitHub Actions에서 kind 클러스터 띄우고 정책 적용 후 sample manifest로 검증하는 스크립트를 돌리고 있다.
마무리
Kyverno 팀이 CEL로 방향을 튼 건 결국 Kubernetes 생태계의 표준을 따라간다는 시트드 뀠인�으로는 옳은 결정이라고 본다. 다만 아직 sunset 단계지 제거 단계는 아니라서, 서두를 필요는 없다. 우선 새로 만드는 정책부터 CEL로 쓰기 시작하고, 기존 정책은 카테고리별로 천천히 옮기는 걸 추천한다.
혹시 Kyverno에서 VAP로 이관하면서 겪은 이슈 있으신 분들 댓글로 공유해주시면 좋겠다. 우리도 아직 배우는 중이다.
'IT > DevSecOps' 카테고리의 다른 글
| External Secrets Operator, IRSA로 시작하는 실전 가이드 (1) | 2026.08.13 |
|---|---|
| Vault vs AWS Secrets Manager, 우리 팀은 왜 하이브리드로 갔나 (0) | 2026.08.08 |
| ClusterSecretStore 하나가 죽으니 클러스터가 조용해졌다 (0) | 2026.08.03 |
| External Secrets Operator vs Vault Agent Injector, 뭘 쓸까 (0) | 2026.07.29 |
| SPIRE Federation trust bundle 붙이다가 사흘 태운 이야기 (0) | 2026.07.18 |