DNS 프록시
DNS 프록시 (DNS Proxying)
Istio는 애플리케이션 트래픽을 캡처하는 것 외에도 DNS 요청을 캡처해서 메시의 성능과 사용성을 개선할 수 있어요. 이 페이지에서는 DNS 프록시를 활성화하고, ServiceEntry 주소 해석과 주소 자동 할당을 구성하는 방법을 배워요.
출처: Istio 문서
본문
Istio는 애플리케이션 트래픽을 캡처하는 것 외에도 DNS 요청을 캡처해서 메시의 성능과 사용성을 개선할 수 있어요. DNS를 프록시할 때 애플리케이션의 모든 DNS 요청은 도메인 이름과 IP 주소의 로컬 매핑을 저장하는 sidecar 또는 ztunnel 프록시로 리디렉션돼요. 요청을 프록시가 처리할 수 있으면 애플리케이션에 응답을 직접 반환해서 업스트림 DNS 서버로의 왕복을 피해요. 그렇지 않으면 요청은 표준 /etc/resolv.conf DNS 구성을 따라 업스트림으로 전달돼요.
쿠버네티스가 쿠버네티스 Service에 대한 DNS 해석을 기본으로 제공하지만, 커스텀 ServiceEntry는 인식되지 않아요. 이 기능을 사용하면 DNS 서버의 커스텀 구성 없이도 ServiceEntry 주소를 해석할 수 있어요. 쿠버네티스 Service의 경우 DNS 응답은 동일하지만 kube-dns에 대한 부하가 줄고 성능이 향상돼요.
이 기능은 쿠버네티스 외부에서 실행되는 서비스에도 사용할 수 있어요. 이는 쿠버네티스 DNS 항목을 클러스터 외부로 노출하는 번거로운 임시방편 없이 모든 내부 서비스를 해석할 수 있다는 뜻이에요.
시작하기
Istio는 일반적으로 HTTP 헤더를 기반으로 트래픽을 라우팅해요. HTTP 헤더 기반 라우팅이 불가능한 경우(ambient 모드, 또는 sidecar 모드의 TCP 트래픽) DNS 프록시를 활성화할 수 있어요.
ambient 모드에서 ztunnel은 4계층(Layer 4)에서만 트래픽을 보고 HTTP 헤더에 접근할 수 없어요. 따라서 특히 egress 트래픽을 waypoint로 보내는 경우 ServiceEntry 주소의 해석을 활성화하려면 DNS 프록시가 필요해요.
Ambient 모드
Istio 1.25부터 ambient 모드에서 DNS 프록시는 기본으로 활성화돼요.
1.25 이전 버전에서는 설치 시 values.cni.ambient.dnsCapture=true와 values.pilot.env.PILOT_ENABLE_IP_AUTOALLOCATE=true를 설정해 DNS 캡처를 활성화할 수 있어요.
개별 파드는 ambient.istio.io/dns-capture=false 어노테이션을 적용해 전역 ambient 모드 DNS 캡처를 거부(opt-out)할 수 있어요.
Sidecar 모드
이 기능은 현재 기본으로 활성화되어 있지 않아요. 활성화하려면 다음 설정으로 Istio를 설치하세요.
$ cat <<EOF | istioctl install -y -f -
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
defaultConfig:
proxyMetadata:
# Enable basic DNS proxying
ISTIO_META_DNS_CAPTURE: "true"
EOF
이는 proxy.istio.io/config 어노테이션으로 파드별로도 활성화할 수 있어요.
kind: Deployment
metadata:
name: curl
spec:
...
template:
metadata:
annotations:
proxy.istio.io/config: |
proxyMetadata:
ISTIO_META_DNS_CAPTURE: "true"
...
DNS 캡처 작동 확인
DNS 캡처를 시험하려면 먼저 일부 외부 서비스에 대한 ServiceEntry를 설정하세요.
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
name: external-address
spec:
addresses:
- 198.51.100.1
hosts:
- address.internal
ports:
- name: http
number: 80
protocol: HTTP
EOF
DNS 요청을 시작할 클라이언트 애플리케이션을 띄우세요.
$ kubectl label namespace default istio-injection=enabled --overwrite
$ kubectl apply -f @samples/curl/curl.yaml@
DNS 캡처가 없으면 address.internal에 대한 요청은 해석에 실패할 가능성이 높아요. 이를 활성화하면 구성된 address를 기반으로 한 응답을 대신 받게 돼요.
$ kubectl exec deploy/curl -- curl -sS -v address.internal
* Trying 198.51.100.1:80...
주소 자동 할당
위 예제에서는 요청을 보낸 서비스에 대해 미리 정의된 IP 주소가 있었어요. 하지만 안정적인 주소가 없고 대신 DNS에 의존하는 외부 서비스에 접근하는 것은 흔한 일이에요. 이 경우 DNS 프록시는 응답을 반환할 충분한 정보가 없으므로 DNS 요청을 업스트림으로 전달해야 해요.
이는 TCP 트래픽에서 특히 문제가 돼요. Host 헤더를 기반으로 라우팅되는 HTTP 요청과 달리 TCP는 훨씬 적은 정보를 담아요. 목적지 IP와 포트 번호로만 라우팅할 수 있어요. 백엔드의 안정적인 IP가 없으므로 그 기준으로도 라우팅할 수 없고, 포트 번호만 남게 되는데, 이는 여러 TCP 서비스의 ServiceEntry가 같은 포트를 공유할 때 충돌로 이어져요. 자세한 내용은 다음 섹션을 참고하세요.
이 문제들을 해결하기 위해 DNS 프록시는 추가로 명시적으로 주소를 정의하지 않은 ServiceEntry에 대해 주소를 자동으로 할당하는 것을 지원해요. DNS 응답에는 각 ServiceEntry에 대해 구별되는 자동 할당 주소가 포함돼요. 그러면 프록시는 이 IP 주소에 요청을 매칭하고 요청을 해당 ServiceEntry로 전달하도록 구성돼요. Istio는 와일드카드 호스트를 사용하지 않는 한 이런 서비스에 라우팅 불가능한 VIP(Class E 서브넷에서)를 자동으로 할당해요. sidecar의 Istio agent는 애플리케이션의 DNS 조회 쿼리에 대한 응답으로 VIP를 사용해요. 이제 Envoy는 각 외부 TCP 서비스로 향하는 트래픽을 명확히 구분하고 올바른 대상으로 전달할 수 있어요.
이를 시험하려면 다른 ServiceEntry를 구성하세요.
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
name: external-auto
spec:
hosts:
- auto.internal
ports:
- name: http
number: 80
protocol: HTTP
resolution: DNS
EOF
이제 요청을 보내세요.
$ kubectl exec deploy/curl -- curl -sS -v auto.internal
* Trying 240.240.0.1:80...
보시다시피 요청은 자동 할당된 주소인 240.240.0.1로 전송돼요. 이 주소들은 실제 서비스와의 충돌을 피하기 위해 예약된 240.240.0.0/16 IP 주소 범위에서 선택돼요.
사용자는 ServiceEntry에 networking.istio.io/enable-autoallocate-ip="true/false" 라벨을 추가해 더 세밀한 구성의 유연성을 가질 수도 있어요. 이 라벨은 spec.addresses가 설정되지 않은 ServiceEntry에 IP 주소가 자동으로 할당되어야 하는지 여부를 구성해요.
이를 시험하려면 기존 ServiceEntry를 opt-out 라벨로 업데이트하세요.
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
name: external-auto
labels:
networking.istio.io/enable-autoallocate-ip: "false"
spec:
hosts:
- auto.internal
ports:
- name: http
number: 80
protocol: HTTP
resolution: DNS
EOF
이제 요청을 보내고 자동 할당이 더 이상 일어나지 않는지 확인하세요.
$ kubectl exec deploy/curl -- curl -sS -v auto.internal
* Could not resolve host: auto.internal
* Store negative name resolve for auto.internal:80
* shutting down connection #0
VIP가 없는 외부 TCP 서비스
기본적으로 Istio는 같은 포트의 여러 TCP 서비스를 구분할 수 없기 때문에 외부 TCP 트래픽을 라우팅할 때 제한이 있어요. 이 제한은 AWS Relational Database Service 같은 타사 데이터베이스나 지리적 중복이 있는 데이터베이스 설정을 사용할 때 특히 두드러져요. 유사하지만 다른 외부 TCP 서비스는 기본적으로 별도로 처리될 수 없어요. sidecar가 메시 외부에 있는 두 개의 서로 다른 TCP 서비스의 트래픽을 구분하려면 서비스가 서로 다른 포트에 있거나 전역적으로 고유한 VIP를 가져야 해요.
예를 들어 mysql-instance1과 mysql-instance2라는 두 개의 외부 데이터베이스 서비스가 있고 둘 다 service entry를 만들었다면, 클라이언트 sidecar는 여전히 0.0.0.0:{port}에 단일 리스너를 가지며, 공개 DNS 서버에서 mysql-instance1의 IP 주소만 조회해서 그 주소로 트래픽을 전달해요. 0.0.0.0:{port}에 도착하는 트래픽이 mysql-instance1로 가는지 mysql-instance2로 가는지 구분할 방법이 없기 때문에 mysql-instance2로 트래픽을 라우팅할 수 없어요.
다음 예제는 DNS 프록시를 사용해 이 문제를 해결하는 방법을 보여줘요. 각 서비스 항목에 가상 IP 주소가 할당되어 클라이언트 sidecar가 각 외부 TCP 서비스로 향하는 트래픽을 명확히 구분할 수 있게 돼요.
- Getting Started 섹션에 지정된 Istio 구성을 업데이트해서, 메시를 istio-injection이 활성화된 네임스페이스로 제한하는 discoverySelectors도 구성하세요. 이렇게 하면 클러스터의 다른 네임스페이스를 사용해 메시 외부의 TCP 서비스를 실행할 수 있게 돼요.
$ cat <<EOF | istioctl install -y -f -
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
defaultConfig:
proxyMetadata:
# Enable basic DNS proxying
ISTIO_META_DNS_CAPTURE: "true"
# discoverySelectors configuration below is just used for simulating the external service TCP scenario,
# so that we do not have to use an external site for testing.
discoverySelectors:
- matchLabels:
istio-injection: enabled
EOF
- 첫 번째 외부 샘플 TCP 애플리케이션을 배포하세요.
$ kubectl create ns external-1
$ kubectl -n external-1 apply -f samples/tcp-echo/tcp-echo.yaml
- 두 번째 외부 샘플 TCP 애플리케이션을 배포하세요.
$ kubectl create ns external-2
$ kubectl -n external-2 apply -f samples/tcp-echo/tcp-echo.yaml
- 외부 서비스에 도달하도록 ServiceEntry를 구성하세요.
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
name: external-svc-1
spec:
hosts:
- tcp-echo.external-1.svc.cluster.local
ports:
- name: external-svc-1
number: 9000
protocol: TCP
resolution: DNS
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
name: external-svc-2
spec:
hosts:
- tcp-echo.external-2.svc.cluster.local
ports:
- name: external-svc-2
number: 9000
protocol: TCP
resolution: DNS
EOF
- 클라이언트 쪽에서 각 서비스에 대해 리스너가 별도로 구성됐는지 확인하세요.
$ istioctl pc listener deploy/curl | grep tcp-echo | awk '{printf "ADDRESS=%s, DESTINATION=%s %s\n", $1, $4, $5}'
ADDRESS=240.240.105.94, DESTINATION=Cluster: outbound|9000||tcp-echo.external-2.svc.cluster.local
ADDRESS=240.240.69.138, DESTINATION=Cluster: outbound|9000||tcp-echo.external-1.svc.cluster.local
정리
$ kubectl -n external-1 delete -f @samples/tcp-echo/tcp-echo.yaml@
$ kubectl -n external-2 delete -f @samples/tcp-echo/tcp-echo.yaml@
$ kubectl delete -f @samples/curl/curl.yaml@
$ istioctl uninstall --purge -y
$ kubectl delete ns istio-system external-1 external-2
$ kubectl label namespace default istio-injection-