후니의 IT인프라 사전
[KANS] kubernetes-service (1) - ClusterIP와 NodePort 본문
이번 주제는 kubernetes의 네트워크 service에 대해서 알아보도록 하겠습니다.
전통적인 트래픽 전송은 pod에 할당된 IP에 직접 호출하는 것이였습니다.
우리가 10.0.1.1이라는 pod IP를 알면 그냥 호출하면 되는거죠.
그런데 우리가 흔히 실제로 운영을 하다보면 컨테이너는 쉽게 문제가 생겨 죽거나 급증하는 트래픽을 감당하기 위해 auto scaling 되기도 합니다. 그럼 이 pod의 IP들을 알 수 없어 감당하기 힘들어지게 되죠.
따라서 가상의 고정 IP를 할당하고, 쉽게 호출할 수 있는 도메인을 제공하는 Service가 등장하게 됩니다.
Service yaml
https://kubernetes.io/docs/concepts/services-networking/service/ 공식문서에서 제공하는 example yaml을 가져왔습니다.
Service의 구성요소 중 가장 중요한 것은 selector를 통해서 target을 지정한다는 점이며, spec.ports.port는 service의 inbound port정보이고, targetPort가 outbound port로 이해하시면 되겠습니다.
다음은 service의 type에 대해서 하나씩 알아보겠습니다.
Service는 다음과 같이 4가지 type(ClusterIP, NodePort, LoadBalancer, ExternalName)을 제공합니다.
ClusterIP (default)
가장 기본이 되는 타입으로 별도의 Service 내부 고정 IP를 가집니다. 따라서 내부에서만 호출이 가능하지만 이를 통해 동일한 애플리케이션의 다수의 파드의 접속을 용이하게 합니다.
ClusterIP에서 접근 확인
아래 명령어를 통해서 각 노드에서 iptable을 조회했을 때 grep으로 service의 IP를 조회해 보았습니다.
모든 노드에 자동으로 service의 IP와 dport(목적지 port)가 9000으로 지정된 것을 확인할 수 있습니다.
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i iptables -t nat -S | grep $SVC1; echo; done
부하분산 여부 확인
서비스의 큰 특징 중 하나는 부하분산 기능이 있다는 것입니다. 마치 L4의 역할을 담당하고 있습니다. 해당 명령어를 통해서 net-pod에서 서비스를 총 100번 호출했을 때, 각 pod 별로 부하분산의 정도를 확인할 수 있었습니다. pod3이 44번, pod2가 30번, pod1이 26번으로 전반적으로 잘 부하분산 되는 것을 확인할 수 있습니다.
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"
Cluster IP의 iptables 정책 적용 순서 확인
Cluster IP의 경우 다음과 같은 정책 적용 순서를 따릅니다.
PREROUTING → KUBE-SERVICES → KUBE-SVC-### → KUBE-SEP-#<파드1> , KUBE-SEP-#<파드2> , KUBE-SEP-#<파드3>
즉, Preroute(nat)에서 DNAT(3개 파드)되고, PostRoute(nat)에서 SNAT되지 않고 나가게 됩니다.
여기서 SEP는 Service EndPoint의 약자로 각각의 개별 Pod의 IP를 말합니다. 또한 가중치도 표시되어 있는데 0.333인 이유는 3개의 pod에 부하분산하기 위해 3분의 1로 계산된 것이며, 0.5는 첫번째를 제외한 나머지 2개의 pod는 2분의 1의 확률로 분산되기 때문에 이렇게 숫자가 나온 것입니다.
Session Affinity
한 번 접속했던 pod에 대한 정보는 유지하여 접근했던 pod로 계속 트래픽을 주는 것을 말합니다. 처음 접속했을 때 접속정보를 기록해둡니다. 동일한 Client IP로 접속하게되면 당시에 보냈던 목적지로 전달하게 됩니다. Session Affinity를 적용하려면 아래 Patch 명령어로 적용시킬 수 있습니다.
kubectl patch svc svc-clusterip -p '{"spec":{"sessionAffinity":"ClientIP"}}'
적용하게되면 아래처럼 sessionAffinity 관련 정보가 적용되는 것을 확인할 수 있습니다.
...
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
...
실제로 100번의 트래픽을 발생시켜보면 전부 webpod2로 향하는 것을 알 수 있습니다.
iptables 정책을 확인해봐도 session Affinity에 대한 내용이 추가된것을 확인할 수 있습니다.
iptables -t nat -S | grep recent
service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 에서 session affinity의 시간을 조절할 수 있습니다. 기본은 3시간(10800초)이지만 원하는 시간으로 조절할 수 있습니다. 30초로 변경하면 다음과 같이 30초로 반영된 것을 확인할 수 있습니다.
kubectl patch svc svc-clusterip -p '{"spec":{"sessionAffinity":"clientIP"}}'
kubectl patch svc svc-clusterip -p '{"spec":{"sessionAffinityConfig":{"clientIP":{"timeoutSeconds":30}}}}'
단점
- 클러스터 외부에서는 ClusterIP의 서비스로 접근이 불가능합니다. -> NodePort로 해결가능
- IPtables는 파드에 대한 헬스체크 기능이 없어 파드에 문제가 있어도 트래픽이 전송될 수 있습니다. -> Readiness Probe 설정 등으로 해결가능
- 서비스에 연동된 파드 갯수로 랜덤 분산 방식인데, 다른 분산 방식은 불가능 -> IPVS의 경우 다양한 분산 방식으로 트래픽 전송가능
NodePort
외부 클라이언트도 접근할 수 있도록 쿠버네티스 Node에서 일정 포트를 오픈하여 Listening하는 방식입니다. 따라서 클라이언트는 Node의 IP와 포트정보를 통해서 해당 클러스터에 접근합니다. iptables rule에 의해서 SNAT/DNAT 되어 목적지 파드와 통신 후 리턴 트래픽은 최초 인입 노드를 경유해 외부로 되돌아 갑니다.
쿠버네티스 NodePort의 기본 할당 범위는 30000-32767 포트입니다. 물론 다른 포트로 사용해도 되지만 기본적으로 해당 포트 내에서 배정됩니다.
NodePort는 각 노드에 동일한 포트를 열어주게 됩니다. 다음 명령어를 통해서 iptables가 동일하게 포트를 오픈하고 있는지 확인해볼 수 있습니다.
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i iptables -t nat -S | grep KUBE-NODEPORTS | grep $NPORT; echo; done
NodePort의 iptables 정책 적용 순서
NodePort는 다음과 같은 정책 적용 순서를 따릅니다. Kube-ext-mark 규칙이 추가됩니다.
PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-EXT-#(MARK) → KUBE-SVC-# → KUBE-SEP-# ⇒ KUBE-POSTROUTING
현재 컨트롤 플레인의 iptables를 보고 있는 것이고, 외부 클라이언트는 노드 IP:port로 접속하기 때문에 --dst-type LOCAL에 매칭되고 kube-nodePorts로 트래픽을 보내게 됩니다.
kube-nodeports에서는 -m nfacct --nfacct-name localhost_nps_accepted_pkts가 추가되는데 패킷 flow를 카운팅하기 위해서 지정합니다.
kube-ext-#에서는 kube-mark-masq -j mark --set-xmark 0x4000/0x4000 으로 지정하고 kube-svc-#로 트래픽을 보낸다.
이 후 3개 파드로 DNAT되어 트래픽을 전달합니다.
postrouting에서는 마킹이 되어 있어 출발지 IP를 접속한 노드의 IP로 SNAT(Masquerade) 처리한다. 최초 출발지 port는 랜덤 port로 변경됩니다.
externalTrafficPolicy 설정
NodePort는 external Traffic Policy를 통해 NodePort로 접속시 해당 노드에 배치된 파드로만 접속되는 설정입니다. 이를 통해 SNAT가 되지 않아 외부 클라이언트의 IP를 확인할 수 있다는 장점이 있습니다. 다만 만약 해당 노드에 배치된 파드가 없다면 통신이 안되기 때문에 해당 부분도 큰 맹점으로 반드시 주의해야 합니다.
NodePort의 단점
- 외부에서 노드의 IP와 포트로 직접 접속이 필요하다. 내부망이 외부에 공개되면 보안에 취약할 수 있습니다. -> LoadBalancer 타입으로 외부 공개를 최소화합니다.
- 클라이언트 IP 보존을 위해서 externalTrafficPolicy: local 사용시 파드가 없는 노드 IP로 NodePort 접속 시 실패 -> Loadbalancer 서비스에서 헬스체크로 대응이 가능하다.
LoadBalancer
CSP 같은 곳에서 제공하는 로드밸런서를 이용하거나, 온프레미스 환경의 경우 Metal LB와 같은 소프트웨어를 통해 로드밸런서로 접근하는 방식입니다. LB의 기능을 그대로 이용하면서 보안적으로나 관리적으로나 유리하다고 볼 수 있습니다.
이상으로 포스팅 마치겠습니다.