---
title: Istio Sidecar CRD 적용 범위(scope) 설정 방법
date: 2026-06-07
type: guide
domain: istio
tags: [sidecar, egress, scope, registry-only]
---
> [!abstract]
> `Sidecar` 리소스는 한 워크로드 Envoy가 받는 설정을 좁혀 **config를 경량화**하고, `outboundTrafficPolicy`와 결합해 **egress 거버넌스**를 만든다. 멘탈모델 하나: scope는 Mesh / Namespace / Workload 세 단계지만 **어느 Pod에든 적용되는 Sidecar는 정확히 하나 — 가장 좁은 것이 통째로 이긴다**(merge 아님, override). 이 문서는 그 override 의미론을 세 scope의 **완전한 YAML(주석 포함)**로 따라 만들고, REGISTRY_ONLY 차단을 실측으로 굳힌다. 개념 전반은 [Sidecar scope 개념 노트](/docs/istio/egress/sidecar-scope/)에.
**대상독자:** 메시 규모가 커져 istiod push 비용이 보이기 시작했거나, egress를 "기본 deny"로 잠그려는 SRE.
**선행개념:** xDS push 모델(CDS/LDS/RDS/EDS), service registry, ServiceEntry.
**환경:** Istio 1.30, Envoy. cluster 이름 규칙 `direction|port|subset|fqdn`.
**범위:** Sidecar의 3 scope YAML 전문 + 판정 규칙 + REGISTRY_ONLY 검증. 차단 메커니즘의 *왜*는 개념 노트로 위임.
## 1. 배경 — Sidecar 리소스가 푸는 문제
Istio의 기본 가정은 "메시 안 모든 서비스가 다른 모든 서비스에 접근 가능하다"이다. 편의성의 산물이다 — 개발자가 아무 서비스나 이름으로 부르면 곧장 라우팅된다. 대가는 데이터 평면 쪽이 치른다: `istiod`는 그 워크로드가 실제로 무엇을 부르는지 알 길이 없으니, **보수적으로 메시 전체 설정**(모든 cluster/endpoint/listener)을 각 Envoy에 푸시한다. 200개 워크로드 규모만 돼도 사이드카 하나당 설정이 수 MB에 이르고, 이것이 메모리·xDS push·`istiod` CPU를 동시에 압박한다.
핵심 질문은 "왜 Envoy는 자기가 부르지도 않는 서비스의 설정까지 받아야 하나?"이다. 받을 이유가 없다 — istiod가 의존 대상을 모르기 때문일 뿐이다. `Sidecar` 리소스(CRD)는 운영자가 바로 그 정보를 명시적으로 주입하는 통로이고, 그것으로 두 방향의 문제를 동시에 푼다:
1. **설정 범위 축소(performance)** — `egress.hosts`로 "이 워크로드가 실제로 호출하는 대상"만 선언하면, istiod가 그 사이드카에 보내는 설정 크기가 극적으로 줄고(`Istio in Action` 11장: 2MB → 644KB), registry 변경이 scope 밖이면 push 자체가 발생하지 않는다. 실측은 §4. 컨트롤 플레인을 좌우하는 다른 요인은 [컨트롤 플레인 성능 요인](/docs/istio/control-plane-ops/performance-factors/).
2. **트래픽 거버넌스(security)** — `outboundTrafficPolicy.mode: REGISTRY_ONLY`와 결합하면 메시 레지스트리에 등록되지 않은 외부 호출을 차단하는 zero-trust egress 기본값이 된다.
> [!note] Sidecar 리소스는 "보안 정책"이자 동시에 "성능 최적화 도구"다. `Istio in Action` 11장이 컨트롤 플레인 성능 튜닝의 첫 권고로 "항상 Sidecar 리소스를 정의하라"고 강조하는 이유가 이것이다.
## 2. 핵심 멘탈모델 — '가장 좁은 하나가 통째로 이긴다'
머릿속에 그릴 단 하나의 그림(ANCHOR):
> **Sidecar는 "이 Pod가 받을 설정"을 정하는 override 계층이다. 어느 Pod에든 적용되는 Sidecar는 항상 정확히 하나 — 가장 좁은 범위가 이긴다 — 그리고 그 하나가 `egress.hosts`(무엇을 알게 할지=범위)와 `outboundTrafficPolicy`(모르는 곳을 어떻게 처리할지=차단)라는 독립된 두 손잡이를 함께 든다.**
이 한 문장에서 나머지가 전부 따라 나온다. 좁은 범위가 넓은 범위에 "합쳐지는" 게 아니라 **통째로 갈아치운다**는 것, 그리고 범위(성능)와 차단(거버넌스)이 **같은 리소스 안의 다른 손잡이**라는 것 — 이 둘이 거의 모든 오해의 진원지다.
**세 scope와 우선순위:**
| 범위 | 우선순위 | 적용 대상 | 주된 활용 | 판정 키 |
|---|---|---|---|---|
| Mesh-wide | ① 가장 낮음 | 메시 안 모든 Pod | egress 기본 차단, 공통 기본값 강제 | rootNamespace + selector 없음 (`name: default`는 권장 관례일 뿐 필수 아님) |
| Namespace-wide | ② | 해당 NS 모든 Pod | 팀별 규칙(내부 호출만 허용) | 대상 NS + `workloadSelector` 없음 |
| Workload-specific | ③ 가장 높음 | selector 일치 특정 Pod | 민감 서비스만 추가 egress 허용 | `workloadSelector.labels` 지정 |
좁은 범위가 넓은 범위를 **덮어쓴다**(Workload > Namespace > Mesh). 직관적으로는 "더 구체적인 규칙이 일반 규칙 위에 얹힌다(덮어쓴다)"고 기대하기 쉬운데, 여기선 **일반 규칙이 통째로 사라진다**. 한 Pod가 어떤 Sidecar를 적용받는지 결정 흐름:
```mermaid
flowchart TD
Pod[Pod needs effective Sidecar] --> WL{workloadSelector match?}
WL -->|yes| UseWL[Use that Sidecar only
NS/mesh default NOT merged]
WL -->|no| NS{NS default Sidecar?}
NS -->|yes| UseNS[Use NS default only]
NS -->|no| Root{rootNamespace default?}
Root -->|yes| UseRoot[Use mesh-wide default]
Root -->|no| Full[No Sidecar matches
full mesh config pushed = heavy default]
```
이 흐름도가 override의 실체다 — 매칭은 **위에서 아래로 첫 hit 하나에서 멈춘다**. workload가 match하면 NS/mesh default는 아예 보지 않는다. §3-3의 누락 함정이 여기서 나온다.
> [!correction] "Mesh-wide는 `istio-system` 네임스페이스에 둔다"는 규칙의 정확한 근거.
> mesh-wide로 동작하는 **기술적** 조건은 "**메시 루트 네임스페이스(기본값 `istio-system`)에 있고, `workloadSelector`가 없을 것**" 두 가지뿐이다. 단순히 `istio-system`에 둔다고 되는 게 아니라, 그 네임스페이스가 `meshConfig.rootNamespace`로 지정된 루트여야 한다. 파일명은 무관하고 **`metadata.namespace: ` + selector 없음**이 판정 조건이다. `metadata.name: default`는 **권장 관례(recommended)**일 뿐 기술적 필수조건이 아니다 — 이름이 달라도(예: `metadata.name: my-root-cfg`) rootNamespace에 있고 selector가 없으면 mesh-wide로 동작한다. 다만 같은 네임스페이스에 selector 없는 Sidecar가 둘 이상 있으면 어느 것이 적용될지 **undefined**(§5 체크리스트)이므로, `default`라는 이름 관례는 실수로 두 번째 selector-less Sidecar를 만드는 사고를 피하기 위한 운영 규범으로 지키는 게 안전하다.
## 3. 구성 따라하기 — 세 scope를 좁은 순으로 쌓기
운영 패턴은 항상 같다: **mesh-wide로 바닥을 깔고(기본 deny) → 예외가 필요한 NS·워크로드가 자체 Sidecar로 허가 목록을 확장**한다. 아래 셋은 같은 메시 위에 순서대로 얹는 한 세트다.
### 3-1. Mesh-wide Sidecar (전역 기본값)
목표: "메시 전체에서 외부로 나가는 트래픽은 명시적으로 허용되지 않으면 차단."
```yaml
apiVersion: networking.istio.io/v1
kind: Sidecar
metadata:
name: default # 권장 관례(판정 필수조건 아님) — 필수는 rootNamespace + selector 없음
namespace: istio-system # = meshConfig.rootNamespace 여야 함
spec:
egress:
- hosts:
- "istio-system/*" # 모니터링·제어 트래픽만 허용
outboundTrafficPolicy:
mode: REGISTRY_ONLY # 레지스트리 미등록 호스트 차단 (zero-trust)
```
줄별로 왜: `namespace: istio-system`(=rootNamespace) + selector 없음 → §2의 판정 조건을 채워 mesh-wide로 인식된다(`name: default`는 그 자체가 조건은 아니지만, 충돌 방지를 위한 관례로 유지). `egress.hosts: ["istio-system/*"]`은 모든 사이드카가 **기본적으로 istio-system만** 알게 좁힌다(성능 손잡이). `mode: REGISTRY_ONLY`는 그 외 미등록 목적지를 차단한다(거버넌스 손잡이).
결과: 모든 워크로드가 클러스터 외부로 직접 호출 시 **BlackHoleCluster**로 라우팅된다 — HTTP 요청은 **502 Bad Gateway**, TCP는 connection reset(close)로 끊긴다. (503은 cluster는 있으나 healthy endpoint가 없을 때(`UH`) 등 다른 상황의 코드이므로 혼동하지 말 것.) access log의 `response_flags` 해석은 [Envoy response flags](/docs/istio/xds-envoy/envoy-response-flags/).
> [!correction] `outboundTrafficPolicy.mode`를 명시하지 않으면 기본값은 `ALLOW_ANY`다.
> `outboundTrafficPolicy`를 빼면 hosts에 없는 외부 호출이 차단되는 게 아니라 PassthroughCluster로 그냥 통과한다. "기본 deny" 거버넌스를 원하면 `mode: REGISTRY_ONLY`를 반드시 함께 설정해야 한다. **egress.hosts는 "사이드카에 푸시할 설정 범위"를 좁히는 것이고, 차단 정책은 `outboundTrafficPolicy`가 결정한다 — 둘은 다른 레버다.** 이 분리가 이 문서에서 가장 자주 헷갈리는 지점이니 표로 못박는다:
| 레버 | 답하는 질문 | 빼면 일어나는 일 |
|---|---|---|
| `egress.hosts` | "이 사이드카가 **무엇을 알게** 할까?" (푸시 범위) | 범위 축소 안 됨 = 메시 전체 설정 유지(성능 이득 없음). 차단과 무관 |
| `outboundTrafficPolicy.mode` | "registry에 **없는** 호스트를 **어떻게** 처리할까?" | 기본값 `ALLOW_ANY` → 미등록 외부 호출이 **PassthroughCluster로 그냥 통과** |
### 3-2. Namespace-wide Sidecar (팀/도메인 규칙)
시나리오: `finance` NS는 사내 결제 게이트웨이 + 모니터링만 접근.
```yaml
apiVersion: networking.istio.io/v1
kind: Sidecar
metadata:
name: default # NS-wide도 이름은 default 관례
namespace: finance
spec:
egress:
- hosts:
- "./payments-gw.finance.svc.cluster.local" # './' = 현재 NS 또는 명시된 NS
- "istio-system/*"
```
`./` 접두사는 "사이드카가 속한 네임스페이스"를 의미한다. mesh-wide 차단 + 이 NS 허용 → finance Pod들은 payments-gw OK, 외부 인터넷 여전히 차단. **이 NS default가 mesh default를 override**하므로 `istio-system/*`을 여기서도 다시 적어야 함에 주목(§3-3과 같은 원리).
### 3-3. Workload-specific Sidecar (가장 세밀)
시나리오: `frontend` Deployment만 외부 Stripe API 호출 필요.
```yaml
apiVersion: networking.istio.io/v1
kind: Sidecar
metadata:
name: frontend-egress
namespace: finance
spec:
workloadSelector:
matchLabels:
app: frontend # 이 라벨의 Pod에만 적용
egress:
- hosts:
- "./payments-gw.finance.svc.cluster.local"
- "istio-system/*"
- "external-apis/stripe-se" # ServiceEntry로 등록한 외부 도메인
```
같은 NS의 backend/worker는 Stripe 접근 불가 — workload-specific 정책은 selector 일치 Pod에만 적용되고, 그 Pod에는 NS-wide가 아니라 이 정책 하나만 유효하기 때문이다.
> [!warning] workloadSelector Sidecar는 NS/mesh default를 **상속·병합하지 않는다**.
> selector가 일치하는 Pod는 오직 이 Sidecar 하나만 적용받으므로, NS default가 허용하던 `egress.hosts`가 통째로 사라진다. 위 예시가 `payments-gw`, `istio-system/*`를 다시 나열한 이유가 이것이다 — frontend Pod가 필요로 하는 대상을 **빠짐없이 재나열**하지 않으면, NS default에서 되던 호출이 이 Pod에서만 깨지는 흔한 사고가 난다. `istio-system/*`(텔레메트리·제어)을 깜빡 빠뜨리면 메트릭/probe까지 막혀 진단이 꼬인다.
> [!note] 외부 호스트(`external-apis/stripe-se`)는 그냥 적는다고 되는 게 아니라 **별도 `ServiceEntry`** 로 메시 레지스트리에 등록돼야 한다. `egress.hosts`는 "이미 레지스트리에 있는 것 중 무엇을 이 사이드카가 알게 할지" 필터일 뿐, 외부 도메인을 메시에 등록하는 것은 ServiceEntry의 역할이다. 즉 외부 허용은 **ServiceEntry(등록) + egress.hosts(노출)** 두 단계 — 둘 다 통과해야 트래픽이 흐른다.
## 4. 떴는지 확인 — REGISTRY_ONLY 차단 실측
§3-1의 mesh-wide 정석을 적용했다면, 두 손잡이가 각각 동작했는지 직접 본다.
```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 + 미등록 호스트 → BlackHoleCluster (차단 손잡이 증거)
```
설정 크기를 바이트 단위로 비교하려면 적용 전후로 `istioctl proxy-config all -o json | wc -c`(또는 Envoy `config_dump` 크기)를 잰다 — §1의 "2MB → 644KB"가 이 측정이다. 첫 명령의 cluster 수 급감이 **성능 경로**의 직접 증거, 둘째 명령의 차단 코드가 **차단 레버**의 직접 증거다.
## 5. 실무 패턴 & 주의사항
| 체크리스트 | 이유 |
|---|---|
| 동일 범위 Sidecar는 1개만 | 같은 범위에 다중 정의 시 동작 미보장(docs 미정의) → 파일 병합 또는 workloadSelector로 분리 |
| mesh-wide `default` + `REGISTRY_ONLY` | "기본 deny, 필요 시 allow" zero-trust egress 구현 |
| NS 생성 파이프라인에 default Sidecar 포함 | 새 NS가 자동으로 보안 베이스라인 확보 |
| ServiceEntry와 함께 사용 | 외부 호스트는 ServiceEntry 등록 + `*.example.com` 또는 `/` 포맷 필요 |
| 좁은 Sidecar 투입 전 audit | 실제 호출 대상을 telemetry/access log로 먼저 수집 → egress.hosts 도출 (안 하면 정상 트래픽 일제 차단) |
## 핵심 정리
- 멘탈모델: **Pod마다 적용되는 Sidecar는 정확히 하나, 가장 좁은 게 이긴다**(merge 아님, override). 그 하나가 두 손잡이를 든다.
- scope는 **Mesh < Namespace < Workload** 우선순위로 좁아지고, 좁은 Sidecar가 붙은 Pod엔 그 하나만 유효 → egress를 **빠짐없이 재나열**(`istio-system/*` 포함).
- mesh-wide 판정 = `metadata.namespace: `(기본 `istio-system`) + `workloadSelector` 없음, 이 두 조건뿐이다. `metadata.name: default`는 권장 관례일 뿐 기술적 필수조건이 아니다 — 다만 같은 NS에 selector 없는 Sidecar가 둘 이상이면 동작이 undefined이므로 이름 관례를 지키는 게 안전하다.
- `egress.hosts`(푸시 범위 축소=성능) ≠ `outboundTrafficPolicy.mode: REGISTRY_ONLY`(차단=거버넌스) — **둘은 다른 레버다**. 기본값은 `ALLOW_ANY`(PassthroughCluster 통과).
- 외부 도메인은 `egress.hosts`만으로 안 통하고 별도 `ServiceEntry` 등록 필요(등록 + 노출 2단계).
- 효과 3가지 동시 달성: **불필요한 egress 억제 + Envoy 설정 경량화(성능) + 보안 거버넌스**.
관련: [Sidecar scope 개념 노트](/docs/istio/egress/sidecar-scope/) · [Egress route 스코핑](/docs/istio/egress/vs-scoping/)
## What you might be missing
- **workload Sidecar 비상속**: workloadSelector Sidecar는 NS/mesh default를 병합하지 않는다. 해당 Pod가 필요로 하는 모든 `egress.hosts`를 재나열하지 않으면 그 Pod만 호출이 깨진다.
- **REGISTRY_ONLY 전역 적용은 audit 후에**: mesh-wide로 `REGISTRY_ONLY`를 켜기 전 현재 실제 외부 호출 대상을 먼저 파악(audit)하지 않으면, 미등록 호스트로 나가던 정상 트래픽이 일제히 끊긴다. egress 운영 절차는 [Egress 운영](/docs/istio/egress/operations/).
- **ServiceEntry 없으면 외부 호스트 무효**: `egress.hosts`에 외부 도메인을 적어도 ServiceEntry로 레지스트리에 등록돼 있지 않으면 의미가 없다 — hosts는 필터일 뿐 등록 수단이 아니다.
- **egress.hosts는 방화벽이 아니다**: scope 밖 트래픽을 *차단*하는 게 아니라 사이드카가 *모르게* 할 뿐이다. 실제 차단은 `outboundTrafficPolicy`(REGISTRY_ONLY)나 AuthorizationPolicy의 몫이다. 둘을 항상 한 세트로 본다.
- **차단 코드 혼동**: REGISTRY_ONLY 차단은 HTTP 502(BlackHoleCluster)·TCP reset이지 503이 아니다. access log `response_flags`로 BlackHole 여부를 식별한다. 개념 노트 [Sidecar scope](/docs/istio/egress/sidecar-scope/)도 이 차단을 일관되게 502로 서술하고 있어 두 문서 사이에 실제 불일치는 없다(2026-07-05 실측 재확인 — 아래 검증 기록 T02 참고).
## 검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)
검증 방법: istio.io/envoyproxy.io 공식 문서 대조 + homelab 클러스터(k8s 1.30.6, Istio 1.30.0) 실측을 병행했다.
| 주장 | 판정 | 근거 |
|---|---|---|
| C1. Sidecar 없으면 istiod가 메시 전체 설정을 보수적으로 push | ✅ 실측 확인 | [istio.io/…/sidecar/](https://istio.io/latest/docs/reference/config/networking/sidecar/) · [T01 실측](/docs/istio/control-plane-ops/performance-factors/files/verify/T01/) |
| C2. "Istio in Action" 2MB → 644KB 수치 | 실측 불가 | 책의 특정 데모 벤치마크 — 공식문서·homelab 재현 대상 아님 |
| C3. 한 Pod엔 Sidecar 정확히 하나, override(merge 아님) | ✅ 실측 확인 | [istio.io/…/sidecar/](https://istio.io/latest/docs/reference/config/networking/sidecar/) · [T36 실측](/docs/istio/egress/sidecar-scope/files/verify/T36/) |
| C4. 우선순위 Workload > Namespace > Mesh | ✅ 문헌 확인 | [istio.io/…/sidecar/](https://istio.io/latest/docs/reference/config/networking/sidecar/) |
| C5. mesh-wide 판정은 name:default 포함 삼중 조합 | ❌ 오류 — 본문 교정 | [istio.io/…/sidecar/](https://istio.io/latest/docs/reference/config/networking/sidecar/) — 실제 조건은 rootNamespace + selector 없음 2가지, name은 관례 |
| C6. outboundTrafficPolicy.mode 기본값은 ALLOW_ANY | ✅ 문헌 확인 | [istio.io/…/egress-control/](https://istio.io/latest/docs/tasks/traffic-management/egress/egress-control/) |
| C7. REGISTRY_ONLY 미등록 HTTP 호출 → BlackHoleCluster 502 | ✅ 실측 확인 | [istio.io blog](https://istio.io/latest/blog/2019/monitoring-external-service-traffic/) · [T02 실측](/docs/istio/egress/gateway-https/files/verify/T02/) |
| C8. REGISTRY_ONLY 차단 시 TCP는 connection reset(close) | ✅ 실측 확인 | 공식문서는 와이어레벨 미기술 — [T02 실측](/docs/istio/egress/gateway-https/files/verify/T02/)로 blackhole 필터체인 경유 확인(RST/FIN 패킷 구분까지는 미실측) |
| C9. 503은 REGISTRY_ONLY 차단과 별개(UH, no healthy upstream) | ✅ 문헌 확인 | [envoyproxy.io/…/panic_threshold](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/panic_threshold) |
| C10. ALLOW_ANY에서 미등록 외부 호출은 PassthroughCluster로 통과 | ✅ 문헌 확인 | [istio.io blog](https://istio.io/latest/blog/2019/monitoring-external-service-traffic/) |
| C11. workloadSelector Sidecar는 NS/mesh default를 상속·병합하지 않음 | ✅ 실측 확인 | [istio.io/…/sidecar/](https://istio.io/latest/docs/reference/config/networking/sidecar/) · [T36 실측](/docs/istio/egress/sidecar-scope/files/verify/T36/) |
| C12. egress.hosts의 `./`는 Sidecar가 속한 현재 네임스페이스 | ✅ 문헌 확인 | [istio.io/…/sidecar/#IstioEgressListener](https://istio.io/latest/docs/reference/config/networking/sidecar/#IstioEgressListener) |
| C13. 외부 호스트는 egress.hosts만으론 부족, ServiceEntry 등록 필요(등록+노출 2단계) | ✅ 문헌 확인 | [istio.io/…/sidecar/#IstioEgressListener](https://istio.io/latest/docs/reference/config/networking/sidecar/#IstioEgressListener) |
| C14. 동일 범위 Sidecar 복수 존재 시 동작은 undefined | ✅ 문헌 확인 | [istio.io/…/sidecar/](https://istio.io/latest/docs/reference/config/networking/sidecar/) |
| C15. 개념 노트가 같은 차단을 503으로 적어 두 문서가 어긋난다는 각주 | ❌ 오류 — 본문 교정 | 지식베이스 원문 대조(gw__note-sidecar-scope.md) — 실제로는 두 문서 모두 502로 일치, 불일치 없음 |