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편입니다.

Admission 단계 — 매니페스트가 etcd로 들어가기 직전 #

kubectl apply -f my-pod.yaml을 친 순간부터 그 매니페스트가 etcd에 저장되기까지의 흐름은 단순한 한 줄이 아닙니다. K8s API 서버는 다음 다섯 단계를 차례로 통과시킵니다.

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가 컴파일되어 있습니다. 운영 클러스터에서 자주 만나는 것들을 짚어 두겠습니다.

컨트롤러종류역할
NamespaceLifecycleValidating삭제 중인 네임스페이스에 객체 생성 차단
LimitRangerMutating + ValidatingLimitRange의 기본값 적용 + 위반 거부
ResourceQuotaValidatingResourceQuota 합계 초과 시 거부
ServiceAccountMutatingPod에 default ServiceAccount 자동 부착
PodSecurityValidatingPod Security Standards 강제 (1.25+ stable)
DefaultStorageClassMutatingPVC에 기본 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 예시 — 필수 라벨 강제 #

constrainttemplate-required-labels.yaml
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 예시 — 위 템플릿의 인스턴스 #

constraint-require-owner.yaml
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에 ownerteam 라벨이 없으면 admission 단계에서 거부됩니다.

라벨 없는 Namespace 생성 시도
$ 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: team

Gatekeeper의 부가 기능 #

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 없는 컨테이너 거부 #

policy-require-limits.yaml
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.cpulimits.memory가 모두 적혀 있어야 합니다. 중급 #4에서 다룬 자원 모델을 admission 차원에서 강제하는 패턴입니다.

Mutate 예시 — 모든 Pod에 라벨 자동 추가 #

policy-add-labels.yaml
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 GatekeeperKyverno
정책 언어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 자체 워크로드가 도는 네임스페이스는 보통 정책 평가에서 제외합니다. 클러스터 부팅 자체가 정책에 막히는 사고를 방지하는 안전장치입니다.

webhook의 namespaceSelector 예시
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 패턴을 다루겠습니다.

X