--- type: src tags: [istio, sidecar, egress] created: 2026-06-07 --- # Istio Sidecar CRD 적용 범위(scope) 설정 방법 > [!abstract] > `Sidecar` 리소스는 한 워크로드 Envoy가 받는 설정을 좁혀 **config를 경량화**하고, `outboundTrafficPolicy`와 결합해 **egress 거버넌스**를 만든다. 멘탈모델 하나: scope는 Mesh / Namespace / Workload 세 단계지만 **어느 Pod에든 적용되는 Sidecar는 정확히 하나 — 가장 좁은 것이 통째로 이긴다**(merge 아님, override). 이 문서는 그 override 의미론을 세 scope의 **완전한 YAML(주석 포함)**로 따라 만들고, REGISTRY_ONLY 차단을 실측으로 굳힌다. 개념 전반은 [Sidecar scope 개념 노트](mm__note-sidecar-scope.html)에. **대상독자:** 메시 규모가 커져 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. 컨트롤 플레인을 좌우하는 다른 요인은 [컨트롤 플레인 성능 요인](../istio/arch__note-control-plane-performance-factors.html). 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 + `name: default` + selector 없음 | | 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`)에 있고, 이름이 `default`이며, `workloadSelector`가 없을 것**"이다. 단순히 `istio-system`에 둔다고 되는 게 아니라, 그 네임스페이스가 `meshConfig.rootNamespace`로 지정된 루트여야 한다. 파일명은 무관하고 **리소스의 `metadata.name: default` + `metadata.namespace: ` + selector 없음** 삼중 조합이 판정 기준이다. 왜 이렇게 빡빡한가 — mesh-wide는 메시 전체 기본값을 갈아치우는 가장 강력한 override라, 우연히 만들어지면 안 되기 때문이다. ## 3. 구성 따라하기 — 세 scope를 좁은 순으로 쌓기 운영 패턴은 항상 같다: **mesh-wide로 바닥을 깔고(기본 deny) → 예외가 필요한 NS·워크로드가 자체 Sidecar로 허가 목록을 확장**한다. 아래 셋은 같은 메시 위에 순서대로 얹는 한 세트다. ### 3-1. Mesh-wide Sidecar (전역 기본값) 목표: "메시 전체에서 외부로 나가는 트래픽은 명시적으로 허용되지 않으면 차단." ```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 삼중 조건을 채워 mesh-wide로 인식된다. `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](../istio/xds__src-envoy-response-flags.html). > [!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.name: default` + `metadata.namespace: `(기본 `istio-system`) + `workloadSelector` 없음. 파일명 무관. - `egress.hosts`(푸시 범위 축소=성능) ≠ `outboundTrafficPolicy.mode: REGISTRY_ONLY`(차단=거버넌스) — **둘은 다른 레버**. 기본값은 `ALLOW_ANY`(PassthroughCluster 통과). - 외부 도메인은 `egress.hosts`만으로 안 통하고 별도 `ServiceEntry` 등록 필요(등록 + 노출 2단계). - 효과 3가지 동시 달성: **불필요한 egress 억제 + Envoy 설정 경량화(성능) + 보안 거버넌스**. 관련: [Sidecar scope 개념 노트](mm__note-sidecar-scope.html) · [Egress route 스코핑](mm__note-vs-scoping.html) ## 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 운영](ref__src-operations.html). - **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](mm__note-sidecar-scope.html)는 같은 차단을 503으로 적고 있어 두 문서가 어긋난다 — 실측 코드로 확정 권장.)