Platform Engineering · Egress Gateway Operations

Egress Gateway 리소스 관리 모델
exportTo 중앙 관리와 목적별 Service 분리

Istio egress 구성(sidecar → egress gateway → Squid CONNECT 터널)에 필요한 CR 세트를 네임스페이스마다 복제하지 않고 전용 네임스페이스 istio-egress에 한 벌만 두고 exportTo로 노출을 제어하는 관리 모델, 그리고 목적별 Service 분리로 Envoy 리소스(LDS·CDS·EDS) 비대화를 예방하는 구성 방안을 정의합니다.

전용 네임스페이스 istio-egress 근거 Istio 공식 Configuration Scoping 선행 문서 Outbound 집약 설계서 4.2·4.7
Part 00

요약 — 두 개의 결정

  1. egress 관련 CR(ServiceEntry·VirtualService·DestinationRule·Gateway)은 istio-egress에 한 벌만 둔다. 소비 네임스페이스는 세 객체의 exportTo 리스트에 이름을 추가하는 것으로 연결되며, 그 리스트가 곧 "누가 egress를 쓰는가"의 감사 가능한 계약이 된다.
  2. egress gateway 파드는 하나의 Deployment로 유지하되, Service는 목적 단위로 분리한다. 분리의 기준은 포트 번호가 아니라 노출 범위(소비자 집합)가 다른 목적이다 — 분리와 exportTo 축소가 결합될 때만 Envoy 리소스가 실제로 줄어든다.
한 문장 원리

sidecar가 짊어지는 설정의 양은 (보이는 서비스 × 포트) × 그것이 보이는 sidecar 수의 곱이다. 중앙 관리는 곱의 앞항을 한 벌로 고정하고, exportTo와 목적별 분리는 뒷항(누구에게 보이는가)을 목적별 최소로 줄인다.

Part 01

배경 — 문제의 정확한 정의

1.1 이 구성에 필요한 객체들

egress 경로는 sidecar → egress gateway(pod) → Squid(:3128, CONNECT 터널) 두 홉이고, 이를 성립시키려면 CR이 최소 네 종 필요합니다 — ServiceEntry(Squid 등재와 리스너 매칭), DestinationRule(홉 1 mTLS 발신), Gateway(수신 리스너), VirtualService(두 홉의 배선). 여기에 AuthorizationPolicy(신원 심사)와 gateway용 Service/Deployment까지 더하면 한 목적지를 잇는 데 예닐곱 개의 객체가 움직입니다. 문제는 이 세트를 누가, 어디에, 몇 벌 두느냐입니다.

1.2 개별 CR 반복 생성의 문제 — 구체적으로

각 앱 네임스페이스가 자기 사본을 만드는 분산 모델(네임스페이스 40개 기준)의 실제 사고 형태입니다:

문제예시 시나리오
드리프트GSLB VIP가 추가된다. 40개 네임스페이스의 SE 사본 중 37개만 갱신된다 — 남은 3개 네임스페이스만 새 VIP를 받은 파드에서 리스너 미스가 나고, "우리 팀만 간헐적으로 안 돼요" 티켓이 원인 불명으로 떠돈다. 사본이 N개라는 것은 진실이 N개라는 뜻이다
충돌한 팀이 addresses를 빼먹은 SE를 만든다 — 같은 호스트·포트를 주장하는 정의 충돌(선행 문서 4.2의 C 상태)로 istiod가 비결정적으로 하나만 반영하고, 다른 팀 트래픽이 엉뚱한 목적지로 오배송된다. 같은 host의 VirtualService 중복도 병합 순서가 보장되지 않는다
변경 증폭Squid 도메인 변경 1건 = 40개 리포지토리에 40개 PR. 롤백도 40번. 절차가 무거우니 "일부만 먼저"가 되고, 그것이 곧 드리프트의 씨앗이 된다
감사 불가"egress를 쓰는 네임스페이스 목록"이라는 감사 질문에 답하려면 전 네임스페이스의 CR을 스캔해야 한다. PCI 심사에서 이 목록은 반복적으로 요구된다
RBAC 표면 확대모든 앱 팀에 networking.istio.io CR 생성 권한이 필요해진다 — 그런데 이 CR들은 기본 exportTo: "*"라 한 팀의 실수(와일드카드 SE 등)가 mesh 전체에 영향을 줄 수 있는 권한이다
xDS 비대기본값에서 모든 CR은 모든 sidecar에 계산·푸시된다 — 40벌의 사본은 400개 sidecar 각각의 설정에 40벌 몫의 항목을 만든다 (1.3에서 정량화)

1.3 "리소스 포화"의 리소스란 정확히 무엇인가

