본문 바로가기
스터디 이야기/Kubernetes Advanced Networking Study

[KANS] Service - ClusterIP, NodePort

by lakescript 2024. 9. 28.
더보기

이 스터디는 CloudNet@에서 진행하는 KANS 스터디를 참여하면서 공부하는 내용을 기록하는 블로그 포스팅입니다.

CloudNet@에서 제공해주는 자료들을 바탕으로 작성되었습니다.

Kubernetes Serivce

 

service란?

Service란 Pod 집합에서 실행중인 애플리케이션을 네트워크 서비스로 노출하는 추상화 방법입니다.

서비스는 Pod에 대하여 고정 IP와 Domain Name을 제공하는데, Pod1 애플리케이션은 서비스의 10.200.1.1 IP로 통신 시에 서비스는 Pod2의 172.16.1.2로 트래픽을 전달하게 됩니다. 만약, Pod2가 restart하게 되면 서비스는 변경된 Pod2의 IP를 자동으로 알게 되어, Pod1의 통신 요청을 변경된 Pod2의 IP인 172.16.1.3으로 전달할 수 있게 됩니다.

 

Cluster IP

클라이언트(TestPod)가 cluster-IP로 접속 시 해당 노드의 iptables 룰(랜덤 분산)에 의해서 DNAT 처리가 되어 목적지(backend) 파드와 통신하는 service 종류입니다. (Cluster 내부에서만 접근 가능!)

 

DNAT(Destination NAT)이란?
DNAT는 목적지 IP 주소를 변환하는 NAT의 한 형태입니다. 외부에서 들어오는 트래픽의 목적지 IP를 내부 네트워크의 사설 IP로 변환하여 특정 서버나 서비스로 전달합니다. 주로 외부에서 내부 네트워크의 특정 서버에 접근할 수 있도록 하기 위해 사용됩니다. 즉, 외부에서 내부로 들어오는 트래픽의 목적지 IP 변환하는 방식입니다.

 

 

ClusterIP 타입의 서비스를 생성하게 되면, kube-apiserver가 kubelet & kube-proxy를 통해 iptables에 rule을 자동으로 생성하게 됩니다. 이때, 모드 노드(마스터 포함)에 iptables rule 설정되므로, 파드에서 접속 시 해당 노드에 존재하는 iptables rule 에 의해서 분산 접속이 됩니다.

 

ClusterIP 접속 확인 실습

파드와 서비스 사용 네트워크 대역 정보 확인

kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"

ubernetes 클러스터의 정보를 덤프(dump)하고, 그 중에서 네트워크와 관련된 설정을 검색해보겠습니다.

service-cluster-ip-range는 클러스터 내에서 서비스에 할당될 IP 주소 범위로 서비스의 가상 IP는 10.200.1.0부터 10.200.1.255 사이의 IP 중 하나가 할당됩니다. cluster-cidr은 클러스터 내 Pod들에게 할당되는 IP 주소 범위로 클러스터 내의 모든 Pod들이 이 범위 내에서 IP를 할당받게 됩니다.

 

webpod 파드의 IP 를 출력

kubectl get pod -l app=webpod -o jsonpath="{.items[*].status.podIP}"

위에서 본 것 처럼 10.10.0.0/16 범위내에 Pod의 IP가 할당된 것을 확인할 수 있습니다.

 

webpod 파드의 IP를 변수에 지정

WEBPOD1=$(kubectl get pod webpod1 -o jsonpath={.status.podIP})
WEBPOD2=$(kubectl get pod webpod2 -o jsonpath={.status.podIP})
WEBPOD3=$(kubectl get pod webpod3 -o jsonpath={.status.podIP})

실습을 편하게 하기 위해 변수에 webpod 파드의 IP를 변수에 지정합니다.

 

서비스 IP 변수 지정

SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})

service ip인 clusterIP도 마찬가지로 변수로 지정합니다.

 

ControlPlane의 Iptables rule 확인

iptables -t nat -S

controlPlane에 접근해서 iptables 정보를 확인해보겠습니다.

-A KUBE-SERVICES -d 10.200.1.83/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SVC-KBDEBIL6IU6WL7RF ! -s 10.10.0.0/16 -d 10.200.1.83/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-MARK-MASQ

dport는 destination port의 약자인데, clusterIP(10.200.1.83/32)의 9000 port로 들어오게 되면 어떤 rule을 통해서 해당 트래픽을 보내게 됩니다. 즉, clusterIP의 목적지 Port가 iptables rule에 의해서 처리가 됩니다. 이때, 동일한 규칙이 worker node들에 생성되게 됩니다. (kube-proxy가 생성합니다.)

 

서비스(ClusterIP) 부하분산 접속 확인

kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

for 문을 이용하여 변수로 설정해놓은 SVC1 IP로 100번 접속을 시도 후 출력되는 내용 중 반복되는 내용의 갯수 출력해보겠습니다.

3개의 파드로 대략 33% 정도로 부하분산 접속됨을 확인할 수 있습니다.

 

worker node1에 접속하여 tcpdump 실행 (port : 80)

tcpdump -i eth0 tcp port 80 -nnq

