K8s 고급 #3 Admission Controller — OPA Gatekeeper / Kyverno
K8s 고급 시리즈의 세 번째 글입니다. 중급 #7에서 RBAC이 K8s API의 권한을, NetworkPolicy가 Pod 사이 트래픽을, ResourceQuota가 자원 합계를 통제한다고 정리했습니다. 이번 글의 주제는 그 위에 한 층 더 얹히는 정책의 결입니다 — 매니페스트 자체의 모양을 강제하는 정책입니다. “limits 없는 컨테이너는 만들 수 없다”, “이미지는 우리 ECR 레지스트리에서만 받아야 한다”, “모든 워크로드에 owner 라벨이 필수다” 같은 규칙은 RBAC만으로는 표현할 수 없습니다. 이런 규칙이 들어가는 곳이 K8s API 서버의 Admission 단계이고, 그 단계에 정책 엔진을 끼워 넣는 두 도구가 OPA Gatekeeper와 Kyverno입니다.
이번 시리즈는 K8s 고급 6편입니다.
- #1 CNI 깊이 — Calico / Cilium / eBPF
- #2 RBAC / ServiceAccount 깊이 — Aggregated ClusterRole / Impersonation / IRSA / Workload Identity
- #3 Admission Controller — OPA Gatekeeper / Kyverno ← 이번 글
- #4 CRD와 Operator 패턴 — controller-runtime
- #5 옵저버빌리티 — Prometheus / Grafana / Loki / OpenTelemetry
- #6 GitOps — ArgoCD / Flux
Admission 단계 — 매니페스트가 etcd로 들어가기 직전 #
kubectl apply -f my-pod.yaml을 친 순간부터 그 매니페스트가 etcd에 저장되기까지의 흐름은 단순한 한 줄이 아닙니다. K8s API 서버는 다음 다섯 단계를 차례로 통과시킵니다.
1. 인증 (Authentication) — 누가 호출했는가
2. 권한 (Authorization) — RBAC 검사. 호출자가 이 동사,자원을 쓸 수 있는가
3. Mutating Admission — 매니페스트를 변형 (defaulting, sidecar 주입 등)
4. Validating Admission — 매니페스트가 정책을 만족하는가
5. etcd 저장3, 4단계가 이번 글의 주제인 Admission Controller입니다. 인증과 RBAC을 통과한 요청이라도 이 단계에서 거부될 수 있고, 저장되기 전에 매니페스트 자체가 변형될 수 있습니다.
Mutating vs Validating #
두 종류의 차이는 명확합니다.
- Mutating Admission — 매니페스트를 변형합니다. 예: 모든 Pod에 sidecar 컨테이너를 자동 주입, 누락된 라벨 자동 채우기, 기본값 적용. 같은 객체에 여러 mutating controller가 차례로 적용될 수 있습니다.
- Validating Admission — 매니페스트를 검사만 합니다. 통과 또는 거부. 변형은 일어나지 않습니다. 모든 mutating이 끝난 뒤의 최종 매니페스트를 본다는 점이 중요합니다.
순서는 항상 mutating → validating입니다. 변형이 모두 끝난 매니페스트가 검사 단계에 넘어가므로, validating 룰은 “최종 형태가 정책을 만족하는가"만 평가하면 됩니다.
빌트인 Admission Controller #
K8s API 서버 안에는 이미 여러 admission controller가 컴파일되어 있습니다. 운영 클러스터에서 자주 만나는 것들을 짚어 두겠습니다.
| 컨트롤러 | 종류 | 역할 |
|---|---|---|
NamespaceLifecycle | Validating | 삭제 중인 네임스페이스에 객체 생성 차단 |
LimitRanger | Mutating + Validating | LimitRange의 기본값 적용 + 위반 거부 |
ResourceQuota | Validating | ResourceQuota 합계 초과 시 거부 |
ServiceAccount | Mutating | Pod에 default ServiceAccount 자동 부착 |
PodSecurity | Validating | Pod Security Standards 강제 (1.25+ stable) |
DefaultStorageClass | Mutating | PVC에 기본 SC 자동 채우기 |
중급 #7에서 다룬 ResourceQuota와 LimitRange가 실제로 동작하는 단계가 이 admission 단계입니다. 매니페스트가 ResourceQuota의 합계를 넘기면 ResourceQuota admission controller가 4단계에서 거부합니다. 빌트인 컨트롤러는 --enable-admission-plugins API 서버 플래그로 활성화,비활성화됩니다.
Webhook — 외부에서 admission 단계에 끼어들기 #
빌트인 컨트롤러는 K8s 코드에 내장되어 있어서 사용자가 정의를 바꿀 수 없습니다. 운영 팀이 자기만의 정책을 admission 단계에 끼워 넣고 싶으면 Webhook을 씁니다. 두 종류가 있습니다.
MutatingWebhookConfiguration— 외부 HTTP 서비스에 매니페스트를 보내고, 그 서비스가 변형된 매니페스트를 돌려줍니다.ValidatingWebhookConfiguration— 외부 HTTP 서비스에 매니페스트를 보내고, 그 서비스가 allow/deny를 돌려줍니다.
K8s API 서버는 이 webhook의 호출 결과를 보고 요청을 그대로 통과시키거나 변형하거나 거부합니다. OPA Gatekeeper와 Kyverno는 둘 다 이 webhook 메커니즘 위에 얹힌 정책 엔진입니다. K8s에 새로운 admission 종류를 추가하는 것이 아니라, 표준 webhook을 잘 쓰도록 추상화한 도구입니다.
OPA Gatekeeper — Rego로 표현하는 정책 #
OPA(Open Policy Agent)는 K8s 외부에도 쓰이는 범용 정책 엔진입니다. Rego라는 자체 언어로 정책을 적고, OPA 엔진이 그 정책을 평가합니다. Gatekeeper는 OPA를 K8s admission webhook으로 감싼 도구입니다.
Gatekeeper의 핵심 객체는 둘입니다.
ConstraintTemplate— Rego로 적은 정책의 청사진. “이런 종류의 정책을 정의한다”Constraint— ConstraintTemplate의 인스턴스. “이 정책을 어떤 자원에 어떤 매개변수로 적용한다”
이 둘의 분리가 Gatekeeper의 모델입니다. 정책의 “형태"는 ConstraintTemplate에 한 번 적어 두고, 그 형태에 매개변수를 넣어 여러 번 인스턴스화하는 방식입니다.
ConstraintTemplate 예시 — 필수 라벨 강제 #
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
required := input.parameters.labels
provided := input.review.object.metadata.labels
missing := required[_]
not provided[missing]
msg := sprintf("Missing required label: %v", [missing])
}rego 블록 안의 코드가 진짜 정책입니다. input.review.object는 admission 단계의 매니페스트이고, input.parameters는 Constraint에서 넘긴 매개변수입니다. violation[...]이 비어 있지 않으면 매니페스트가 거부됩니다. ConstraintTemplate을 적용하면 K8s에 K8sRequiredLabels라는 새 CRD가 생깁니다.
Constraint 예시 — 위 템플릿의 인스턴스 #
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: namespace-must-have-owner
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
parameters:
labels: ["owner", "team"]이 Constraint를 적용하면 그 순간부터 새로 만드는 모든 Namespace에 owner와 team 라벨이 없으면 admission 단계에서 거부됩니다.
$ kubectl create ns test
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request:
[namespace-must-have-owner] Missing required label: owner
[namespace-must-have-owner] Missing required label: teamGatekeeper의 부가 기능 #
Gatekeeper에는 정책 평가 외에 몇 가지 운영 친화 기능이 있습니다.
- dry-run / audit 모드 — Constraint의
enforcementAction: dryrun으로 적용하면 거부하지 않고 violation만 기록합니다. 정책을 실서비스에 강제 적용하기 전에 영향 범위를 측정하는 데 씁니다. Config객체로 평가 대상 제한 —kube-system같은 시스템 네임스페이스를 평가에서 제외할 수 있습니다.- 외부 데이터 referrer — Constraint가 OPA의
data객체를 참조해 다른 K8s 객체나 외부 데이터를 보고 정책을 평가할 수 있습니다.
Kyverno — YAML로 표현하는 정책 #
Kyverno는 OPA Gatekeeper와 같은 카테고리의 도구이지만 접근이 다릅니다. 새 언어를 배우지 않고 YAML로 정책을 적는다는 게 Kyverno의 가장 큰 차별점입니다. K8s 사용자는 이미 YAML에 익숙하므로, 정책 도입의 진입 장벽이 낮습니다.
Kyverno의 세 가지 동작 #
Kyverno의 정책은 셋 중 하나(또는 그 이상)를 합니다.
- validate — 매니페스트가 규칙을 만족하는지 검사 (Validating Admission)
- mutate — 매니페스트를 변형 (Mutating Admission)
- generate — 다른 객체를 자동으로 만들어 냄 (Kyverno만의 기능)
generate는 admission 단계 자체의 동작은 아니지만, “Namespace가 만들어지면 그 안에 기본 NetworkPolicy를 자동 생성” 같은 패턴을 한 정책으로 표현합니다.
Validate 예시 — limits 없는 컨테이너 거부 #
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits
spec:
validationFailureAction: Enforce
rules:
- name: require-cpu-memory-limits
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Pod must have CPU and memory limits."
pattern:
spec:
containers:
- resources:
limits:
memory: "?*"
cpu: "?*"pattern 안의 ?*은 “어떤 값이든 좋지만 비어 있으면 안 된다"는 뜻입니다. 이 정책이 적용되면 모든 새 Pod의 모든 컨테이너에 limits.cpu와 limits.memory가 모두 적혀 있어야 합니다. 중급 #4에서 다룬 자원 모델을 admission 차원에서 강제하는 패턴입니다.
Mutate 예시 — 모든 Pod에 라벨 자동 추가 #
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-default-labels
spec:
rules:
- name: add-managed-by
match:
any:
- resources:
kinds: ["Deployment", "StatefulSet"]
mutate:
patchStrategicMerge:
metadata:
labels:
managed-by: platform-team이 정책이 적용되면 매니페스트에 managed-by 라벨이 없어도 admission 단계에서 자동으로 추가됩니다. 코드 한 줄 안 바꾸고 라벨 표준을 강제하는 길입니다.
Gatekeeper vs Kyverno — 둘 중 무엇을 쓸까 #
두 도구의 비교를 한 표로 정리하겠습니다.
| 차원 | OPA Gatekeeper | Kyverno |
|---|---|---|
| 정책 언어 | Rego (새로 배워야 함) | YAML |
| 표현력 | 매우 높음 (튜링 완전한 Rego) | 보통 (선언적 패턴 매칭) |
| 학습 곡선 | 가파름 | 낮음 |
| 정책 동작 | validate, mutate (1.0+) | validate, mutate, generate, cleanup |
| K8s 외 정책 | OPA 자체는 K8s 외에도 사용 가능 | K8s 전용 |
| 정책 라이브러리 | 풍부 (gatekeeper-library) | 풍부 (kyverno/policies) |
선택의 결은 보통 다음 결을 따라갑니다.
- Rego의 학습 부담을 감수할 수 있고, 같은 정책 엔진을 K8s 외에도 쓰고 싶다면 Gatekeeper가 자연스럽습니다. 큰 조직에서 정책 일관성을 여러 시스템에 걸쳐 가져갈 때 유리합니다.
- K8s 운영 팀이 정책을 직접 쓰고 유지보수하길 원한다면, 정책 작성 자체의 진입 장벽이 가장 큰 비용이라면 Kyverno가 빠릅니다. 도입 첫 달의 학습 비용 차이가 큽니다.
두 도구 모두 운영 규모로 굴러간 실적이 충분합니다. 표현력이 압도적으로 필요한 경우가 아니라면 Kyverno를 먼저 고려하고, Rego의 표현력이 정말 필요해진 시점에 Gatekeeper로 넘어가는 흐름도 자연스럽습니다.
운영 시 잡아 둘 원칙 #
Admission webhook을 도입할 때 운영 측면에서 반드시 잡아 두어야 하는 원칙 몇 가지를 짚겠습니다.
1. failurePolicy의 두 선택 — Fail vs Ignore #
webhook을 호출할 수 없을 때(타임아웃, 네트워크 단절, 정책 엔진 Pod 다운) API 서버가 어떻게 행동할지를 정하는 필드입니다.
failurePolicy: Fail— webhook이 응답 못 하면 요청 거부. 정책이 우회되는 일이 없지만, 정책 엔진의 가용성이 클러스터 전체의 가용성과 묶입니다. 정책 엔진이 죽으면 새 워크로드를 띄울 수 없습니다.failurePolicy: Ignore— webhook이 응답 못 하면 그냥 통과. 가용성은 좋지만 정책이 우회됩니다.
운영의 정공법은 중요 정책은 Fail, 부수적 정책은 Ignore로 갈라 두는 것입니다. 그리고 정책 엔진 자체를 다중화(replicas 2 이상)하고 PDB로 보호하는 게 기본입니다.
2. namespaceSelector로 시스템 네임스페이스 제외 #
kube-system, kube-public 같은 K8s 자체 워크로드가 도는 네임스페이스는 보통 정책 평가에서 제외합니다. 클러스터 부팅 자체가 정책에 막히는 사고를 방지하는 안전장치입니다.
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: ["kube-system", "kube-public", "kube-node-lease"]3. dry-run으로 점진 도입 #
운영 클러스터에 새 정책을 곧장 enforce 모드로 적용하면 기존 워크로드의 update가 줄줄이 깨질 수 있습니다. 표준 흐름은 다음과 같습니다.
1. dry-run 모드로 적용 (Gatekeeper의 dryrun, Kyverno의 Audit)
2. 일정 기간 violation 로그 수집 → 영향 범위 측정
3. 위반 워크로드를 우선 정리
4. enforce 모드로 전환이 사이클을 건너뛰면 기존 매니페스트가 줄줄이 거부되어 GitOps 동기화가 멈추는 사고로 이어집니다. 정책의 의도가 옳더라도 도입 절차는 점진적이어야 합니다.
4. webhook latency 모니터링 #
Admission webhook은 모든 매니페스트 변경의 critical path에 있습니다. 정책 엔진이 느려지면 kubectl apply도 같이 느려집니다. Gatekeeper / Kyverno 모두 자체 메트릭을 노출하므로, P99 latency와 거부율을 #5에서 다룰 옵저버빌리티 스택에 묶어 두는 게 표준입니다.
마무리 #
K8s API 서버의 admission 단계와 그 위에 얹히는 정책 엔진을 정리했습니다. 매니페스트가 etcd에 저장되기 직전의 5단계 중 mutating,validating admission이 정책의 진입점이고, K8s에 빌트인되어 있는 컨트롤러 외에 webhook으로 외부 정책 엔진을 끼워 넣을 수 있다는 모델을 따라갔습니다. 그 외부 엔진의 두 표준이 OPA Gatekeeper와 Kyverno이고, 표현력의 Gatekeeper 대 진입 장벽의 Kyverno라는 결을 비교했습니다. 마지막으로 failurePolicy / 시스템 네임스페이스 제외 / dry-run 도입 / webhook latency 모니터링까지 운영 원칙 넷을 짚었습니다. 다음 글에서는 K8s API 자체를 확장하는 길 — CRD로 새 객체 종류를 정의하고 controller-runtime으로 그 객체를 운영하는 Operator 패턴을 다루겠습니다.