막연히 "리소스"라 부르는 것의 실체는 istiod가 각 프록시에 xDS로 밀어넣는 Envoy 설정 객체입니다. 세 종류가 각각 다른 공식으로 증식합니다:

Envoy 리소스무엇인가per-proxy 증식 공식
CDS (cluster)보낼 곳의 정의 — outbound|포트||호스트 하나씩Σ(보이는 서비스 × 그 서비스의 포트 수)
EDS (endpoint)각 cluster의 실제 주소 목록포트 수 × 파드 수 (cluster마다 파드 전체가 반복 수록)
LDS (listener)잡을 문 — 포트·프로토콜 조합별보이는 포트·프로토콜 조합 수

그리고 이 per-proxy 양에 × 그것이 보이는 프록시 수가 곱해진 것이 istiod의 유지·푸시 비용입니다. 변경(파드 롤링, 포트 추가)이 일어날 때마다 이 곱만큼의 갱신이 mesh를 흐릅니다. 쿠버네티스 쪽에도 대응물이 있습니다 — Service마다 EndpointSlice 세트가 있고, Service의 어떤 변경(포트 하나 추가)도 그 Service의 모든 슬라이스 갱신 → 모든 워처(kube-proxy·istiod)의 재계산을 유발합니다.

예시 — fat Service의 계산

egressgateway 파드 6개에 목적이 뒤섞인 포트 8개(3128, 8888, 443, 5432, 3306, 6379, 9092, 587)를 하나의 Service로 노출하고 mesh의 sidecar 400개 전부가 보는 경우:

per-proxy:  cluster  +8         (포트마다 outbound|PORT||egressgateway…)
            EDS 항목 +48        (8 포트 × 6 파드)
            listener +8
mesh 전체:  istiod가 유지·푸시하는 EDS 항목  = 48 × 400 = 19,200
변경 파급:  gateway 파드 1회 롤링  → 8개 cluster의 EDS 갱신이 400개 프록시 전부에게
            DB 포트 1개 추가       → DB와 무관한 400개 프록시 전원이 Service 갱신을 수신
정직한 명제

여기서 중요한 사실 — Service를 목적별로 나누는 것 "단독"으로는 총량이 줄지 않습니다. 포트 8개를 Service 1개에 두든 4개로 나누든, 전부가 모든 네임스페이스에 노출되면 per-proxy cluster는 똑같이 8개입니다. 감소는 분리가 노출 범위 축소를 가능하게 만들 때 일어납니다 — fat Service의 노출은 all-or-nothing이라 축소가 원천 불가능하고, 목적별 Service는 목적별 exportTo를 가질 수 있다는 것. 이것이 Part 03의 논리 전체입니다.

Part 02

모델 1 · exportTo 중앙 관리

2.1 모델 정의

egress 관련 CR 한 벌을 istio-egress에만 두고, 소비는 세 객체(SE·VS·DR)의 exportTo 리스트로 제어합니다. 앱 네임스페이스에는 아무 CR도 생기지 않습니다 — 연결은 리스트에 이름을 추가하는 PR 하나입니다.

분산 — 네임스페이스마다 복제 ns: payments SE · VS · DR 사본 v1.3 ns: settlement SE · VS · DR 사본 v1.1 (drift) ns: fraud SE · VS · DR addresses 누락! ns: … ×40 SE · VS · DR 진실이 40개 VIP 변경 = PR 40개 · 감사 = 전 ns 스캔 중앙 — istio-egress 한 벌 + exportTo ns: istio-egress SE · VS · DR · Gateway (1벌) exportTo: [".", payments, …] payments settlement fraud 앱 ns에는 CR 없음 — 연결 = 리스트에 이름 추가 PR 1개 진실을 한 벌로 고정하고, 노출을 리스트로 계약한다
그림 1 · 분산 복제 vs 중앙 exportTo — 사본 수가 곧 사고 표면이다
# istio-egress에만 존재하는 한 벌 — 세 객체의 exportTo는 항상 동일 리스트
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata: {name: squid-gw, namespace: istio-egress}
spec:
  hosts: ["squid.gw-zone.example"]
  addresses: ["<SQUID_VIP_1>/32", "<SQUID_VIP_2>/32"]
  ports: [{number: 3128, name: tcp-squid, protocol: TCP}]
  location: MESH_EXTERNAL
  resolution: DNS
  exportTo: [".", "payments", "settlement"]     # "."=gateway 자신(불변 멤버)
# VirtualService·DestinationRule에도 같은 리스트 — kustomize patch로 3객체 동시 강제

2.2 왜 이것이 정석인가 — 공식 근거

