Runbook — ServiceEntry resolution: DNS 동작과 진단 (STRICT_DNS)
외부 도메인(httpbin.org)을 ServiceEntry(resolution: DNS)로 등록하면 istiod가 이를 Envoy의
STRICT_DNS cluster로 변환한다. 이 문서는 그 변환·DNS 갱신 메커니즘, "죽은 IP" 처리, ambient DNS
질의 경로를 홈랩 실측·재현 명령과 함께 한 줄의 멘탈모델로 꿴다.
결론 한 문장: DNS refresh는 health check가 아니다 — DNS는 "목록"만 줄 뿐 IP의 생사를 모르므로,
죽은 IP 회피는 outlier detection으로 명시해야 한다.
환경: cluster homelab(k8s v1.30.6, istiod 1.30.0 / istioctl 1.27.0 client·skew) / egress gateway 경유 / 관련: ingress·egress 통합 리포트
1. 배경 — 왜 mesh가 외부 도메인의 IP를 직접 들어야 하나
mesh 안 트래픽은 전부 Envoy가 가로채 라우팅한다(sidecar/gateway). 그런데 Envoy는 "IP:port의 묶음"인
cluster 단위로만 upstream을 안다 — Envoy의 세계에는 호스트네임이 없고 endpoint 집합만 있다.
mesh 내부 서비스는 istiod가 k8s Endpoints/EndpointSlice를 watch해서 cluster를 자동으로 채워준다.
하지만 httpbin.org 같은 mesh 밖 외부 도메인은 k8s에 Endpoints가 없다. istiod가 그 IP를 알 길이 없다.
ServiceEntry는 바로 이 공백을 메우는 CRD다 — "이 호스트는 mesh의 일부로 취급하라, IP는 이렇게 알아내라"를
선언한다. 그 "IP를 알아내는 방식"이 spec.resolution 필드이고, 이 한 필드가 Envoy cluster의 종류를
결정한다. 즉 외부 도메인 한 줄을 등록할 때 진짜로 고르는 것은 "Envoy가 IP를 들고 LB하는 전략"이다.
여기서 자연스럽게 따라오는 질문 4개가 이 문서의 뼈대다 — 이 순서대로 읽으면 길을 잃지 않는다:
resolution -> cluster type -> 갱신 주기 -> 죽은 IP -> 회피 구성
(어떻게 (STRICT_DNS (DNS TTL을 (DNS는 (outlier
IP를 안다) / LOGICAL) 따라 재질의) 생사 모름) detection+retry)
선행 개념: Envoy cluster = endpoint(IP:port) 집합 + LB 정책 / cluster 이름 규칙 direction|port|subset|fqdn
(예: outbound|443||httpbin.org) / egress gateway = mesh 밖으로 나가는 트래픽을 한 노드로 모으는 전용 Envoy.
cluster 구조 자체는 Cluster 해부 참조.
2. 머릿속에 둘 그림 — DNS는 "목록", 생사는 별도 책임
이 문서 전체를 꿰는 앵커 한 문장:
resolution이 Envoy cluster type을 정하고, cluster type이 "IP를 어떻게 들고 LB할지"를 정한다. 그러나 어느 type이든 DNS는 IP "목록"만 줄 뿐 그 IP가 살아있는지는 모른다 — liveness는 outlier detection의 별도 책임이다.
이 한 문장에서 나머지가 전부 파생된다. resolution을 STRICT_DNS로 두면 A record의 모든 IP를 펼쳐 Envoy가 직접 LB하고(§3), 갱신 주기는 DNS TTL을 따른다(§4). 하지만 TTL 만료 전에 IP가 죽으면 Envoy는 그 IP를 여전히 HEALTHY로 들고 있어 연결이 실패한다(§5) — DNS refresh는 단지 "목록 다시 받기"이지 "이 IP 살아있냐"를 묻는 게 아니기 때문이다. 그래서 죽은 IP 회피는 outlier detection + retry로 명시해야 하고(§7), GSLB 뒤라면 LB 권한을 DNS에 위임하는 LOGICAL_DNS가 더 맞는다(§6). 마지막으로 "그 DNS 질의를 실제로 누가 쏘는가"가 진단의 출발점이다 — egress 경유 시 그 주체는 egress gateway의 Envoy(c-ares)다(§8).
왜 이렇게 책임을 쪼갰는가? DNS와 health는 원래 다른 시스템이기 때문이다. DNS 서버는 "이 이름에 어떤 IP들이 매핑되는가"의 권위자일 뿐, 그 IP 뒤 프로세스가 지금 요청을 받을 수 있는지는 모른다(특히 같은 A record 안에서 일부만 죽은 경우). Envoy는 이 둘을 의도적으로 분리한다 — DNS는 endpoint 발견(discovery), outlier detection은 endpoint 축출(ejection). 이 분리를 모르면 "DNS만 믿으면 알아서 죽은 IP 빠지겠지"라는 가장 흔한 오해에 빠진다.
3. 변환 구조 — resolution이 cluster type을 결정한다
ServiceEntry.spec.resolution 필드가 Envoy cluster type을 결정한다. 이 매핑을 먼저 확립해야
이후 갱신 주기(§4)·죽은 IP(§5)를 STRICT/LOGICAL 대비로 읽을 수 있다.
resolution |
Envoy type | endpoint 적재 | 적합 대상 |
|---|---|---|---|
DNS |
STRICT_DNS | A record 모든 IP를 펼침 | 개별 IP를 Envoy가 직접 LB하고 싶을 때 |
DNS_ROUND_ROBIN |
LOGICAL_DNS | 첫 IP 1개만, 연결 재사용 | CDN/GSLB/대형 LB 뒤 "논리적 단일 endpoint" |
차이의 본질은 "endpoint를 펼치느냐(STRICT) vs 한 점으로 접느냐(LOGICAL)" 한 곳이다 — 나머지(갱신·LB·연결
재사용)는 전부 이 한 델타에서 따라온다. resolution: DNS(STRICT_DNS) 변환 결과:
ServiceEntry(resolution: DNS, hosts: httpbin.org)
| istiod 변환
v
Envoy cluster "outbound|443||httpbin.org"
type: STRICT_DNS <- Envoy가 직접 DNS 질의
respect_dns_ttl: true <- 갱신 주기 = DNS 응답의 TTL
dns_refresh_rate: 60s <- TTL이 0/없을 때만 쓰는 fallback
endpoints: [8 IPs] <- A record 전체를 펼쳐서 보관
경유 구성 주의: 트래픽 경로가
sleep → egress gateway → httpbin.org이므로 실제 외부 DNS를 도는 주체는 egress gateway의 Envoy다. sleep proxy에도 같은 cluster가 보이지만 라우팅이 egress gateway로 향하므로 실질 사용처가 아니다. 진단은 egress gateway 기준.
STRICT_DNS vs LOGICAL_DNS 실측 (example.com을 DNS_ROUND_ROBIN으로 별도 등록):
SERVICE FQDN TYPE endpoints(proxy-config)
httpbin.org STRICT_DNS 다수 (A record 전체)
example.com LOGICAL_DNS 1
→ istioctl proxy-config endpoints <pod> --cluster "outbound|443||<host>"의 endpoint 개수가
STRICT=다수 / LOGICAL=1로 갈리는 것이 Envoy 반영의 결정적 증거다.
LOGICAL_DNS의 핵심은 "DNS 레코드가 자주 바뀌어도 기존 연결을 유지(connection draining/cycling 제거)"라서,
공식 문서가 "large web scale services accessed via DNS"에 권장한다(상세 권장 근거는 §6).
4. 갱신 주기 결정 규칙 — 왜 60s가 함정값인가
| 필드 | 현재 값 | 의미 |
|---|---|---|
respect_dns_ttl |
true |
DNS 응답 TTL을 갱신 주기로 사용 |
dns_refresh_rate |
60s |
TTL이 0/없을 때만 쓰는 fallback |
httpbin.org 실제 TTL |
10s (dig) |
→ 실제 갱신 ≈ 10초 |
dns_refresh_rate: 60s는 함정값이다 — config_dump에 60s가 박혀 있어도 respect_dns_ttl: true면 거의
안 쓰인다(TTL=0이거나 응답에 TTL이 없을 때만 fallback으로 등판). 즉 실제 갱신 주기를 알려면 cluster 설정이
아니라 권위 DNS의 TTL을 봐야 한다 — config만 읽고 "60초마다 갱신되겠지"라고 단정하면 틀린다(실측은 10초).
mesh 전역 기본은 meshConfig.dnsRefreshRate 필드(configmap/istio의 .data.mesh)로 조정한다.
단 효력 범위는 좁다: respect_dns_ttl=true인 외부 DNS cluster에서는 TTL이 우선이라 거의 무효이고,
TTL을 무시하는 cluster나 NDS(DNS auto-allocation) 경로에 실질 영향을 준다. 확인:
kubectl --context homelab -n istio-system get cm istio -o jsonpath='{.data.mesh}' | grep -i dnsRefreshRate
# 미설정이면 출력 없음 = Envoy 기본 fallback(위 60s)
5. "죽은 IP" 처리 — 이 문서의 심장
§2 앵커의 핵심이 여기서 구체화된다. DNS refresh는 liveness 체크가 아니므로, TTL 만료 전에 IP가 죽으면 Envoy는 그 IP를 HEALTHY로 들고 있다가 LB가 고르면 연결 실패한다. 갱신 주기를 아무리 줄여도 이 창은 닫히지 않는다 — TTL이 10초여도, 죽고 나서 다음 재질의 전까지(그리고 권위 DNS가 그 IP를 빼줄 때까지)는 무방비다.
[갱신 전, IP 죽음]
client --> egress GW --> [죽은 IP 선택] --X TCP connect 실패
+-- retry O --> 다른 IP 재시도 --> 성공
+-- retry X --> 요청 실패 (503/connect error)
IP가 LB 목록에서 빠지는 경로는 3가지이고 서로 독립적이다 — 어느 것을 켜느냐가 죽은 IP 회피의 설계다:
| 메커니즘 | 트리거 | 기본 구성 |
|---|---|---|
| DNS refresh | TTL 만료 → 재질의 시 DNS가 해당 IP 제외 | O (수동적) |
| Outlier detection (passive HC) | 연속 5xx/connect 실패 → 일시 ejection | X (DestinationRule 미설정) |
| Active health check | Envoy가 직접 주기적 probe | X |
세 경로를 갈라 보는 이유: DNS refresh는 권위 DNS가 빼주길 기다리는 수동적 경로라 내 통제 밖이다. outlier detection은 내가 본 실패로 즉시 빼는 능동적 경로라 통제 안에 있다. 그래서 —
→ 프로덕션 결론: 죽은 IP 회피는 DNS TTL에 기대지 말고
DestinationRule.trafficPolicy.outlierDetection + VirtualService.http.retries로 명시 설정한다.
구체 구성(YAML)·passthrough 함정은 §7, outlier 필드 의미의 정본은
circuit-breaking 메커니즘 note 참조.
6. GSLB 환경 권장 — LOGICAL_DNS
GSLB(DNS가 클라이언트 위치·health로 최적 IP를 고르는 모델) 뒤 호스트는 LOGICAL_DNS 권장. 이유를 한 축으로 압축하면 "LB 권한을 누구에게 둘 것인가" — GSLB가 이미 최적 IP를 골라 주는데 STRICT_DNS로 전체 IP를 펼치면 Envoy가 그 위에서 또 LB하며 GSLB 결정을 덮어쓴다.
근거: 1. GSLB 결정 존중: STRICT_DNS는 A record 전부를 펼쳐 Envoy가 자체 LB → GSLB가 고른 IP 외 원격 리전까지 섞어 GSLB 의도 무력화. LOGICAL_DNS는 DNS가 준 첫 IP만 써 결정을 따른다. 2. 동적 변경 + 짧은 TTL 친화: LOGICAL_DNS는 새 연결 시 재해석해 최신 GSLB 결정 반영. STRICT_DNS는 전체 IP에 conn pool 유지 → stale·원격 연결 부담. 3. 자원 낭비 방지: GSLB 뒤는 사실상 "하나의 논리적 endpoint" → 전체 IP에 사전 연결은 낭비.
Trade-off: LOGICAL_DNS는 endpoint가 1개라 Envoy outlier/세밀 LB 효력이 약화 → "장애 판정을 GSLB에 위임"하는 셈. GSLB health가 느슨하면 STRICT_DNS + outlier가 나을 수도. 본질은 "LB 권한을 DNS(GSLB)에 둘 것인가 Envoy에 둘 것인가"의 선택.
7. 죽은 IP 회피 구성 — outlier detection + retry
DNS refresh는 liveness를 모르므로(§5), 죽은 IP 회피는 아래로 명시한다. outlier detection 필드 자체의
일반 의미(consecutive5xxErrors/baseEjectionTime 배수 증가 등)는
circuit-breaking 메커니즘 note가 정본이고,
여기서는 egress passthrough(L4) 특수성에 집중한다. 이 특수성 한 줄이 §7 전체를 지배한다:
egress gateway가 TLS PASSTHROUGH면 HTTP/5xx를 못 보므로, L7에 기대는 회피 장치는 전부 무력화된다.
outlier detection (DestinationRule.trafficPolicy.outlierDetection — 적용 후 Envoy 반영 확인됨):
trafficPolicy:
connectionPool: { tcp: { connectTimeout: 2s } } # connect 실패 빨리 판정
outlierDetection:
consecutiveLocalOriginFailures: 3 # ★ connect 실패(L4) 3회 → eject (passthrough 핵심)
splitExternalLocalOriginErrors: true
consecutive5xxErrors: 5 # 5xx(L7) 5회 → eject
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50 # 전멸 방지
minHealthPercent: 40
egress 특화 포인트: TLS PASSTHROUGH(L4)에서 egress gateway는 HTTP/5xx를 볼 수 없으므로
consecutive5xxErrors는 발화하지 않는다.consecutiveLocalOriginFailures(connect 실패 기반)가 유일한 자동 회피 경로다. 따라서 짧은connectTimeout과 함께 써야 빠르게 eject된다.
retry (VirtualService.http.retries) — L7 HTTP route에서만 동작:
http:
- route: [...]
retries:
attempts: 3
perTryTimeout: 2s
retryOn: connect-failure,refused-stream,5xx,gateway-error
⚠️ 함정: 현재 egress는 TLS PASSTHROUGH(L4) 라 egress gateway가 HTTP를 못 본다 →
http.retries는 조용히 무시된다. retry를 쓰려면 TLS origination(L7) 구성으로 전환해야 한다. - passthrough(L4):outlierDetection(consecutiveLocalOriginFailures)+ 짧은connectTimeout이 유일한 자동 회피 - TLS origination(L7): 위 +http.retries(retryOn: connect-failure)
8. 적용·검증 (재현 가능)
8.1 진단 명령 — cluster가 어떻게 IP를 들고 있나
CTX=homelab
GW=deploy/istio-egressgateway.istio-system
CL="outbound|443||httpbin.org"
# (1) 현재 endpoint IP 목록 + outlier 상태
istioctl --context $CTX proxy-config endpoints $GW --cluster "$CL"
# (2) cluster DNS 설정 (type / refresh_rate / respect_dns_ttl)
kubectl --context $CTX -n istio-system exec deploy/istio-egressgateway -c istio-proxy -- \
curl -s "localhost:15000/config_dump?resource=dynamic_active_clusters" \
| grep -A10 '"'"$CL"'"'
# (3) 런타임 endpoint 상태(success_rate, healthy flags)
kubectl --context $CTX -n istio-system exec deploy/istio-egressgateway -c istio-proxy -- \
curl -s "localhost:15000/clusters" | grep "httpbin.org"
# (4) DNS 원천 데이터(IP + TTL)
dig +noall +answer httpbin.org # TTL = 첫 컬럼 숫자
# (5) DNS 갱신 카운터를 보려면 sidecar stats 필터를 풀어야 함(기본 미노출):
# pod annotation proxy.istio.io/config 의 proxyStatsMatcher.inclusionRegexps 에 ".*httpbin\.org.*" 추가
검증 실험 — "DNS 주기 = endpoint 갱신 주기": dig IP 집합과 proxy-config endpoints를 ~10초 간격
2회씩 떠서, IP 변동 시 endpoint도 따라 바뀌면 둘이 동기임이 확인된다. 이게 §4의 "실제 갱신 ≈ TTL(10s)"의
실측 근거다.
8.2 retry가 정말 무시됨을 입증 (gap finding: 기대 vs 실제)
§7의 "L4에서 http.retries는 조용히 무시"라는 주장은 추정이 아니라 두 관측으로 입증된다 —
HTTP route가 없으면 retry 정책이 붙을 곳이 없고, 붙을 곳이 없으면 retry 카운터가 영영 0이다:
# (a) egress gateway에 HTTP route가 없음 = retry 정책이 붙을 곳이 없음을 확인.
# passthrough는 TCP/SNI 매칭만 존재 → routes 출력이 비거나 TCP proxy만 보임.
istioctl --context homelab proxy-config routes deploy/istio-egressgateway.istio-system \
| grep -i httpbin # 기대: 매칭 없음(HTTP route 부재)
# (b) retry 카운터가 0으로 고정 = 재시도가 한 번도 발생하지 않음.
kubectl --context homelab -n istio-system exec deploy/istio-egressgateway -c istio-proxy -- \
curl -s localhost:15000/stats | grep 'httpbin.*upstream_rq_retry'
# 기대: upstream_rq_retry: 0 (혹은 카운터 자체 부재) — L4라 retry 경로 미존재
9. "ambient DNS" — 도메인 질의는 어디로 가나 (실측)
§2 앵커의 마지막 가닥: "그 DNS 질의를 누가 쏘는가"가 진단의 출발점이다. config_dump의 cluster를 아무리 들여다봐도, 실제 질의를 누가/어디로 보내는지를 모르면 "왜 stale IP가 안 빠지나"를 추적할 수 없다.
공식 문서의 "querying the ambient DNS"에서 ambient는 Istio ambient mesh가 아니라
"그 프록시 환경에 깔린 기본 resolver(/etc/resolv.conf)"라는 영어 일반어다. STRICT/LOGICAL 둘 다 동일.
측정 결과 질의 경로(실선 = DNS capture OFF 실측 경로, 점선 = capture ON 시 분기):
핵심: 질의 주체는 egress gateway의 Envoy(c-ares)이며, capture OFF에서는 istio-agent를 거치지 않고
resolv.conf의 nodelocaldns → CoreDNS → upstream으로 직접 나간다.
확인된 사실:
- 질의 주체 = Envoy의 c-ares (typed_dns_resolver_config: envoy.network.dns_resolver.cares).
resolver 주소 미지정 → c-ares가 resolv.conf를 그대로 읽음.
- sleep/egress gateway pod의 nameserver = 169.254.25.10 (nodelocaldns) → CoreDNS → 외부 upstream.
- Istio DNS capture(ISTIO_META_DNS_CAPTURE)는 비활성 (meshConfig·pod env 모두 미주입)
→ istio-agent DNS proxy를 안 거치고 Envoy가 직접 나간다.
DNS capture를 켜면: istio-agent가 53을 가로채(DNS proxy), 메시 내부 호스트는 NDS로 로컬 응답, 외부만 upstream 포워딩 → "질의 위치"가 istio-agent로 바뀐다. 진단 전에 capture 여부를 먼저 봐야 하는 이유.
진단 명령:
# resolver(=ambient DNS) 확인
kubectl -n mesh-test exec deploy/sleep -c istio-proxy -- cat /etc/resolv.conf
# Envoy가 쓰는 resolver 종류
kubectl -n istio-system exec deploy/istio-egressgateway -c istio-proxy -- \
curl -s "localhost:15000/config_dump?resource=dynamic_active_clusters" \
| grep -A60 '"outbound|443||httpbin.org"' | grep -i dns_resolver
# DNS capture 활성 여부
kubectl -n istio-system get cm istio -o jsonpath='{.data.mesh}' | grep -i DNS_CAPTURE
핵심 정리
resolution이 cluster type을 결정:DNS→STRICT_DNS(A record 전 IP 펼침, Envoy 자체 LB),DNS_ROUND_ROBIN→LOGICAL_DNS(첫 IP 1개, 연결 재사용). endpoint 개수가 결정적 증거.- DNS refresh ≠ health check. DNS는 endpoint 발견, outlier detection은 endpoint 축출 — liveness는 outlier detection / active HC의 몫이지 DNS TTL이 아니다.
respect_dns_ttl=true면 실제 갱신 주기는 권위 DNS TTL(실측 10s)이지dns_refresh_rate(60s)가 아니다.- egress gateway 경유 시 DNS 질의 주체는 gateway의 Envoy(c-ares), 클라이언트 sidecar가 아니다.
- passthrough(L4)에서는
consecutiveLocalOriginFailures가 유일한 자동 회피 —http.retries·consecutive5xxErrors는 HTTP를 못 봐서 무효(§7). - GSLB 뒤 호스트는 LOGICAL_DNS — LB 권한을 DNS에 위임. STRICT_DNS는 GSLB 의도를 무력화.
What you might be missing
- 갱신 주기를 줄여도 죽은 IP 창은 안 닫힌다: TTL을 10초로 줘도, IP가 죽고 다음 재질의 + 권위 DNS가 빼줄 때까지는 무방비. 능동 축출(outlier)이 없으면 DNS 주기 튜닝은 근본 해법이 아니다(§5).
dnsRefreshRate의 좁은 효력: meshConfig 필드지만respect_dns_ttl=true외부 cluster에선 TTL이 우선이라 거의 무효다. 실제로는 TTL을 무시하는 cluster나 NDS auto-allocation 경로에만 작동(§4).- "silent" 함정:
http.retries는 passthrough에서 에러 없이 그냥 무시된다. proxy-config에 HTTP route가 없고 retry 카운터가 0인지로만 확인 가능(§8.2 (a)(b)). - "ambient DNS"는 Istio ambient mesh가 아니다 — 프록시 환경의 기본 resolver(
resolv.conf)를 뜻하는 일반어. - DNS capture 토글이 질의 위치를 바꾼다: capture ON이면 istio-agent가 53을 가로채 질의 주체가 바뀌므로, 진단 시 capture 활성 여부를 먼저 확인해야 경로 추론이 어긋나지 않는다(§9).
참조
- ingress·egress 통합 리포트
- circuit-breaking 메커니즘 (outlier detection 정본)
- Cluster 해부 (STRICT_DNS/LOGICAL_DNS cluster 구조)
- east-west gateway SNI
- 시연 manifest:
scenarios/20-egress/{serviceentry-example-logicaldns,destinationrule-httpbin-outlier}.yaml