🏠 목록 Egress Gateway 운영 가이드 — passthrough + ISTIO_MUTUAL (출처: LLM 답변 + Istio 1.30 공식 문서) 📄 MD 원본 🌓 테마
istioegresspassthroughmonitoringgraceful-shutdownoperations

Egress Gateway 운영 가이드 — passthrough + ISTIO_MUTUAL (출처: LLM 답변 + Istio 1.30 공식 문서)

ℹ 대상 구조

앱이 HTTPS로 직접 호출(end-to-end TLS 유지) + 게이트웨이는 PASSTHROUGH(복호화 안 함) + proxy→egress 구간은 ISTIO_MUTUAL로 mTLS. 결론부터: 이 게이트웨이가 보는 것은 "암호화된 장기 TCP 스트림" 하나뿐이고, 모니터링(L4+SNI만)·운영 함정(연결을 소수 노드에 모음)·graceful shutdown(요청이 아니라 연결이 닫히길 기다림)이 전부 이 한 사실에서 연역된다. 이 문서는 그 연역을 따라간다.

대상환경 Istio 1.30 / networking.istio.io/v1 · 대상독자 egress를 운영하는 DevOps/SRE · 범위 관측·운영함정·종료. 구조의 조립구조 정본에 위임 · 선행개념 egress gateway 기본 구성


00. 배경 — 왜 이 게이트웨이는 운영이 다른가

일반 게이트웨이(ingress, 또는 TLS를 종단하는 egress)는 트래픽을 복호화해서 안을 본다. HTTP method·path·status code가 다 보이니, 모니터링은 istio_requests_total로 에러율을 보고, 정책은 path 기반 라우팅을 걸고, graceful shutdown은 "in-flight 요청이 끝나길" 기다린다. 이 모든 운영 상식이 L7이 보인다는 전제 위에 서 있다.

그런데 이 구조는 그 전제를 의도적으로 깬다. 앱이 외부와 end-to-end TLS를 맺고(게이트웨이가 중간에서 복호화하면 그 보장이 깨지므로), 게이트웨이는 PASSTHROUGH — 암호문 봉투를 뜯지 않고 그대로 중계한다. 왜 그렇게 하나: 외부 파트너에 대한 기밀성을 게이트웨이가 신뢰 경계에서 빼앗지 않으면서도, egress를 소수 노드로 강제 경유시켜 출구 IP를 고정(파트너 방화벽 allowlist)하고 중앙에서 관측·통제하려는 것이다. proxy→egress 안쪽 한 구간만 ISTIO_MUTUAL로 감싸 메시 내부 신원을 입힌다.

이 절충의 대가가 바로 운영의 차이다. 게이트웨이는 자기가 나르는 트래픽의 내용을 원리적으로 알 수 없다 — 복호화를 안 하기로 설계했으니까. 그래서 L7에 기대던 운영 상식이 전부 한 단계 내려가야 한다. 무엇이 어떻게 내려가는지가 이 문서의 전부다.


01. 핵심 — "암호화된 장기 TCP 스트림" 하나에서 모든 게 따라 나온다

ℹ 머릿속 앵커 (이거 하나만)

게이트웨이 입장에서 트래픽은 요청이 아니라 연결이다. 외부 TLS를 안 풀기 때문에 L7(HTTP)이 없고, 한 번 맺힌 암호화 TCP 스트림이 오래 살아 있다. 이 그림에서 세 가지 운영 특성이 기계적으로 연역된다 — ①관측은 L4+SNI만 ②연결을 소수 노드에 모으니 연결 단위 자원이 병목 ③종료는 연결이 닫히길 기다림.

세 갈래로 펼쳐 보면:

Gateway sees only: encrypted long-lived TCP stream — no L7 (connection, not request) Monitoring L4 metrics + SNI only L7 must come from app Operations connections pinned to few nodes → SNAT / NAT-idle / SPOF Shutdown wait for connection close, not request completion
그림 1. 앵커 — 게이트웨이가 보는 것은 암호화된 장기 TCP 스트림 하나뿐. ①관측 ②운영 함정 ③종료가 전부 여기서 연역된다

