ambient mode가 Istio 1.24에서 GA된 게 2024년 말이다. 지난주에 우리 팀도 일부 네임스페이스를 sidecar에서 ambient로 옮기는 작업을 마무리했다. 옮기고 나서 가장 많이 받은 질문이 "그래서 mTLS는 누가 거는 거냐", "L7 정책은 어디서 검사되냐"였다. 문서를 봐도 그림 한 장으로 퉁쳐버리니까 막상 트러블슈팅을 시작하면 답답하다.
그래서 이 글은 패킷이 Pod A에서 떠나서 Pod B에 도착할 때까지, 정확히 어떤 컴포넌트가 어떤 순서로 끼어드는지를 우리 팀이 확인하면서 정리한 내용이다. Istio 공식 문서가 적당히 추상화한 부분을 한 단계 더 내려가서 본다.
sidecar 없는 데이터플레인이라는 말의 진짜 의미
ambient mode의 가장 큰 광고 문구는 "sidecar가 사라진다"는 거다. 근데 이 표현 때문에 종종 오해가 생긴다. 마치 Pod가 그냥 평범한 Pod처럼 네트워크를 쓰고, 어디선가 마법으로 mTLS가 걸린다고 생각하는 사람이 의외로 많다.
사실 내부적으로는, Pod의 트래픽을 가로채는 게 노드 수준으로 옮겨갔을 뿐이다. 각 노드에는 ztunnel이라는 데몬셋이 떠 있고, 노드의 네트워킵 스택이 Pod에서 나가는 트래픽을 ztunnel로 강제로 redirect 한다. Pod 안에서 보면 트래픽이 그냥 평범한 TCP로 나가는 것처럼 보이지만, 노드 입장에서는 ztunnel을 거치게 된다.
이 redirect를 거는 방식이 sidecar 시절과 가장 크게 달라진 지점이다. sidecar 때는 Pod 내부의 iptables 규칙으로 트래픽을 Envoy로 보냈다. ambient에서는 노드의 CNI 플러그인(istio-cni라고 부른다)이 Pod의 network namespace에 진입해서 정책을 심는다. 결과적으로 Pod의 veth pair에서 나가는 모든 트래픽이 ztunnel의 inbound 리스너로 가게 된다.
여기서 한 가지 디테일. ztunnel은 Pod의 network namespace가 아니라 노드의 root network namespace에서 동작하지만, 각 Pod의 identity는 그대로 유지해야 한다. 그래서 ztunnel은 노드 안의 모든 Pod에 대한 SPIFFE 인증서를 한꺼번에 들고 있고, 어떤 Pod의 트래픽인지에 따라 적절한 인증서를 골라서 mTLS를 건다. 노드 하나에 Pod가 50개 있으면 ztunnel은 SPIFFE ID 50개를 동시에 관리한다. 이게 ztunnel이 Rust로 새로 쓰여진 이유 중 하나다 — Envoy로는 메모리가 너무 많이 든다.
HBONE이라는 터널 — CONNECT 위에 mTLS
ztunnel 두 개가 서로 통신할 때 쓰는 프로토콜이 HBONE이다. 풀어 쓰면 HTTP-Based Overlay Network Environment. 이름이 거창한데, 실체는 의외로 단순하다. HTTP CONNECT 메서드 위에 mTLS를 얹은 거다.
흐름은 이렇다. Pod A가 Pod B의 ClusterIP로 TCP 패킷을 보내면, 노드 A의 ztunnel이 그 패킷을 받는다. ztunnel은 destination이 메시 안의 다른 Pod라는 걸 알면, 노드 B의 ztunnel을 향해서 mTLS over TLS 연결을 연다. 그 위에서 HTTP/2 CONNECT 요청을 보내고("CONNECT pod-b.namespace.svc:8080"), 노드 B의 ztunnel이 OK를 응답하면 그 스트림 안으로 원래 TCP 페이로드가 흐른다.
왜 굳이 CONNECT를 쓰냐? 두 가지 이유가 있다. 첫째, HTTP/2의 멀티플렉싱을 활용해서 여러 Pod 사이의 트래픽을 하나의 mTLS 커넥션으로 묶을 수 있다. 노드에 Pod가 많아도 mTLS 핸드셰이크는 노드 페어당 한 번만 일어난다. 둘째, 표준 HTTP 메서드 위에 있으니까 디버깅이 훨씬 편하다. tcpdump 떠서 4443 포트(ztunnel의 HBONE 포트) 트래픽을 보면 평범한 HTTP/2 트래픽처럼 분석된다.
여기서 자주 헷갈리는 게 있다. mTLS는 ztunnel과 ztunnel 사이에 걸리는 거지, Pod와 Pod 사이가 아니다. Pod 입장에서는 자기가 보낸 평범한 TCP가 그대로 도착하는 것처럼 보인다. 암호화가 풀린 평문이 Pod B에 들어가는 거다. 이게 보안적으로 괜찮냐? — 같은 노드 안의 트래픽은 노드 커널을 신뢰한다는 전제가 깔린다. ambient의 보안 모델은 노드 단위로 trust boundary를 잡는다.
waypoint가 끼어드는 순간
여기까지가 L4 메시(secure overlay) 얘기다. mTLS, identity, 기본적인 L4 AuthorizationPolicy까지는 ztunnel만으로 끝난다. 그런데 헤더 기반 라우팅, retry, JWT 검증, L7 RBAC 같은 게 필요하면 waypoint가 등장한다.
waypoint는 서비스 단위 혹은 네임스페이스 단위로 배포되는 Envoy 기반 프록시다. sidecar처럼 Pod에 붙는 게 아니라, 별도의 Deployment로 떠 있고, 그쪽으로 트래픽을 라우팅하는 건 ztunnel의 책임이다.
흐름이 이렇게 바뀐다. Pod A → 노드 A의 ztunnel → (HBONE) → waypoint of service B → (HBONE) → 노드 B의 ztunnel → Pod B. 홉이 하나 늘어난다. 그래서 모든 서비스에 waypoint를 다는 건 좋은 생각이 아니다. 실제로 ambient를 채택한 팀들이 공유하는 패턴을 보면, 보통 80% 정도의 서비스는 ztunnel만 거치고, L7 기능이 진짜로 필요한 20%에만 waypoint를 붙인다.
waypoint를 어디에 둘지에 대한 결정이 ambient mode의 새로운 운영 포인트다. sidecar 시절에는 "Envoy를 붙일까 말까"가 namespace label 하나로 끝났는데, ambient에서는 "L7 기능이 필요한가? 그렇다면 waypoint를 서비스마다 둘 것인가, 네임스페이스 공용으로 둘 것인가?"라는 결정이 추가된다. 우리 팀은 처음에 모든 서비스마다 waypoint를 따로 뒀다가, Envoy Pod가 너무 많아져서 네임스페이스 공용으로 다시 정리했다.
ztunnel이 waypoint를 찾아내는 방식
이 부분이 의외로 잘 안 알려져 있다. ztunnel이 어떻게 "이 destination은 waypoint를 거쳐야 한다"는 걸 알까?
istiod가 xDS API를 통해 ztunnel에게 노드 안의 모든 Pod와 서비스 정보를 푸시해준다. 이 정보에는 각 서비스가 어떤 waypoint와 연결되어 있는지도 들어있다. ztunnel은 destination address를 보고 매핑 테이블을 조회해서, waypoint가 있으면 그쪽으로 트래픽을 돌린다.
여기서 디버깅 팁 하나. ztunnel의 xDS 상태가 꼬이면 — 우리 팀이 한 번 겪었다 — Pod 간 트래픽이 갑자기 waypoint를 못 찾고 평문으로 흘러가거나, 반대로 waypoint를 거쳐야 할 트래픽이 우회된다. istioctl ztunnel-config workloads와 istioctl ztunnel-config services로 ztunnel이 들고 있는 매핑을 직접 확인할 수 있다. 트러블슈팅의 시작점은 거의 항상 여기다.
sidecar에서 옮길 때 진짜 신경 써야 하는 것
이론은 위와 같은데, 실제로 우리가 옮기면서 뜻밖에 시간을 잡아먹은 부분 몇 개를 적어둔다.
첫째, NetworkPolicy. sidecar 시절에는 Pod 안에서 트래픽이 가로채지니까 NetworkPolicy를 짤 때 destination IP를 기준으로 거의 직관적으로 잡으면 됐다. ambient에서는 트래픽이 ztunnel을 거치고 다시 Pod로 들어오기 때문에, NetworkPolicy의 source가 ztunnel의 IP가 되는 경우가 있다. CNI에 따라 다르게 보인다. Calico와 Cilium에서 다르게 보였고, 우리 팀은 결국 ambient를 쓰는 네임스페이스에서는 NetworkPolicy를 새로 짰다.
둘째, headless service. ambient의 초기 GA 직후 우리가 한 번 멘붕이 왔던 부분인데, headless service로 가는 트래픽은 ztunnel이 처리하지 않고 그대로 통과시키는 시점이 있었다. 1.25 이후로 많이 개선됐지만, 여전히 StatefulSet 기반 워크로드를 옮길 때는 짧은 통신 테스트를 꼭 거쳐야 한다.
셋째, Pod 간 IP 직접 통신. 일부 레거시 컴포넌트가 service를 거치지 않고 Pod IP로 직접 통신하는 경우가 있다. ambient는 service 단위로 정책을 적용하는 게 자연스럽기 때문에, Pod IP 직접 통신이 많은 워크로드는 마이그레이션이 까다롭다. 이건 ambient의 한계라기보다는 기존 코드를 정리할 좋은 기회로 보고 가는 게 낫다.
그래서 결론은
ambient mode가 sidecar를 완전히 대체할 거냐고 묻는다면, 우리 팀의 현재 답은 "새 클러스터는 무조건 ambient로 시작, 기존 클러스터는 천천히"다. 새로 만드는 서비스는 ambient의 단순함이 압도적으로 좋다. Pod 재시작 없이 mesh 설정을 바꿀 수 있다는 것만으로도 운영이 편해진다.
다만 내부 동작을 모르고 "그냥 켜면 된다"고 생각하면 위 같은 함정에 빠진다. ztunnel과 waypoint가 어디서 무엇을 하는지 한 번 정리하고 들어가면, 트러블슈팅 시간이 1/3로 준다. 적어도 우리 팀에서는 그랬다.
혵시 다른 팀에서 ambient로 옮기면서 겪은 함정이 있으면 댓글로 공유해주시면 좋겠다. 우리도 더 옮길 클러스터가 남아있다.
'IT > 기타' 카테고리의 다른 글
| cert-manager HTTP-01 challenge가 자꾸 timeout 나면 1.17 확인해보세요 (0) | 2026.06.14 |
|---|---|
| Istio Ambient mode 내부 동작 - ztunnel과 waypoint proxy는 실제로 어떻게 패킷을 주고받는가 (0) | 2026.06.13 |
| ndots:1 한 줄 바꿨다가 클러스터 내부 DNS가 깨진 이야기 (0) | 2026.06.11 |
| Cilium vs Calico, eBPF 시대에 우리가 다시 고른 CNI (0) | 2026.06.05 |
| ingress-nginx EOL, Gateway API로 옮기는 실전 가이드 (1) | 2026.06.03 |