이 모델은 관습이 아니라 Istio가 설계한 두 방향의 가시성 통제 중 한쪽을 그대로 쓰는 것입니다. 공식 Configuration Scoping 문서의 정의가 정확히 이 구도입니다:

버전 주의

mesh 전역 기본값(default*ExportTo)을 "."로 뒤집는 강수는 구버전에서 구체 네임스페이스 나열 미지원, delegate VS 병합 등 이슈 이력이 있습니다. 이 문서의 모델은 전역 기본값은 건드리지 않고 egress 객체의 exportTo만 나열하는 보수적 형태이며, 전역 뒤집기는 mesh 성숙 후 별도 검토 대상입니다.

2.3 중앙 vs 분산 — 정직한 비교

중앙 (istio-egress 한 벌 + exportTo)분산 (네임스페이스별 사본)
장점진실 1벌(드리프트 원천 차단) · 변경 1 PR · exportTo 리스트 = 감사 문서 · 앱팀에 networking CR 권한 불필요 · 충돌 계급 소멸(같은 host의 SE가 1개) · per-proxy 설정 최소팀 자율성·속도(플랫폼팀 대기 없음) · 네임스페이스 삭제 시 자동 정리 · 팀별 커스텀(타임아웃 등) 자유
단점플랫폼팀이 병목(리스트 PR 승인 대기) · 리스트 오타 하나의 파급이 여러 ns · exportTo는 라벨 셀렉터 미지원(명시 나열만)이라 ns 수십 개면 리스트가 길어짐 · 팀별 커스텀 요구는 별도 협의 필요1.2의 여섯 문제 전부 · 특히 RBAC 표면(기본 "*"인 CR 권한을 전 팀에)과 감사 불가가 규제 환경에서 치명적
완화책리스트 관리 자동화 — kustomize 변수/생성기로 ns 목록을 한 곳에서 세 객체에 주입, PR 템플릿·CODEOWNERS로 승인 지연 최소화, istioctl analyze CI 게이트(사실상 없음 — 정책·리뷰로 여섯 문제를 개별 방어해야 함)

규제 환경(PCI)의 결정 가중치는 감사 가능성과 권한 최소화에 있으므로 중앙 모델을 채택합니다. 분산의 유일한 실질 장점(팀 속도)은 "리스트 추가 PR"의 승인 SLA로 흡수합니다.

2.4 운영 규칙

Part 03

모델 2 · 목적별 Service 분리 — 포화 예방

3.1 원리 — Service는 selector일 뿐이다

쿠버네티스 Service는 파드의 소유물이 아니라 파드를 가리키는 독립 객체입니다. 같은 selector를 가진 Service를 몇 개 만들든 전부 같은 egressgateway 파드들을 가리키고, 파드는 아무것도 모릅니다. 새 목적의 수신 포트가 필요하면 Gateway CR에 server를 추가하는데, 이는 xDS로 gateway Envoy에 핫 반영됩니다(리스너 추가에 재시작 불요, Deployment의 containerPort 선언은 정보성이라 무변경). 즉 파드 재생성 없이 Service와 Gateway server만으로 목적을 증설할 수 있고, 이것이 이 모델의 전제입니다.

3.2 분리 기준 — 포트가 아니라 "노출 범위가 다른 목적"

1.3의 정직한 명제를 다시 소환하면: 분리 단독으로는 총량이 안 줄고, 분리가 목적별 exportTo를 가능하게 만들 때 줄어듭니다. 따라서 분리 단위의 판정 기준은 하나입니다 — 이 포트들의 소비자 집합이 서로 다른가? 소비자가 같은 포트들은 한 Service에 묶어도 손해가 없고, 소비자가 다른 포트를 한 Service에 묶는 순간 좁은 쪽이 넓은 쪽의 노출을 강제로 물려받습니다. 쿠버네티스 Service의 노출은 Istio 주석 networking.istio.io/exportTo로 제어합니다(Service에는 spec 필드가 없음).

목적 (Service)포트소비자 집합노출 (exportTo)
egressgw-squid3128외부 API를 쓰는 전 앱 ns앱 ns 40개 (→ 최종 "*")
egressgw-squid-alt8888테스트·특수 정책 경로 2개 ns2개 ns만
egressgw-tls-direct443직결 허용 대상(파트너 mTLS 등) 6개 ns6개 ns만
egressgw-db5432, 3306외부 DB 연동 4개 ns4개 ns만 — 두 포트는 소비자가 같아 한 Service
fat Service — 노출이 all-or-nothing egressgw 파드 ×6 (Deployment 1개) Service 1개 · 포트 8개 3128·8888·443·5432·3306·… sidecar 400개 전부에게 cluster 8 · EDS 48 씩 DB 포트 추가 → 무관한 400 프록시 전원 갱신 목적별 분리 — 같은 파드, 노출은 목적별 egressgw 파드 ×6 (같은 Deployment — 무재생성) squid :3128 → 앱 ns 40 alt :8888 → 2 ns db :5432·3306 → 4 ns 대다수 sidecar: cluster 1 · EDS 6 만 자기 목적의 몫만 수신 DB 포트 추가 → DB 소비 4개 ns의 프록시만 갱신 분리의 효용 = 목적별 노출이 가능해지는 것 — 파드는 그대로다
그림 2 · fat Service vs 목적별 분리 — 같은 파드 위에서 노출 범위만 갈라진다

