K8s Admission Webhook 심화

Kubernetes Admission Webhook이란?

Kubernetes Admission Webhook은 API 서버가 리소스를 etcd에 저장하기 전, 요청을 가로채서 검증(Validating)하거나 변형(Mutating)하는 확장 메커니즘입니다. OPA Gatekeeper, Istio sidecar injection, cert-manager 등 핵심 에코시스템이 모두 Admission Webhook 위에 구축되어 있습니다.

이 글에서는 Admission Webhook의 내부 동작 원리부터, Go로 직접 Validating/Mutating Webhook을 구현하고, TLS 인증서 설정, 실전 배포까지 전 과정을 다룹니다.

Admission Controller 파이프라인 이해

kubectl → API Server 요청 흐름에서 Admission 단계는 다음 순서로 실행됩니다:

Authentication → Authorization → Mutating Admission → Object Schema Validation → Validating Admission → etcd Persist

핵심 포인트:

  • Mutating Webhook이 먼저 실행 → 객체를 수정할 수 있음 (sidecar 주입, 기본값 설정)
  • Validating Webhook이 나중에 실행 → 수정 불가, 거부만 가능
  • 하나의 Webhook이 동시에 둘 다는 될 수 없음 — 별도 등록 필요
  • 여러 Webhook이 있으면 순차 실행, 하나라도 거부하면 전체 요청 거부

Webhook 서버 구조 (Go 구현)

Admission Webhook은 결국 HTTPS 엔드포인트입니다. API 서버가 AdmissionReview JSON을 POST로 보내고, 같은 형식으로 응답을 받습니다.

// main.go — Validating Webhook 서버 골격
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"

    admissionv1 "k8s.io/api/admission/v1"
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func validate(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)

    var review admissionv1.AdmissionReview
    json.Unmarshal(body, &review)

    var pod corev1.Pod
    json.Unmarshal(review.Request.Object.Raw, &pod)

    allowed := true
    reason := "OK"

    // 정책: latest 태그 금지
    for _, c := range pod.Spec.Containers {
        if c.Image[len(c.Image)-7:] == ":latest" {
            allowed = false
            reason = fmt.Sprintf(
                "컨테이너 %s: latest 태그 사용 금지. 명시적 버전 태그를 사용하세요.", c.Name)
            break
        }
    }

    review.Response = &admissionv1.AdmissionResponse{
        UID:     review.Request.UID,
        Allowed: allowed,
        Result:  &metav1.Status{Message: reason},
    }
    review.Response.SetGroupVersionKind(
        admissionv1.SchemeGroupVersion.WithKind("AdmissionReview"))

    resp, _ := json.Marshal(review)
    w.Header().Set("Content-Type", "application/json")
    w.Write(resp)
}

func main() {
    http.HandleFunc("/validate", validate)
    http.ListenAndServeTLS(":8443", "/certs/tls.crt", "/certs/tls.key", nil)
}

이 서버는 :latest 태그가 붙은 이미지를 사용하는 Pod 생성을 거부합니다. 실무에서 가장 흔한 보안 정책 중 하나입니다.

Mutating Webhook: Sidecar 자동 주입

Mutating Webhook은 JSON Patch를 통해 객체를 변형합니다. Istio의 sidecar injection이 바로 이 메커니즘입니다.

func mutate(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    var review admissionv1.AdmissionReview
    json.Unmarshal(body, &review)

    var pod corev1.Pod
    json.Unmarshal(review.Request.Object.Raw, &pod)

    // annotation 체크: inject-sidecar=true인 경우만
    if pod.Annotations["example.com/inject-sidecar"] != "true" {
        allow(&review, w)
        return
    }

    // JSON Patch로 sidecar 컨테이너 추가
    patch := []map[string]interface{}{
        {
            "op":   "add",
            "path": "/spec/containers/-",
            "value": map[string]interface{}{
                "name":  "log-sidecar",
                "image": "fluent/fluent-bit:2.2",
                "resources": map[string]interface{}{
                    "requests": map[string]string{
                        "cpu": "50m", "memory": "64Mi",
                    },
                    "limits": map[string]string{
                        "cpu": "100m", "memory": "128Mi",
                    },
                },
            },
        },
    }

    patchBytes, _ := json.Marshal(patch)
    patchType := admissionv1.PatchTypeJSONPatch

    review.Response = &admissionv1.AdmissionResponse{
        UID:       review.Request.UID,
        Allowed:   true,
        Patch:     patchBytes,
        PatchType: &patchType,
    }

    resp, _ := json.Marshal(review)
    w.Header().Set("Content-Type", "application/json")
    w.Write(resp)
}

TLS 인증서 설정 (cert-manager 활용)

API 서버 → Webhook 서버 통신은 반드시 TLS여야 합니다. 가장 깔끔한 방법은 cert-manager의 Certificate 리소스를 사용하는 것입니다.

# certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: webhook-tls
  namespace: webhook-system
spec:
  secretName: webhook-tls-secret
  dnsNames:
    - webhook-svc.webhook-system.svc
    - webhook-svc.webhook-system.svc.cluster.local
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
  duration: 8760h    # 1년
  renewBefore: 720h  # 30일 전 갱신

