Istio Sidecar CRD 적용 범위(scope) 설정 방법
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)는 운영자가 바로 그 정보를 명시적으로 주입하는 통로이고, 그것으로 두 방향의 문제를 동시에 푼다:
- 설정 범위 축소(performance) —
egress.hosts로 "이 워크로드가 실제로 호출하는 대상"만 선언하면, istiod가 그 사이드카에 보내는 설정 크기가 극적으로 줄고(Istio in Action11장: 2MB → 644KB), registry 변경이 scope 밖이면 push 자체가 발생하지 않는다. 실측은 §4. 컨트롤 플레인을 좌우하는 다른 요인은 컨트롤 플레인 성능 요인. - 트래픽 거버넌스(security) —
outboundTrafficPolicy.mode: REGISTRY_ONLY와 결합하면 메시 레지스트리에 등록되지 않은 외부 호출을 차단하는 zero-trust egress 기본값이 된다.
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를 적용받는지 결정 흐름:
이 흐름도가 override의 실체다 — 매칭은 위에서 아래로 첫 hit 하나에서 멈춘다. workload가 match하면 NS/mesh default는 아예 보지 않는다. §3-3의 누락 함정이 여기서 나온다.
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를 빼면 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가 아니라 이 정책 하나만 유효하기 때문이다.
selector가 일치하는 Pod는 오직 이 Sidecar 하나만 적용받으므로, NS default가 허용하던 egress.hosts가 통째로 사라진다. 위 예시가 payments-gw, istio-system/*를 다시 나열한 이유가 이것이다 — frontend Pod가 필요로 하는 대상을 빠짐없이 재나열하지 않으면, NS default에서 되던 호출이 이 Pod에서만 깨지는 흔한 사고가 난다. istio-system/*(텔레메트리·제어)을 깜빡 빠뜨리면 메트릭/probe까지 막혀 진단이 꼬인다.
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 도출 (안 하면 정상 트래픽 일제 차단) |
핵심 정리
- 멘탈모델: Pod마다 적용되는 Sidecar는 정확히 하나, 가장 좁은 게 이긴다(merge 아님, override). 그 하나가 두 손잡이를 든다.
- scope는 Mesh < Namespace < Workload 우선순위로 좁아지고, 좁은 Sidecar가 붙은 Pod엔 그 하나만 유효 → egress를 빠짐없이 재나열(
istio-system/*포함). - mesh-wide 판정 =
metadata.name: default+metadata.namespace: <rootNamespace>(기본istio-system) +workloadSelector없음. 파일명 무관. egress.hosts(푸시 범위 축소=성능) ≠outboundTrafficPolicy.mode: REGISTRY_ONLY(차단=거버넌스) — 둘은 다른 레버. 기본값은ALLOW_ANY(PassthroughCluster 통과).- 외부 도메인은
egress.hosts만으로 안 통하고 별도ServiceEntry등록 필요(등록 + 노출 2단계). - 효과 3가지 동시 달성: 불필요한 egress 억제 + Envoy 설정 경량화(성능) + 보안 거버넌스.
관련: Sidecar scope 개념 노트 · Egress route 스코핑
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 운영. - 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는 같은 차단을 503으로 적고 있어 두 문서가 어긋난다 — 실측 코드로 확정 권장.)