mutating webhook으로 파드에 사이드카 컨테이너를 하나 추가했다고 하자. 그 컨테이너의 imagePullPolicy는 누가 채워주는가? admission 파이프라인에서 "모든 컨테이너의 imagePullPolicy를 기본값으로 채우는" 내장 plugin은 이미 그 앞에서 한 번 실행되고 지나갔는데 말이다. Kubernetes 공식 문서(Dynamic Admission Control)를 보다가 이 질문에 걸렸고, 답을 찾아가다 보니 mutating과 validating이 왜 이렇게 다르게 설계됐는지가 같이 풀렸다.
mutating admission은 한 번의 pass로 끝나지 않는다. 더 이상 아무 plugin도 객체를 바꾸지 않을 때까지 수렴적으로 반복된 뒤에야 확정되고, validating admission은 그렇게 확정된 최종 객체 하나만을 대상으로 정확히 한 번 실행된다. 이 비대칭이 필요한 이유는 단순하다 — mutating 단계 안에서는 어떤 plugin이 방금 만든 필드를 뒤에 실행되는 다른 plugin이 또 바꿀 수 있어서, mutating 쪽 스스로는 "내가 마지막으로 본 상태가 최종 상태"라고 가정할 수 없다.
앞의 질문으로 돌아가면, 사실 imagePullPolicy 기본값 채우기는 내장 plugin이라 걱정할 게 없다. 문서에 명시된 대로 내장 mutating plugin은 어떤 mutating webhook이든 객체를 수정하면 무조건 다시 실행되기 때문이다. 원래 이 재실행 규칙이 없던 시절 실제로 문제가 됐던 사례가 GitHub issue #64333인데, 지금은 이 재실행이 무조건이라 새로 추가된 컨테이너도 다음 라운드에서 반드시 기본값을 받는다.
문제는 그 다음이다 — 내가 추가한 컨테이너를 내 소유가 아닌 다른 mutating webhook이 봐야 하는 경우다. 여기서는 재실행이 옵션이다. reinvocationPolicy: Never(기본값)면 이번 admission 평가에서 그 webhook은 절대 두 번 불리지 않는다. IfNeeded로 바꾸면 자신이 호출된 뒤 다른 admission plugin이 객체를 추가로 수정했을 때 재호출될 수 있다. 다만 문서는 이 "될 수 있다"에 세 가지 단서를 명시적으로 붙인다.
IfNeeded를 쓰는 webhook들끼리는 호출 횟수를 줄이는 방향으로 순서 자체가 재배치될 수 있다.즉 IfNeeded는 "최신 상태를 보고 싶다는 요청은 받아주겠지만, 몇 번을 어떤 순서로 줄지는 API 서버 재량"이라는 계약에 가깝다. 그래서 문서는 이 옵션을 쓰는 webhook은 반드시 멱등(idempotent)해야 한다고 강조한다 — 자신이 이미 적용한 패치가 들어있는 객체를 다시 받아도 같은 패치를 중복 적용하면 안 된다는 뜻이다.
IfNeeded를 켜놓고 "그냥 patch만 돌려주면 되겠지"라고 짜면 이렇게 깨진다. 컨테이너에 환경변수를 주입하는 webhook이 배열 끝에 무조건 append하는 JSON Patch를 돌려준다고 하자. 이 patch는 admission이 받는 Pod 객체(spec.containers[]) 기준이다.
[
{"op": "add", "path": "/spec/containers/0/env/-",
"value": {"name": "INJECTED", "value": "true"}}
]
reinvocation으로 이 webhook이 두 번 불리면, 두 번째 호출 시점의 객체에는 이미 첫 호출이 넣은 INJECTED 항목이 들어있다. 그런데 patch는 조건 없이 또 append이므로 같은 환경변수가 두 번 박힌다. 멱등하게 고치려면 patch를 만들기 전에 받은 객체를 먼저 확인해야 한다.
def build_patch(pod_spec):
env = pod_spec["containers"][0].get("env", [])
if any(e["name"] == "INJECTED" for e in env):
return [] # 이미 적용됨 — 재호출이어도 빈 패치
return [{"op": "add", "path": "/spec/containers/0/env/-",
"value": {"name": "INJECTED", "value": "true"}}]
핵심은 "몇 번째 호출인지"를 webhook이 알 방법이 없다는 것이다. 대신 매번 "이 객체에 이미 내 patch가 적용돼 있는가"만 스스로 검사하면, 몇 번이 불리든 결과는 같다.
validating webhook이 side-effect가 있는 최종 검증을 맡아야 하는 이유는 "정확히 한 번, 확정된 객체에 대해서만" 불리기 때문이고, mutating webhook의
reinvocationPolicy: IfNeeded는 몇 번·어떤 순서로 다시 불릴지 보장하지 않으므로 멱등성은 옵션이 아니라 계약이다.
다음에 더 파볼 만한 것은 ValidatingAdmissionPolicy/MutatingAdmissionPolicy다 — 별도 webhook 서버 없이 CEL만으로 admission 로직을 API 서버 안에서 평가해서, 여기서 다룬 reinvocation·타임아웃 문제 자체를 우회하는 접근이라 비교해볼 가치가 있다.