Sidecar 리소스는 '좁은 범위가 넓은 범위를 통째로 이기는' override 계층으로, 사이드카에 푸시할 설정을 좁혀 성능과 egress 거버넌스를 동시에 얻는다
Istio의 기본 가정은 "메시 안 모든 워크로드가 다른 모든 워크로드에 접근 가능"이다. 그래서 istiod는 각 Envoy에 메시 전체 설정을 푸시하고, 규모가 커지면 이것이 메모리·xDS push·istiod CPU를 동시에 압박한다. Sidecar 리소스는 두 개의 서로 다른 레버로 이를 푼다 — egress.hosts(이 워크로드가 알아야 할 설정의 범위를 축소 → 성능)와 outboundTrafficPolicy(레지스트리 밖 트래픽의 차단 정책 → 거버넌스). 이 문서가 세우려는 단 하나의 멘탈모델: 세 scope(Mesh / Namespace / Workload)는 merge가 아니라 가장 좁은 하나가 통째로 이기는 override 의미론이다. 운영 detail·YAML 전문은 Sidecar scope 운영 가이드 참조.
1. 배경 — Istio의 "모두가 모두를 안다"는 기본 가정과 그 비용
Istio를 쓰는 순간 service registry(Service / ServiceEntry로 채워지는 메시의 주소록)에 등록된 모든 서비스가 그 워크로드의 잠재적 upstream으로 취급된다. 이건 편의성의 산물이다 — 개발자가 아무 서비스나 이름으로 부르면 곧장 라우팅되니까. 대가는 데이터 평면 쪽 Envoy 하나하나가 메시 전체의 cluster/listener/route/endpoint를 통째로 들고 있어야 한다는 것이다.
규모가 작을 땐 안 보이지만, 워크로드가 N개로 늘면 사이드카 하나가 보유할 설정은 대략 O(N)으로 커지고, push 비용은 변경 빈도와 곱해져 더 빠르게 나빠진다. 구체적으로 세 곳을 동시에 누른다.
- 메모리: 사이드카 하나가 수 MB 설정 보유 (실측 사례: 2MB → Sidecar 적용 후 644KB).
- xDS push 폭증: registry의 어느 서비스가 바뀌어도, 그걸 호출하지도 않는 워크로드까지 새 설정을 받는다.
- istiod CPU: push 대상 × 설정 크기가 그대로 부하.
여기서 던질 질문은 "왜 Envoy는 자기가 부르지도 않는 서비스의 설정까지 받아야 하나?"이다. 받을 이유가 없다 — istiod가 그 워크로드의 실제 의존 대상을 알 길이 없어서 보수적으로 전부 보낼 뿐이다. Sidecar 리소스는 바로 그 정보를 운영자가 명시적으로 주입하는 통로다. "이 워크로드가 실제로 호출하는 대상만 알면 된다"고 선언해 위 곱셈을 끊는다. 이는 control-plane 성능 튜닝의 1순위 권고이며, 동일 맥락의 다른 요인(스코핑 외의 push 빈도·proxy 수 등)은 control plane 성능 요인에서 다룬다.
전제 개념 정리: registry(메시가 아는 목적지 목록), xDS(istiod가 Envoy에 설정을 밀어 넣는 푸시 프로토콜 — CDS/LDS/RDS/EDS), PassthroughCluster / BlackHoleCluster(registry에 매칭 안 되는 outbound를 각각 "통과" / "차단"으로 처리하는 두 합성 cluster). 이 세 가지가 아래 메커니즘의 부품이다.
2. 핵심 멘탈모델 — Sidecar는 'override 계층'이고, 그 위에 두 개의 독립 레버가 얹힌다
머릿속에 그릴 단 하나의 그림은 이것이다.
Sidecar는 "이 Pod가 받을 설정"을 정하는 override 계층이다. 어느 Pod에든 적용되는 Sidecar는 항상 정확히 하나뿐이며 — 가장 좁은 범위가 이긴다 — 그 하나가
egress.hosts(무엇을 알게 할지=범위)와outboundTrafficPolicy(모르는 곳을 어떻게 처리할지=차단)라는 서로 독립된 두 손잡이를 함께 든다.
이 한 문장에서 나머지가 전부 따라 나온다. 좁은 범위가 넓은 범위에 "합쳐지는" 게 아니라 통째로 갈아치운다는 것, 그리고 범위(성능)와 차단(거버넌스)이 같은 리소스 안의 다른 손잡이라는 것 — 이 둘이 거의 모든 오해의 진원지다.
2-1. 두 개의 레버: egress.hosts vs outboundTrafficPolicy
가장 흔한 오해는 "egress.hosts에 안 적으면 차단된다"는 것이다. 아니다. 둘은 독립된 레버이고, 답하는 질문 자체가 다르다.
| 레버 | 답하는 질문 | 빼면 일어나는 일 |
|---|---|---|
egress.hosts |
"이 사이드카가 무엇을 알게 할까?" (푸시할 cluster/listener의 범위) | 범위 축소 안 됨 = 메시 전체 설정 유지(성능 이득 없음). 차단과는 무관 |
outboundTrafficPolicy.mode |
"registry에 없는 호스트로 가는 트래픽을 어떻게 처리할까?" | 기본값 ALLOW_ANY → 미등록 외부 호출이 PassthroughCluster로 그냥 통과 |
왜 굳이 둘을 쪼갰나? "범위를 줄이는 일"과 "모르는 목적지를 막는 일"은 본질적으로 다른 결정이기 때문이다. 설정을 가볍게 하고 싶다고 해서 반드시 외부를 차단하고 싶은 건 아니다(그 반대도 마찬가지). 그래서 egress.hosts만 좁히고 outboundTrafficPolicy를 생략하면, 설정은 가벼워져도 "기본 deny" 거버넌스는 생기지 않는다. zero-trust egress("등록 안 된 외부는 막는다")를 원하면 반드시 mode: REGISTRY_ONLY를 함께 둬야 한다.
미등록 목적지의 처리는 결국 어느 합성 cluster로 보내느냐로 갈린다.
REGISTRY_ONLY: registry(Service / ServiceEntry)에 없는 목적지는BlackHoleCluster로 보내 차단 → 호출 측은 보통 502.ALLOW_ANY(기본): 미등록 목적지는PassthroughCluster로 원본 IP:port 그대로 통과.
2-2. 세 scope와 override 우선순위 — 왜 'merge 아님'이 함정인가
Sidecar는 적용 범위가 셋이며, 좁은 범위 하나가 넓은 범위를 통째로 덮어쓴다(병합이 아니다).
| 범위 | 우선순위 | 적용 대상 | 판정 키 |
|---|---|---|---|
| Mesh-wide | ① 가장 낮음 | 메시 안 모든 Pod | rootNamespace + name: default + selector 없음 |
| Namespace-wide | ② | 해당 NS 모든 Pod | 대상 NS + workloadSelector 없음 |
| Workload-specific | ③ 가장 높음 | selector 일치 Pod | workloadSelector.labels 지정 |
override 의미론이 왜 위험한가: workload-specific Sidecar가 붙은 Pod에는 NS-wide나 mesh-wide가 추가로 합쳐지지 않는다. 그 Pod에는 가장 좁은 정책 하나만 유효하다. 직관적으로는 "더 구체적인 규칙이 일반 규칙 위에 얹힌다(덮어쓴다)"고 기대하기 쉬운데, 여기선 일반 규칙이 통째로 사라진다. 따라서 workload Sidecar를 쓸 때는 그 워크로드가 필요로 하는 egress를 빠짐없이 다시 나열해야 한다(istio-system 모니터링·제어 트래픽 포함). 빠뜨리면 "갑자기 어떤 호출만 502" 류 장애가 난다 — 이게 실무에서 가장 자주 밟는 지뢰다.
2-3. Mesh-wide 판정 조건의 정확한 근거
"mesh-wide는 istio-system에 두면 된다"는 흔한 요약은 부정확하다. 실제 판정 조건은 세 가지 동시 충족이다.
metadata.namespace가meshConfig.rootNamespace(기본값istio-system)와 일치metadata.name: defaultworkloadSelector없음
파일명은 무관하다. 결정하는 것은 리소스의 name/namespace/selector 조합이다. rootNamespace를 별도로 지정한 클러스터라면 istio-system이 아니라 그 NS에 둬야 mesh-wide로 동작한다. 왜 이렇게 빡빡하게 정의했나 — mesh-wide는 메시 전체의 기본값을 갈아치우는 가장 강력한 override라서, 우연히 만들어지면 안 되기 때문이다. 그래서 "특정 이름 + 특정 NS + selector 없음"이라는 명시적 삼중 조건을 요구한다.
2-4. ServiceEntry와의 분업 — '등록'과 '노출'은 다른 일
egress.hosts는 "이미 registry에 있는 것 중 무엇을 이 사이드카가 알게 할지" 필터일 뿐이다. 외부 도메인(예: Stripe API)을 메시에 등록하는 것은 ServiceEntry의 역할이다. 따라서 외부 호출을 허용하려면 두 단계가 필요하다.
ServiceEntry로 외부 host를 registry에 등록 (*.example.com또는<ns>/<name>포맷)- 해당 워크로드
Sidecar의egress.hosts에 그 host(또는 NS)를 포함
REGISTRY_ONLY에서 ServiceEntry 없이 egress.hosts에만 외부 도메인을 적으면 매칭되는 cluster가 없어 여전히 차단된다. 등록(registry에 존재하게 함)과 노출(이 사이드카가 그걸 알게 함)을 분리해서 생각하는 것이 핵심이다 — 둘 다 통과해야 트래픽이 흐른다.
2-5. 성능 경로: 좁은 scope → 작은 xDS → 적은 메모리
좁은 egress.hosts는 istiod가 그 워크로드용으로 계산하는 CDS(cluster)/LDS(listener)/RDS(route)/EDS(endpoint) 집합을 줄인다. 결과적으로:
- 사이드카가 받는 xDS config 크기 감소 (cluster/listener 수가 의존 대상으로 한정).
- registry 변경이 발생해도, 그 변경이 이 워크로드 scope 밖이면 push 자체가 발생하지 않음 → push 빈도 감소. (이게 §1의 "곱셈을 끊는다"의 실체다.)
- Envoy 프로세스 메모리 footprint 감소.
sync 상태가 실제로 줄었는지는 proxy 동기화 진단으로 확인한다 — SYNCED/NOT SENT/STALE의 의미와 xDS 타입별 보고는 data-plane sync state 참조. (참고: outboundTrafficPolicy로 만든 연결 거버넌스와, DestinationRule 기반의 connection pool·outlier detection으로 만드는 장애 격리는 별개 레버다 — 후자는 circuit breaking 메커니즘.)
3. 떴는지 확인 — mesh-wide zero-trust 예시와 검증
위 멘탈모델을 가장 작은 실물로 굳히는 예시: mesh-wide default Sidecar로 (a) 범위를 istio-system/*로 좁히고 (b) 미등록 외부를 차단한다. 두 손잡이를 한 리소스에서 동시에 쓰는 정석 형태다.
apiVersion: networking.istio.io/v1
kind: Sidecar
metadata:
name: default # mesh-wide 필수 조건
namespace: istio-system # = meshConfig.rootNamespace
spec:
egress:
- hosts:
- "istio-system/*" # 모니터링·제어 트래픽만 알림
outboundTrafficPolicy:
mode: REGISTRY_ONLY # 미등록 외부 차단 (zero-trust 기본값)
줄별로 왜: name: default + namespace: istio-system(=rootNamespace) + selector 없음 → §2-3의 삼중 조건을 채워 mesh-wide로 인식된다. egress.hosts: ["istio-system/*"]은 모든 사이드카가 기본적으로 istio-system만 알게 좁힌다(성능 손잡이). mode: REGISTRY_ONLY는 그 외 미등록 목적지를 BlackHole로 보내 차단한다(거버넌스 손잡이). 둘이 함께 있어야 "가볍고 + 막힌" 상태가 된다.
검증:
$ istioctl proxy-config clusters <pod>.<ns> | grep -c outbound
# Sidecar 적용 후 cluster 수가 메시 전체 → 의존 대상만으로 급감하면 OK
$ kubectl exec <pod> -c istio-proxy -- curl -s -o /dev/null -w "%{http_code}\n" http://example.com
502 # REGISTRY_ONLY + 미등록 호스트 → BlackHole
첫 명령의 cluster 수 급감이 §2-5 성능 경로의 직접 증거이고, 둘째 명령의 502이 §2-1 차단 레버의 직접 증거다. NS·workload 단위 예시와 ./ 접두사(현재 NS) 의미, 실무 체크리스트는 Sidecar scope 운영 가이드로 위임한다.
핵심 정리
- Sidecar의 멘탈모델: Pod마다 적용되는 Sidecar는 정확히 하나, 가장 좁은 게 이긴다(override). 그 하나가 두 손잡이를 든다.
Sidecar는 성능 도구이자 보안 정책이다.egress.hosts(범위 축소) ≠outboundTrafficPolicy(차단 정책) — 둘은 독립 레버.- scope는 Mesh < Namespace < Workload 우선순위로 override(merge 아님). 좁은 Sidecar가 붙은 Pod엔 그 하나만 유효 → egress를 빠짐없이 재나열.
- mesh-wide 조건 = rootNamespace + name: default + selector 없음 (파일명 무관).
- zero-trust egress = mesh-wide
default+outboundTrafficPolicy.mode: REGISTRY_ONLY. 기본값은ALLOW_ANY(PassthroughCluster 통과). - 외부 host 허용은 ServiceEntry(등록) + egress.hosts(노출) 두 단계.
What you might be missing
- 동일 scope 중복은 미정의 동작: 같은 범위(예: 한 NS에 selector 없는 Sidecar 2개)에 다중 정의 시 Istio docs가 동작을 보장하지 않는다. "가장 좁은 하나가 이긴다"는 규칙은 범위가 다를 때 얘기이고, 같은 범위가 둘이면 승자가 정의되지 않는다. 하나로 합치거나 workloadSelector로 분리할 것.
- workload Sidecar의 누락 함정: workload-specific을 쓰면 NS-wide/mesh-wide가 합쳐지지 않으므로,
istio-system/*(텔레메트리·제어) 같은 항목을 깜빡 빠뜨리면 메트릭/제어 트래픽까지 막혀 진단이 꼬인다. - REGISTRY_ONLY 차단은 502으로 보인다: 호출 측에서 보면 DNS/네트워크 오류가 아니라 BlackHoleCluster로 인한 502이다. Envoy access log의 response flag(예:
NR/UH)와istioctl proxy-config clusters의 BlackHole/Passthrough 존재로 원인을 분별해야 한다. - mesh-wide만으로 egress가 0이 되진 않는다:
egress.hosts에istio-system/*만 남겨도 그건 "사이드카가 그 외엔 모른다"일 뿐, 실제 차단을 보장하는 것은outboundTrafficPolicy다. 둘을 항상 한 세트로 본다. - scope 축소의 안전 절차: 운영 메시에서 갑자기 좁은 Sidecar를 넣으면 미처 파악 못 한 의존 호출이 끊긴다. 먼저 access log/telemetry로 실제 호출 대상을 수집한 뒤 egress.hosts를 도출하는 순서가 안전하다.