homelab89 Docs Logs Legacy Files ☰ TOC 🌓
noteistio 2026-06-07sidecaregressscopeoverrideperformance

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 운영 가이드 참조.

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 성능 요인에서 다룬다.

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 그대로 통과.
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 + selector 없음 (name: default는 권장 컨벤션)
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” 류 장애가 난다 — 이게 실무에서 가장 자주 밟는 지뢰다.

flowchart TD
  M["Mesh-wide<br/>(rootNs/default, no selector)"] -->|overridden by| N["Namespace-wide<br/>(ns/default, no selector)"]
  N -->|overridden by| W["Workload-specific<br/>(workloadSelector)"]
  W -.->|"selector Pod엔 이것만 적용"| P[(target Pod)]

2-3. Mesh-wide 판정 조건의 정확한 근거

“mesh-wide는 istio-system에 두면 된다"는 흔한 요약은 부정확하다. 다만 실제 판정 조건은 세 가지 동시 충족이 아니라 두 가지뿐이다.

  1. metadata.namespacemeshConfig.rootNamespace(기본값 istio-system)와 일치
  2. workloadSelector 없음

metadata.name: default는 판정 조건이 아니라, 공식 Sidecar 레퍼런스가 네임스페이스별 기본 사이드카 이름으로 쓰길 “권장(recommended)“하는 컨벤션일 뿐이다 — 리소스 이름 자체는 istiod의 mesh-wide 판정 로직에 관여하지 않는다. 다만 같은 namespace(rootNamespace 포함)에 selector 없는 Sidecar가 2개 이상 존재하면 동작이 정의되지 않으므로(undefined behavior), 이름을 default로 통일해두는 것은 실수로 중복 생성하는 것을 막는 안전한 관례로 이해해야 한다.

파일명은 무관하다. 결정하는 것은 리소스의 namespace/selector 조합(과 관례상의 name)이다. rootNamespace를 별도로 지정한 클러스터라면 istio-system이 아니라 그 NS에 둬야 mesh-wide로 동작한다. 왜 이렇게 엄격하게 정의했나 — mesh-wide는 메시 전체의 기본값을 갈아치우는 가장 강력한 override라서, 우연히 만들어지면 안 되기 때문이다. 그래서 “특정 NS + selector 없음"이라는 두 조건을 명시적으로 요구하고, 이름은 실수 방지용 컨벤션으로 얹혀 있을 뿐이다.

2-4. ServiceEntry와의 분업 — ‘등록’과 ‘노출’은 다른 일

egress.hosts“이미 registry에 있는 것 중 무엇을 이 사이드카가 알게 할지” 필터일 뿐이다. 외부 도메인(예: Stripe API)을 메시에 등록하는 것은 ServiceEntry의 역할이다. 따라서 외부 호출을 허용하려면 두 단계가 필요하다.

  1. ServiceEntry로 외부 host를 registry에 등록 (spec.hosts는 와일드카드 프리픽스가 붙은 DNS 이름만 받는다 — 예: *.example.com, api.dropboxapi.com. <ns>/<name> 포맷은 ServiceEntry가 아니라 Sidecaregress.hosts 필드 전용 문법이다)
  2. 해당 워크로드 Sidecaregress.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              # 권장 컨벤션(필수 조건 아님, §2-3)
  namespace: istio-system    # = meshConfig.rootNamespace
spec:
  egress:
    - hosts:
        - "istio-system/*"   # 모니터링·제어 트래픽만 알림
  outboundTrafficPolicy:
    mode: REGISTRY_ONLY      # 미등록 외부 차단 (zero-trust 기본값)