세 갈래의 공통 뿌리가 하나(앵커)라는 게 핵심이다. 아래 섹션은 각 갈래를 "앵커에서 왜 이렇게 되는가"로 전개한다. 갈래마다 디테일이 다르지만, 막힐 때마다 앵커로 돌아오면 답이 나온다.


02. 갈래 1 — 모니터링: L7이 없으니 관측이 한 단계 내려간다

앵커에서 곧장 따라 나오는 제약을 한 줄로: 게이트웨이에서는 HTTP 메트릭(요청 수, 상태 코드, 지연)이 나오지 않는다. 외부 TLS를 종단하지 않으니 Envoy가 그 트래픽을 TCP로만 인식해서다. 그래서 게이트웨이 메트릭은 전부 istio_tcp_* 계열이다. "요청이 없고 연결만 있다"가 메트릭 이름표에 그대로 박힌다.

게이트웨이에서 나오는 메트릭 (L4)

메트릭 의미
istio_tcp_connections_opened_total 열린 연결 수 — "어느 외부로 몇 번 연결했나"
istio_tcp_connections_closed_total 닫힌 연결 수 — opened와의 차이로 현재 활성 연결 추정
istio_tcp_sent_bytes_total / istio_tcp_received_bytes_total 송수신 바이트 — 트래픽 양·이상 급증 탐지

비-HTTP egress 트래픽의 표준 관측이 바로 이 TCP 메트릭이다. 게이트웨이 워크로드를 기준으로 집계하면 "egress 게이트웨이를 통과한 전체 외부 연결"을 중앙에서 볼 수 있다.

# 외부 호스트별 신규 연결률 (source = egress 게이트웨이)
sum(rate(istio_tcp_connections_opened_total{
  source_workload="istio-egressgateway"
}[5m])) by (destination_service_name)

# 외부 호스트별 송신 바이트율
sum(rate(istio_tcp_sent_bytes_total{
  source_workload="istio-egressgateway"
}[5m])) by (destination_service_name)

# 현재 활성 연결 추정 (opened - closed 누적)
sum(istio_tcp_connections_opened_total{source_workload="istio-egressgateway"})
- sum(istio_tcp_connections_closed_total{source_workload="istio-egressgateway"})
⚠ opened−closed의 한계

