K8s ValidatingAdmissionPolicy란?
Kubernetes 1.30에서 GA(정식 출시)된 ValidatingAdmissionPolicy는 CEL(Common Expression Language)을 사용해 Webhook 없이 클러스터 내부에서 직접 입학 정책을 검증하는 기능입니다. 기존 ValidatingWebhookConfiguration이나 OPA Gatekeeper 대비 지연 시간 제로, 외부 의존성 제거, YAML만으로 정책 정의가 가능합니다.
왜 ValidatingAdmissionPolicy인가?
기존 Admission 제어 방식과 비교하면 차이가 명확합니다.
| 비교 항목 | Webhook | OPA Gatekeeper | ValidatingAdmissionPolicy |
|---|---|---|---|
| 외부 서비스 필요 | ✅ 필수 | ✅ 컨트롤러 | ❌ 불필요 |
| 정책 언어 | 자유 (코드) | Rego | CEL |
| 레이턴시 | 네트워크 왕복 | 네트워크 왕복 | 인프로세스 |
| 장애 영향 | Webhook 다운 시 전체 차단 | 컨트롤러 장애 | API 서버 내장 |
| K8s 버전 | 1.9+ | 1.16+ | 1.30+ (GA) |
CEL 표현식 기초
CEL은 Google이 개발한 경량 표현식 언어로, K8s API 서버에 내장되어 있습니다. ValidatingAdmissionPolicy에서 사용하는 핵심 변수와 문법을 알아봅니다.
// 핵심 변수
object // 생성·수정 요청의 대상 리소스
oldObject // 업데이트 시 기존 리소스 (CREATE 시 null)
request // AdmissionRequest 메타데이터
params // 파라미터 리소스 (바인딩에서 지정)
namespaceObject // 네임스페이스 객체
authorizer // 권한 확인 유틸리티
// 자주 쓰는 CEL 표현식
object.spec.replicas <= 10 // 숫자 비교
object.metadata.labels.exists(k, k == "team") // 라벨 존재 확인
object.spec.containers.all(c, c.resources.limits.memory != "") // 전체 순회
request.userInfo.username.startsWith("system:") // 문자열 매칭
has(object.metadata.annotations["approved"]) // 필드 존재 확인
실전 1: 리플리카 수 제한 정책
Deployment의 replicas를 최대 20개로 제한하는 정책입니다. ValidatingAdmissionPolicy와 ValidatingAdmissionPolicyBinding 두 리소스를 함께 생성해야 합니다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: max-replicas
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas <= 20"
message: "Deployment replicas는 20개를 초과할 수 없습니다."
reason: Invalid
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: max-replicas-binding
spec:
policyName: max-replicas
validationActions:
- Deny
matchResources:
namespaceSelector:
matchLabels:
environment: production
이 정책은 environment: production 라벨이 붙은 네임스페이스에서만 적용됩니다. validationActions에는 Deny, Warn, Audit 세 가지를 조합할 수 있습니다.
실전 2: 필수 라벨 강제 (파라미터 활용)
파라미터 리소스를 활용하면 하나의 정책으로 팀별·환경별 다른 값을 적용할 수 있습니다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-labels
spec:
failurePolicy: Fail
paramKind:
apiVersion: v1
kind: ConfigMap
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments", "statefulsets"]
validations:
- expression: >
params.data.requiredLabels.split(",").all(label,
has(object.metadata.labels) &&
label in object.metadata.labels
)
messageExpression: >
"필수 라벨 누락: " + params.data.requiredLabels
---
apiVersion: v1
kind: ConfigMap
metadata:
name: label-params-prod
namespace: policy-config
data:
requiredLabels: "team,cost-center,on-call"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-labels-prod
spec:
policyName: require-labels
paramRef:
name: label-params-prod
namespace: policy-config
validationActions:
- Deny
- Audit
ConfigMap의 requiredLabels 값만 바꾸면 팀별로 다른 필수 라벨을 요구할 수 있어 정책 재사용성이 크게 높아집니다.
실전 3: 컨테이너 이미지 레지스트리 화이트리스트
프로덕션 환경에서 허용된 레지스트리의 이미지만 배포할 수 있도록 제한합니다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: allowed-registries
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: >
object.spec.containers.all(c,
c.image.startsWith("registry.company.com/") ||
c.image.startsWith("gcr.io/my-project/")
)
message: "허용되지 않은 컨테이너 레지스트리입니다. registry.company.com 또는 gcr.io/my-project만 사용 가능합니다."
- expression: >
!has(object.spec.initContainers) ||
object.spec.initContainers.all(c,
c.image.startsWith("registry.company.com/") ||
c.image.startsWith("gcr.io/my-project/")
)
message: "initContainers도 허용된 레지스트리만 사용해야 합니다."
- expression: >
object.spec.containers.all(c,
c.image.contains("@sha256:") || c.image.contains(":")
)
message: "이미지 태그 또는 digest를 반드시 지정해야 합니다. latest 암묵 사용 금지."
실전 4: 리소스 Quota 사전 검증
LimitRange와 별개로 컨테이너 단위 리소스 요청 범위를 CEL로 직접 검증하는 패턴입니다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: resource-bounds
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.limits)
)
message: "모든 컨테이너에 resources.requests와 limits를 설정해야 합니다."
- expression: >
object.spec.template.spec.containers.all(c,
quantity(c.resources.requests.memory) >= quantity("64Mi") &&
quantity(c.resources.limits.memory) <= quantity("8Gi")
)
message: "메모리 requests는 64Mi 이상, limits는 8Gi 이하여야 합니다."
- expression: >
object.spec.template.spec.containers.all(c,
quantity(c.resources.requests.cpu) >= quantity("50m") &&
quantity(c.resources.limits.cpu) <= quantity("4")
)
message: "CPU requests는 50m 이상, limits는 4코어 이하여야 합니다."
quantity() 함수는 K8s 리소스 수량 문자열을 비교 가능한 값으로 변환합니다. CEL에서 네이티브로 지원되므로 파싱 로직이 필요 없습니다.
validationActions 전략: Deny vs Warn vs Audit
정책을 점진적으로 롤아웃하는 전략이 중요합니다.
# 1단계: 감사 모드 (기존 리소스 영향 없음)
validationActions: [Audit]
# 2단계: 경고 추가 (사용자에게 알림, 차단은 안 함)
validationActions: [Warn, Audit]
# 3단계: 차단 활성화
validationActions: [Deny, Audit]
Audit 로그는 kube-apiserver의 감사 로그에 기록됩니다. Warn은 kubectl 응답에 경고 헤더를 추가합니다. 프로덕션 도입 시 반드시 Audit → Warn → Deny 순서로 단계적 적용을 권장합니다.
matchConditions로 정밀 필터링
matchConditions를 사용하면 특정 조건에서만 정책을 평가할 수 있습니다.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: no-privileged-except-system
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
matchConditions:
# 시스템 네임스페이스 제외
- name: exclude-system-ns
expression: >
!request.namespace.startsWith("kube-") &&
request.namespace != "istio-system"
# 서비스 어카운트 기반 예외
- name: exclude-ci-sa
expression: >
request.userInfo.username != "system:serviceaccount:ci:deployer"
validations:
- expression: >
object.spec.containers.all(c,
!has(c.securityContext) ||
!has(c.securityContext.privileged) ||
c.securityContext.privileged != true
)
message: "privileged 컨테이너는 허용되지 않습니다."
auditAnnotations로 감사 추적
정책 평가 결과를 API 서버 감사 로그에 구조화된 어노테이션으로 남길 수 있습니다.
spec:
auditAnnotations:
- key: "policy-result"
valueExpression: >
"replicas=" + string(object.spec.replicas) +
" user=" + request.userInfo.username +
" namespace=" + request.namespace
- key: "image-list"
valueExpression: >
object.spec.template.spec.containers.map(c, c.image).join(", ")
이 어노테이션은 audit.k8s.io/v1 이벤트에 포함되어, 누가 어떤 리소스를 어떤 설정으로 배포했는지 추적할 수 있습니다.
OPA Gatekeeper에서 마이그레이션
기존 Gatekeeper ConstraintTemplate을 ValidatingAdmissionPolicy로 전환하는 패턴입니다.
# Before: Gatekeeper Rego
package k8srequiredlabels
violation[{"msg": msg}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("필수 라벨 누락: %v", [missing])
}
# After: ValidatingAdmissionPolicy CEL
validations:
- expression: >
params.data.requiredLabels.split(",").all(label,
label in object.metadata.labels
)
messageExpression: >
"필수 라벨 누락: " + params.data.requiredLabels
마이그레이션 시 주의점:
- CEL은 Rego보다 표현력이 제한적입니다. 복잡한 외부 데이터 참조가 필요하면 Gatekeeper를 유지하세요.
- Mutation은 미지원입니다. 리소스를 수정하려면 여전히 MutatingAdmissionWebhook이 필요합니다.
- 두 방식을 병행 운영할 수 있으므로, 단순 정책부터 점진적으로 전환하세요.
실전 트러블슈팅
ValidatingAdmissionPolicy 운영 시 자주 만나는 문제와 해결법입니다.
# 정책이 적용되지 않을 때: 바인딩 상태 확인
kubectl get validatingadmissionpolicybindings -o wide
# CEL 표현식 오류 디버깅
kubectl describe validatingadmissionpolicy max-replicas
# Status.TypeChecking에 CEL 타입 에러가 표시됨
# 정책 테스트: dry-run으로 사전 검증
kubectl apply -f deployment.yaml --dry-run=server
# Warning 또는 Error 메시지 확인
# 감사 로그에서 정책 결과 확인 (kube-apiserver)
grep "policy-result" /var/log/kubernetes/audit.log | jq .
# failurePolicy: Fail vs Ignore
# - Fail: CEL 평가 오류 시 요청 거부 (안전)
# - Ignore: CEL 평가 오류 시 요청 허용 (가용성 우선)
성능과 한계
ValidatingAdmissionPolicy는 API 서버 프로세스 내에서 CEL을 평가하므로 네트워크 왕복이 없어 1ms 미만의 레이턴시를 보입니다. 하지만 알아야 할 한계도 있습니다.
- CEL 비용 제한: 각 표현식에 런타임 비용 예산이 있어 무한 루프나 과도한 연산을 방지합니다.
- 외부 데이터 참조 불가: CEL은 요청 객체와 파라미터만 접근 가능합니다. 외부 API 호출이 필요하면 Webhook을 사용하세요.
- Mutation 미지원: 검증(Validation)만 가능하며, 리소스 수정은 별도 Webhook이 필요합니다.
- K8s 1.30+ 필수: 이전 버전에서는 beta feature gate를 활성화해야 합니다.
프로덕션 도입 체크리스트
ValidatingAdmissionPolicy를 프로덕션에 도입할 때 확인해야 할 항목입니다.
- K8s 버전 1.30 이상 확인 (
kubectl version) - 기존 Webhook/Gatekeeper 정책과 중복 검증 여부 파악
- Audit → Warn → Deny 단계적 롤아웃
--dry-run=server로 기존 리소스 호환성 검증- 감사 로그 수집 파이프라인에 auditAnnotations 파싱 추가
- CI/CD에서 정책 YAML 린트·테스트 자동화
failurePolicy는 초기에 Ignore, 안정화 후 Fail로 전환
ValidatingAdmissionPolicy는 K8s 네이티브 정책 엔진의 새로운 표준입니다. 외부 도구 없이 선언적으로 클러스터 거버넌스를 구현할 수 있어, OPA Gatekeeper의 경량 대안으로 빠르게 자리잡고 있습니다. 더 자세한 정책 패턴은 K8s OPA Gatekeeper 정책 관리 글과 K8s Pod Security Standards 가이드를 함께 참고하세요.