cert-manager 없이 수동으로 할 경우, openssl로 CA + 서버 인증서를 생성하고 caBundle에 CA 인증서를 Base64 인코딩하여 넣어야 합니다.

Webhook Configuration 등록

Webhook 서버가 준비되면, 클러스터에 등록합니다:

# validating-webhook.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: pod-policy
  annotations:
    cert-manager.io/inject-ca-from: webhook-system/webhook-tls
webhooks:
  - name: pod-policy.example.com
    admissionReviewVersions: ["v1"]
    sideEffects: None
    failurePolicy: Fail        # Webhook 장애 시 요청 거부
    timeoutSeconds: 5
    clientConfig:
      service:
        name: webhook-svc
        namespace: webhook-system
        path: /validate
        port: 443
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
    namespaceSelector:
      matchLabels:
        policy: enforced         # 이 라벨이 있는 NS만 적용

주요 설정 옵션 정리

  • failurePolicy: Fail(기본) vs Ignore — 프로덕션에서는 Ignore로 시작 후 안정화되면 Fail로 전환 권장
  • sideEffects: None이면 dry-run 요청도 통과, 외부 API 호출이 있으면 NoneOnDryRun
  • namespaceSelector / objectSelector: 범위 제한 필수 — 전체 클러스터에 적용하면 kube-system도 걸림
  • reinvocationPolicy: IfNeeded로 설정하면 다른 Mutating Webhook이 변경한 후 재실행

Deployment + Service 매니페스트

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webhook-server
  namespace: webhook-system
spec:
  replicas: 2                   # HA 구성
  selector:
    matchLabels: { app: webhook }
  template:
    metadata:
      labels: { app: webhook }
    spec:
      containers:
        - name: webhook
          image: myregistry/admission-webhook:v1.0.0
          ports:
            - containerPort: 8443
          volumeMounts:
            - name: tls
              mountPath: /certs
              readOnly: true
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8443
              scheme: HTTPS
            initialDelaySeconds: 5
          resources:
            requests: { cpu: 50m, memory: 64Mi }
            limits:   { cpu: 200m, memory: 128Mi }
      volumes:
        - name: tls
          secret:
            secretName: webhook-tls-secret
---
apiVersion: v1
kind: Service
metadata:
  name: webhook-svc
  namespace: webhook-system
spec:
  selector: { app: webhook }
  ports:
    - port: 443
      targetPort: 8443

실전 운영 베스트 프랙티스

1. kube-system 제외 필수

namespaceSelector:
  matchExpressions:
    - key: kubernetes.io/metadata.name
      operator: NotIn
      values: ["kube-system", "kube-public"]

Webhook이 kube-system에 적용되면 컨트롤 플레인 컴포넌트가 배포 불가해지는 치명적 상황이 발생합니다.

2. 타임아웃과 Failure Policy 전략

  • timeoutSeconds: 3~5 권장 (기본 10초는 너무 김)
  • 신규 Webhook 배포 초기: failurePolicy: Ignore + 로깅으로 모니터링
  • 안정화 후: failurePolicy: Fail로 전환

3. 모니터링 메트릭

API 서버는 Webhook 관련 메트릭을 자동 노출합니다:

# Prometheus에서 확인할 핵심 메트릭
apiserver_admission_webhook_rejection_count    # 거부 횟수
apiserver_admission_webhook_admission_duration_seconds  # 응답 시간
apiserver_admission_webhook_fail_open_count    # failurePolicy=Ignore로 통과된 횟수

4. 디버깅 팁

# Webhook 등록 상태 확인
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

# 특정 Webhook 상세 확인
kubectl get validatingwebhookconfiguration pod-policy -o yaml

# Webhook이 요청을 거부하는 경우 이벤트 확인
kubectl describe pod <실패한-pod>

# API 서버 로그에서 Webhook 호출 확인 (audit log)
kubectl logs -n kube-system kube-apiserver-* | grep admission

Webhook vs OPA Gatekeeper vs Kyverno

  • 직접 구현 Webhook: 최대 유연성, 코드 유지보수 부담 큼. 복잡한 비즈니스 로직(외부 API 조회 등)에 적합
  • OPA Gatekeeper: Rego 언어로 정책 작성, ConstraintTemplate 기반. 정책이 많고 표준화가 필요한 대규모 클러스터에 적합
  • Kyverno: YAML 네이티브 정책 엔진, 학습 곡선 낮음. 빠른 도입이 필요한 팀에 적합

셋 다 내부적으로는 Admission Webhook 메커니즘을 사용합니다. 원리를 이해하면 어떤 도구를 쓰더라도 트러블슈팅이 가능합니다.

마무리

Kubernetes Admission Webhook은 클러스터 보안과 거버넌스의 핵심 확장점입니다. :latest 태그 금지, 필수 라벨 강제, sidecar 자동 주입, 리소스 제한 검증 등 무한한 정책을 구현할 수 있습니다. KEDA 오토스케일링이나 Helm Chart와 결합하면 완전한 GitOps 기반 클러스터 운영이 가능합니다.

핵심 정리: Mutating → Validating 순서, TLS 필수, kube-system 제외, failurePolicy 전략적 운용 — 이 네 가지만 기억하면 Admission Webhook을 안전하게 운영할 수 있습니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux