ENGINEERING NOTES
EKS에서 Istio sidecar와 ambient를 직접 재 봤습니다: 지연, 롤아웃 실패, L7 정책 함정, 관측성까지
Istio 문서는 양이 많고, 모드마다 동작이 다릅니다. 이 글은 EKS 기준으로 정리해 둔 Istio 문서 세트를 한 편으로 다시 엮으면서, 판단이 갈리는 지점을 실제 클러스터에서 측정해 덧붙인 것입니다. 같은 echo 서비스를 네 가지 데이터 플레인(메시 없음, sidecar, ambient L4, ambient L7)에 올리고 지연, 프록시 CPU, 롤아웃 중 오류, 인가 정책, outlier detection, 텔레메트리를 비교했습니다. 측정은 2026년 10월 20일과 21일에 했고, 원시 결과와 재현 스크립트를 글 끝에 공개합니다.
2) 가장 위험했던 것은 성능이 아니라 정책이었습니다. 경로 조건(
paths: /admin*)만 있는 DENY AuthorizationPolicy를 selector로 ambient 워크로드에 걸면, ztunnel은 그 조건을 버리고 아무것도 막지 않았습니다. 적용 시 경고 한 줄 외에는 오류가 없어서 /admin이 열린 채로 남습니다.3) waypoint에 붙인 L7 정책은 Service 주소로 오는 요청만 막았고, Pod IP로 직접 부르면 그대로 통과했습니다.
4) 롤아웃 중 오류는 메시 종류보다 종료 처리(preStop)가 더 크게 좌우했습니다. preStop sleep 10초만으로 base, sidecar, ambient L4는 오류 0건이 되었고, ambient L7만 30,000건 중 503이 26~27건 남았습니다.
5) ambient L4는 HTTP 응답 코드와 경로를 전혀 남기지 않습니다. "어느 요청이 500이었나"를 메시에서 보려면 sidecar나 waypoint가 필요합니다.
1. 시험 환경과 방법
| 항목 | 값 |
|---|---|
| 클러스터 | Amazon EKS 1.36.4, 기존 VPC의 private 서브넷, 노드에 public IP 없음 |
| 노드 | m7g.xlarge(Graviton, 4 vCPU) 3대, Amazon Linux 2023, 모두 ap-northeast-2a |
| 배치 | client 노드 1대에 부하 생성기(fortio)와 istiod, server 노드 2대에 echo 3개와 waypoint |
| Istio | 1.31.2, istioctl install --set profile=ambient, Gateway API v1.6.0 CRD, Envoy access log 켬 |
| 워크로드 | fortio 서버를 echo로 사용(3 replica), 네임스페이스마다 같은 manifest |
| 모드 | base(메시 없음), sidecar(istio-injection=enabled), ambient-l4(istio.io/dataplane-mode=ambient), ambient-l7(ambient + 네임스페이스 waypoint) |
| mTLS | 세 메시 네임스페이스 모두 PeerAuthentication STRICT |
네 네임스페이스의 워크로드는 똑같고, 네임스페이스 레이블만 다릅니다. 요청은 항상 client 노드에서 server 노드로 건너가므로 노드 간 홉이 한 번씩 들어갑니다. 한 AZ에 모아서 AZ 간 지연은 섞이지 않게 했습니다. 측정 스크립트는 결과를 모두 JSON으로 남기고, 각 결과에는 Kubernetes 버전, istiod 이미지, 노드 타입과 커널 버전이 함께 기록됩니다.
2. 서비스 메시가 정말 필요한가
서비스 메시는 마이크로서비스 사이의 통신을 애플리케이션 밖의 인프라 계층에서 다룹니다. Istio를 넣으면 코드 수정 없이 트래픽 라우팅, mTLS와 인가, 메트릭과 로그, retry와 circuit breaking을 얻습니다. 대신 프록시가 추가되고, control plane을 운영해야 하고, 장애가 났을 때 봐야 할 계층이 하나 늘어납니다. 이번 측정에서도 그 비용은 숫자로 보였습니다(4절).
메시가 도움이 되는 상황
- 서비스 간(East-West) 호출이 많고, 여러 팀이 서로 다른 언어로 서비스를 만드는 환경
- 서비스 간 암호화와 워크로드 신원 기반 접근 제어가 요구되는 환경. 서비스마다 보안 로직과 인증서 관리를 따로 구현하면 정책이 일관되지 않기 쉽습니다.
- Canary 배포, 헤더 기반 라우팅, traffic mirroring, fault injection 같은 L7 트래픽 제어가 필요한 경우
- 코드 수정 없이 일관된 형식의 메트릭과 서비스 토폴로지를 보고 싶은 경우
메시가 오히려 부담인 상황
- 모놀리식 애플리케이션 하나와 DB 정도의 단순한 구조. Ingress 컨트롤러나 Gateway API 구현, 로드 밸런서로 충분합니다.
- 서비스 수가 적고 L3/L4 격리만 필요한 경우. CNI가 지원하면 NetworkPolicy로 격리할 수 있습니다. 단, NetworkPolicy는 mTLS나 HTTP 수준 인가를 제공하지 않습니다.
- Istio와 Envoy의 업그레이드, 패치, 디버깅을 감당할 인력이 없는 경우
- 요청당 1~2ms도 부담인 경우. 이번 측정에서 sidecar와 waypoint는 P50을 약 2ms 늘렸습니다. 이때는 ambient L4나 CNI 기반 대안을 먼저 검토합니다.
도입하기로 했다면 한 번에 모든 기능을 켜지 않는 것이 핵심입니다. 1~2개 네임스페이스로 시작해 관찰성을 먼저 확보하고, mTLS를 PERMISSIVE에서 STRICT로 옮긴 뒤, 트래픽 관리를 붙이고, 검증이 끝나면 넓힙니다. "Day 1에 모든 기능 활성화", "PERMISSIVE 장기 방치", "모니터링 없이 프로덕션 배포"는 문서가 꼽는 대표적인 안티패턴입니다.
3. Istio 구조: sidecar와 ambient
Control plane (istiod) 은 하나의 바이너리입니다. 예전의 Pilot, Citadel, Galley는 Istio 1.5부터 istiod 내부 기능으로 통합되었습니다. istiod는 Kubernetes Service, EndpointSlice, Pod 상태, ServiceEntry를 감시하고, Istio 리소스를 프록시 구성으로 바꿔 xDS로 내려보냅니다.
| xDS API | 역할 | 대응하는 Istio 개념 |
|---|---|---|
| LDS (Listener) | 수신 포트 구성 | 15001, 15006 같은 리스너 |
| RDS (Route) | HTTP 라우팅 규칙 | VirtualService, HTTPRoute |
| CDS (Cluster) | 업스트림 서비스 | DestinationRule |
| EDS (Endpoint) | 파드 IP 목록 | Service Endpoints |
| SDS (Secret) | TLS 인증서 | mTLS 인증서 |
기본적으로 각 Envoy는 메시 전체의 서비스 정보를 받습니다. Sidecar 리소스로 가져올 호스트 범위를 좁히면 구성 크기와 프록시 메모리를 줄일 수 있지만, 이것은 구성 범위 제어일 뿐 egress 방화벽이 아닙니다.
Sidecar 모드
등록된 파드마다 Envoy 컨테이너를 주입하고, 파드의 트래픽을 가로채 Envoy를 거치게 합니다. 요청 하나가 client 쪽 Envoy와 server 쪽 Envoy를 모두 지나므로 L7 프록시를 두 번 통과합니다. 파드 단위로 세밀하게 제어할 수 있지만, 파드마다 프록시 리소스가 들고 프록시를 바꾸려면 파드를 다시 만들어야 합니다. 최근 Istio는 istio-proxy를 native sidecar(restartPolicy: Always인 init container)로 주입할 수 있어서, 이 경우 spec.containers에는 앱 컨테이너만 보이고 READY는 2/2로 표시됩니다(앞선 EKS Hybrid Nodes 시험에서 Istio 1.30으로 확인).
Ambient 모드
sidecar 없이 두 계층으로 나눕니다. Istio 1.24에서 core 기능이 GA가 되었습니다.
- ztunnel: 노드마다 DaemonSet으로 뜨는 L4 프록시입니다. Istio CNI가 등록된 파드의 트래픽을 넘겨주면 ztunnel이 mTLS, L4 인가, L4 텔레메트리를 처리합니다. 노드 사이에는 HBONE(HTTP CONNECT 기반 터널, TCP 15008)을 씁니다.
- waypoint: L7 기능이 필요할 때만 두는 Envoy입니다. 네임스페이스, Service, Pod 단위로 등록해 여러 워크로드가 공유합니다.
이번 시험의 waypoint는 아래처럼 만들었습니다. istioctl waypoint apply가 만드는 것과 같은 Gateway에 parametersRef만 더해 waypoint Pod를 server 노드에 고정했습니다. istiod가 관리하는 Deployment를 kubectl patch로 고치면 되돌려지기 때문입니다.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: waypoint
namespace: ambient-l7
labels:
istio.io/waypoint-for: service
spec:
gatewayClassName: istio-waypoint
infrastructure:
parametersRef: {group: "", kind: ConfigMap, name: waypoint-params}
listeners:
- name: mesh
port: 15008
protocol: HBONE
네임스페이스에는 istio.io/dataplane-mode=ambient와 istio.io/use-waypoint=waypoint를 붙였습니다. ServiceAccount에 레이블을 붙여도 waypoint가 선택되지 않는다는 점은 예전 preview 자료와 다른 부분이라 자주 헷갈립니다.
어떤 모드를 고를까
| 판단 기준 | Sidecar 쪽 | Ambient L4 | Ambient L7 (waypoint) |
|---|---|---|---|
| P50 증가분 (이번 측정) | 약 1.9~2.0ms | 약 0.35~0.46ms | 약 2.1~2.5ms |
| L7 기능 (retry, 라우팅, L7 정책, HTTP 메트릭) | 있음 | 없음 | 있음 |
| 프록시 CPU (1,000 req/s, 이번 측정) | 프록시 4개 합계 약 0.37 vCPU | ztunnel 합계 약 0.09 vCPU | waypoint 0.11 + ztunnel 0.09 vCPU |
| 등록과 롤아웃 | 파드 재생성 필요 | 재시작 없이 등록 | 재시작 없이 등록, waypoint 운영 추가 |
| 롤아웃 중 오류 (preStop 적용 후) | 0건 | 0건 | 30,000건 중 503 26~27건 |
ambient에서 retry, timeout, 헤더 조작, L7 인가는 waypoint가 있어야 동작합니다. 현재 문서는 ambient의 VirtualService를 Alpha로 설명하므로 waypoint 라우팅에는 Gateway API의 HTTPRoute를 쓰고 둘을 섞지 않는 편이 안전합니다.
4. 실측 1: 지연과 프록시 CPU
client에서 http://echo:8080/echo로 연결 16개를 열어 200 req/s와 1,000 req/s로 60초씩 보냈습니다. 각 회차 앞에 10초 워밍업을 따로 돌려 버렸고, 회차마다 모드 순서를 돌려 특정 모드가 늘 먼저 측정되지 않게 했습니다. 3회 반복의 중앙값입니다. 오류는 전 구간 0건이었습니다.
| 모드 | 200 req/s P50 | P90 | P99 | P99.9 | 1,000 req/s P50 | P90 | P99 | P99.9 |
|---|---|---|---|---|---|---|---|---|
| base | 0.29ms | 0.38ms | 0.49ms | 0.88ms | 0.21ms | 0.29ms | 0.39ms | 0.60ms |
| ambient-l4 | 0.75ms | 0.88ms | 1.12ms | 1.73ms | 0.56ms | 0.72ms | 0.93ms | 1.68ms |
| sidecar | 2.27ms | 2.74ms | 2.98ms | 3.58ms | 2.10ms | 2.47ms | 2.95ms | 3.41ms |
| ambient-l7 | 2.75ms | 3.06ms | 3.46ms | 3.89ms | 2.30ms | 2.75ms | 2.99ms | 4.14ms |
base 대비 P50 증가분으로 다시 보면, 200 req/s에서 ambient-l4 +0.46ms, sidecar +1.98ms, ambient-l7 +2.46ms이고, 1,000 req/s에서 각각 +0.35ms, +1.89ms, +2.09ms입니다.
읽을 때 주의할 점이 있습니다.
- 2ms를 넘는 값은 덜 정밀합니다. 0.1ms 해상도로 설정해도 fortio 히스토그램은 2ms 위로 0.5ms 폭 구간을 쓰고, 백분위수는 그 안에서 보간합니다. 그래서 sidecar와 ambient-l7의 P99(2.95ms 대 2.99ms 같은)는 구분할 수 있는 차이가 아닙니다. 반대로 2ms 아래인 base와 ambient-l4의 차이는 0.1ms 단위로 믿을 만합니다.
- 긴 꼬리는 회차마다 흔들렸습니다. P99.9가 sidecar에서 한 번 7.1ms, ambient-l7에서 한 번 12.3ms까지 튀었습니다. 나머지 두 회차는 4ms 안쪽이었습니다. 3회로는 꼬리의 분포를 말하기 어렵습니다.
- 부하가 높을 때 P50이 오히려 낮았습니다. 모든 모드에서 1,000 req/s의 P50이 200 req/s보다 작았습니다. 요청 간격이 좁아져 연결과 CPU가 덜 쉬기 때문으로 보이며, 이 부하 범위는 어느 모드도 포화시키지 않았다는 뜻이기도 합니다.
프록시 CPU
같은 60초 동안 kubelet의 컨테이너 누적 CPU 사용량(/stats/summary)을 앞뒤로 읽어 평균 사용 코어 수를 계산했습니다. 측정 구간이 fortio 실행 시간보다 5초 넘게 길어지면 값을 버리도록 했고, 실제 구간은 모두 63초 안팎이었습니다.
| 모드 | 200 req/s | 1,000 req/s | 무엇을 합친 값인가 |
|---|---|---|---|
| sidecar | 0.10 vCPU | 0.37 vCPU | client sidecar 1개 + echo sidecar 3개 |
| ambient-l4 | 0.024 vCPU | 0.09 vCPU | 세 노드의 ztunnel 합계 |
| ambient-l7 | 0.028 + 0.020 vCPU | 0.11 + 0.09 vCPU | waypoint 1개 + ztunnel 합계 |
1,000 req/s에서 sidecar 모드의 프록시 CPU는 ambient L4의 약 4배, ambient L7의 약 1.9배였습니다(200 req/s에서는 약 4배와 2배). sidecar 모드에서는 요청이 Envoy를 두 번 지나고, ambient L7에서는 waypoint Envoy를 한 번, ztunnel을 양쪽에서 지납니다. 다만 sidecar의 비용은 파드 수에 비례하고 ambient의 비용은 노드 수와 waypoint replica 수에 비례하므로, 파드가 많고 노드가 적은 클러스터일수록 차이가 커집니다. 이 시험은 echo 3개, 노드 3대였으니 실제 비율은 각자의 파드 밀도로 다시 계산해야 합니다.
5. EKS에 설치할 때
버전부터 맞춥니다
2026년 10월 기준 Istio 1.31.2가 최신 patch이며 ISTIO-SECURITY-2026-007 보안 수정을 포함합니다. Istio 1.31의 Kubernetes 지원 범위는 1.32~1.37이고, 이 시험의 EKS 1.36은 그 안에 있습니다. 지원 상태는 계속 바뀌니 설치 전에 Istio 지원 매트릭스와 EKS 버전 수명 주기를 다시 확인하세요. Istio 1.31.2는 Gateway API v1.6.0에 맞춰 빌드되어 있어(소스의 go.mod 기준) 이 시험도 v1.6.0 standard CRD를 썼습니다.
노드와 플랫폼 조건
- Fargate는 Istio CNI나 ztunnel 같은 DaemonSet과 특권 네트워킹을 실행할 수 없습니다. Linux EC2 워커 노드를 씁니다.
- EKS Auto Mode는 네트워킹과 로드 밸런서 관리 방식이 달라 별도의 사전 검증이 필요합니다.
- ambient에서 VPC CNI의 Pod ENI trunking과 SecurityGroupPolicy를 함께 쓴다면 strict 모드가 link-local 헬스 프로브를 막을 수 있습니다. EKS ambient 사전 요구사항을 확인하세요.
- VPC CNI의 warm IP 기본값은 노드마다 IP를 넉넉히 잡아 둡니다. 이 시험은 남은 IP가 적은 기존 서브넷을 써서
WARM_IP_TARGET=4,MINIMUM_IP_TARGET=8로 줄였습니다.
istioctl 또는 Helm, 하나만
설치 방법은 istioctl과 Helm 중 하나를 고르고, 같은 control plane에 섞어 쓰지 않습니다. in-cluster operator는 1.24에서 제거되었고, IstioOperator 형식 파일은 istioctl install -f의 입력으로만 남았습니다. Istio 1.31부터 Helm 차트는 https://blob.istio.io/istio-release/charts에서 받습니다. 이 시험은 istioctl로 설치했습니다.
istioctl install -y --set profile=ambient \
--set meshConfig.accessLogFile=/dev/stdout
ambient profile은 istiod, Istio CNI, ztunnel을 설치하고, sidecar 주입도 그대로 지원합니다. 그래서 한 클러스터에서 네임스페이스 레이블만으로 sidecar와 ambient를 나란히 비교할 수 있었습니다. 설치 후에는 istioctl analyze -A와 istioctl proxy-status, ambient라면 istioctl ztunnel-config workloads로 등록 상태를 확인합니다.
6. 트래픽 관리와 retry, 실측 2: 롤아웃 중 실패
세 가지 리소스의 역할 분담
- Gateway: 메시로 들어오는 진입점(호스트, 포트, 프로토콜, TLS)을 정의합니다.
- VirtualService (또는 Gateway API HTTPRoute): 요청을 어디로 보낼지(경로, 헤더, 가중치, retry, timeout) 정합니다.
- DestinationRule: 목적지에 도착한 뒤의 정책(subset, 로드 밸런싱, connection pool, outlier detection, TLS)을 정합니다.
VirtualService가 참조하는 subset은 DestinationRule에 정의되어 있어야 하고, 그 레이블과 일치하는 ready 파드가 있어야 합니다. Istio Gateway(networking.istio.io)와 Kubernetes Gateway API(gateway.networking.k8s.io)는 이름이 비슷하지만 다른 리소스이고, istio.io는 Gateway API를 앞으로의 기본 API로 삼겠다고 밝히고 있습니다(Gateway API 가이드). 새로 시작한다면 Gateway API를 우선 검토하되, 같은 호스트에 두 API를 섞지 않도록 경계를 정합니다.
Retry는 기본으로 켜져 있습니다
retries를 생략해도 retry는 꺼지지 않습니다. VirtualService 참조 문서의 기본 정책은 attempts: 2, retryOn: connect-failure,refused-stream,unavailable,cancelled이고, 최대 전달 횟수는 3회입니다. 끄려면 route에 attempts: 0을 명시해야 합니다. 그리고 retry는 계층마다 곱해집니다. 세 계층이 모두 최대 3회씩 전달하면 최하단 백엔드는 사용자 요청 하나에 최대 27번 호출될 수 있습니다. retry는 체인에서 한 곳에만 둡니다.
비멱등 쓰기에는 메시 retry를 끕니다. POST가 서버에서 실제로 처리되고 응답만 유실되었다면 프록시는 그 사실을 알 수 없어 같은 요청을 다시 보냅니다.
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- name: writes-no-mesh-retry
match:
- method:
regex: "^(POST|PUT|PATCH|DELETE)$"
route:
- destination:
host: order-service
timeout: 10s
retries:
attempts: 0
- name: reads-limited-retry
route:
- destination:
host: order-service
timeout: 5s
retries:
attempts: 2
perTryTimeout: 2s
retryOn: connect-failure,refused-stream
실측: 롤아웃 중 오류와 retry가 가린 실패
200 req/s(연결 16개)로 150초 동안 요청을 보내면서 echo Deployment를 kubectl rollout restart로 세 번 재시작했습니다(replica 3, 기본 RollingUpdate 25%/25%). 기본 spec과, echo 컨테이너에 preStop: sleep 10s를 준 spec을 비교했습니다. 각 셀은 30,000건 중 오류 수이고, 괄호는 같은 시험을 전날 한 번 더 돌린 값입니다.
| 모드 | 기본 spec | preStop sleep 10s |
|---|---|---|
| base | 연결 오류 33 (24) | 0 (0) |
| sidecar | HTTP 503 37 (30) | 0 (0) |
| ambient-l4 | 연결 오류 24 (23) | 0 (0) |
| ambient-l7 | HTTP 503 76, 연결 오류 3 (503 84, 연결 오류 5) | HTTP 503 27 (26) |
"연결 오류"는 HTTP 응답을 받지 못한 경우(fortio 코드 -1)로, 로그에는 connection reset by peer와 3초 timeout이 찍혔습니다. 두 회차의 수치가 비슷해서 경향은 재현된다고 볼 수 있습니다.
client 쪽 Envoy의 재시도 카운터도 함께 읽었습니다. Istio는 기본적으로 클러스터별 Envoy 통계를 대부분 수집하지 않으므로, client sidecar와 waypoint에 proxyStatsMatcher로 echo 클러스터의 upstream_rq_* 통계를 켜야 했습니다.
| 모드와 spec | 사용자에게 보인 503 | 프록시가 재시도해 성공시킨 요청 | upstream 연결 실패 |
|---|---|---|---|
| sidecar, 기본 | 37 | 60 | 59 |
| sidecar, preStop | 0 | 0 | 0 |
| ambient-l7, 기본 | 76 | 0 | 0 |
| ambient-l7, preStop | 27 | 19 | 19 |
여기서 읽을 수 있는 것은 이렇습니다.
- 기본 retry는 연결 실패만 가려 줍니다. sidecar 기본 spec에서 upstream 연결 실패 59건은 모두 재시도로 성공해 사용자에게 보이지 않았지만, upstream이 돌려준 503 37건은 그대로 전달되었습니다. 기본
retryOn에 일반 503 응답이 들어 있지 않기 때문입니다. 즉 "메시가 있으니 롤아웃 오류는 retry가 흡수한다"는 기대는 반만 맞습니다. - 종료 처리가 먼저입니다. preStop 10초는 파드가 종료 신호를 받은 뒤에도 잠시 요청을 받게 해서, EndpointSlice 갱신이 모든 프록시와 kube-proxy에 퍼질 시간을 벌어 줍니다. 이것만으로 base, sidecar, ambient-l4는 0건이 되었습니다. 앱 쪽에서 SIGTERM을 받으면 readiness를 내리고 진행 중인 요청을 마무리하는 구현이 정석이고, preStop sleep은 그것을 대신하는 최소한의 장치입니다.
- ambient-l7은 preStop 뒤에도 503이 남았습니다. waypoint가 종료 중인 파드로 HBONE 연결을 열다 실패한 경우는 19건이 재시도로 가려졌고, 27건은 503으로 드러났습니다. 이 시험만으로 원인을 waypoint의 엔드포인트 갱신 지연이라고 단정할 수는 없지만, waypoint를 두는 서비스라면 롤아웃 중 오류를 따로 측정해 볼 이유는 충분합니다.
7. 보안, 실측 3: mTLS와 L7 인가 정책 함정
Istio는 각 워크로드에 SPIFFE 형식의 신원(spiffe://cluster.local/ns/<namespace>/sa/<serviceaccount>)을 부여하고, 기본 24시간짜리 인증서를 자동 갱신합니다. 이 신원은 ServiceAccount에 묶여 있으므로 ServiceAccount를 서비스별로 나눌수록 인가 정책도 세밀해집니다.
실측: PERMISSIVE와 STRICT
메시 밖(base 네임스페이스)의 Pod가 세 메시 네임스페이스의 echo를 평문으로 불렀습니다.
| PeerAuthentication | sidecar | ambient-l4 | ambient-l7 | 메시 안 client의 호출 |
|---|---|---|---|---|
| PERMISSIVE | 200 | 200 | 200 | 200 |
| STRICT | 연결 reset | 연결 reset | 연결 reset | 200 |
자동 mTLS가 메시 피어끼리 암호화를 해 주는 것과, 평문을 거부하는 것은 다른 이야기라는 점이 그대로 드러납니다. PERMISSIVE에서는 세 모드 모두 평문을 받아들였습니다. 전환 전에는 수신 측 메트릭을 connection_security_policy 레이블로 나눠 평문 호출자를 찾습니다. 이 시험에서도 sidecar는 istio_requests_total에, ambient는 ztunnel의 istio_tcp_connections_opened_total에 mutual_tls와 평문 호출 시기의 값(none, unknown)이 함께 남아 있었습니다.
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: sidecar
spec:
mtls:
mode: STRICT
PeerAuthentication은 수신 측이 무엇을 허용할지 정하고, 송신 측이 어떤 TLS를 쓸지는 DestinationRule의 trafficPolicy.tls가 정합니다. DestinationRule에 tls.mode: DISABLE이 남아 있으면 목적지의 STRICT와 충돌해 연결이 실패합니다.
실측: 경로 조건 하나짜리 AuthorizationPolicy
이번 측정에서 가장 중요한 결과입니다. /admin 경로만 막는 흔한 정책을 모드와 연결 방식만 바꿔 걸었습니다.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: lab-admin
spec:
selector: # 또는 targetRefs: [{kind: Service, group: "", name: echo}]
matchLabels: {app: echo}
action: DENY
rules:
- to:
- operation:
paths: ["/admin*"]
정책을 만든 뒤 2초 간격으로 60초 동안 /echo와 /admin/report를 호출했고, 그 뒤에 echo Pod IP로 직접 /admin/report를 불렀습니다.
| 모드 | 정책 연결 방식 | /echo |
/admin/report |
Pod IP로 /admin/report |
|---|---|---|---|---|
| sidecar | selector, DENY | 200 | 403 | 503 (아래 설명) |
| ambient-l4 | selector, DENY | 200 | 200 (막지 못함) | 200 |
| ambient-l4 | selector, ALLOW (같은 규칙) | 연결 reset | 연결 reset | 연결 reset |
| ambient-l7 | targetRefs Service, DENY | 200 | 403 | 200 (waypoint 우회) |
| ambient-l7 | selector, DENY | 200 | 200 (막지 못함) | 200 |
ambient에서 selector로 건 L7 DENY는 아무것도 막지 않았습니다. ambient 워크로드에 selector로 건 정책은 waypoint가 아니라 ztunnel이 집행합니다. ztunnel은 HTTP 경로를 볼 수 없으니 paths 조건을 버리는데, istioctl ztunnel-config policies -o json으로 보면 규칙이 빈 조건 [[{}]]로 바뀌어 echo 세 Pod에 모두 연결되어 있었습니다. 그런데도 60초 내내 /admin/report가 200이었습니다. waypoint가 있는 ambient-l7 네임스페이스에서도 selector로 걸면 결과가 같았습니다. 적용할 때 나온 신호는 kubectl apply의 경고 한 줄("only HTTP attributes in a DENY rule ... will deny all traffic to TCP ports")뿐이었고, 이 경고는 실제 동작(아무것도 막지 않음)과도 맞지 않았습니다.
istio.io의 L4 정책 문서는 ztunnel이 집행하는 정책에 L7 조건이 있으면 "fail safe by becoming a DENY policy"라고 설명하고, ambient 마이그레이션 문서는 L7 조건만 있는 DENY 규칙도 조건이 빠지면 모든 트래픽을 거부하게 된다고 설명합니다. 이번 구성(Istio 1.31.2)에서 ALLOW 쪽은 문서대로 전체 거부가 되었지만, DENY 쪽은 문서 설명과 달리 아무것도 거부하지 않았습니다. 버전에 따라 동작이 다를 수 있으니 각자의 버전에서 직접 확인하세요. 어느 쪽이든 결론은 같습니다. sidecar에서 ambient로 옮길 때 selector 기반 L7 정책을 그대로 두면 안 됩니다. ALLOW는 서비스를 막아 버리고, DENY는 보호를 조용히 잃습니다.
waypoint 정책은 waypoint를 지나는 트래픽만 막습니다. targetRefs로 Service에 붙인 정책은 Service 주소(echo:8080)로 온 요청을 403으로 막았지만, 같은 경로를 Pod IP로 부르자 waypoint를 거치지 않아 200이 나왔습니다. istio.io도 waypoint 정책은 waypoint에 도달한 트래픽에만 적용된다고 경고하고, waypoint의 신원만 허용하는 L4 정책을 ztunnel에 함께 걸어 우회를 막으라고 권합니다.
# 이 서비스로 들어오는 연결은 waypoint에서 온 것만 허용 (ztunnel이 집행하는 L4 정책)
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: allow-only-waypoint
namespace: ambient-l7
spec:
selector:
matchLabels: {app: echo}
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/ambient-l7/sa/waypoint"]
이 보완 정책은 이번 시험에서 측정하지 않았습니다. 적용한다면 Pod IP 직접 호출이 실제로 막히는지 같은 방식으로 확인하세요.
sidecar의 Pod IP 직접 호출이 503이었던 것은 정책 때문이 아닙니다(정책 거부는 403입니다). 서비스로 알려지지 않은 IP로 가는 요청은 client sidecar가 통과(passthrough)시키는데, 목적지가 STRICT라 평문 연결이 거부된 것으로 보입니다.
정책이 퍼지는 시간도 봐야 합니다. 위 표는 마지막 측정의 결과이고, 이때 sidecar 정책은 첫 호출부터 적용되어 있었습니다. 하지만 같은 정책을 앞서 두 번 걸었을 때는 10초 뒤에도 200이 나왔고, 그중 한 번은 25초 뒤에도 replica 하나가 계속 200을 돌려줬습니다. 정책을 배포한 직후의 몇십 초는 아직 보호되지 않은 구간일 수 있다는 뜻입니다.
기본 거부와 명시적 허용
인가의 출발점은 "기본 거부, 필요한 것만 허용"입니다. 빈 spec의 정책은 어떤 ALLOW 규칙과도 일치하지 않으므로 해당 네임스페이스의 모든 요청을 거부합니다.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: allow-nothing
namespace: default
spec: {}
평가 순서는 CUSTOM, DENY, ALLOW입니다. ALLOW 정책이 하나라도 적용되면 그중 하나에 일치해야 통과합니다. principals나 namespaces 조건은 mTLS 피어 신원이 있어야 의미가 있으니 STRICT와 짝을 이뤄 적용하고, 적용 후에는 istioctl x authz check <pod>(sidecar)나 istioctl ztunnel-config policies(ambient)로 실제로 무엇이 적용되었는지 확인합니다.
RequestAuthentication은 토큰을 "요구"하지 않습니다
RequestAuthentication은 잘못된 JWT는 거부하지만 토큰이 없는 요청은 통과시킵니다. 토큰을 필수로 만들려면 AuthorizationPolicy로 requestPrincipals를 요구해야 합니다. ambient에서는 JWT 검증도 L7 정책이므로 waypoint에 targetRefs로 붙여야 하고, 위의 우회 문제도 똑같이 적용됩니다.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: require-jwt
namespace: default
spec:
selector:
matchLabels:
app: myapp
action: ALLOW
rules:
- from:
- source:
requestPrincipals: ["*"]
8. 복원력, 실측 4: outlier detection과 L4의 연결 단위 분산
Outlier detection은 각 호출자 프록시가 관측한 응답으로 문제 있는 엔드포인트를 일정 시간 부하 분산 후보에서 빼는 passive 검사입니다. istio.io 참조 문서 기준 기본값은 consecutive5xxErrors 5, interval 10s, baseEjectionTime 30s, maxEjectionPercent 10%입니다.
실측: 4개 중 1개가 항상 500을 낼 때
echo 3개 옆에 같은 레이블로 항상 500을 돌려주는 Pod 하나를 추가하고(정상 3, 불량 1), 100 req/s, 연결 8개로 60초(6,000건) 보냈습니다. outlierDetection은 아래 설정입니다.
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: echo-outlier
spec:
host: echo.<namespace>.svc.cluster.local
trafficPolicy:
outlierDetection:
consecutive5xxErrors: 5
interval: 5s
baseEjectionTime: 60s
maxEjectionPercent: 50
| 모드 | 기본값 (DestinationRule 없음) | outlierDetection 적용 |
|---|---|---|
| base | 0.02% (1건) | 해당 없음 |
| sidecar | 25.1% | 0.07% (4건) |
| ambient-l4 | 0% | 0% |
| ambient-l7 | 24.7% | 0.03% (2건) |
sidecar와 ambient-l7은 예상대로였습니다. Envoy가 요청 단위로 부하를 나누니 불량 엔드포인트 비율(25%)만큼 실패하다가, outlierDetection이 처음 몇 건의 500을 보고 불량 Pod를 빼자 실패가 거의 사라졌습니다.
이상한 쪽은 base와 ambient-l4였습니다. 불량 Pod가 있는데도 실패가 거의 없었습니다. 원인을 확인하려고 같은 조건에서 keep-alive를 끄고(요청마다 새 연결) 다시 쟀습니다.
| 모드 | keep-alive 켬 | keep-alive 끔 |
|---|---|---|
| base | 0% | 25.1% |
| ambient-l4 | 0% | 25.4% |
| ambient-l4 + outlierDetection | 0% | 25.7% |
kube-proxy와 ztunnel은 요청이 아니라 TCP 연결 단위로 목적지를 고릅니다. keep-alive 연결 8개가 처음에 정상 Pod에만 붙으면 불량 Pod는 요청을 하나도 받지 않습니다. 반대로 연결 하나가 불량 Pod에 붙었다면 그 연결의 요청이 전부 실패했을 것입니다. 새 연결을 계속 열게 하자 정확히 불량 비율만큼 실패했습니다. 그리고 ambient-l4에 DestinationRule outlierDetection을 걸어도 결과가 바뀌지 않았습니다. ztunnel은 DestinationRule의 L7 복원력 설정을 적용하지 않으므로, ambient에서 outlier detection이 필요하면 waypoint가 있어야 합니다.
운영에서의 의미는 이렇습니다. L4 경로에서 "평소에 오류가 없다"는 것은 불량 엔드포인트가 없다는 증거가 아닙니다. 연결이 오래 유지되는 클라이언트라면 일부 사용자만 계속 실패하고 나머지는 멀쩡한 형태로 장애가 나타날 수 있고, 이때 메시 쪽에서 그 엔드포인트를 빼 줄 장치도 없습니다.
Circuit breaking과 locality
Connection pool 설정은 호출자 프록시가 한 업스트림에 열 수 있는 연결과 대기 요청을 제한합니다. 한도를 넘는 요청은 즉시 503(UO 플래그)이 되며, 이 한도는 서버 전역이 아니라 호출자 프록시 단위입니다(이번 시험에서는 측정하지 않았습니다).
EKS 노드에는 topology.kubernetes.io/zone 레이블이 붙고 istiod는 이것으로 엔드포인트의 locality를 압니다. istio.io에 따르면 기본 메시 구성은 locality 로드 밸런싱이 켜져 있어서, DestinationRule에 outlierDetection을 설정하는 순간 같은 AZ 우선과 locality failover가 함께 활성화됩니다. 원하지 않으면 localityLbSetting.enabled: false를 명시합니다. 이번 시험은 한 AZ에 모았기 때문에 locality 동작은 측정하지 않았습니다. 같은 AZ 안에서 호출이 끝날수록 AZ 간 데이터 전송 요금이 줄어드는 방향인 것은 맞지만, 실제 절감은 바이트와 경로에 따라 달라지니 EKS 네트워크 비용 최적화 가이드와 청구 데이터를 함께 보세요.
9. 실측 5: 모드별로 남는 텔레메트리
"ambient의 mTLS 터널 때문에 트러블슈팅이 어려워지지 않느냐"는 질문을 자주 받습니다. 같은 요청 조합(정상 /echo 100건, /echo?status=500 50건, /admin/report 30건)을 네 모드에 보내고, 메시가 무엇을 남기는지 봤습니다.
| 모드 | 요청 메트릭 (istio_requests_total) |
응답 코드 | 요청 지연 히스토그램 | ztunnel TCP 메트릭 | 요청별 access log |
|---|---|---|---|---|---|
| base | 없음 | 없음 | 없음 | 없음 | 없음 |
| sidecar | 있음 (server sidecar) | 200, 500 | 있음 | 해당 없음 | 180줄, 경로와 응답 코드 포함 |
| ambient-l4 | 없음 | 없음 | 없음 | 연결 수와 바이트 4종 | 없음 (ztunnel 연결 로그만) |
| ambient-l7 | 있음 (waypoint) | 200, 500 | 있음 | 연결 수와 바이트 4종 | 180줄 (waypoint), 경로와 응답 코드 포함 |
ambient-l4의 ztunnel 로그에는 연결마다 한 줄씩 출발지와 목적지 워크로드, SPIFFE 신원, 바이트 수가 남았습니다. 누가 누구에게 연결했는지는 알 수 있지만, 그 연결 안에서 어떤 경로를 불렀고 몇 번이 500이었는지는 알 수 없습니다.
info access connection complete src.workload="client-..." src.namespace="ambient-l4"
src.identity="spiffe://cluster.local/ns/ambient-l4/sa/default" dst.hbone_addr=10.0.2.28:8080
dst.service="echo.ambient-l4.svc.cluster.local" dst.workload="echo-..." ...
그러니 답은 이렇습니다. 암호화 자체가 가시성을 빼앗지는 않습니다. sidecar도 선 위의 트래픽은 똑같이 암호문이고, 메트릭과 로그는 프록시가 암호화 전후 지점에서 만듭니다. 가시성의 차이는 암호화가 아니라 L4냐 L7이냐에서 옵니다. ambient L4는 지연과 CPU가 가장 작았지만(4절) HTTP 수준의 관측은 아예 없습니다. 메시에서 HTTP 오류율을 봐야 한다면 waypoint를 두거나 애플리케이션 쪽 APM이 그 역할을 해야 하고, waypoint를 두면 4절의 지연과 6절의 롤아웃 503이 함께 따라옵니다.
분산 추적은 모드와 상관없이 애플리케이션이 추적 헤더를 전파해야 하나의 trace로 이어집니다.
10. AWS 통합 지점
Ingress gateway 앞단의 로드 밸런서
Istio ingress gateway 앞에는 AWS Load Balancer Controller로 NLB나 ALB를 둡니다.
| 구분 | NLB 앞단 | ALB 앞단 |
|---|---|---|
| 계층 | L4 | L7 |
| TLS 종료 | TCP 패스스루로 Istio에서 종료하거나 NLB TLS 리스너에서 종료 | ALB에서 ACM 인증서로 종료 |
| 잘 맞는 경우 | gRPC, WebSocket 같은 장시간 연결, Istio에서 TLS 종료 | 경로 기반 라우팅, AWS WAF, Cognito나 OIDC 인증 연동 |
| 게이트웨이 Service | type: LoadBalancer에 NLB annotation |
ClusterIP로 두고 Ingress로 ALB 생성 |
- 대상 유형은
ip로 두고, 헬스 체크는 게이트웨이의 상태 포트15021의/healthz/ready를 봅니다. - ALB가 TLS를 종료하면 뒤쪽 Istio Gateway는 HTTP 리스너로 받고 HTTPS 리다이렉트를 넣지 않습니다.
- ACM 인증서 ARN을 Istio의
credentialName에 직접 넣을 수는 없습니다. Istio에서 TLS를 종료하려면 인증서를 Kubernetes Secret으로 넣고 갱신을 자동화해야 합니다. - 로드 밸런서는 가능하면 internal로 두고, 외부 진입은 CloudFront 같은 관리형 엣지를 앞에 두는 구성을 먼저 검토합니다. 보안 그룹은 앞단에서 오는 트래픽만 허용합니다.
AWS API 자격 증명과 관찰성
메시 안의 워크로드가 AWS API를 호출할 때는 IRSA나 EKS Pod Identity를 그대로 씁니다. 이것은 AWS API 접근용 자격 증명이고, Istio의 SPIFFE 신원은 메시 안 서비스 간 인증용입니다. 둘 다 ServiceAccount 기준이니 ServiceAccount 설계를 한 번 잘 해 두면 양쪽이 함께 깔끔해집니다.
관찰성 백엔드로는 CloudWatch Container Insights(Prometheus 수집 후 EMF metric declaration에 선언한 메트릭만 게시), AWS X-Ray나 Jaeger(추적), Prometheus와 Grafana, Kiali를 씁니다. 9절에서 본 것처럼 ambient L4에는 HTTP 메트릭 자체가 없으므로, 대시보드를 모드에 맞게 따로 설계해야 합니다.
11. 운영에서 자주 막히는 곳
503이 보일 때 먼저 의심할 것
| 상황 | 의심할 원인 | 확인 방법 |
|---|---|---|
| subset으로 보내는 route에서만 실패 | DestinationRule에 subset이 없거나 일치하는 ready 엔드포인트가 없음 | istioctl proxy-config endpoints, Service selector와 Pod 레이블 |
| STRICT 적용 직후 연결 실패 | 호출자에 메시가 없거나 DestinationRule TLS override가 DISABLE | istioctl proxy-config clusters, kubectl get destinationrule -A |
| 부하가 오를 때만 실패 | connection pool 한도 초과(UO) | Envoy circuit breaker overflow 메트릭 |
| 배포나 스케일인 때만 실패 | 파드 종료 처리, 엔드포인트 갱신 지연 | preStop과 SIGTERM 처리, access log의 응답 플래그 |
| waypoint 뒤에서 롤아웃 직후 첫 요청이 실패 | waypoint가 새 Pod로 HBONE 연결을 열지 못함(UF,URX) |
waypoint access log의 응답 플래그 |
마지막 줄은 이번 시험에서 한 번 본 현상입니다. 롤아웃이 끝나고 몇 분 쉰 뒤 부하를 시작하자 waypoint가 새 echo Pod로 HBONE 연결을 열다 실패(UF,URX)해 첫 요청이 503이 되었고, 곧 정상으로 돌아왔습니다. 재현 횟수가 적어 원인은 확인하지 못했습니다.
Sidecar 주입과 시작, 종료 순서
- 네임스페이스 레이블(
istio-injection=enabled또는istio.io/rev)은 새로 만들어지는 파드에만 적용됩니다. 기존 파드에는 롤아웃이 필요합니다. - native sidecar에서는 프록시가 initContainers에 있으므로, "프록시가 없다"고 판단하기 전에 initContainers도 봅니다.
istioctl x check-inject도 도움이 됩니다. - 시작 순서 문제에는
holdApplicationUntilProxyStarts: true가 있지만, Envoy가 준비되지 못하는 근본 원인(istiod 연결, 인증서)을 고치지는 않습니다. - 종료 순서는 6절의 측정처럼 앱의 종료 처리가 먼저입니다. native sidecar는 앱 컨테이너가 끝난 뒤 프록시를 내리므로 기존 방식의 "프록시가 먼저 죽는" 문제를 줄여 줍니다.
업그레이드와 디버깅
control plane은 revision 기반 canary로 올리고(istioctl install --set revision=<rev>), 모든 프록시가 옮겨 간 것을 istioctl proxy-status로 확인한 뒤 이전 revision을 지웁니다.
istioctl analyze -A # 정적 구성 검사
istioctl proxy-status # 프록시 동기화 상태
istioctl proxy-config all <pod> -n <ns> # 실제 Envoy 구성 (sidecar, waypoint)
istioctl x authz check <pod> -n <ns> # sidecar에 적용된 인가 정책
istioctl ztunnel-config workloads # ambient: 등록 상태와 프로토콜(HBONE/TCP)
istioctl ztunnel-config policies -o json # ambient: ztunnel이 실제로 받은 정책 규칙
istioctl analyze와 kubectl apply의 경고는 정적 검사일 뿐입니다. 7절의 정책처럼 경고 내용과 실제 동작이 다를 수 있으니, 최종 판단은 실제 요청과 프록시가 받은 구성으로 합니다.
12. Istio와 대안 비교
| 후보 | 이럴 때 검토 | 주의할 점 |
|---|---|---|
| Istio | 세밀한 L7 트래픽 제어, 워크로드 신원 기반 mTLS와 인가, 멀티 클러스터, VM 통합이 필요할 때 | control plane과 프록시 수명 주기, 인증서, 정책을 직접 운영 |
| Amazon VPC Lattice | 여러 VPC나 계정에 걸친 서비스 연결, EKS와 EC2, Lambda가 섞인 환경을 관리형으로 연결하고 싶을 때 | fault injection이나 Istio 수준의 circuit breaker 정책 API는 없음 |
| Linkerd | Kubernetes 중심으로 더 단순한 메시를 원할 때 | stable 아티팩트의 릴리스 채널과 지원 조건을 따로 확인 |
| Cilium | L3/L4 네트워크 정책과 eBPF 데이터패스가 주 목적일 때 | L7은 Envoy 같은 프록시가 필요하고, 상호 인증과 암호화는 별도 설정 |
Istio와 VPC Lattice는 배타적이지 않습니다. 클러스터 안은 Istio로, 클러스터 간이나 계정 간 연결은 Lattice로 처리하는 구성이 가능하며, 이때 메시의 mTLS가 Lattice의 IAM(SigV4) 인증을 대신하지는 않습니다. 대안 비교는 측정하지 않았습니다.
13. 이번 측정의 한계
- 워크로드가 너무 단순합니다. echo 서버는 처리 시간이 거의 0이라 프록시의 몫이 크게 보입니다. 실제 서비스의 처리 시간이 수십 ms라면 1~2ms의 비중은 훨씬 작습니다.
- 반복이 적습니다. 지연은 3회, 롤아웃은 2회, 나머지는 1~2회입니다. 꼬리 지연(P99.9)과 롤아웃 오류의 작은 차이는 오차 범위로 봐야 합니다.
- 규모가 작습니다. 노드 3대, echo 3개, client 1개, 한 AZ였습니다. istiod의 구성 푸시 부하, 파드 수에 따른 sidecar 메모리, AZ 간 지연과 locality 동작은 보지 못했습니다.
- 2ms 위의 백분위수는 0.5ms 폭 구간에서 보간한 값입니다. sidecar와 ambient-l7의 P99 비교에는 쓰지 마세요.
- Graviton(arm64) 한 종류만 썼습니다. x86 인스턴스에서는 절대값이 다를 수 있습니다.
- 측정하지 않은 것: connection pool 한도, waypoint 우회를 막는 보완 정책, HTTPS를 앱이 직접 하는 경우(이중 암호화), 처리량 한계와 포화 지점.
14. 원시 결과, 재현 방법, 더 읽기
측정 결과 JSON과 재현 스크립트를 공개합니다. 스크립트에서 VPC, 서브넷, 계정, 접속 IP는 자리표시자로 바꿨습니다.
- 결과 설명 (README)
- 지연과 CPU: latency-20261021-0121.json
- 롤아웃: rollout-20261021-0051.json, 첫 회차 rollout-20261020-1413.json
- 인가 정책: policy-20261020-1340.json, 첫 시도 (10초 대기)
- mTLS: mtls-20261020-1342.json
- 텔레메트리: obs-20261020-1325.json
- outlier detection: outlier-20261020-1351.json, 연결 단위 outlier-conn-20261021-0028.json
- 재현 스크립트: cluster.yaml, 01-istio.sh, workloads.sh, run.py
재현 순서는 eksctl create cluster -f cluster.yaml, ./01-istio.sh, python3 tests/run.py <latency|rollout|policy|mtls|obs|outlier|outlier-conn>, 끝나면 ./teardown.sh입니다. 시험 클러스터는 측정이 끝난 뒤 삭제했습니다.
이 글에서 줄인 개념과 YAML 예제는 원본 문서 세트에 자세히 있습니다.
- Istio 문서 홈
- 설치 및 초기 설정, 아키텍처, AWS 통합
- Ambient Mode
- Traffic Management와 Retry 및 Timeout
- Security: mTLS, 권한 부여
- Resilience: Outlier Detection
- 문제 해결, 모범 사례, 대안 비교
참고 자료
- Istio 지원 릴리스 매트릭스, Istio 1.31.2, ISTIO-SECURITY-2026-007
- Ambient overview, Waypoint 등록, HBONE, Ambient 플랫폼 사전 요구사항
- Ambient L4 정책, Ambient L7 기능, Ambient 마이그레이션: ambient 모드 켜기
- Kubernetes Gateway API (Istio)
- VirtualService 참조, DestinationRule 참조, AuthorizationPolicy 참조
- Locality Load Balancing
- Istio Security 개념, Proxy 진단 도구
- Istio 성능과 확장성
- Amazon EKS Kubernetes 버전 수명 주기, EKS Pod Identity, EKS 네트워크 비용 최적화
- AWS Load Balancer Controller, Amazon VPC Lattice란
- fortio