*_opened_total/*_closed_totalmonotonic counter라 Pod 재시작·카운터 리셋 시 두 합의 차가 음수나 과대값으로 튄다. 또 두 합을 다른 라벨 집합으로 묶으면 매칭이 어긋난다. 정밀한 활성 연결은 게이지를 직접 보는 편이 정확하다 — Prometheus(15090)에서는 envoy_cluster_upstream_cx_active(상류 기준)·envoy_http_downstream_cx_active(하류 기준), Envoy admin /stats raw 이름으로는 upstream_cx_active·downstream_cx_active다(이 raw 이름으로 PromQL을 던지면 매칭이 안 된다). opened−closed는 어디까지나 "추세" 용도로만 쓴다.

SNI는 어디서 보나 — TCP access log

메트릭은 "몇 개·몇 바이트"를 세지만 "이 연결 하나가 어디로 갔나"는 못 짚는다. 연결 단위 추적·감사는 access log가 정확하다. 그런데 여기서도 앵커가 작동한다 — passthrough 구간의 로그는 HTTP가 아니라 TCP 포맷이라, L7 필드 대신 다음 L4 필드가 핵심이다.

access log 필드 passthrough에서의 의미
requested_server_name SNI — 목적지 외부 호스트명. passthrough에서 "어디로 갔나"의 핵심 단서
upstream_host 실제로 연결된 외부 IP:port
bytes_sent / bytes_received 이 연결의 송수신 바이트
duration 연결 지속 시간(ms)
response_flags 실패 사유 코드(UF/UH/UC 등) — L4 레벨

SNI가 "어디로 갔나"의 유일한 단서인 이유: TLS handshake의 ClientHello는 평문으로 SNI(목적지 호스트명)를 싣는다. 그래서 게이트웨이가 암호 본문은 못 봐도 봉투 겉면의 SNI는 읽을 수 있다. 반대로 HTTP access log의 method·path·response_code여기 없다 — 암호문이라 Envoy가 채울 수 없다. access log는 meshConfig에서 켠다.

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  meshConfig:
    accessLogFile: /dev/stdout
    # 기본 포맷에 requested_server_name(SNI), upstream_host, response_flags 포함

그럼 L7(상태 코드·지연)은 어디서? — 책임 주체 분리

게이트웨이에서 못 보는 L7은 앱 쪽에 있다. 앵커가 말하는 "L7 없음"은 게이트웨이 한정이고, TLS 종단 주체인 앱은 응답 코드·지연을 안다. 그래서 이 구조의 관측은 두 곳을 합치는 것이 정석이다 — 관측 책임이 L4(게이트웨이)와 L7(앱)으로 쪼개진다.

관측 질문 어디서 무엇으로
"어느 외부로 얼마나 나갔나" (감사·통제·이상탐지) 게이트웨이 istio_tcp_* + TCP access log(SNI)
"그 호출이 200이었나, 얼마나 느렸나" (L7 SLO) 앱 계측(OpenTelemetry/APM), 앱 사이드카는 이 구간 L4만

proxy→egress 구간(ISTIO_MUTUAL)은 어떻게 확인하나

앵커의 "안쪽 한 구간만 mTLS"가 실제로 걸렸는지는 메트릭이 아니라 config 덤프로 확인한다(런타임 트래픽이 아니라 Envoy가 받은 설정을 직접 본다). 게이트웨이로 가는 cluster의 transportSocket이 mTLS인지 본다.

# proxy(앱 사이드카) 기준: 게이트웨이로 가는 cluster의 TLS 컨텍스트
istioctl proxy-config cluster deploy/myapp \
  --fqdn istio-egressgateway.istio-system.svc.cluster.local -o json \
  | grep -A8 transportSocket

# 게이트웨이 기준: 외부로 나가는 cluster (passthrough라 originate TLS 없음 확인)
istioctl proxy-config cluster deploy/istio-egressgateway -n istio-system \
  --fqdn api.partner.example.com -o json | grep -A5 transportSocket

ISTIO_MUTUAL이 걸린 안쪽 cluster의 기대 출력transportSocket.nameenvoy.transport_sockets.tls이고, SDS로 워크로드 인증서(default)와 루트(ROOTCA)를 받아온다.

"transportSocket": {
  "name": "envoy.transport_sockets.tls",
  "typedConfig": {
    "commonTlsContext": {
      "tlsCertificateSdsSecretConfigs": [{ "name": "default" }],
      "combinedValidationContext": {
        "validationContextSdsSecretConfig": { "name": "ROOTCA" }
      }
    }
  }
}

대비 — passthrough 외부 cluster: 게이트웨이가 외부 TLS를 originate하지 않으므로 위 transportSocket 블록이 아예 없거나 raw_buffer로 잡힌다(평문 봉투로 암호문 TCP를 그대로 중계). 즉 "안쪽은 mTLS, 바깥은 평문 봉투"라는 앵커가 이 두 출력의 차이로 그대로 입증된다.

대시보드·알람으로 무엇을 걸 수 있나

L7이 없어도 L4만으로 실무 알람은 충분히 만들 수 있다. 단, "에러율"이 아니라 "연결 실패율"이 기준이 된다 — 앵커의 직접적 귀결이다.

ℹ 모니터링에서 자주 놓치는 것
  • SNI는 클라이언트(앱)가 채워 넣는 값이라 위조 가능하다 — 모니터링·감사 근거로는 쓰되, "이 SNI니까 안전하다"는 보안 판단은 약하다. 진짜 통제는 목적지 IP allowlist(Cilium·방화벽)에 둬야 한다.
  • 앱 사이드카도 이 구간에선 L7을 못 본다 — 앱→외부가 end-to-end TLS라 사이드카 역시 암호문만 본다. 그래서 "사이드카 메트릭으로 L7 보겠지"는 오해이고, L7은 반드시 앱 애플리케이션 계측에서 나와야 한다.
  • istio_requests_total 같은 L7 메트릭이 게이트웨이에서 0으로 나오는 것은 정상 — 장애가 아니라 passthrough의 당연한 결과라, 이걸 SLO 대시보드의 빈 패널로 두면 오해를 부른다. 패널 자체를 TCP 메트릭으로 바꿔야 한다.

03. 갈래 2 — 운영 함정: 연결을 소수 노드에 모으면 연결 단위 자원이 병목

앵커의 두 번째 면: 게이트웨이가 다루는 건 장기 연결이고, egress를 강제하려면 그 연결을 소수 노드로 모은다. 이 두 성질이 곱해지면 "연결 단위로 세는 자원"이 전부 병목 후보가 된다. 아래 함정 다섯은 모두 이 한 문장의 파생이다 — 부딪히는 순서는 좁은 깔때기부터: Envoy 1,024(함정 0) → 커널 포트 28K(함정 1) → conntrack 26만. 산술·완화 운영값 전체는 TCP 병목 정본에 따로 박아 두었다.

0. Envoy cluster 연결 상한 1,024 — 커널보다 먼저 부딪히는 벽

커널 포트보다 앞에, Envoy 레벨에 더 좁은 병목이 있다. Envoy는 목적지(cluster)별 동시 upstream 연결 기본 상한이 1,024다. sidecar 시절엔 절대 안 보이던 값이다 — pod 하나가 한 목적지로 1,024개를 열 일이 없으니까. 게이트웨이에선 전사 트래픽이 외부 cluster 하나로 합쳐지면서 가장 먼저 부딪히는 벽이 된다.

1. SNAT 소스 포트 고갈 — 가장 현실적인 한계

egress를 소수 노드로 모으면, 그 노드의 출구 IP 하나가 모든 외부 연결의 source가 된다. TCP 연결은 (source IP, source port, dest IP, dest port) 4-tuple로 구분되는데, source IP가 고정이고 외부 목적지(dest IP:port)가 소수면 쓸 수 있는 source port(약 28K~64K)가 병목이 된다. 동시 연결이 그 수에 근접하면 새 연결이 실패한다. 이게 "연결 단위 자원이 병목"의 가장 직접적 사례다.

2. 유휴 연결이 조용히 끊긴다 — NAT/방화벽 idle timeout

장기 연결(스트리밍, gRPC, DB 커넥션 풀)은 중간 NAT·방화벽의 idle timeout(보통 5~10분)에 걸려 한쪽 모르게 끊긴다. 끊긴 줄 모르고 데이터를 보내려다 reset을 받는다. "연결이 오래 산다"는 앵커의 성질이 정확히 역으로 물어버리는 지점이다. TCP keepalive로 연결을 살아있게 유지하는 게 표준 대응이다.

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: egressgateway-partner
  namespace: istio-system
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
    - name: partner
      trafficPolicy:
        connectionPool:
          tcp:
            tcpKeepalive:          # 중간 NAT idle timeout 보다 짧게
              time: 60s            # 유휴 60s 후 keepalive 시작
              interval: 20s        # 20s 간격 probe
              probes: 5
        portLevelSettings:
          - port: { number: 15443 }    # PASSTHROUGH 443과 분리된 ISTIO_MUTUAL 전용 포트
            tls: { mode: ISTIO_MUTUAL, sni: api.partner.example.com }

핵심: tcpKeepalive.time경로상 가장 짧은 NAT idle timeout보다 작게 설정해야 연결이 죽기 전에 살린다.

단, keepalive가 못 막는 절단이 하나 있다 — Envoy 자신의 idleTimeout(tcp_proxy 기본 1h). keepalive probe는 데이터가 아니라서 Envoy의 idle timer를 리셋하지 않는다. 역할이 다른 두 장치다: keepalive는 중간장비(패킷만 보면 됨)를 깨어 있게 하고, idleTimeout은 Envoy 자신의 기준이라 채널의 최장 유휴 간격보다 길게 직접 설정해야 한다. "keepalive 넣었는데 정확히 1시간마다 끊긴다"의 정체가 이것 — 재현 절차는 재현 랩 §4.

⚠ 안쪽 mTLS leg은 PASSTHROUGH 443과 다른 포트(15443)를 써야 한다

이 구조의 게이트웨이는 이미 443에서 PASSTHROUGH 서버를 돌린다. 같은 443에 ISTIO_MUTUAL(종단) 서버를 겹쳐 올리면 listener의 filter-chain merge가 충돌해(filter_chain_not_found) 연결이 끊긴다. 그래서 proxy→egress 안쪽 mTLS는 egress Service의 전용 tls 포트 15443으로 보낸다(portLevelSettings.port.number: 15443). 정본 패턴·실험 매니페스트는 구조 정본 — HTTPS over mTLS 참조.

3. 노드 핀닝 = 단일 장애점 만들기

외부행을 소수 egress 노드로 모으는 순간(앵커가 요구하는 바로 그 집중), 그 노드들이 전체 외부 통신의 SPOF가 된다. 노드 가용성 $p$, 노드 수 $N$, 독립 장애 가정에서 경로 가용성은:

$$A_{\text{sys}} = 1 - (1-p)^{N}, \qquad D_{\text{year}} = (1 - A_{\text{sys}}) \times 525{,}600 \ \text{min}$$

$N=1$은 가용성이 노드 하나와 같아 운영 금지에 가깝고, 최소 2~3개 + AZ 분산이 권장이다. 단, 노드를 늘려도 공통 NAT 게이트웨이·ToR 스위치·방화벽 정책을 공유하면 그게 진짜 SPOF라 이 수식은 거짓 위안이 된다 — 독립성이 깨지는 공통 요소부터 점검해야 한다.

4. mTLS 인증서는 두 종류, 만료가 다르다

앵커는 mTLS가 한 군데(proxy→egress, ISTIO_MUTUAL)뿐임을 못박는다. 이건 Istio CA가 발급하는 메시 내부 인증서로 기본 24시간 수명·자동 로테이션이라 대개 신경 쓸 일이 없다. 반면 외부 파트너와의 TLS 인증서는 앱이 책임지므로(게이트웨이가 아니라 — 게이트웨이는 그 봉투를 안 푸니까), 그 만료·갱신은 게이트웨이 운영 밖이다. "인증서 만료"를 디버깅할 때 어느 쪽 인증서인지부터 구분해야 헛다리를 안 짚는다.

ℹ 운영에서 자주 놓치는 것
  • 이 구조에서 게이트웨이는 L7 정책(경로 기반 라우팅, 헤더 조작, HTTP 레벨 재시도)을 걸 수 없다 — 암호문이라서. 재시도·타임아웃도 L4(연결) 수준만 가능하다. "게이트웨이에서 특정 path만 차단" 같은 요구가 나오면 그건 이 구조로는 불가능하고, 종단(가로채기)이나 앱 레벨로 가야 한다.
  • REGISTRY_ONLY를 켜면 평소 안 보이던 외부 의존성(로그 수집, 메트릭 푸시, OCSP/CRL 인증서 검증, 컨테이너 레지스트리)이 한꺼번에 막힌다 — 전역 적용 전에 네임스페이스 단위 audit으로 트래픽 인벤토리부터 확보해야 한다.
  • passthrough라도 egress 강제는 게이트웨이가 아니라 네트워크 계층(Cilium egress 정책 + egress 노드만 NAT)이 만든다 — 게이트웨이로 라우팅했다고 우회가 막히는 게 아니다.

04. 갈래 3 — Graceful shutdown: 요청이 아니라 연결이 닫히길 기다린다

egress 게이트웨이 Pod가 종료되는 상황은 일상이다 — 롤링 업데이트, 스케일 인, 노드 드레인, 수동 kubectl delete. 이때 처리가 부실하면 진행 중이던 외부 연결이 강제로 끊겨 503·연결 reset이 발생한다. 앵커의 세 번째 면이 여기서 위력을 발휘한다 — HTTP 게이트웨이라면 "in-flight 요청"이 끝나면 되지만, 이 구조는 장기 TCP 연결이라 그 스트림이 닫혀야 끝난다. 그래서 종료가 특히 민감하다.

Pod 종료 시 실제로 일어나는 일

Kubernetes가 Pod를 종료하기로 하면 두 가지가 동시에 시작된다 — 이 동시성이 문제의 근원이다.

Pod 종료 결정 (A) endpoint 제거 시작 Pod가 endpoint 목록에서 빠짐 — EDS 전파는 몇 초 걸림 (B) SIGTERM 전송 컨테이너(istio-proxy)에 전달 → Envoy drain 시작 문제 — (A) 전파가 (B)보다 느리다 전파 지연 동안 다른 워크로드는 Pod가 살아있다고 믿고 새 연결을 계속 보냄 → 종료 중인 Pod가 받음 → reset 동시에
그림 2. Pod 종료 결정 시 (A) endpoint 제거와 (B) SIGTERM이 동시에 시작 — EDS 전파 지연이 reset의 근원

Envoy(istio-proxy)는 SIGTERM을 받으면 새 연결은 안 받고, 기존 연결이 끝나길 기다린 뒤 종료한다. 그런데 그 대기 시간(drain)의 기본값이 5초다. 5초 안에 안 끝난 연결은 그대로 끊긴다. 장기 TCP 연결엔 5초는 턱없이 짧다 — 앵커의 "장기"가 기본값과 충돌하는 지점.

다뤄야 할 네 개의 시간 파라미터

네 파라미터(preStop/terminationDrainDuration/EXIT_ON_ZERO_ACTIVE_CONNECTIONS/terminationGracePeriodSeconds)의 일반 메커니즘과 grace > preStop+drain 산출 공식은 graceful-termination 시리즈가 정본이다 — Envoy drain & listener 동작, graceful shutdown 런북 참조. 여기서는 표로 위치만 정리하고, egress passthrough 고유 각도(아래 본문·What you might be missing)에 집중한다.

파라미터 위치 역할
preStop (sleep) Pod spec lifecycle SIGTERM 전에 잠깐 대기 → endpoint 제거가 전파될 시간 확보(보통 3~5초). 위 (A)·(B) 경쟁 완화의 핵심
terminationDrainDuration proxy.istio.io/config Envoy가 기존 연결을 기다리는 시간. 기본 5초 → 장기 연결 맞게 상향
EXIT_ON_ZERO_ACTIVE_CONNECTIONS proxyMetadata (대략 v1.12경 도입) 고정 타임아웃 대신 활성 연결이 0이 되면 즉시 종료. 빠른 배포 + 안전 양립
terminationGracePeriodSeconds Pod spec K8s가 SIGKILL 전까지 주는 전체 시간. 위 합보다 길어야 함. 짧으면 drain 중 강제 종료

종료 타임라인 (권장 구성 기준)

Kubernetes terminationGracePeriodSeconds 75s — 이후 SIGKILL preStop sleep 5s — endpoint 전파 대기 안전여유 10s (65s→75s) Envoy proxy t=5s SIGTERM 수신 → draining 진입 기존 TCP 연결 drain (최대 60s) 연결 0 도달 시 조기 종료 (시점 가변) 0s 5s 65s 75s
그림 3. 종료 타임라인 (권장 구성, t=0 = 종료 결정) — preStop 5s + drain 60s = 65s < grace 75s, 10초 안전여유. 조기 종료(EXIT_ON_ZERO)는 가변 시점

읽는 법: preStop(0~5s)이 끝난 5초 시점에 SIGTERM이 와서 Envoy draining이 시작되고, drain은 최대 65초 시점까지 간다. EXIT_ON_ZERO의 조기 종료 milestone은 고정 시점이 아니라 활성 연결이 0이 되는 가변 시점이므로(그림의 ?) drain 구간 어디서든 일어날 수 있다. preStop(5s)+drain(60s)=65s가 grace 75s보다 짧아 생기는 10초 안전여유가 그림 끝에 보인다.

순서 규칙 하나만 기억하면 된다: preStop + drain < terminationGracePeriodSeconds. drain이 grace period보다 길면 K8s가 중간에 SIGKILL을 날려 결국 연결이 끊긴다.


05. 예시 — egress 게이트웨이에 종료 구성을 실제로 적용하고 확인

앵커에서 연역한 종료 규칙을 실제 매니페스트로 굳히고, 떴는지 한 번 확인한다.

설정 — IstioOperator egressGateways 오버레이

게이트웨이는 IstioOperator의 egressGateways 오버레이로 Pod 어노테이션·grace period를 설정한다.

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  components:
    egressGateways:
      - name: istio-egressgateway
        enabled: true
        k8s:
          # K8s가 강제 종료(SIGKILL)까지 주는 전체 시간 — drain 보다 길게
          # (overlay로 podTemplate의 terminationGracePeriodSeconds 설정)
          overlays:
            - kind: Deployment
              name: istio-egressgateway
              patches:
                - path: spec.template.spec.terminationGracePeriodSeconds
                  value: 75
                - path: spec.template.spec.containers.[name:istio-proxy].lifecycle
                  value:
                    preStop:
                      exec:
                        command: ["sleep", "5"]   # endpoint 전파 대기
        # proxy 설정: drain 시간 + 활성연결 0 시 조기 종료
  meshConfig:
    defaultConfig:
      terminationDrainDuration: 60s        # 장기 TCP 연결 고려해 상향(기본 5s)
      proxyMetadata:
        EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true"

terminationDrainDuration: 60s는 "최대 60초까지 기다린다"이고, EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true"가 있으면 연결이 그 전에 다 닫히는 즉시 종료해 배포 속도를 안 깎는다. terminationGracePeriodSeconds: 75는 preStop 5초 + drain 60초보다 길어 SIGKILL 여유를 둔다 — 위 순서 규칙을 숫자로 만족(5+60=65 < 75).

⚠ meshConfig.defaultConfig는 전역 적용

위 예시에서 meshConfig.defaultConfig에 둔 terminationDrainDuration/EXIT_ON_ZERO_ACTIVE_CONNECTIONSegress 게이트웨이뿐 아니라 메시의 모든 사이드카·게이트웨이에 전역 적용된다. egress 게이트웨이에만 다른 drain을 주려면 메시 전역이 아니라 워크로드 한정으로 — overlayspodTemplateproxy.istio.io/config 어노테이션을 박아({"terminationDrainDuration":"60s"}) 해당 Deployment에만 적용해야 한다.

⚠ EXIT_ON_ZERO_ACTIVE_CONNECTIONS의 알려진 함정

이 기능은 활성 연결 수를 Envoy stats(downstream_cx_active)로 폴링해 판단한다. 그런데 실제 운영에서 그 stats 조회가 일시적으로 실패하면(타임아웃) 프록시가 비정상 종료(abort) 되는 사례가 보고됐다 — 연결이 많을 때 특히. 또 장기 연결이 끝나지 않으면 영원히 0이 안 돼서 결국 grace period의 SIGKILL에 의존하게 된다. 따라서 (a) terminationGracePeriodSeconds를 충분히 길게 두는 안전망은 반드시 함께 두고, (b) 끝나지 않는 장기 스트림에는 별도 상한(연결 최대 수명)을 두는 걸 고려할 것. (istio/istio 이슈 #50596)

떴는지 한 번 확인

# 종료되는 게이트웨이 Pod의 proxy 로그 — graceful termination 메시지 확인
kubectl logs <egress-pod> -n istio-system -c istio-proxy -f
#  "Graceful termination period is 60s, starting..."
#  "Graceful termination period complete, terminating remaining proxies."

# 현재 적용된 drain 값 확인
kubectl exec <egress-pod> -n istio-system -c istio-proxy -- \
  pilot-agent request GET config_dump | grep -i drainDuration

# 배포 중 외부 연결 reset/503 모니터링 (앱 쪽 지표와 교차 확인)
#  → istio_tcp_connections_closed_total 의 비정상 급증과 시점 대조

기대: 로그에 Graceful termination period is 60s, starting...가 뜨고 정상 종료 시 ... complete가 뒤따른다. config_dump의 drainDuration60s로 잡히면 설정이 실제 반영된 것. 배포 창에서 istio_tcp_connections_closed_total이 평소 추세 이상으로 튀지 않으면 reset이 없었다는 신호다(앱 쪽 503 지표와 교차).

ℹ Shutdown에서 자주 놓치는 것
  • 이 구조의 drain은 "요청 완료"가 아니라 "TCP 연결 종료"를 기다린다. HTTP 게이트웨이라면 in-flight 요청이 끝나면 되지만, passthrough는 암호화된 TCP 스트림이라 그 스트림이 닫혀야 끝난다 — 그래서 장기 연결(gRPC 스트림, DB 풀, 웹소켓)은 drain 시간을 한참 넘길 수 있고, 결국 grace period의 SIGKILL로 끊긴다. "graceful"이 모든 경우를 막아주지 않는다.
  • HTTP/2·gRPC는 GOAWAY로 우아하게 끊을 수 있지만, 그건 게이트웨이가 그 프로토콜을 인식할 때 얘기 — passthrough에선 게이트웨이가 암호문만 보므로 GOAWAY를 보낼 주체가 아니다. 그 graceful 종료는 TLS 끝점인 앱·외부 서버의 몫이다.
  • preStop sleep을 빼먹으면 drain을 아무리 늘려도 배포 때마다 reset이 난다 — endpoint 전파 전에 SIGTERM이 와서, 다른 워크로드가 여전히 종료 중인 Pod로 새 연결을 보내기 때문. drain은 "기존 연결"을 지키지 "새로 들어온 연결"을 막지 못한다.
  • 노드 드레인(노드 자체 종료)은 Pod 종료와 또 다르다 — PodDisruptionBudget(minAvailable: 1)이 없으면 여러 egress Pod가 동시에 빠져 경로가 통째로 끊길 수 있다.

06. 정리 · 운영 체크리스트

한 그림으로: 게이트웨이는 "암호화된 장기 TCP 스트림"만 본다. 이 사실 하나에서 — ①관측은 L4+SNI, L7은 앱에 위임 ②연결을 소수 노드에 모으니 SNAT·NAT idle·SPOF가 병목 ③종료는 연결이 닫히길 기다림 — 운영 전부가 연역된다. 막히면 앵커로 돌아오라.

운영 체크리스트

영역 점검 항목
모니터링 accessLogFile 활성 + requested_server_name 포함 확인
모니터링 게이트웨이 대시보드를 L7 패널이 아닌 istio_tcp_* 기반으로 구성
모니터링 L7 SLO는 앱 계측(OpenTelemetry/APM)에서 수집 — 책임 주체 명시
모니터링 알람: 연결 실패율(UF/UH/UC), 미등록 SNI 출현, 활성 연결 포화
운영 egress 노드 conntrack/SNAT 포트 사용률 모니터 + 상한 튜닝
운영 장기 연결 호스트에 tcpKeepalive(NAT idle보다 짧게) 설정
운영 egress 노드 최소 2~3개 + podAntiAffinity + PDB minAvailable: 1
운영 공통 NAT/ToR/방화벽 SPOF 여부 점검(노드 수로 안 풀림)
운영 ISTIO_MUTUAL 적용 여부를 proxy-config cluster ... transportSocket으로 확인
shutdown preStop sleep 3~5초로 endpoint 전파 대기
shutdown terminationDrainDuration 장기 연결 고려해 상향(기본 5초)
shutdown EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true" + 충분한 grace period 안전망
shutdown 배포 중 istio_tcp_connections_closed_total 급증/503 교차 모니터

핵심 정리

What you might be missing


See also


ℹ 출처 (검증 기준 Istio 1.30 / `networking.istio.io/v1`)
  • Envoy drain 기본 5초, SIGTERM 시 새 연결 거부 후 대기 동작, EXIT_ON_ZERO_ACTIVE_CONNECTIONS(대략 v1.12경 도입 — 적용 전 config_dump로 확인 권장) — Istio change notes / pilot-agent 문서, 및 운영 가이드 자료.
  • terminationDrainDuration / terminationGracePeriodSeconds 관계(drain < grace), preStop sleep으로 endpoint 전파 대기, 종료 타임라인 — Istio 데이터플레인 graceful shutdown 운영 가이드 및 deployment best practices.
  • EXIT_ON_ZERO_ACTIVE_CONNECTIONS stats 조회 실패 시 abort 사례 — istio/istio GitHub 이슈 #50596.
  • TCP egress 메트릭(istio_tcp_connections_opened_total 등) 및 egress 게이트웨이 메트릭 집계 — Istio egress 모니터링 가이드. TCP keepalive(tcpKeepalive)로 NAT idle 끊김 방지 — Istio persistent/keepalive 운영 가이드.
  • ※ 일부 수치(기본 drain 5초 등)는 버전에 따라 달라질 수 있어 적용 전 config_dump로 실제 값 확인을 권장함. terminationGracePeriodSeconds가 drain보다 짧으면 SIGKILL로 강제 종료되는 점은 버전 불문 동일.

관련 파일 · 참조

관련 검증Egress mTLS 리포트 — HTTPS over mTLS (ISTIO_MUTUAL) · 구조 정본 — CRD·장단점·활용·운영