3.3 구성 예시

# 같은 selector — 전부 동일한 egressgateway 파드를 가리킴
apiVersion: v1
kind: Service
metadata:
  name: egressgw-squid
  namespace: istio-egress
  annotations:
    networking.istio.io/exportTo: "payments,settlement,fraud"   # Service의 노출 제어는 주석
spec:
  selector: {istio: egressgateway}
  ports:
  - {name: tls-squid, port: 7777, targetPort: 7777}
---
apiVersion: v1
kind: Service
metadata:
  name: egressgw-db
  namespace: istio-egress
  annotations:
    networking.istio.io/exportTo: "billing,ledger,dw,ml-batch"
spec:
  selector: {istio: egressgateway}                              # 같은 파드
  ports:
  - {name: tls-db-pg, port: 7791, targetPort: 7791}
  - {name: tls-db-my, port: 7792, targetPort: 7792}
# Gateway CR에 목적별 server(7777/7791/7792)를 추가 — gateway Envoy에 핫 반영
# 각 목적의 SE·VS·DR exportTo 리스트는 해당 Service 주석과 동일하게 유지
정합 규칙 확장

2.4의 "세 객체 동일 리스트"가 목적 단위로 확장됩니다 — 목적 하나 = {Service 주석, SE·VS·DR exportTo} 네 곳의 동일 리스트. kustomize에서 목적별 디렉토리에 ns 목록 변수 하나를 두고 네 곳에 주입하는 구조를 권장합니다.

3.4 효과 — 같은 예시의 재계산

지표fat Service (before)목적별 + exportTo (after)
대다수 sidecar의 egress 몫 (cluster / EDS)8 / 481 / 6
istiod 유지 EDS 항목 (egress 몫, mesh 합계)19,200≈ 3,400 (−82%)
DB 포트 추가 시 갱신 대상 프록시400 (전원)40 (DB 소비 ns만)
gateway 파드 롤링 시 EDS 갱신 파급8 cluster × 400 프록시목적별 cluster × 해당 소비자만

수치는 예시 규모(sidecar 400 · 파드 6 · 포트 8)의 산술이며, 요점은 절대값이 아니라 변경 파급의 분모가 "전원"에서 "해당 목적의 소비자"로 바뀐다는 구조입니다.

3.5 한계와 주의

Part 04

도입 절차와 측정

  1. 현재 상태 측정 (기준선): 대표 sidecar에서
    istioctl proxy-config clusters <pod> | wc -l
    istioctl proxy-config listeners <pod> | wc -l
    kubectl exec <pod> -c istio-proxy -- pilot-agent request GET config_dump | wc -c   # config 크기
    istioctl proxy-status | head    # 푸시 동기화 상태
  2. istio-egress에 CR 한 벌 정리 — 기존 분산 사본이 있다면 중앙 벌 생성 → exportTo에 해당 ns 추가 → 사본 삭제 순으로 네임스페이스별 이관 (동시 존재 구간에서 같은 host 중복이 생기므로 istioctl analyze로 충돌 감시, 이관은 ns 단위로 짧게).
  3. 목적별 Service·Gateway server 구성 — 기존 fat Service가 있다면 새 목적 Service를 병행 생성 → 소비자를 VS destination 전환 → fat Service 축소 (파드 무변경이므로 트래픽 전환은 라우팅 객체만의 일).
  4. 사후 측정 — 1의 커맨드 재실행으로 per-proxy cluster/listener 수·config 크기 감소를 기록. istioctl proxy-config endpoints <pod> | grep egressgw로 목적 몫만 남았는지 확인.
  5. CI 게이트 — exportTo 네 곳 동일성 검사 스크립트 + istioctl analyze를 리스트 변경 PR의 필수 체크로.
Part 05

참고 자료

한계 고지

3.4의 수치는 예시 규모의 산술 모델이며 실측이 아닙니다. Part 04의 기준선 측정 커맨드로 실제 감소율을 기록해 이 문서를 갱신하는 것을 도입 완료 조건에 포함하세요.