istio deep dive: Sidecar Traffic Intercepting & Routing Process
개요
아래 그림은 istio 공식 페이지에서 제공하고 있는 bookinfo 데모 프로젝트 중 reviews.default.svc.cluster.local로 요청을 보낼 때 내부 사이드카 프록시가 트래픽을 인터셉트하고 라우팅하는 흐름, ratings로 향하는 아웃바운드 트래픽의 흐름을 보여준다.
이 글에서는 envoy 프록시가 사이드카로 포함되어 있는 pod에 대해 인바운드 트래픽과 아웃바운드 트래픽이 어떻게 인터셉트 되고 처리 되는지 알아본다.
Iptable 분석
# minikube login for root
$ minikube ssh
docker@minikube:~$ sudo -i
# productpage pod의 프로세스 확인
root@minikube:~# docker top `docker ps|grep "istio-proxy_productpage"|cut -d " " -f1`
UID PID PPID C STIME TTY TIME CMD
1337 346524 346435 0 07:36 ? 00:00:00 /usr/local/bin/pilot-agent proxy sidecar --domain default.svc.cluster.local --proxyLogLevel=warning --proxyComponentLogLevel=misc:error --log_output_level=default:info
1337 346665 346524 0 07:36 ? 00:00:00 /usr/local/bin/envoy -c etc/istio/proxy/envoy-rev.json --drain-time-s 45 --drain-strategy immediate --local-address-ip-version v4 --file-flush-interval-msec 1000 --disable-hot-restart --allow-unknown-static-fields --log-format %Y-%m-%dT%T.%fZ?%l?envoy %n %g:%#?%v?thread=%t -l warning --component-log-level misc:error --concurrency 2
# 사이드카 컨테이너의 네임스페이스로 접속 (위의 어떤 PID로도 가능)
root@minikube:~# nsenter -n --target 346524
# NAT 테이블 확인
root@minikube:~# iptables -t nat -L -v
# PREROUTING chain: 수신되는 모든 TCP 트래픽을 ISTIO_INBOUD 체인으로 점프하기 위해 대상 주소 변환에 사용
Chain PREROUTING (policy ACCEPT 58627 packets, 3518K bytes)
pkts bytes target prot opt in out source destination
58627 3518K ISTIO_INBOUND tcp -- any any anywhere anywhere
# INPUT chain: 들어오는 패킷과 non-TCP 트래픽을 처리하며 OUTPUT chain에서 계속됨
Chain INPUT (policy ACCEPT 58627 packets, 3518K bytes)
pkts bytes target prot opt in out source destination
# OUTPUT chain: 모든 나가는 패킷을 ISTIO_OUTPUT chain으로 넘김
Chain OUTPUT (policy ACCEPT 4880 packets, 417K bytes)
pkts bytes target prot opt in out source destination
594 35640 ISTIO_OUTPUT tcp -- any any anywhere anywhere
# POSTROUTING chain: 모든 패킷은 네트워크 카드를 떠날 때 먼저 POSTROUTING chain에 들어가야 하며, 커널은 패킷 대상에 따라 패킷을 전달할 지 여부를 결정
Chain POSTROUTING (policy ACCEPT 4880 packets, 417K bytes)
pkts bytes target prot opt in out source destination
# ISTIO_INBOUND chain: 15008, 15090, 15021, 15020 포트를 제외한 모든 인바운드 트래픽을 ISTIO_IN_REDIRECT chain으로 리디렉션. 위 포트로 전송되는 트래픽은 INPUT chain으로 전달
Chain ISTIO_INBOUND (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN tcp -- any any anywhere anywhere tcp dpt:15008
0 0 RETURN tcp -- any any anywhere anywhere tcp dpt:15090
58165 3490K RETURN tcp -- any any anywhere anywhere tcp dpt:15021
462 27720 RETURN tcp -- any any anywhere anywhere tcp dpt:15020
0 0 ISTIO_IN_REDIRECT tcp -- any any anywhere anywhere
# ISTIO_IN_REDIRECT chain: 모든 인바운드 트래픽을 local 15006 포트로 전달하여 사이드카로의 트래픽 차단
Chain ISTIO_IN_REDIRECT (3 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 15006
# ISTIO_OUTPUT chain
Chain ISTIO_OUTPUT (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- any lo 127.0.0.6 anywhere
0 0 ISTIO_IN_REDIRECT tcp -- any lo anywhere !localhost tcp dpt:!15008 owner UID match 1337
7 420 RETURN all -- any lo anywhere anywhere ! owner UID match 1337
587 35220 RETURN all -- any any anywhere anywhere owner UID match 1337
0 0 ISTIO_IN_REDIRECT tcp -- any lo anywhere !localhost tcp dpt:!15008 owner GID match 1337
0 0 RETURN all -- any lo anywhere anywhere ! owner GID match 1337
0 0 RETURN all -- any any anywhere anywhere owner GID match 1337
0 0 RETURN all -- any any anywhere localhost
0 0 ISTIO_REDIRECT all -- any any anywhere anywhere
# ISTIO_REDIRECT chain: 모든 트래픽을 사이드카 포트 15001로 리디렉션
Chain ISTIO_REDIRECT (1 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 15001
위의 chain들 중 ISTIO_OUTPUT chain을 표와 다이어그램으로 만들면 아래와 같다.
Rule | target | in | out | source | destination |
1 | RETURN | any | lo | 127.0.0.6 | anywhere |
2 | ISTIO_IN_REDIRECT | any | lo | anywhere | !localhost owner UID match 1337 |
3 | RETURN | any | lo | anywhere | anywhere !owner UID match 1337 |
4 | RETURN | any | any | anywhere | anywhere owner UID match 1337 |
5 | ISTIO_IN_REDIRECT | any | lo | anywhere | !localhost owner GID match 1337 |
6 | RETURN | any | lo | anywhere | anywhere !owner GID match 1337 |
7 | RETURN | any | any | anywhere | anywhere owner GID match 1337 |
8 | RETURN | any | any | anywhere | localhost |
9 | ISTIO_REDIRECT | any | any | anywhere | anywhere |
규칙 5, 6, 7은 각각 규칙 2, 3, 4를 확장한 것으로 유사한 목적으로 갖기 때문에 함께 설명한다.
위 규칙들은 순서대로 평가되며 out이 lo인 경우 트래픽의 대상이 로컬 pod임을 뜻한다. 즉, 외부로 전송되는 트래픽은 4, 7, 8, 9 규칙만 적용된다.
RETURN target
RETURN target은 해당 규칙이 지정될 때, 규칙 chain을 점프하고 iptables의 호출지점(OUTPUT)으로 돌아가 나머지 라우팅 규칙(POSTROUTING; 임의의 목적지 주소로 트래픽을 보내는 규칙, 직관적으로는 통과라고 이해하면 됨)을 계속 실행한다.
127.0.0.6 IP 주소
IP 127.0.0.6은 istio의 InboundPassthroughClusterIPv4 기본 값이며, 트래픽은 envoy 프록시에 진입한 후 이 IP로 바인딩된다.
아웃바운드 트래픽이 아웃바운드 핸들러를 통과하지 않고 pod의 애플리케이션 컨테이너로 전송될 수 있도록 하는 역할을 한다.
이 트래픽은 실제 아웃바운드 트래픽이 아니라 자신의 pod에 대한 엑세스이다.
ISTIO_OUTPUT chain
ISTIO_OUTPUT chain의 규칙들을 아래 표에서 정리해 보았다.
규칙 이름 | 목적 | Bookinfo 트래픽 흐름도 상 위치 | 세부 정보 |
rule1 | Envoy 프록시가 보낸 트래픽을 로컬 애플리케이션 컨테이너로 전달해 envoy 프록시 우회 | 6~7단계 | • 127.0.0.6의 모든 요청이 체인을 통하지 않고 iptables의 호출지점(즉, OUTPUT)으로 전달돼 local pod 내의 컨테이너 등 임의의 대상으로 트래픽을 전송하는 POSTROUTING 규칙을 수행 • 이 규칙이 없을 경우 규칙 2를 실행하고 트래픽은 인바운드 핸들러로 다시 진입하여 데드 루프가 생성됨 |
rule 2, 5 | • Envoy 프록시로부터의 인바운드 트래픽을 처리하지만, 로컬 호스트로의 요청은 처리하지 않음 • 후속 규칙을 통해 envoy 프록시의 인바운드 핸들러에 전달 • 이 규칙은 pod 내부의 서비스들끼리 통신하는 경우 적용 |
- | 트래픽 대상이 localhost가 아니고 1337 UID(i.e. istio-proxy 유저, envoy 프록시)로부터 온 패킷일 경우 ISTIO_IN_REDIRECT chain을 통해 envoy 프록시의 인바운드 핸들러로 포워딩 됨 |
rule 3, 6 | • pod 내부의 애플리케이션 컨테이너의 내부 트래픽이 통과하는 규칙 • 이 규칙은 컨테이너 내의 트래픽(pod 내에서 pod’s ip 또는 localhost에 대한 엑세스)에 적용 |
6~7단계 | 트래픽이 envoy 유저로부터 온 것이 아닐 경우, chain을 건너뛰고 OUTPUT을 거쳐 POSTROUTING을 통해 목적지로 바로 전달 |
rule 4,7 | envoy 프록시로부터 온 아웃바운드 요청이 통과하는 규칙 | 14~15단계 | 트래픽이 envoy 유저로부터 만들어진 것일 경우, OUTPUT을 거쳐 POSTROUTING을 통해 목적지로 바로 전달 |
rule 8 | pod에서 localhost로의 트래픽이 통과하는 규칙 | - | 요청의 목적지가 localhost일 경우, OUTPUT을 거쳐 POSTROUTING을 통해 localhost로 바로 전달 |
rule 9 | 다른 모든 트래핏은 최종적으로 envoy 프록시의 아웃바운드 핸들러에 도달한 후 ISTIO_REDIRECT에 포워딩 | 10~11단계 | - |
트래픽 라우팅 프로세스
인바운드 핸들러
인바운드 핸들러는 iptable에 의해 차단된 다운 스트림의 트래픽을 Localhost로 전달하고, pod 내의 애플리케이션 컨테이너에 대한 연결을 설정하는 역할을 한다.
pod 이름이 reviews-v1-86896b7648-sp59z 라고 가정할 때 istioctl proxy-config listener reviews-v1-86896b7648-sp59z --port 15006을 실행하면 아래와 같은 결과가 나온다.
ADDRESSES PORT MATCH DESTINATION
0.0.0.0 15006 Addr: *:15006 Non-HTTP/Non-TCP
0.0.0.0 15006 Trans: tls; App: istio-http/1.0,istio-http/1.1,istio-h2; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4
0.0.0.0 15006 Trans: raw_buffer; App: http/1.1,h2c; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4
0.0.0.0 15006 Trans: tls; App: TCP TLS; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4
0.0.0.0 15006 Trans: raw_buffer; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4
0.0.0.0 15006 Trans: tls; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4
0.0.0.0 15006 Trans: tls; App: istio,istio-peer-exchange,istio-http/1.0,istio-http/1.1,istio-h2; Addr: *:9080 Cluster: inbound|9080||
0.0.0.0 15006 Trans: raw_buffer; Addr: *:9080 Cluster: inbound|9080||
- address: downstream 주소
- port: envoy listener가 수신 대기하는 포트
- match: 요청에 사용되는 전송 프로토콜 또는 일치하는 downstream 주소
- destination: 라우팅 목적지
위 결과를 통해 알 수 있는 것은 다음과 같다.
- pod의 iptable은 envoy 프록시의 인바운드 핸들러가 트래픽을 수신할 수 있도록 트래픽을 15006 포트로 리디렉션
- 인바운드 리스너에는 특정 규칙이 있어서, 9080 포트로 가는 트래픽은 특정 클러스터(inbound|9080||)로 라우팅됨
- 0.0.0.0:15006/TCP에 대한 리스너인 virtualInbound는 match 규칙이 포함된 트래픽 또는 9080 포트에 대한 모든 인바운드 트래픽을 수신
위 내용을 json 형식으로 더 자세히 보려면 istioctl proxy-config listeners [pod 이름] --port 15006 -o json을 실행하면 된다.
[
/*omit*/
{
"name": "virtualInbound",
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 15006
}
},
"filterChains": [
/*omit*/
{
"filterChainMatch": {
"destinationPort": 9080,
"transportProtocol": "tls",
"applicationProtocols": [
"istio",
"istio-peer-exchange",
"istio-http/1.0",
"istio-http/1.1",
"istio-h2"
]
},
"filters": [
/*omit*/
{
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"statPrefix": "inbound_0.0.0.0_9080",
"routeConfig": {
"name": "inbound|9080||",
"virtualHosts": [
{
"name": "inbound|http|9080",
"domains": [
"*"
],
"routes": [
{
"name": "default",
"match": {
"prefix": "/"
},
"route": {
"cluster": "inbound|9080||",
"timeout": "0s",
"maxStreamDuration": {
"maxStreamDuration": "0s",
"grpcTimeoutHeaderMax": "0s"
}
},
"decorator": {
"operation": "reviews.default.svc.cluster.local:9080/*"
}
}
]
}
],
"validateClusters": false
},
/*omit*/
}
}
],
/*omit*/
],
"listenerFilters": [
/*omit*/
],
"listenerFiltersTimeout": "0s",
"continueOnListenerFiltersTimeout": true,
"trafficDirection": "INBOUND"
}
]
인바운드 핸들러는 9080 포트에 대한 트래픽을 inbound|9080|| 클러스터로 라우팅하기 때문에 istioctl pc cluster reviews-v1-86896b7648-sp59z --port 9080 --direction inbound -o json를 실행하면 클러스터 구성을 확인할 수 있다.
[
{
"name": "inbound|9080||",
"type": "ORIGINAL_DST",
"connectTimeout": "10s",
"lbPolicy": "CLUSTER_PROVIDED",
"circuitBreakers": {
"thresholds": [
{
"maxConnections": 4294967295,
"maxPendingRequests": 4294967295,
"maxRequests": 4294967295,
"maxRetries": 4294967295,
"trackRemaining": true
}
]
},
"upstreamBindConfig": {
"sourceAddress": {
"address": "127.0.0.6",
"portValue": 0
}
},
"commonLbConfig": {},
"metadata": {
"filterMetadata": {
"istio": {
"services": [
{
"host": "reviews.default.svc.cluster.local",
"name": "reviews",
"namespace": "default"
}
]
}
}
}
}
]
- ORGINAL_DST 타입을 볼 수 있는데, 이는 트래픽을 원래 대상 주소(pod ip)로 전달하는 타입이다.
- 원래 대상 주소는 현재 pod이므로 upstreamBindingConfig.sourceAddress.address는 127.0.0.6으로 설정된다.
- 127.0.0.6에 대한 트래픽은 iptables의 ISTIO_OUTPUT chain의 규칙 1을 적용받는다.
아웃바운드 핸들러
reviews 서비스에서는 http://ratings.default.svc.cluster.local:9080/에 위치한 ratings 서비스로 HTTP 요청을 보내는데, 이때 Envoy 프록시의 아웃바운드 핸들러가 등장한다.
애플리케이션 컨테이너에서 나가는 요청은 iptables에 의해 가로채여 아웃바운드 핸들러로 전달된다. 이 핸들러를 통과한 후 가상 아웃바운드 리스너, 0.0.0.0_9080 리스너를 거쳐 Route 9080을 통해 상위 클러스터를 찾게 되는데, 이 과정에서 EDS(Endpoint Discovery Service)를 참고하여 엔드포인트를 찾아 라우팅 작업을 수행한다.
istioctl proxy-config routes reviews-v1-86896b7648-sp59z --name 9080 -o json으로 라우팅 구성을 확인할 수 있다.
[{
...
{
"name": "ratings.default.svc.cluster.local:9080",
"domains": [
"ratings.default.svc.cluster.local",
"ratings",
"ratings.default.svc",
"ratings.default",
"10.98.49.62"
],
"routes": [
{
"name": "default",
"match": {
"prefix": "/"
},
"route": {
"cluster": "outbound|9080||ratings.default.svc.cluster.local",
"timeout": "0s",
"retryPolicy": {
"retryOn": "connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes",
"numRetries": 2,
"retryHostPredicate": [
{
"name": "envoy.retry_host_predicates.previous_hosts",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate"
}
}
],
"hostSelectionRetryMaxAttempts": "5",
"retriableStatusCodes": [
503
]
},
"maxGrpcTimeout": "0s"
},
"decorator": {
"operation": "ratings.default.svc.cluster.local:9080/*"
}
}
],
"includeRequestAttemptCount": true
},
...
]
Reference