줄별로 왜: namespace: istio-system(=rootNamespace) + selector 없음 → §2-3의 두 조건을 채워 mesh-wide로 인식된다(name: default는 관례). 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 + selector 없음 (name: default는 권장 컨벤션일 뿐 필수 아님, 파일명 무관).
  • 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.hostsistio-system/*만 남겨도 그건 “사이드카가 그 외엔 모른다"일 뿐, 실제 차단을 보장하는 것은 outboundTrafficPolicy다. 둘을 항상 한 세트로 본다.
  • scope 축소의 안전 절차: 운영 메시에서 갑자기 좁은 Sidecar를 넣으면 미처 파악 못 한 의존 호출이 끊긴다. 먼저 access log/telemetry로 실제 호출 대상을 수집한 뒤 egress.hosts를 도출하는 순서가 안전하다.

검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)

검증 방법 요약: 공식 문서(istio.io 레퍼런스/블로그) 대조 + homelab 클러스터 실측(namespace-wide/workload-specific Sidecar를 실제로 적용해 cluster 목록·HTTP 응답 코드 관찰).

주장 판정 근거
기본 설정(Sidecar 없음)에서 istiod는 메시 전체 cluster/listener/route/endpoint를 모든 사이드카에 푸시하며, 실제 호출 여부와 무관함 ✅ 문헌 확인 istio.io/…/reference/config/networking/sidecar/
egress.hosts(범위/성능)와 outboundTrafficPolicy.mode(차단/거버넌스)는 독립 레버 — egress.hosts만 좁히고 outboundTrafficPolicy를 생략하면 성능은 개선돼도 기본 deny는 생기지 않음 ✅ 실측 확인 istio.io/…/reference/config/networking/sidecar/ · T01 실측
Sidecar의 세 scope(Mesh/NS/Workload)는 merge가 아니라 가장 좁은 하나가 통째로 override — workload-specific이 있으면 NS-wide/mesh-wide는 합쳐지지 않음 ✅ 실측 확인 istio.io/…/reference/config/networking/sidecar/ · T36 실측
outboundTrafficPolicy.mode 기본값은 ALLOW_ANY ✅ 문헌 확인 istio.io/…/reference/config/istio.mesh.v1alpha1/
ALLOW_ANY에서 미등록 목적지는 PassthroughCluster로 원본 IP:port 그대로 통과 ✅ 문헌 확인 istio.io/…/blog/2019/monitoring-external-service-traffic/
REGISTRY_ONLY에서 미등록 목적지는 BlackHoleCluster로 보내져 차단되며 호출 측은 보통(HTTP) 502를 받음 ✅ 실측 확인 istio.io/…/blog/2019/monitoring-external-service-traffic/ · T02 실측
mesh-wide 판정은 rootNamespace 일치 + name:default + selector 없음, 세 조건 동시 충족이 필수 ❌ 오류 — 본문 교정 istio.io/…/reference/config/networking/sidecar/ (실제로는 rootNamespace 일치 + selector 없음 두 조건뿐이며 name:default는 권장 컨벤션)
meshConfig.rootNamespace 기본값은 istio-system ✅ 문헌 확인 github.com/istio/istio/…/pkg/config/mesh/mesh.go
Sidecar 리소스의 apiVersion은 networking.istio.io/v1 ✅ 문헌 확인 istio.io/…/reference/config/networking/sidecar/
동일 범위에 selector 없는 Sidecar 2개 이상 존재 시 동작은 정의되지 않음(undefined behavior) ✅ 문헌 확인 istio.io/…/reference/config/networking/sidecar/
ServiceEntry 호스트 포맷은 *.example.com 또는 <ns>/<name> ❌ 오류 — 본문 교정 istio.io/…/reference/config/networking/service-entry/ (ServiceEntry는 와일드카드 DNS 이름만 허용, <ns>/<name>은 Sidecar egress.hosts 전용 문법)
egress.hosts는 이미 registry에 있는 것 중 무엇을 노출할지 필터이고, 외부 도메인 등록 자체는 ServiceEntry의 역할 — 등록과 노출은 별개 단계 ✅ 문헌 확인 istio.io/…/reference/config/networking/sidecar/
실측 사례: 사이드카 설정 크기가 2MB → Sidecar 적용 후 644KB로 감소 실측 불가 공식 문서/블로그에서 확인 불가한 저자 개인 사례 수치 (방향성 자체는 T36의 cluster 수 급감(11→0, 전체 9개)으로 방증)
REGISTRY_ONLY 차단은 502로 보이며, Envoy response flag(NR/UH)와 istioctl proxy-config clusters의 BlackHole/Passthrough 존재로 원인을 분별해야 함 실측 불가 istio.io/…/blog/2019/monitoring-external-service-traffic/ (502 자체는 문헌 확인되나 NR/UH 플래그 매핑은 envoy 문서에서 명확히 확인되지 않음)

Files