---
type: note
tags: [istio, sidecar, egress, performance]
created: 2026-06-07
---
# Sidecar 리소스는 '좁은 범위가 넓은 범위를 통째로 이기는' override 계층으로, 사이드카에 푸시할 설정을 좁혀 성능과 egress 거버넌스를 동시에 얻는다
> [!abstract]
> Istio의 기본 가정은 "메시 안 모든 워크로드가 다른 모든 워크로드에 접근 가능"이다. 그래서 istiod는 각 Envoy에 **메시 전체 설정**을 푸시하고, 규모가 커지면 이것이 메모리·xDS push·istiod CPU를 동시에 압박한다. `Sidecar` 리소스는 두 개의 **서로 다른 레버**로 이를 푼다 — `egress.hosts`(이 워크로드가 알아야 할 설정의 *범위*를 축소 → 성능)와 `outboundTrafficPolicy`(레지스트리 밖 트래픽의 *차단* 정책 → 거버넌스). 이 문서가 세우려는 단 하나의 멘탈모델: 세 scope(Mesh / Namespace / Workload)는 merge가 아니라 **가장 좁은 하나가 통째로 이기는 override 의미론**이다. 운영 detail·YAML 전문은 [Sidecar scope 운영 가이드](gw__src-sidecar-scope.html) 참조.
## 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 성능 요인](arch__note-control-plane-performance-factors.html)에서 다룬다.
```mermaid
flowchart LR
subgraph NoSidecar["No Sidecar (default)"]
I1[istiod] -->|full mesh config| E1[Envoy A]
I1 -->|full mesh config| E2[Envoy B]
I1 -->|full mesh config| E3[Envoy C]
end
subgraph WithSidecar["With Sidecar egress.hosts"]
I2[istiod] -->|only A's deps| F1[Envoy A]
I2 -->|only B's deps| F2[Envoy B]
end
```
전제 개념 정리: **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 그대로 통과.
```mermaid
flowchart TD
REQ[outbound request] --> Q{registry에 host 존재?}
Q -->|yes| UP[matched cluster -> upstream]
Q -->|no, ALLOW_ANY| PASS[PassthroughCluster - 통과]
Q -->|no, REGISTRY_ONLY| BH[BlackHoleCluster - 502 차단]
```
### 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" 류 장애가 난다 — 이게 실무에서 가장 자주 밟는 지뢰다.
```mermaid
flowchart TD
M["Mesh-wide
(rootNs/default, no selector)"] -->|overridden by| N["Namespace-wide
(ns/default, no selector)"]
N -->|overridden by| W["Workload-specific
(workloadSelector)"]
W -.->|"selector Pod엔 이것만 적용"| P[(target Pod)]
```
### 2-3. Mesh-wide 판정 조건의 정확한 근거
"mesh-wide는 `istio-system`에 두면 된다"는 흔한 요약은 부정확하다. 실제 판정 조건은 **세 가지 동시 충족**이다.
1. `metadata.namespace`가 `meshConfig.rootNamespace`(기본값 `istio-system`)와 일치
2. `metadata.name: default`
3. `workloadSelector` 없음
파일명은 무관하다. 결정하는 것은 리소스의 name/namespace/selector 조합이다. rootNamespace를 별도로 지정한 클러스터라면 `istio-system`이 아니라 그 NS에 둬야 mesh-wide로 동작한다. 왜 이렇게 빡빡하게 정의했나 — mesh-wide는 메시 전체의 기본값을 갈아치우는 가장 강력한 override라서, 우연히 만들어지면 안 되기 때문이다. 그래서 "특정 이름 + 특정 NS + selector 없음"이라는 명시적 삼중 조건을 요구한다.
### 2-4. ServiceEntry와의 분업 — '등록'과 '노출'은 다른 일
`egress.hosts`는 **"이미 registry에 있는 것 중 무엇을 이 사이드카가 알게 할지"** 필터일 뿐이다. 외부 도메인(예: Stripe API)을 메시에 *등록*하는 것은 `ServiceEntry`의 역할이다. 따라서 외부 호출을 허용하려면 두 단계가 필요하다.
1. `ServiceEntry`로 외부 host를 registry에 등록 (`*.example.com` 또는 `/` 포맷)
2. 해당 워크로드 `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](xds__note-data-plane-sync-state.html) 참조. (참고: outboundTrafficPolicy로 만든 연결 거버넌스와, DestinationRule 기반의 connection pool·outlier detection으로 만드는 장애 격리는 별개 레버다 — 후자는 [circuit breaking 메커니즘](gw__note-circuit-breaking-mechanisms.html).)
## 3. 떴는지 확인 — mesh-wide zero-trust 예시와 검증
위 멘탈모델을 가장 작은 실물로 굳히는 예시: mesh-wide `default` Sidecar로 (a) 범위를 `istio-system/*`로 좁히고 (b) 미등록 외부를 차단한다. 두 손잡이를 한 리소스에서 동시에 쓰는 정석 형태다.
```yaml
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로 보내 차단한다(거버넌스 손잡이). 둘이 함께 있어야 "가볍고 + 막힌" 상태가 된다.
검증:
```bash
$ istioctl proxy-config clusters . | grep -c outbound
# Sidecar 적용 후 cluster 수가 메시 전체 → 의존 대상만으로 급감하면 OK
$ kubectl exec -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 운영 가이드](gw__src-sidecar-scope.html)로 위임한다.
## 핵심 정리
- 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를 도출하는 순서가 안전하다.