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(기본) vsIgnore— 프로덕션에서는 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을 안전하게 운영할 수 있습니다.