worker node 1에 접근해서 eth0 인터페이스에서 TCP 80번 포트(HTTP)의 트래픽을 캡처하는 위의 명령어를 실행합니다. 

 

clusterIP로 통신

kubectl exec -it net-pod -- zsh -c "for i in {1..10};   do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

위에서 실습해봤던 부하분산 명령어를 실행해보겠습니다.

 

worker node1에 접속하여 tcpdump 결과 확인 (port: 80)

첫줄의 패킷을 살펴보면, 10.10.0.5의 sourceIP와 랜덤 포트인 39786에서 10.10.2.2.80으로 전달되었습니다. 이때, testPod는 ControlPlane에 있고, ControlPlane에 있는 iptables 분산 rule에 의해 NAT가 발생합니다. 그렇기 때문에 Control Plane을 빠져나갈 때는 목적지 IP에 NAT가 적용된 것을 확인하실 수 있습니다.

 

worker node1에 접속하여 tcpdump 실행 (port : 9000)

tcpdump -i eth0 tcp port 9000 -nnq

이번엔 9000 port로 tcpdump를 해보겠습니다.

 

clusterIP로 통신

kubectl exec -it net-pod -- zsh -c "for i in {1..10};   do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

마찬가지로 트래픽을 보내보겠습니다.

2번의 트래픽이 전달되었습니다.

 

worker node1에 접속하여 tcpdump 결과 확인(port: 9000)

하지만 9000 port로는 전달받지 못한 것을 확인하실 수 있습니다. 이것은 ClusterIP로 이미 설정되어서 이미 모든 node의 iptables rule이 있기 때문에 해당 노드에서 NAT로 인해 변경되어 처리 해버리기 때문에 9000 port로는 확인이 불가능합니다.
 

wireshark를 통해 tcpdump 패킷 확인

tcpdump -i eth0 tcp port 80 -w /root/svc1-1.pcap

트래픽을 캡처하고 그 결과를 svc1-1.pcap 파일로 저장하는 명령어를 실행합니다.

그 후 트래픽을 주입하고, 로컬로 해당 파일을 받아옵니다.

위의 사진과 같이 해당 파일을 wireshark를 통해 불러와 살펴볼 수 있습니다.

 

ClusterIP 통신 흐름

 

TestPod에서 ClusterIP 주소인 10.200.1.111:9000(TCP)로 접속을 시도합니다. 이때, 출발지 IP는 TestPod의 IP인 172.16.116.5이고, 출발지 Port는 랜덤하게 할당됩니다. ControlPlane에 iptables의 NAT 테이블에 규칙과 매칭되어 목적지 IP와 목적지 Port가 변환되고, 랜덤 부하 분산되게 됩니다. 또한 NAT가 수행 시 NAT 연결 정보를 기록하고, webpod를 통해 되돌아온 트래픽을 확인하고 TestPod로 전달합니다. (IP가 변환 -> NAT, Port가 변환 -> PAT)

 

Iptables 정책 적용 순서

PREROUTING → KUBE-SERVICES → KUBE-SVC-### → KUBE-SEP-#<파드1> , KUBE-SEP-#<파드2> , KUBE-SEP-#<파드3>

iptables 체인은 규칙의 연결 순서이며, 규칙에 매칭되면 해당 패킷을 어떻게 처리할 지 결정하게 됩니다. 일반적으로는 허용 or 차단이며, 다른 체인으로 넘길 수도 있습니다. ClusterIP로 접속 시, 랜덤 부하 분산 처리가 되며 DNAT 처리되어 Pod로 접속하게 됩니다. 즉, ClusterIP이 정체는 Node에 설정되어 있는 iptables 규칙에 의해서 처리가 되며, iptables 규칙은 service 리소스를 생성하게 되면, kube-proxy에 의해서 규칙이 추가 되게 됩니다. (iptables는 L4 장비인 LoadBalancer처럼 부하를 분산하고, NAT 동작을 처리합니다.) 

 

ClusterIP의 부족한 점

클러스터 외부에서는 서비스(ClusterIP)로 접속이 불가능하고, Iptables는 Pod에 대한 health check기능이 없어서 Pod가 문제가 생겨도 트래픽이 연결될 수도 있습니다. 이때 Pod에 ReadinessProbe를 설정하여 문제가 생겼을 때 Service의 Endpoint에서 제거되게 해야 합니다. 또한, Service에 연동된 Pod 갯수따라 랜덤분산방식을 통해 트래픽이 분산되어 전달되는데, SessionAffinify를 통해 처음 연결된 ServiceEndPoint로 고정하여 전달하는 거외에 다른 분산 방식은 불가능합니다. 

 

NodePort

ClusterIP와 동일하게 NodePort 서비스도 생성 시 kube-proxy에 의해서 모든 Node에 iptables 규칙이 생성됩니다. 이때, Kubernetes Cluster 외부에서 각 Node의 IP:NodePort로 접속할 수 있으며 Node1에 들어온 트래픽은 해당 Node의 iptables 규칙에 의해 Service에 연동된 Pod로 부하 분산되어 접속이 가능해집니다. 이때, NodePort는 모든 Node(ControlPlane 포함)에 Listen됩니다.

 

