내가 추가한 컨테이너를 다른 webhook은 볼 수 있는가 — reinvocationPolicy와 멱등성

seonwoo_jung·2일 전

1. 도입

mutating webhook으로 파드에 사이드카 컨테이너를 하나 추가했다고 하자. 그 컨테이너의 imagePullPolicy는 누가 채워주는가? admission 파이프라인에서 "모든 컨테이너의 imagePullPolicy를 기본값으로 채우는" 내장 plugin은 이미 그 앞에서 한 번 실행되고 지나갔는데 말이다. Kubernetes 공식 문서(Dynamic Admission Control)를 보다가 이 질문에 걸렸고, 답을 찾아가다 보니 mutating과 validating이 왜 이렇게 다르게 설계됐는지가 같이 풀렸다.

2. 핵심 개념

mutating admission은 한 번의 pass로 끝나지 않는다. 더 이상 아무 plugin도 객체를 바꾸지 않을 때까지 수렴적으로 반복된 뒤에야 확정되고, validating admission은 그렇게 확정된 최종 객체 하나만을 대상으로 정확히 한 번 실행된다. 이 비대칭이 필요한 이유는 단순하다 — mutating 단계 안에서는 어떤 plugin이 방금 만든 필드를 뒤에 실행되는 다른 plugin이 또 바꿀 수 있어서, mutating 쪽 스스로는 "내가 마지막으로 본 상태가 최종 상태"라고 가정할 수 없다.

3. 내부 동작

앞의 질문으로 돌아가면, 사실 imagePullPolicy 기본값 채우기는 내장 plugin이라 걱정할 게 없다. 문서에 명시된 대로 내장 mutating plugin은 어떤 mutating webhook이든 객체를 수정하면 무조건 다시 실행되기 때문이다. 원래 이 재실행 규칙이 없던 시절 실제로 문제가 됐던 사례가 GitHub issue #64333인데, 지금은 이 재실행이 무조건이라 새로 추가된 컨테이너도 다음 라운드에서 반드시 기본값을 받는다.

문제는 그 다음이다 — 내가 추가한 컨테이너를 내 소유가 아닌 다른 mutating webhook이 봐야 하는 경우다. 여기서는 재실행이 옵션이다. reinvocationPolicy: Never(기본값)면 이번 admission 평가에서 그 webhook은 절대 두 번 불리지 않는다. IfNeeded로 바꾸면 자신이 호출된 뒤 다른 admission plugin이 객체를 추가로 수정했을 때 재호출될 수 있다. 다만 문서는 이 "될 수 있다"에 세 가지 단서를 명시적으로 붙인다.

  • 추가 호출이 정확히 1번이라는 보장이 없다.
  • 재호출로 또 바뀌어도, 그걸 다시 보게 된다는 보장이 없다(무한 수렴을 강제하지 않는다).
  • IfNeeded를 쓰는 webhook들끼리는 호출 횟수를 줄이는 방향으로 순서 자체가 재배치될 수 있다.

IfNeeded는 "최신 상태를 보고 싶다는 요청은 받아주겠지만, 몇 번을 어떤 순서로 줄지는 API 서버 재량"이라는 계약에 가깝다. 그래서 문서는 이 옵션을 쓰는 webhook은 반드시 멱등(idempotent)해야 한다고 강조한다 — 자신이 이미 적용한 패치가 들어있는 객체를 다시 받아도 같은 패치를 중복 적용하면 안 된다는 뜻이다.

4. 예시 / 코드

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가 적용돼 있는가"만 스스로 검사하면, 몇 번이 불리든 결과는 같다.

5. 정리

validating webhook이 side-effect가 있는 최종 검증을 맡아야 하는 이유는 "정확히 한 번, 확정된 객체에 대해서만" 불리기 때문이고, mutating webhook의 reinvocationPolicy: IfNeeded는 몇 번·어떤 순서로 다시 불릴지 보장하지 않으므로 멱등성은 옵션이 아니라 계약이다.

다음에 더 파볼 만한 것은 ValidatingAdmissionPolicy/MutatingAdmissionPolicy다 — 별도 webhook 서버 없이 CEL만으로 admission 로직을 API 서버 안에서 평가해서, 여기서 다룬 reinvocation·타임아웃 문제 자체를 우회하는 접근이라 비교해볼 가치가 있다.

참고 자료

  • Kubernetes 공식 문서: Reference / Access Authn Authz / Extensible Admission Controllers (Dynamic Admission Control)
  • kubernetes/kubernetes GitHub issue #64333

0개의 댓글