Ztunnel DNS Capture 동작원리 및 ServiceEntry IP-autoallocate
Table of Contents
Istio Ambient mode에서 ServiceEntry
를 사용하기 위해서는 2가지 옵션을
켜야합니다.
- istio-cni
values.yaml
에서values.cni.ambient.dnsCapture=true
values.pilot.env.PILOT_ENABLE_IP_AUTOALLOCATE=true
이 기능은 Istio 1.25부터 default로 활성화되어 있습니다. 이를 이용하면,
ServiceEntry
를 이용할 수 있게되지만, key에 명시되어 있는 것처럼 DNS를 capture
한다는 것은 서비스 메시 운영에 큰 임팩트를 끼칠 수 있는 operation처럼 보입니다.
해당 문서에는 해당 Ambient mode에서 ServicEntry
사용을 위해 이 dnsCapture가 왜
필요한지 설명하고, Ztunnel DNS Capture가 어떻게 동작하는지 서술합니다.
How Ztunnel capture DNS #
values.cni.ambient.dnsCapture=true
옵션을 이용해서, istio-cni는 이제 모든 DNS
요청 또한 ztunnel 프록시의 15053 포트로 리디렉션합니다.
Ztunnel의 DNS proxy는 먼저 ServiceEntry 매핑과 Kubernetes 서비스에 정보를
확인합니다. 일치하는 항목을 찾으면 실제 클러스터 IP 또는
240.240.0.0/16(Class E Subnet) 범위에서 자동 할당된 Virtual IP를
반환합니다. 알 수 없는 도메인의 경우, (pod’s) /etc/resolv.conf
설정에 따라
upstream DNS server(CoreDNS)로 요청을 전달합니다. 이 아키텍처는 세 가지 이점을
제공합니다:
- 로컬 캐싱을 통한 DNS 지연 시간 감소
- CoreDNS 부하 감소
- 별도의 DNS 서버 설정 없이 ServiceEntry 해결
왜 IP-Autoallocate? #
DNS capture와 IP 자동 할당(PILOT_ENABLE_IP_AUTOALLOCATE
로 제어) 간의 관계는
ambient mode의 근본적인 제약에 대한 해결책을 나타냅니다: ztunnel은 Layer
4에서만 작동하여 IP addr, protocol,만 처리하고 HTTP header나 hostname에 접근할
수 없습니다.
이 제한은 External TCP service에 심각한 제약을 가집니다. 예를 들어, 두 개의
MySQL 데이터베이스가 ServiceEntry 리소스로 정의되고 둘 다 포트 3306을 사용하는
경우를 생각해봅시다. Sidecar 모드에서 Envoy는 HTTP Host 헤더나 SNI를 사용하여
구별할 수 있습니다. 그러나 ztunnel의 Layer 4 작동은 IP:Port
조합만으로
listener를 생성할 수 있습니다. 고유한 IP 주소가 없으면 두 서비스로 라우팅되는
Istio Service object를 생성할 수 없습니다.
IP-Autoallocate는 명시적 주소가 없는 각 ServiceEntry에 고유한 가상 IP를
할당하여 이를 해결합니다. pilot-discovery 컨트롤러는 예약된 Class E
서브넷(IPv4의 경우 240.240.0.0/16)에서 할당하고 이러한 주소로 ServiceEntry
상태를 업데이트합니다. 애플리케이션이 이러한 서비스의 DNS를 쿼리하면 ztunnel DNS
프록시가 자동 할당된 IP를 반환하여 명확한 라우팅을 위한 240.240.0.1:3306
및
240.240.0.2:3306
과 같은 고유한 listener를 가능하게 합니다.
Sidecar vs Ambient mode (w/ ServiceEntry) #
ServiceEntry DNS 해결이 사이드카와 ambient 모드 간에 어떻게 작동하는지에 대한 중요한 아키텍처 차이가 존재합니다:
Reference: https://istio.io/latest/docs/ops/configuration/traffic-management/dns/
Sidecar 모드에서:
- Envoy 사이드카가 아웃바운드 트래픽을 가로채고 DNS 해결을 수행
- 앱이 외부 서비스 DNS를 쿼리할 때 사이드카는 다음을 사용하여 해결 가능:
resolution: DNS
- Envoy가 주기적으로 DNS 조회 수행 (30초마다)resolution: STATIC
- 사전 정의된 엔드포인트 사용resolution: NONE
- 원본 대상 IP 통과
- 사이드카는 L7 속성(Host 헤더, SNI)을 사용하여 요청 매칭 가능
Ambient 모드에서:
- Ztunnel DNS 프록시가 트래픽이 pod를 떠나기 전에 ServiceEntry 도메인을 해결해야 함
- L4에서 라우팅이 올바르게 작동하도록 Virtual IP를 활용함
- 실제 외부로 라우팅은 ServiceEntry를 attach한 Envoy(Istio Waypoint or
Gateway)가 담당합니다.
- e.g. Waypoint envoy listener config
Ztunnel DNS resolve 작동 방식 #
Source code: https://github.com/istio/ztunnel/tree/master/src/dns
-
server.rs
: Main DNS 서버 구현, TCP/UDP socket for DNS-
get_aliases()
// src/dns/server.rs: 227 /// Enumerates the possible aliases for the requested hostname fn get_aliases(&self, client: &Workload, name: &Name) -> Vec<Alias> { let mut out = Vec::new(); let mut added = HashSet::new(); let mut add_alias = |alias: Alias| { if !added.contains(&alias.name) { added.insert(alias.name.clone()); out.push(alias); } }; // Insert the requested name. add_alias(Alias { name: name.clone(), stripped: None, }); let namespaced_domain = append_name(as_name(&client.namespace), &self.svc_domain); // If the name can be expanded to a k8s FQDN, add that as well. for kube_fqdn in self.to_kube_fqdns(name, &namespaced_domain) { add_alias(Alias { name: kube_fqdn, stripped: None, }); }
-
get_addresses()
// src/dns/server.rs: 437 /// Gets the list of addresses of the requested record type from the server. fn get_addresses( &self, client: &Workload, server: &Address, record_type: RecordType, ) -> Vec<IpAddr> { let mut addrs: Vec<IpAddr> = match server { Address::Workload(wl) => wl .workload_ips .iter() .filter_map(|addr| { if is_record_type(addr, record_type) && self.record_type_enabled(addr) { Some(*addr) } else { None } }) .collect(), Address::Service(service) => { if service.vips.is_empty() { // Headless service. Use the endpoint IPs. let workloads = &self.state.read().workloads; service .endpoints .iter() .filter_map(|ep| { let Some(wl) = workloads.find_uid(&ep.workload_uid) else { debug!("failed to fetch workload for {}", ep.workload_uid); return None; }; wl.workload_ips.iter().copied().find(|addr| { is_record_type(addr, record_type) && self.record_type_enabled(addr) }) }) .collect() } else { // "Normal" service with VIPs. // Add service VIPs that are callable from the client. service .vips .iter() .filter_map(|vip| { if is_record_type(&vip.address, record_type) && client.network == vip.network && self.record_type_enabled(&vip.address) { Some(vip.address) } else { None } }) .collect() } } }; // Randomize the order of the returned addresses. addrs.shuffle(&mut rng()); addrs }
-
-
handler.rs
: Main request handler -
resolver.rs
: Rust trait for DNS resolution -
forwarder.rs
: Forward to upstream DNS server -
metrics.rs
: Istio 내 dns 관련 metrics 집계
src/dns/server.rs:62-187] B --> C[DNS Handler
src/dns/handler.rs:32-121] C -->|handle_request| D{Query Type
Supported?} D -->|No| E[Forward to Upstream
src/dns/forwarder.rs:33-121] D -->|Yes A/AAAA| F[Store::lookup
src/dns/server.rs:540-713] F --> G[get_aliases
src/dns/server.rs:227-281] G -->|Expand names| H[find_server
src/dns/server.rs:342-428] H --> I{Service Found?} I -->|No| E I -->|Yes| J{Service Type} J -->|ServiceEntry| K[Return VIP
from XDS cache] J -->|K8s Service| L{Has VIP?} L -->|Yes| M[get_addresses
src/dns/server.rs:437-498] L -->|No Headless| N[Get Endpoint IPs
from Workloads] M --> O[Filter by Network
& Record Type] N --> O K --> O O --> P[Create DNS Records
src/dns/server.rs:673-712] P --> Q[Return Answer
Authoritative=true] E --> R[Upstream DNS
/etc/resolv.conf] R --> S[Cache Response
by TTL] S --> T[Return Answer
Authoritative=false] Q --> U[Pod Receives
DNS Response] T --> U style B fill:#f9f,stroke:#333,stroke-width:4px style F fill:#bbf,stroke:#333,stroke-width:4px style H fill:#bbf,stroke:#333,stroke-width:4px
Gateway API HTTPRoute
에서 ServiceEntry 활용하기 #
Istio Ambient Mesh 환경에서 외부 서비스(External Service)에 안전하게 트래픽을
보내야 할 때 가장 표준적인 방식은
ServiceEntry
리소스를 활용하는 방법입니다.
이 문서는 실제 Istio ambient + Gateway API 기반 트래픽 관리에서, HTTPRoute의 backendRefs를 통해 외부 URL로 트래픽을 리라우팅하는 방법을 중심으로 설명합니다.
특히, Istio 소스코드(Go)에서만 “존재”하는 특수한 backendRefs 값인
kind: Hostname
활용법과, 해당 리소스가 실제로는 Kubernetes CRD가 아니라 Istio
코드베이스 내부에서 별도 분기 처리되는 방식에 소스코드를 살펴보겠습니다.
1. External Service 트래픽 관리 기본 개념 #
Istio는 다음 두 가지 방법 중 하나로 외부 트래픽을 식별/관리합니다.
- Kubernetes
ExternalName
서비스 - Istio
ServiceEntry
(권장 방식)
ServiceEntry
를 사용하면, 외부 서비스의 호스트명·포트·프로토콜 등 트래픽 제어
정책을 Istio가 완전히 인지하고 mesh 내부 서비스처럼 트래픽 정책을 통합적으로
제어할 수 있습니다.
Istio에서는 K8s Service 이외에도 Istio가 인지하고 있는 Istio Service가 별도로
존재합니다. ServiceEntry
는 해당 object에 외부 호스트명 or IP를 등록합니다.
2. Ambient Mesh + Gateway API 환경에서 외부 서비스 연동 #
2.1. HTTPRoute에서 backendRefs로 외부 URL 라우팅하기 #
Ambient 모드에서 트래픽 라우팅의 핵심은 HTTPRoute 리소스다.
다음 예시는 두 가지 HTTPRoute
를 보여준다.
- 내부 서비스로 라우팅 (일반적인 Service Reference)
backendRefs:
- name: httpbin
port: 80
- 외부 도메인(api.channel.io)으로 라우팅 (Hostname Reference + ServiceEntry 필요)
backendRefs:
- group: networking.istio.io
kind: Hostname
name: api.channel.io
port: 443
---
# backendRefs[0].name에 있는 hostname을 `ServiceEntry`로 꼭 등록해야함
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: external-api
spec:
hosts:
- api.channel.io
location: MESH_EXTERNAL
ports:
- number: 443
name: https
protocol: HTTPS
resolution: DNS_ROUND_ROBIN
주의: 여기서
group: networking.istio.io, kind: Hostname
은 Kubernetes CRD에서 정의되는 리소스가 아닙니다. Istio 코드 내부(Go)에서 특수하게 하드코딩되어 있는 타입이며, HTTPRoute가 외부 호스트명을 인식/사용할 수 있게 해준다.
2.2. Istio 코드에서의 backendRefs 동작 방식 #
아래는 pilot/pkg/config/kube/gateway/conversion.go 코드에서 발췌한 일부입니다.
case config.GroupVersionKind{Group: gvk.ServiceEntry.Group, Kind: "Hostname"}:
if to.Namespace != nil {
return nil, nil, &ConfigError{Reason: InvalidDestination, Message: "namespace may not be set with Hostname type"}
}
hostname = string(to.Name)
if ctx.LookupHostname(hostname, namespace) == nil {
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
}
kind: Hostname
이 오면, 단순히.name
필드(즉, 호스트명 문자열)를 그대로 받아서 사용.- 실제로는 ServiceEntry로 등록된 host 인지
ctx.LookupHostname
으로 검증. - 즉, ServiceEntry 리소스가 반드시 먼저 존재해야 backendRefs에 해당 host를 쓸 수 있음.
kind: Service
일 때는 반대로 K8s 서비스 이름을 기반으로 FQDN을 만들어 처리.
참고: 실제 트래픽 흐름
- Gateway/Waypoint → HTTPRoute backendRefs (Hostname) → ServiceEntry host 등록 → 외부 도메인으로 HTTPS 트래픽 요청.
3. 정리 및 권장 사용법 #
- 외부 서비스 호출은 반드시 ServiceEntry를 통해 등록해야 합니다.
- HTTPRoute의
backendRefs
에서 외부 도메인 사용 시, 반드시group: networking.istio.io
,kind: Hostname
사용. 이때.name
에 ServiceEntry에 등록한 host와 정확히 동일해야 합니다. kind: Hostname
은 실제 K8s CRD가 아니라 Istio 코드에서 하드코딩된 타입임을 주의.- k8s 서비스(
kind: Service
)와는 처리 방식이 다릅니다.