외부 client의 출발지IP도 SNAT되어 목적지 Pod에 도착합니다.(DNAT 포함)

SNAT(Source NAT)이란?
SNAT는 주로 출발지 IP 주소를 변환하는 NAT의 일종입니다. 내부 네트워크에서 외부 네트워크로 나가는 트래픽의 출발지 IP 주소를 공인 IP로 변환합니다. 
즉, 내부에서 외부로 나가는 트래픽의 출발지 IP 변환하는 방식입니다.

 

즉, NodePort는 외부에서 Cluster의 Service로 접근이 가능하도록 하며, 이후의 통신에서는 ClusterIP와 동일하게 동작하는 Service Type입니다. 특히, 모든 Node에 iptables 규칙이 설정되므로, 모든 Node에 NodePort로 접속 시 iptables 규칙에 의해서 분산 접속이 됩니다. 또한, Node의 모든 Loca IP(Local Host Interface IP) 사용이 가능하며, Local IP도 지정이 가능합니다. 이때, Kubernetes NodePort의 할당 범위는 기본으로 30000-32767사이의 Port가 랜덤하게 배정됩니다.

 

 

NodePort 통신 흐름

 

client 가상 머신에서 마스터 노드에 NodePort로 접속을 시도합니다. 이때, 출발지의 IP는 Client 가상 머신의 IP인 192.168.10.200이고, 출발지 Port는 랜덤하게 할당됩니다. 목적지 IP는 마스터 노드의 IP인 192.168.10.10이고, 목적지Port는 NodePort Type의 Service 생성시 할당된 30286(랜덤)입니다. 그 후 마스터 노드에 iptables의 NAT테이블 규칙과 매칭되어 목적지 IP와 목적지 Port는 변환(DNAT)되며, 랜덤하게 부하 분산하게 됩니다.

NodePort 접속 확인 실습

현재 배포된 리소스 확인

kubectl get deploy,pod -o wide

kubectl get svc svc-nodeport

kubectl get endpoints svc-nodeport

 

NodePort 확인 후 변수 지정

NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')

 

Node의 IP와 NodePort를 변수에 지정

## CNODE=<컨트롤플레인노드의 IP주소>
## NODE1=<노드1의 IP주소>
## NODE2=<노드2의 IP주소>
## NODE3=<노드3의 IP주소>

 

접속 테스트 확인용 Container 생성

docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity;

위의 통신흐름에서 client 가상머신 역할을 할 접속 테스트를 확인해보기 위해 container를 생성합니다.

 

서비스(NodePort) 부하분산 접속 확인

docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

부하 분산 확인을 위해 controlPlane에 100번 접속해보겠습니다.

각 Pod로 랜덤 부하 분산이 이루어진 것을 확인해볼 수 있습니다.

 

현재 pod들은 control plane에 떠있는 것이 아닌데 어떻게 통신이 되는 걸까요?

즉, controlplane의 NodePort로 들어왔지만 iptables 규칙에 따라 각 Node로 부하가 전달됩니다. (마찬가지로 다른 Node에 들어와도 iptables 규칙에 따라 각 Node로 전달됩니다. -> 매우 비효율!)

docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE1:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

node1에 100번 접속해보겠습니다.

docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE2:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

node2에 100번 접속해보겠습니다.

docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE3:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

 

node3에 100번 접속해보겠습니다.

 

NodePort 서비스는 ClusterIP 를 포함

kubectl get svc svc-nodeport

 

 

Iptables 정책 적용 순서

PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-SVC-# ->  KUBE-MARK-MASQ → KUBE-SEP-# ⇒ KUBE-POSTROUTING (MASQUERADE)

 

기본 규칙은 ClusterIP와 동일합니다. 차이점은 KUBE_NODEPORTS, KUBE-MARK-MASQ, KUBE-POSTROUTING 체인이 추가되었습니다. 즉, NodePort 매칭 시에 Marking 후 출발지IP에서 해당 Node에 있는 네트워크 인터페이스의 IP로 변환(SNAT)하여 목적지 Pod로 전달됩니다. (mark된 트래픽만 SNAT이 일어난다.)

 

MASQUERADE이란?
NAT(Network Address Translation)의 한 형태로, 동적으로 IP 주소를 변환하는 방법입니다. NAT를 설정할 때, MASQUERADE는 NAT가 동작하는 인터페이스의 IP 주소를 자동으로 감지하고, 그 IP 주소를 사용하여 출발지 IP를 변환합니다. 그래서 네트워크 인터페이스의 공인 IP가 변경될 때마다 이를 수동으로 설정할 필요가 없습니다. SNAT는 고정된 공인 IP 주소를 사용하는 환경에서 사용되는 반면, MASQUERADE는 동적으로 IP가 할당되는 환경에 적합합니다.

 

NodePort의 부족한 점

외부에서 Node의 IP와 Port로 직접 접속이 필요하기에 내부망이 외부에 공개(라우팅 가능)되어 보안에 취약해집니다. (LoadBalancer Type으로 외부 공개를 최소화 할 수 있습니다.)