brunch

You can make anything
by writing

C.S.Lewis

by 이권수 Jul 21. 2023

istio proxy의 작동원리

istio sidecar proxy의 화려한 패킷 가로채기

istio는 쿠버네티스 클러스터 내에서 네트워크 흐름을 통제하는 오픈소스이다. istio는 Envoy 프록시를 사이드카(sidecar) 형태로 배포하여 트래픽을 관리한다. 프록시 컨테이너는 iptable을 조작하여 네트워크 패킷을 중간에 가로챈다. 가로채는 과정은 로그로 확인하기 어려워서, istio 사용 시 트래픽 흐름을 이해하지 못하는 경우가 많다.


이번 글에서는 istio가 내부적으로 어떤 과정을 통해 패킷을 라우팅 하는지 알아보고자 한다.



사이드카 주입과 init 컨테이너

쿠버네티스 네임스페이스에 istio-injection=enabled 라벨을 붙이면 webhook을 통해 istiod에 /inject API를 호출한다. istiod는 파드에 프록시 컨테이너와 init컨테이너를 주입한다. init 컨테이너의 이름은 istio-init이다.

init 컨테이너는 서비스 컨테이너가 생성되기 전에 먼저 작업을 수행한다. 작업의 내용은 istio-iptables 명령어이다. istio-iptables는 네트워크 트래픽을 제어하기 위한 iptable 작업을 수행한다.


istio-iptables 명령어 뒤에 붙은 플래그의 의미는 다음과 같다.


-u: Redirect 하지 않을 UID. 보통 프록시의 UID를 지정한다.

-p: 모든 TCP 트래픽을 Redirect 할 Envoy 포트

-z: 모든 Inbound 트래픽을 Redirect 할 포트

-b: Redirect 할 Inbound Port

-d: Envoy로 Redirect 하지 않을 Inbound Port. 

-i: Redirect 할 IP Range

-x: Envoy로 Redirect 하지 않을 IP Range


위 설명대로 명령어를 실행하면 istio-iptables는 파드의 리눅스 네임스페이스 상에 iptable 규칙을 추가한다. 해당 iptable은 파드 내에서 동작하는데, 결론적으로 프록시가 트래픽을 가로채기 위한 설정이다.


위의 설명을 프록시 기준으로 해석해 보면 다음과 같다.

-p 15001: 프록시는 15001번 포트로 TCP 트래픽을 수신한다.

-z 15006: 프록시는 15006번 포트로 TCP 인바운드 트래픽을 수신한다.

-u 1337: UID 1337번은 프록시로 라우팅 하지 않는다. 

-i * - x : 모든 IP Range에 해당하는 트래픽을 라우팅 한다.

-b * -d 15090,15021,15020: 15090, 15021,15020 포트를 제외하고 모든 포트에 해당하는 트래픽을 라우팅 한다.


정리하면, 

프록시는 15006번 포트로 인바운드 트래픽을, 15001번 포트로 아웃바운드 트래픽을 수신한다. 

프록시는 1337 UID를 사용하고, 프록시의 패킷은 프록시로 라우팅 하지 않는다. 프록시 -> 프록시로 패킷이 라우팅 되면 무한루프에 빠질 수 있다.

프록시는 15090,15021,15020 포트를 목적지로 한 패킷을 제외한 모든 트래픽을 가로챈다.


이렇게 init 컨테이너를 통해 iptable에 규칙을 추가한 후에, istio-proxy 컨테이너를 생성한다. istio-proxy는 init 컨테이너가 라우팅에서 제외시켜 준 1337번 UID를 사용한다. 프록시가 주입된 파드의 템플릿을 보면 다음과 같이 runAsUser값이 1337 임을 확인할 수 있다.


iptable 이해하기

istio는 iptable을 조작하여 프록시로 트래픽을 전달한다. 여기서 iptable은 파드의 네임스페이스 내 iptable을 의미한다. 각 파드는 독립된 프로세스이고, 파드별로 별도의 리눅스 네임스페이스를 가진다. 각 파드 내에서 프록시는 15001, 15006번 포트를 사용하고, 자신의 리눅스 네임스페이스에 정의된 iptable 규칙에 따라 패킷을 라우팅 한다.


특정 파드의 네임스페이스에서 init 컨테이너가 생성한 iptable을 살펴보면 다음과 같다. 



아래 그림을 통해서 이해해 보도록 하자.

출처: jimmysong.io


트래픽이 외부에서 애플리케이션 컨테이너로 들어오는 경우(Port 9080)

1. 트래픽은 PREROUTING을 거쳐 ISTIO_INBOUND로 넘어간다. 왜냐하면 PREROUTING에서는 모든 트래픽을 ISTIO_INBOUND 타깃으로 전달하기 때문이다.


