Istio 3-2편: Partially Enrolled Pod와 Untaint Controller
Ambient mode 트러블슈팅: istio-cni가 준비되지 않은 노드에 Pod가 스케쥴링 될 때
Jetty (정재홍) • DevOps Engineer
- DevOps
안녕하세요, 채널코퍼레이션 DevOps팀의 딜런, 재티입니다.
이 글은 Istio Ambient mode 도입기 시리즈의 3-2편입니다. 1편에서 Ambient mode의 traffic redirection을 설명하며, ztunnel과 istio-cni가 항상 Running 상태여야 한다고 언급한 적이 있습니다. 당시에는 "istio-cni가 준비되지 않은 상태에서 Pod이 스케줄되면, 해당 Pod이 mesh에 불완전하게 참여할 수 있다" 정도로만 짚고 넘어갔습니다. 이번 글에서는 그 문장을 조금 더 자세히 풀어보려 합니다.
3편: 프로덕션에서 만난 당황스러운 이슈들과 트러블슈팅
3-2편: Partially Enrolled Pod와 Untaint Controller (현재 글)
3-3편: Ambient mode 안전하게 업그레이드하기
3-4편: 부록 — 507 status code와 istiod disconnected 탐지
Istio 팀도 ztunnel과 istio-cni를 Ambient mode의 critical component로 취급하고 있습니다. 그 중 istio-cni-node-agent는 Ambient mode에서 필수 컴포넌트이며, workload Pod의 traffic redirection을 실제로 구성하는 수단입니다. 그래서 Ambient mode에서는 이 컴포넌트들은 늘 준비되어 있어야 하는 컴포넌트입니다.
이 글에서 다룰 문제도 바로 이 전제에서 출발합니다. Kubernetes 관점에서는 Pod이 Running이고 readiness probe도 통과했지만, Ambient mesh 관점에서는 ztunnel, istio-cni의 상태에 따라서 Workload Pod은 완전히 준비되지 않은 상태가 될 수 있습니다. 이 글에서는 이런 종류의 문제를 넓게 partially enrolled 문제라고 부르겠습니다.
들어가기 전에: Ambient mode에서 Pod이 mesh에 들어간다는 것
Ambient mode에서 workload Pod이 mesh에 들어간다는 것은, 단순히 namespace에 istio.io/dataplane-mode=ambient label이 붙어 있다는 뜻만은 아닙니다. 그 label은 "이 Pod은 Ambient mesh 대상이어야 한다"는 의도 혹은 표시에 가깝습니다. 실제로 트래픽이 ztunnel을 통과하려면 node-local 컴포넌트들이 Pod의 네트워크를 준비해야 합니다.
여기서 중요한 점은 istio-cni가 단순히 iptables rule만 주입하는 컴포넌트가 아니라는 점입니다. istio-cni node agent는 Pod network namespace에 redirection rule을 설정하고, ztunnel에게는 해당 Pod의 workload 정보와 network namespace file descriptor를 전달합니다. 그래야 ztunnel이 해당 Pod의 socket을 통해서 inbound/outbound 트래픽을 처리할 수 있습니다.
즉 Ambient mode에서 Pod이 mesh에 완전히 참여하려면 두 가지가 함께 완료되어야 합니다.
Pod network namespace 안에 traffic redirection rule이 설정되어야 합니다.
ztunnel이 해당 Pod을 workload로 인지하고 proxy할 준비가 되어야 합니다. 둘 중 하나라도 빠지면 Kubernetes 입장에서는 정상 Pod처럼 보이는 window가 있더라도, mesh dataplane 입장에서는 아직 완전하지 않은 상태가 됩니다.
1. 문제 상황
프로덕션에 Ambient mode를 롤아웃하는 과정에서, 특정 노드에 올라간 신규 Pod들이 간헐적으로 트래픽을 받지 못하는 현상을 만났습니다. Pod 자체는 Running 상태였고 readiness probe도 통과했습니다. Kubernetes Service endpoint에도 정상적으로 포함되어 있었습니다. 그런데 다른 mesh workload나 waypoint에서 해당 Pod으로 트래픽을 보내면 요청이 실패했습니다.
애플리케이션 로그에는 특별한 에러가 없었고, Pod readiness도 정상이라 Kubernetes 입장에서는 문제가 없어 보였습니다. 반면 client 쪽에서는 다음과 같은 Envoy 계열 에러가 관찰되었습니다.
upstream connect error or disconnect/reset before headers
reset reason: connection failure
reset reason: connection termination이전 3-1편에서도 503 에러를 다뤘지만, 이 문제는 half-open connection 이슈를 겪고 난 뒤에 별도로 마주친 문제였습니다. 처음에는 같은 계열의 503처럼 보였지만, 관찰된 에러 메시지가 달랐습니다. half-open connection 이슈에서는 주로 upstream connect error or disconnect/reset before headers 메시지만 보였던 반면, 이번에는 connection failure, connection termination 같은 reset reason이 함께 나타났습니다. 그래서 기존에 분석했던 stale connection 재사용 문제와는 다른 원인일 가능성이 높다고 판단했습니다.
이 문제를 이해하려면 waypoint가 목적지 Pod에 트래픽을 보내는 흐름을 다시 떠올려야 합니다. Ambient mode에서 waypoint는 L7 처리를 마친 뒤 목적지 workload로 upstream 연결을 만들고, 정상 상태에서는 목적지 Pod의 redirection rule과 node-local ztunnel이 이 트래픽을 처리합니다.
그런데 Kubernetes 관점에서는 Pod이 Ready인데, mesh dataplane 관점에서는 Pod redirection이나 ztunnel registration이 아직 완성되지 않은 window가 생길 수 있습니다. 이 경우 client 입장에서는 애플리케이션 문제가 아니라 upstream 연결 실패처럼 보일 수 있습니다.
이 문제를 한 문장으로 표현하면 이렇습니다. Kubernetes는 Pod이 준비됐다고 보지만, Ambient mesh는 아직 이 Pod를 처리할 준비가 되지 않은 상태입니다.
2. 소스코드로 보는 partially enrolled 상태
앞에서 본 흐름을 다시 그림으로 표현하면 다음과 같습니다. 이 섹션에서 살펴볼 소스코드는 결국 밑 그림의 step들, 즉 istio-cni가 workload Pod의 network namespace에 들어가 redirection rule을 설정하고, ztunnel에 workload proxy 생성을 알리는 과정을 코드로 확인하는 내용입니다.
Istio 소스코드를 보면 Ambient redirection 상태가 Pod annotation으로 관리된다는 점을 확인할 수 있습니다. 핵심 annotation은 ambient.istio.io/redirection입니다. 이 값이 enabled이면 redirection 구성이 완료되어 Pod이 captured 상태라는 뜻이고, pending이면 일부 단계는 완료됐지만 아직 ztunnel이 해당 workload를 proxy하지 못하는 상태를 의미합니다.
여기서 pending은 단순히 "조금 늦게 반영되는 중"이라는 의미에서 그치지 않습니다. 주석에 적혀 있듯이, active ztunnel이 해당 Pod을 proxy하기 전까지는 ingress/egress traffic이 동작하지 않을 수 있는 상태입니다.
Pod를 mesh에 추가하는 흐름을 보면 이 상태가 어떻게 만들어지는지 더욱 분명해집니다. cni/pkg/nodeagent/meshdataplane_linux.go의 AddPodToMesh는 내부적으로 Pod network namespace를 열고, in-pod redirection rule을 만든 이후에 ztunnel에 workload 정보를 전달합니다. 이 과정에서 redirection rule 설정은 성공했지만 ztunnel 등록이 실패하면, Istio는 Pod에 pending annotation을 남깁니다.
좀 더 안쪽으로 들어가면 netServer.AddPodToMesh는 다음 순서로 동작합니다. 먼저 Pod UID를 snapshot/cache에 등록하고, Pod network namespace를 열거나 찾습니다. 그 다음 Pod netns 안에 redirection rule을 만들고, 마지막으로 ztunnel에 AddWorkload 메시지를 보냅니다.
ztunnel 등록은 PodAdded에서 이루어집니다. 여기서 ztunnel connection이 아직 없다면 no ztunnel connection 에러가 발생합니다.
이 경우 이미 Pod netns에는 redirection rule이 들어갔을 수 있지만, ztunnel은 아직 이 Pod을 모릅니다. 그래서 Istio는 이 Pod을 pending 상태로 표시하고 이후 retry를 시도합니다.
물론 모든 실패가 항상 pending annotation으로 남는 것은 아닙니다. CNI plugin이 아예 호출되지 않았거나 ambient 대상 판정이 실패해 CNI event가 node agent까지 전달되지 않으면 redirection rule 자체가 빠질 수 있습니다. 반대로 CNI plugin이 ambient Pod으로 판정한 뒤 node-agent 전송에 실패하면 CNI ADD error가 반환됩니다. 그래서 코드상 pending은 "redirection은 일부 적용됐지만 ztunnel 등록이 완료되지 않은 상태"에 더 가깝습니다.
3. retry만으로는 충분하지 않았던 이유
Istio는 pending 상태의 Pod을 그냥 방치하지 않습니다. Pod update event를 처리할 때 PodPartiallyEnrolled를 별도로 확인하고, 이 상태의 Pod을 retry 대상으로 봅니다.
즉 pending 상태의 Pod은 이후 이벤트에서 다시 AddPodToMesh를 시도할 수 있습니다. ztunnel이 뒤늦게 연결되면 최종적으로 enabled 상태까지 갈 수도 있습니다.
하지만 운영 관점에서는 이 retry에 의존하기에는 어려웠습니다. retry가 성공하기 전까지는 트래픽 유실이 발생할 수 있기 때문입니다. 또한 readiness probe가 애플리케이션 관점에서만 통과하면, Kubernetes Service endpoint에는 포함되지만 mesh traffic만 실패하는 상태가 됩니다.
더 나아가, istio-cni redirection이 빠진 Pod은 ztunnel을 우회할 수 있고, mTLS, AuthorizationPolicy, telemetry 등이 적용되지 않을 수 있는 가능성을 뜻하기도 합니다. Ambient Mesh 문서에서도 이 경우에 Istio policy를 bypass할 수 있다고 설명합니다.
4. 왜 partially enrolled pod가 생기는가
문제의 출발점은 Kubernetes scheduler가 DaemonSet의 준비 완료를 일반 workload scheduling의 선행 조건으로 보장하지 않는다는 점입니다. 새 노드가 cluster에 추가되면 istio-cni DaemonSet Pod, ztunnel DaemonSet Pod, 일반 workload Pod scheduling이 거의 동시에 진행될 수 있습니다.
우리가 기대하는 순서는 다음과 같습니다.
하지만 실제로는 새 노드에서 DaemonSet Pod과 일반 workload Pod의 scheduling이 거의 동시에 진행될 수 있습니다. 이때 istio-cni가 아직 CNI chain에 들어가지 않았거나 ambient 대상 판정이 실패하면, workload Pod이 mesh redirection 없이 먼저 Running/Ready 상태로 진행될 수 있습니다.
이때 실패 시나리오는 크게 두 가지로 나눠볼 수 있습니다.
첫 번째는 istio-cni가 Pod 생성 시점에 아예 호출되지 않았거나, ambient 대상 판정이 실패한 경우입니다. 이 경우 Pod network namespace에 redirection rule이 들어가지 않고
ambient.istio.io/redirection=enabledannotation도 붙지 않을 수 있습니다. 이 상태의 Pod은 mesh dataplane에 완전히 들어온 workload로 보기 어렵고, 트래픽이 ztunnel을 우회해 app port로 직접 도달하거나 mTLS, AuthorizationPolicy, telemetry 적용을 우회할 수 있습니다.두 번째는 istio-cni node agent가 Pod netns의 redirection rule 생성까지는 성공했지만, ztunnel 연결 또는
AddWorkloadACK가 준비되지 않은 경우입니다. 이때 Pod은ambient.istio.io/redirection=pending으로 표시될 수 있으며, 이 상태는 active ztunnel이 해당 workload를 proxying하기 전까지 ingress/egress traffic이 정상 동작하지 않는 상태로 정의됩니다.
정리하자면 "istio-cni가 없다"인데, 조금 더 나눠보자면
istio-cni가 아예 호출되지 않았거나 ambient 대상 판정 실패로 CNI event가 전달되지 않으면 redirection 자체가 빠질 수 있고,
CNI agent가 redirection rule은 만들었지만 ztunnel 연결이나 ACK가 준비되지 않았다면
pendingpartially enrolled 상태가 될 수 있습니다.
5. 해결책: untaint-controller
해결책의 아이디어 자체는 매우 단순합니다. Istio 팀도 이런 문제를 인지하고 있으며, 해결책으로 untaint-controller를 제시하고 있습니다.
다만 당시에는 이 내용이 Istio 공식문서에 정리되어 있지 않았습니다. Istio 1.22 release note에는 cni.istio.io/not-ready taint를 제거하는 node taint controller가 추가되었다고 짧게 언급되어 있었고, Helm chart values에도 pilot.taint.enabled 같은 설정이 존재했습니다. 하지만 실제로 어떤 taint를 누가 붙여야 하는지, pilot.taint.enabled만 켜면 되는지, PILOT_ENABLE_NODE_UNTAINT_CONTROLLERS도 같이 켜야 하는지, CNI namespace가 다르면 무엇을 설정해야 하는지, Karpenter startupTaints와 어떻게 조합해야 하는지 등을 공식 문서에서 한 번에 확인하기는 어려웠습니다.
결국 당시 우리가 참고할 수 있었던 가장 명확한 문서는 ambientmesh.io의 untaint-controller 가이드였습니다. 이 문서에는 startup taint를 걸고, istio-cni가 Ready가 되면 untaint-controller가 taint를 제거한다는 흐름이 비교적 명확하게 설명되어 있었습니다.
그렇다면 untaint-controller가 하는 일은 무엇일까요? 새 노드가 생성될 때 cni.istio.io/not-ready startup taint를 미리 걸어둡니다. 이 taint가 있는 동안에는 일반 workload Pod이 해당 노드에 스케줄되지 않습니다. 반면 istio-cni DaemonSet Pod은 toleration을 가지고 먼저 올라옵니다. 이후 istio-cni Pod이 Ready가 되면 istiod 안의 untaint-controller가 이 taint를 제거하고, 그때부터 일반 workload Pod이 스케줄될 수 있습니다.
즉 untaint-controller는 CNI 미준비를 scheduling 단계에서 줄이고, ztunnel 미준비 race는 CNI ADD의 동기 AddWorkload/ACK 경로가 상당 부분 막아주는 구조입니다. (다만 이 구조가 이미 존재하는 Pod을 나중에 다시 enroll하는 경로, redirection 적용 이후 ztunnel disconnect, pending 상태의 짧은 window까지 완전히 없애지는 않습니다)
흐름을 짧게 쓰면 다음과 같습니다.
Node 생성
Node에
cni.istio.io/not-readystartupTaint 존재일반 workload Pod scheduling 차단
istio-cni DaemonSet Pod Ready
untaint-controller가 taint 제거
일반 workload Pod scheduling 허용
즉 이 해결책은 "Pod이 먼저 뜬 뒤 고친다"가 아니라, 아예 istio-cni가 준비되기 전에는 workload Pod이 뜨지 못하게 하는 방식입니다. Ambient mode처럼 traffic redirection이 node-local component readiness에 강하게 의존하는 구조에서는 자연스러운 접근입니다. 다만 이때의 "준비"는 어디까지나 istio-cni 준비를 의미하며, ztunnel readiness까지 보장해주지는 않습니다.
여기서 중요한 점은 untaint-controller가 taint를 추가하지 않는다는 것입니다. taint는 Karpenter NodePool, node group, autoscaling group 같은 인프라 레벨에서 새 노드 생성 시점에 붙여야 합니다. untaint-controller는 이름 그대로, istio-cni가 준비된 뒤 그 taint를 제거하는 역할만 합니다.
당시에는 Istio 공식 문서에 관련 내용이 부재했지만, 이제는 추가된 것으로 보입니다. istio/istio.io#17190이 untaint-controller 문서를 CNI setup page에 추가하는 내용으로 merge되었고, 이 글을 작성하는 2026년 6월 15일 기준 preliminary 문서에는 Untaint controller 섹션이 추가되어 있습니다.
6. 설정 방법과 헷갈렸던 지점
Istio 쪽 설정에서 핵심은 두 가지를 모두 설정해야 한다는 점입니다.
pilot.taint.enabled=true만으로는 controller가 동작하지 않을 수 있습니다. 이 값은 node patch 권한과 CNI namespace 설정에 더 가깝게 동작하고, 실제 controller 실행은 PILOT_ENABLE_NODE_UNTAINT_CONTROLLERS feature flag가 켜져야 합니다. 이 지점이 문서 부재와 맞물려 헷갈렸던 부분 중 하나입니다. values에는 pilot.taint.enabled가 있으니 이것만 켜면 될 것처럼 보이지만, 실제로는 env flag까지 같이 필요했습니다. (현재 Istio latest 버전에서는 해당 이슈는 패치된 것으로 보입니다; https://istio.io/latest/news/releases/1.30.x/announcing-1.30/upgrade-notes/#untaint-controller)
NodePool 쪽에서는 새 노드에 startup taint를 붙여야 합니다. Karpenter를 사용한다면 다음과 같이 설정할 수 있습니다.
Karpenter에서 startupTaints는 노드 초기화 중 임시로 존재하는 taint로 취급됩니다. 외부 controller가 이를 제거할 수 있고, 이 경우 그 외부 controller가 istiod의 untaint-controller입니다.
7. 정리
Ambient mode에서는 sidecar가 사라진 대신, node-local 컴포넌트의 준비 상태가 더 중요해졌습니다. 특히 istio-cni와 ztunnel은 workload Pod의 traffic redirection과 workload registration을 담당하므로, 새 Pod이 뜨는 순간 이미 준비되어 있어야 합니다.
partially enrolled 문제는 결국 이 순서가 깨질 때 발생합니다. Pod은 Kubernetes 관점에서 정상이고 readiness probe도 통과했지만, mesh 관점에서는 redirection 또는 ztunnel registration이 완성되지 않은 상태가 될 수 있습니다. Ambient mode를 운영할 때는 애플리케이션 Pod의 readiness뿐 아니라, 그 Pod을 mesh에 넣는 node-local dataplane의 readiness까지 함께 봐야 하는 이유가 여기에 있습니다.
