🏠 목록 Istio Sidecar CRD 적용 범위(scope) 설정 방법 📄 MD 원본 🌓 테마
istiosidecaregress

Istio Sidecar CRD 적용 범위(scope) 설정 방법

NOTE

Sidecar 리소스는 한 워크로드 Envoy가 받는 설정을 좁혀 config를 경량화하고, outboundTrafficPolicy와 결합해 egress 거버넌스를 만든다. 멘탈모델 하나: scope는 Mesh / Namespace / Workload 세 단계지만 어느 Pod에든 적용되는 Sidecar는 정확히 하나 — 가장 좁은 것이 통째로 이긴다(merge 아님, override). 이 문서는 그 override 의미론을 세 scope의 완전한 YAML(주석 포함)로 따라 만들고, REGISTRY_ONLY 차단을 실측으로 굳힌다. 개념 전반은 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. 컨트롤 플레인을 좌우하는 다른 요인은 컨트롤 플레인 성능 요인.
  2. 트래픽 거버넌스(security)outboundTrafficPolicy.mode: REGISTRY_ONLY와 결합하면 메시 레지스트리에 등록되지 않은 외부 호출을 차단하는 zero-trust egress 기본값이 된다.
ℹ 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를 적용받는지 결정 흐름:

Pod needs effective Sidecar workloadSelector match? yes Use that Sidecar only NS/mesh default NOT merged no NS default Sidecar? yes Use NS default only no rootNamespace default? yes Use mesh-wide default no No Sidecar matches full mesh config pushed = heavy default
그림 1. effective Sidecar 결정 흐름 — workloadSelector 일치 시 그 Sidecar만(병합 없음), 없으면 NS default, 그것도 없으면 rootNamespace mesh-wide default, 모두 없으면 full mesh config가 통째로 push되는 무거운 default로 떨어진다(override 의미론).

이 흐름도가 override의 실체다 — 매칭은 위에서 아래로 첫 hit 하나에서 멈춘다. workload가 match하면 NS/mesh default는 아예 보지 않는다. §3-3의 누락 함정이 여기서 나온다.

ℹ "Mesh-wide는 `istio-system` 네임스페이스에 둔다"는 규칙의 정확한 근거.

mesh-wide로 동작하는 조건은 "메시 루트 네임스페이스(기본값 istio-system)에 있고, 이름이 default이며, workloadSelector가 없을 것"이다. 단순히 istio-system에 둔다고 되는 게 아니라, 그 네임스페이스가 meshConfig.rootNamespace로 지정된 루트여야 한다. 파일명은 무관하고 리소스의 metadata.name: default + metadata.namespace: <rootNamespace> + selector 없음 삼중 조합이 판정 기준이다. 왜 이렇게 빡빡한가 — mesh-wide는 메시 전체 기본값을 갈아치우는 가장 강력한 override라, 우연히 만들어지면 안 되기 때문이다.

3. 구성 따라하기 — 세 scope를 좁은 순으로 쌓기

운영 패턴은 항상 같다: mesh-wide로 바닥을 깔고(기본 deny) → 예외가 필요한 NS·워크로드가 자체 Sidecar로 허가 목록을 확장한다. 아래 셋은 같은 메시 위에 순서대로 얹는 한 세트다.

3-1. Mesh-wide Sidecar (전역 기본값)

목표: "메시 전체에서 외부로 나가는 트래픽은 명시적으로 허용되지 않으면 차단."

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.

ℹ `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는 사내 결제 게이트웨이 + 모니터링만 접근.

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 호출 필요.

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가 아니라 이 정책 하나만 유효하기 때문이다.

⚠ workloadSelector Sidecar는 NS/mesh default를 **상속·병합하지 않는다**.

selector가 일치하는 Pod는 오직 이 Sidecar 하나만 적용받으므로, NS default가 허용하던 egress.hosts가 통째로 사라진다. 위 예시가 payments-gw, istio-system/*를 다시 나열한 이유가 이것이다 — frontend Pod가 필요로 하는 대상을 빠짐없이 재나열하지 않으면, NS default에서 되던 호출이 이 Pod에서만 깨지는 흔한 사고가 난다. istio-system/*(텔레메트리·제어)을 깜빡 빠뜨리면 메트릭/probe까지 막혀 진단이 꼬인다.

ℹ 외부 호스트(`external-apis/stripe-se`)는 그냥 적는다고 되는 게 아니라 **별도 `ServiceEntry`** 로 메시 레지스트리에 등록돼야 한다. `egress.hosts`는 "이미 레지스트리에 있는 것 중 무엇을 이 사이드카가 알게 할지" 필터일 뿐, 외부 도메인을 메시에 등록하는 것은 ServiceEntry의 역할이다. 즉 외부 허용은 **ServiceEntry(등록) + egress.hosts(노출)** 두 단계 — 둘 다 통과해야 트래픽이 흐른다.

4. 떴는지 확인 — REGISTRY_ONLY 차단 실측

§3-1의 mesh-wide 정석을 적용했다면, 두 손잡이가 각각 동작했는지 직접 본다.

$ 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 + 미등록 호스트 → BlackHoleCluster (차단 손잡이 증거)

설정 크기를 바이트 단위로 비교하려면 적용 전후로 istioctl proxy-config all <pod> -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 또는 <ns>/<name> 포맷 필요
좁은 Sidecar 투입 전 audit 실제 호출 대상을 telemetry/access log로 먼저 수집 → egress.hosts 도출 (안 하면 정상 트래픽 일제 차단)

핵심 정리

관련: Sidecar scope 개념 노트 · Egress route 스코핑

What you might be missing