2. ISTIO_INBOUND에서 패킷은 ISTIO_IN_REDIRECT으로 넘어간다. 왜냐하면 "dpt:포트"로 지정된 규칙에 9080은 걸리지 않기 때문이다. 참고로 15008, 15090, 15021, 15020 은 모두 istio에서 공식적으로 사용하는 포트이다.


3. ISTIO_IN_REDIRECT에서는 모든 패킷을 15006번 포트로 redirect 한다. 15006은 init 컨테이너가 지정한 프록시 포트이다. 프록시는 15006번 포트를 통해 모든 inbound 트래픽은 수신한다.


4. 15006번 포트를 통해 패킷을 전달받은 프록시는 아래의 규칙을 통해 트래픽을 전달한다. 복잡해 보이지만, 맨 밑에 Addr: *:9080 만 보면 된다. 이 말은 9080번 포트로 가는 트래픽은 Cluster: inbound|9080||으로 보낸다는 의미이다. 

그러면 Cluster: inbound|9080|| 은 무엇을 의미할까?

istioctl의 cluster 설정을 보면 다음과 같이 inbound|9080||의 상세 내용을 볼 수 있다. 여기서 ORIGINAL_DST란 원래 목적지인 파드 IP를 의미한다. 즉, 프록시는 해당 트래픽을 파드의 9080번 포트로 트래픽을 보낸다. 

여기서 한 가지 주목해야 할 사실은 upstreamBindConfig.sourceAddress이다. upstream으로 트래픽을 전송할 때 source 주소로 127.0.0.6을 지정하고 있다. 즉, 이제는 출발지가 127.0.0.6이 되는 셈이다. 이는 istio에서 지정한 InboundPassthrough IP 대역이다. 이 주소는 다음 라우팅 규칙에서 아주 중요한 역할을 하므로 반드시 기억하기 바란다.


5. 프록시는 트래픽을 파드의 9080 포트로 전달한다. 패킷은 OUTPUT을 통해 외부로 나갈 준비를 한다.


6. OUTPUT은 모든 트래픽을 ISTIO_OUTPUT으로 전달한다.


7. ISTIO_OUTPUT은 source가 127.0.0.6인 규칙을 적용하여 lo(localhost) 인터페이스로 전달한다. 


8. 패킷은 POSTROUTING을 통해 애플리케이션 컨테이너로 전달된다.


트래픽이 애플리케이션 컨테이너에서 외부로 나가는 경우

9. 애플리케이션 컨테이너는 OUTPUT을 통해 트래픽을 외부로 내보낸다.


10. OUTPUT은 모든 트래픽을 ISTIO_OUTPUT으로 전달한다.


11. ISTIO_OUTPUT의 규칙 중에 해당되는 게 없기 때문에, 마지막에 나온 ISTIO_REDIRECT로 향한다. 

참고로 UID/GID 1337번은 프록시로부터 트래픽이 나왔다는 의미이다.


12. ISTIO_REDRECT는 15001번 포트로 패킷을 전달한다. 즉, 지금까지 애플리케이션 컨테이너에서 프록시 컨테이너로 패킷이 전달된 셈이다.


13. 프록시는 OUTPUT을 통해 패킷을 외부로 내보낼 준비를 합니다.


14. OUTPUT은 모든 트래픽을 ISTIO_OUTPUT으로 전달한다.


15. 지금은 프록시가 내보낸 패킷이기 때문에 owner UID가 1337번이 된다. 따라서 다시 프록시로 돌아가지 않고, 바로 외부로 나간다.


지금까지 트래픽이 파드 외부에서 내부로, 내부에서 외부로 전달되는 과정을 살펴보았다. 


여기서 꼭 짚고 넘어가야 할 부분은 iptable을 거치는 순간 owner UID/GID가 1337인 경우이다. 이는 프록시가 트래픽을 내보냈다는 의미이므로, 다시 프록시의 15006번 포트로 들어가지 않는다. 이 부분이 없다면 트래픽은 영원히 제자리를 맴돌 것이다.


앞서 설명한 케이스를 포함해 더욱 다양한 경우들은 아래의 페이지에서 다루고 있으니 참고하기 바란다.



마무리하며

지금까지 istio의 프록시가 패킷을 다루는 방법에 대해서 알아보았다. 위의 과정을 보면 느끼겠지만, ip table은 조건에 따라 체인으로 연결되어 다음 목적지가 결정된다. 또한 istio는 무한 루프를 방지하기 위해 1337 UID/GID를 사용한다. 이러한 원리를 이해하고 istio를 사용하면 추후 네트워크 트러블슈팅할 때 도움이 될 것이라 생각한다. 


이번에 istio에 대해서 공부하면서 아래의 문서들을 참조했다. 필자가 설명한 내용보다 더 상세한 분석을 담은 문서들이다. 시간이 된다면 꼭 한 번 읽어볼 것을 추천한다.


매거진의 이전글 Ansible 제대로 사용하기
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari