본문 바로가기

IT

쿠버네티스 파드 네트워킹 정리

728x90

시간이 지날수록 파드 네트워킹에 대한 개념이 흔들리는 것 같아 현재 내가 생각하고 있는 파드 네트워킹에 대해 기록해보려고 한다. 다음에 내가 이 글을 본다면 계속 수정해나가면서 개념을 잡을 수 있지 않을까 싶다.

 

쿠버네티스의 파드의 모든 컨테이너는 동일한 network namespace를 공유하기에 로컬 호스트로 서로 호출이 가능하다. 파드가 생성될 때 kubelet은 namespace와 croup을 설정하고 pause 컨테이너를 실행한다. 파드는 여러 컨테이너로 구성되어 있고 모든 파드 안에는 pause 컨테이너가 포함되어 있다.

 

pause 컨테이너가 Network/IPC/UTS namespace를 생성, 유지, 공유하는 역할을 한다. 파드 내 컨테이너는 pause 컨테이너가 생성한 network namespace를 공유하기에 로컬 호스트로 호출이 가능한 것이며 pause 컨테이너가 이 namespace를 유지하기에 컨테이너가 재시작되어도 IP가 유지된다. 

pause 컨테이너

 

그리고 CNI는 namespace를 pause 컨테이너에 binding 한 이후에, application 컨테이너를 실행한다.

쿠버네티스의 각 파드는 고유한 IP 주소를 가진다. 이를 가능하게 해주는 것이 CNI(Container Network Interface)인데 간단히 설명하자면 컨테이너 간의 네트워킹을 제어할 수 있는 플러그인을 만들기 위한 표준이다. 대표적인 구현체로 Calico, Flannel, Weavenet, AWS VPC CNI 등 이 있는데 혼자 학습할 때는 Calico를 사용했지만 실무에서는 AWS EKS를 사용하면서 자연스레 AWS VPC CNI를 활용했다. 네트워크 플러그인 참고

CNI 예시

 

위 그림 처럼 CNI가 브릿지 인터페이스를 만들고 컨테이너 네트워크 대역대를 나눠준다고 이해하였다. 다만 CNI 따라 다르지만 AWS VPC CNI를 사용하는 경우는 파드의 IP 네트워크 대역과 노드의 IP 대역이 같아서 직접 통신이 가능하다. IP 대역이 같기 때문에 각각의 Pod에 보안그룹도 적용할 수 있었다. Pod SecurityGroupPolicy

Calico, AWS VPC CNI

 

AWS VPC CNI와 달리 Calico를 사용하였을 때는 (정확히 기억은 안나지만)노드 eth0(192.xx.xx.xx)위에 veth(172.xx.xx.xx)환경을 만들어 파드에게 네트워크를 할당해주고 파드 간 통신을 하였다.

 

파드 간 통신 시 일반적으로 Calico 등의 K8S CNI는 오버레이 (VXLAN, IP-IP 등) 통신을 하고,  AWS VPN CNI는 동일 대역으로 직접 통신을 한다.

오버레이 네트워킹

 

직접 통신은 동일 네트워크 대역에서 원본 패킷을 직접 전달하니 이해 안될게 없지만 K8S CNI의 오버레이 통신이라는 말이 반갑지 않을 수 있다. K8S CNI의 경우는 네트워크 대역대가 다르기 때문에 10.1.1.1 파드에서 다른 노드의 10.1.1.2 파드를 찾을 수 없다. 추가 홉을 거치거나 무언가 다른 방법을 해야할텐데 오버레이 네트워크라는 것이 물리적 네트워크 구조에 관계없이 서로 다른 노드에 있는 Pod들이 마치 같은 네트워크에 있는 것 처럼 통신할 수 있게 해준다.

오버레이 네트워크에서 Pod 간 통신할 때 트래픽을 특별한 패킷 형태로 캡슐화하는데 이것이 위 그림에서 볼 수 있는 Outer 패킷이다. 

 

다른 노드에 있는 파드와 통신을 할 때는 위 처럼 오버레이 네트워킹을 활용하지만 동일 노드의 파드끼리의 통신은 아래 그림의 bridge를 통해 가능하다. 

 

동일 노드에서의 Pod 통신

 

위 bridge(가상 네트워크 브릿지)가 파드 네트워크 인터페이스와 호스트 네트워크를 연결해주며 파드 간 통신 및 외부 네트워크 접근을 가능하게 한다. CNI0 인터페이스라고도 하는데 각 Pod의 네트워크 인터페이스(veth pair 한쪽 끝)을 하나의 네트워크로 묶어주는 역할을 한다. CNI0 브리지는 CNI을 통해 자동으로 관리되므로 복잡한 네트워크 설정 없이 Pod 간 네트워킹을 구성할 수 있는 것이다. 여기서 veth pair는 위 그림 처럼 두 개의 가상 네트워크 인터페이스를 연결하는 네트워크 케이블 같은 역할이다. 호스트 입장에서는 v(virtual)이지만 파드 입장에서는 eth가 되는 기분을 누릴 수 있는 것이라 생각한다. 

 

실제로 파드끼리 통신할 떄는 주로 서비스 디스커버리를 활용한다. 파드의 IP는 파드가 배포될 때마다 변경되어 이 파드의 IP가 변동될 때마다 이를 최신화하여 연결해주는 논리적 객체인 Service를 사용하기에 안정적인 통신이 가능하다. 

Service는 클러스터 내부 혹은 외부에 고정된 엔드포인트를 제공하며 파드의 IP가 바뀌더라도 클라이언트는 서비스의 DNS 이름이나 IP를 통해 계속해서 접근할 수 있다. Service는 패킷 전달 외에도 로드 밸런싱 등의 기능도 지원한다.

Service에는 ClusterIP, NodePort, LoadBalancer, Headless Service가 있는데 자세한 설명은 생략하도록 하겠다. 

 

대신 Kube-proxy와의 관계는 알고 넘어가면 좋을 것 같다. frontend pod가 백엔트 서비스 ports로 접속하면 pod ports로 패킷이 전달된다. 

Service & kube-proxy

 

kube-proxy가 Service의 가상 IP를 구현하게 된다. 포트 또한 kube-proxy가 관리하게 된다. kube-proxy의 이러한 동작 방식은 총 세가지가 있다. 대표적인 (default) 모드인 iptables 모드가 있다. 초기에는 userspace가 기본 관리 모드였고, 2019년 12월에 iptables가 기본 관리 모드로 업데이트 되었다고 한다. 이는 리눅스 iptables를 이용하여 서비스로 오고 나가는 패킷을 제어한다. iptables 모드일 때, Service라는 쿠버네티스 리소스는 실체가 iptables인 거 같다.

Service & kube-proxy & iptables

 

kube-proxy는 api server를 감시하여 pod가 삭제되거나 생성되면 iptables를 최신 IP 등으로 업데이트 한다. kube-proxy는 iptables nat 규칙을 이용하여 서비스에 오고 나가는 패킷의 주소를 변환하면서 파드까지 전달된다. pod 수가 많아지면 iptables rule 체인이 길어지면서 패킷 검사 시간이 증가하여 성능 저하가 발생할 수 있다고 한다.

 

userspace 모드, iptables 모드 말고도 IPVS (IP Virtual Service) 모드라는 것도 있는데 커널 스페이스에서 동작하는 L4 로드밸런싱 기술로 데이터 구조를 해시 테이블로 저장해 더 빠르고 좋은 성능을 낸다고 한다.

 

커널 레벨에서 네트워크 패킷을 로드밸런싱하여 iptables 보다 성능이 뛰어나고 대규모 트래픽에도 효율적이라고 하며. (커널 공간에서 직접 패킷을 처리), 다양한 로드밸런싱 알고리즘도 지원한다고 한다. ( Round Robin, Weighted Round Robin, Least Connections 등), 허나 추가적인 커널 모듈이 필요하고 설정이 복잡한 편이라고 한다. 아직 사용해본적은 없다. 

 

다시 정리하자면 IPVS의 경우 iptables를 거치지 않고, 커널의 직접 패킷을 라우팅하는데 IPVS 커널 모듈이 해시 테이블을 사용하여 백엔드 Pod을 즉시 찾는다. --> O(1)

iptables처럼 체인을 순차적으로 확인하는 방식이 아니다. <-- O(N)

 

또한 패킷을 NAT 없이 직접 Pod로 전달한다. (iptables처럼 NAT 변환이 필요하지 않고, 커널 레벨에서 바로 라우팅 수행)

 

iptable의 경우, 5000건 이상의 룰셋이 등록되었을 때, 시스템이 급격하게 떨어진다고 한다. 

이에 대응하여 나온 것이 ipset인데 iptables 확장 기능으로, 대량의 IP 목록을 효율적으로 관리하기 위해 iptables처럼 개별적인 규칙을 하나씩 저장하지 않고, 전체 서비스와 엔드포인트를 그룹으로 관리하기 때문에 규칙 수가 훨씬 적다고 한다.

 

따라서 IPVS는 ipset을 사용하여 룰셋을 해시 테이블(Hashtable) 기반으로 관리해 검색 속도가 빠르고 룰 수가 적다.

 

필자 또한 EKS를 운영하다가 파드 수가 많아져 성능이 저하되고 개선이 필요한 시점이라 생각되면 적용해 볼 예정이다.

https://aws.github.io/aws-eks-best-practices/ko/networking/ipvs/ 

 

IPVS 모드에서 kube-proxy 실행 - EKS 모범 사례 가이드

IPVS 모드에서 kube-proxy 실행 IPVS (IP 가상 서버) 모드의 EKS는 레거시 iptables 모드에서 실행되는 kube-proxy와 함께 1,000개 이상의 서비스가 포함된 대규모 클러스터를 실행할 때 흔히 발생하는 네트워

aws.github.io

 

그렇다면 조금 더 나아가 실제로 kube-proxy가 default 모드인 iptables nat 테이블 규칙(rule)을 이용하여 서비스에 오고 나가는 패킷을 보도록 해보자.

 

워커 노드에 들어가 순차적으로 실행하며 결과 값을 확인해보았다. 참고로 필자는 EKS 환경이다.

 

iptables의 NAT(Network Address Translation) 테이블의 체인들을 볼 수 있다. 

iptables --table nat --list

iptables는 처음 들어오는 패킷은 PREROUTING 테이블 규칙을 만나게 되며 패킷이 다시 나갈 때는 POSTROUTING을 만나게 된다. 이 둘의 역할은 이 정도는 알아 놓자.

 

  • PREROUTING (DNAT) : 패킷의 도착지(deatination) 주소를 변경한다. D(estination)NAT
  • POSTROUTING (SNAT 또는 masquerade) : 패킷의 출발지(source) 주소를 변경한다. S(ource)NAT

 

 

그렇다면 PREROUTING 체인에 있는 규칙들 부터 출력해보면서 따라가보자.

iptables -v --numeric --table nat --list PREROUTING

쿠버네티스 서비스로 오는 패킷은 KUBE-SERVICES 규칙을 따르게 된다.

 

iptables -t nat --list KUBE-SERVICES

KUBE-SERVICE 체인을 확인해보자. (KUBE-SVC-RGMHA2R6ZUMGA3YR를 예시로 들어본다.)

만약 Service의 DNS로 호출하면 CoreDNS같은 클러스터 내의 DNS 서버에 의해 ClusterIP로 요청이 갈 것이다. 이 때 ClusterIP를 172.20.109.225라고 한다면 위 예시로 든  KUBE-SVC-RGMHA2R6ZUMGA3YR 체인으로 전달될 것이다.

 

이를 조회해보자

iptables -t nat --list KUBE-SVC-RGMHA2R6ZUMGA3YR

위 그림에서 파악할 수 있는 것은 iptables의 경우 로드밸런싱이 확률적으로 균등하게 분배되게끔 규칙이 작성되어 있는 것을 볼 수 있다. static mode random probability에 퍼센트 비율을 볼 수 있다. 위 서비스 KUBE-SVC-RGMHA2R6ZUMGA3YR의 규칙에는 결국 6개(실제 해당 서비스의 backend pod 수)가 있고 첫번째로 만난 16%의 SVC KUBE-SEP-ZMDEKEQHUKSYHM3J 체인으로 라우팅된다고 가정하고 이를 조회해보았다.

 

iptables -t nat --list KUBE-SEP-ZMDEKEQHUKSYHM3J

이렇게 10.250.52.208(목적지 파드)까지 정상적으로 트래픽이 전달되게 된다. 

 

위 요약하자면 iptables --> PREROUTING --> KUBE-SVC --> KUBE-SEP 순으로 조회되었다.

 

iptables을 빠져나갈때는 mark match ! 0x4000규칙에 매칭되어 호출한 체인으로 돌아간다. Ruturn 타입을 확인할 수 있다.

iptables -v --numeric --table nat --list KUBE-POSTROUTING

 

따라서 아래 있는 MARK, MASQUERADE 규칙은 실행하지 않는다. 마치 프로그래밍에서 함수 호출이 끝나는 것처럼 흘러간다. 호출한 곳은 아마도 KUBE-SERVICE SEP에 해당하여 pod와 통신을 시작할 것이다. SEP는 Kubernetes 서비스(ClusterIP, NodePort 등)를 실제 Pod의 IP로 연결하는 체인인데 Service Endpoint를 뜻한다고 한다. 

 

참고로 NodePort가 ClusterIP로 라우팅되고 동일하게 파드로 전송되는 것도 동일한 방법으로 확인하다. 

iptables -t nat -L KUBE-NODEPORTS -v -n

만약 노드 포트가 31160이면 KUBE-EXT-A32MGCDFPRQGQDBB 룰을, 32101이면 KUBE-EXT-TFRZ6Y6WOLX5SOWZ가 적용될 것이다.

 

동일하게 이를 조회하면 KUBE-SVC 체인을 볼 수 있다. 

KUBE-MARK-MASQ는 외부(클러스터 외부)로 나가는 패킷에 대해 SNAT 처리가 필요할 때 마킹하는 규칙으로 참고하면 될 거 같다.

NodePort의 경우 외부로부터 노드 자체의 포트를 타고 들어와 ClusterIP로 전달된다는 것을 확인할 수 있다. 이 후부터는 이전에 체인을 조회한 방식과 동일하게 추적할 수 있다.

 

필요한 정보와 궁금한 점은 위 처럼 iptables의 NAT 규칙을 조회하여 NodePort, ClusterIP, Pod 간 트래픽이 어떻게 처리되는지 확인할 수 있었다.

 

파드의 네트워크에 대한 생각을 논리적으로 정리해보고 서비스를 통한 호출을 직접 조회해보는 시간을 가지면서 놓친 점도 있을 수 있고 더 깊이 있게 파고들 수 있을 만한 부분도 있을 것 같다. 

 

계속해서 도전해보자.