[Study 4주차] 쿠버네티스 Service - NodePort
- -
CoudNet@ 팀의 가시다님께서 리딩하시는 KANS Study (Kubernetes Advanced Networking Study) 4주차 스터디 내용 정리
이번 글에는 Service 타입 중 NodePort 에 대해서 알아보겠습니다.
1. Service NodePort


k8s 클러스터 외부에서 k8s 내부 파드에 접근하기 위해 사용되는 NodePort 입니다.
말 그대로 k8s 노드의 포트를 하나 뚫어서 외부와 통신이 가능하게 만드는 구조입니다.
외부 클라이언트가 NodeIP:NodePort 로 접속 시,
해당 노드의 iptables 규칙에 의해 SNAT/DNAT 되어 목적지 파드와 통신 후 리턴 트래픽은 최초 인입 노드를 경유하여 외부로 되돌아갑니다.
NodePort 의 기본 할당 포트 범위는 30000 ~ 32767 입니다.
1.1. 실습 환경 구성
실습은 kind 클러스터를 사용했습니다.
kind 클러스터 생성
cat <<EOT> kind-svc-1w.yaml kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 featureGates: "InPlacePodVerticalScaling": true "MultiCIDRServiceAllocator": true nodes: - role: control-plane labels: mynode: control-plane topology.kubernetes.io/zone: ap-northeast-2a extraPortMappings: - containerPort: 30000 hostPort: 30000 - containerPort: 30001 hostPort: 30001 - containerPort: 30002 hostPort: 30002 kubeadmConfigPatches: - | kind: ClusterConfiguration apiServer: extraArgs: runtime-config: api/all=true controllerManager: extraArgs: bind-address: 0.0.0.0 etcd: local: extraArgs: listen-metrics-urls: http://0.0.0.0:2381 scheduler: extraArgs: bind-address: 0.0.0.0 - | kind: KubeProxyConfiguration metricsBindAddress: 0.0.0.0 - role: worker labels: mynode: worker1 topology.kubernetes.io/zone: ap-northeast-2a - role: worker labels: mynode: worker2 topology.kubernetes.io/zone: ap-northeast-2b - role: worker labels: mynode: worker3 topology.kubernetes.io/zone: ap-northeast-2c networking: podSubnet: 10.10.0.0/16 serviceSubnet: 10.200.1.0/24 EOT # k8s 클러스터 설치 kind create cluster --config kind-svc-1w.yaml --name myk8s --image kindest/node:v1.31.0 # 노드에 기본 툴 설치 docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping git vim arp-scan -y' for i in worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done # mypc 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 직접 지정 docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity
서비스 생성
# Pod 생성 cat <<EOT> echo-deploy.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deploy-echo spec: replicas: 3 selector: matchLabels: app: deploy-websrv template: metadata: labels: app: deploy-websrv spec: terminationGracePeriodSeconds: 0 containers: - name: kans-websrv image: mendhak/http-https-echo ports: - containerPort: 8080 EOT # Service 생성 cat <<EOT> svc-nodeport.yaml apiVersion: v1 kind: Service metadata: name: svc-nodeport spec: ports: - name: svc-webport port: 9000 # 서비스 ClusterIP 에 접속 시 사용하는 포트 port 를 의미 targetPort: 8080 # 타킷 targetPort 는 서비스를 통해서 목적지 파드로 접속 시 해당 파드로 접속하는 포트를 의미 selector: app: deploy-websrv type: NodePort EOT # 배포 kubectl apply -f echo-deploy.yaml,svc-nodeport.yaml

1.2. NodePort 접속 테스트
외부 클라이언트 (mypc 컨테이너) 에서 접속 테스트 & 서비스(NodePort) 부하분산 접속을 확인해보겠습니다.
# NodePort 확인 kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}' # NodePort 를 변수에 지정 NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}') echo $NPORT # 노드의 IP와 NodePort를 변수에 지정 CNODE=172.18.0.3 NODE1=172.18.0.2 NODE2=172.18.0.4 NODE3=172.18.0.5 # MyPC 컨테이너에서 서비스(NodePort) 부하분산 접속 확인 docker exec -it mypc curl -s $CNODE:$NPORT | jq

NodePort 를 사용할 경우, Pod 가 없는 노드에서도 접속이 가능합니다.
현재, Control Plane 에는 Pod 가 없지만 노드포트로 접근할 경우 파드와 통신이 됩니다.
# Control Plane 의 노드포트로 100번 호출 docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

또한 NodePort 타입일지라도 서비스의 ClusterIP 로 접근 시 통신이 잘 되는 것을 알 수 있습니다.
# ClusterIP 와 Port 를 변수로 등록 CIP=$(kubectl get service svc-nodeport -o jsonpath="{.spec.clusterIP}") CIPPORT=$(kubectl get service svc-nodeport -o jsonpath="{.spec.ports[0].port}") echo $CIP $CIPPORT # 서비스 ClusterIP 호출 docker exec -it myk8s-control-plane curl -s $CIP:$CIPPORT | jq

하지만, 클러스터 외부에서는 ClusterIP 로 호출이 되지 않습니다.
docker exec -it mypc curl -s $CIP:$CIPPORT

1.3. NodePort iptables 확인


ClusterIP 의 iptables 와 다른 점은 KUBE-NODEPORT, KUBE-MARK-MASQ, POSTROUTING 이 추가된 것입니다.
외부에서 NodePort 를 통한 서비스 접근 시에는 출발지 IP 가 접근한 노드의 IP 로 변환(SNAT) 되어 목적지 파드로 접속하게 됩니다.
그렇기 때문에 목적지 파드에서 응답할 경우, 무조건 진입한 노드를 거치게 되는 단점이 있습니다.
iptables 확인
# Control Plane 노드에 접근 docker exec -it myk8s-control-plane bash # 1단계) PREROUTING 테이블 정보 확인 iptables -v --numeric --table nat --list PREROUTING # 2단계) KUBE-NODEPORTS 테이블 정보 확인 iptables -v --numeric --table nat --list KUBE-NODEPORTS # 3단계) KUBE-EXT 테이블 정보 확인 iptables -v --numeric --table nat --list KUBE-EXT-VTR7MTHHNMFZ3OFS # 4단계) KUBE-SVC 테이블 정보 확인 iptables -v --numeric --table nat --list KUBE-SVC-VTR7MTHHNMFZ3OFS # 5단계) KUBE-SEP 테이블 정보 확인 iptables -v --numeric --table nat --list KUBE-SEP-XEXGJWEWSC2GPNPZ # 6단계) POST-ROUTING 테이블 정보 확인 iptables -v --numeric --table nat --list KUBE-POSTROUTING


1.3. externalTrafficPolicy
externalTrafficPolicy 설정을 통해 NodePort 로 접속 시 해당 노드에 배치된 파드로만 접속할 수 있습니다.

# 기본 정보 확인 kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'

externalTrafficPolicy 적용
kubectl patch svc svc-nodeport -p '{"spec":{"externalTrafficPolicy": "Local"}}' kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'

# 파드 3개를 2개로 줄임 kubectl scale deployment deploy-echo --replicas=2 # pod 확인 kubectl get pod -o wide

이제 파드는 노드1, 노드3 에만 배포되어 있습니다.
# 파드가 배포되지 않은 컨트롤 플레인의 노드 포트로 접속 -> 실패 docker exec -it mypc curl -s --connect-timeout 1 $CNODE:$NPORT | jq ## 각 노드의 포트에 접근 # 컨트롤플레인 docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr" # 노드1 docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE1:$NPORT | grep hostname; done | sort | uniq -c | sort -nr" # 노드2 docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE2:$NPORT | grep hostname; done | sort | uniq -c | sort -nr" # 노드3 docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE3:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

externalTrafficPolicy 로 인해 다른 노드의 파드로 부하 분산이 되지 않는 것을 알 수 있습니다.
externalTrafficPolicy 에는 치명적인 단점이 존재합니다.
바로 파드가 배포되지 않은 노드에 접근할 경우, 서비스 연결이 불가능하다는 것입니다.
또한, 파드가 있는 노드의 연결 트래픽이 가중될 수 있다는 점이 있습니다.

1.4. Endpoint Slices

기존 Endpoints 는 단일 리소스로 모든 엔드포인트를 관리했지만 대규모 백엔드 파드를 운영할 경우,
Endpoints 에 상당한 네트워크 트래픽이 발생하여 성능 저하와 CPU 비용 발생의 원인이 되었습니다.
Endpoint Slices 를 사용하게 되면서 Endpoints 를 여러 조각으로 나누어 관리함으로써 Endpoints 의 단점을 해결할 수 있었습니다.
Endpoint Slices 로 인해 Dual Stack Networking 및 Topology Aware Routing 기능이 가능하게 되었습니다.
Endpoint Slice 예시
apiVersion: discovery.k8s.io/v1beta1 kind: EndpointSlice metadata: name: demo-slice-1 labels: kubernetes.io/service-name: demo addressType: IPv4 ports: - name: http protocol: TCP port: 80 endpoints: - addresses: - "10.0.0.1" conditions: ready: true
Endpoint Slices 실습
kubectl get endpointslice # Endpoint Slices Service 배포 cat <<EOF | kubectl create -f - apiVersion: apps/v1 kind: Deployment metadata: name: mario labels: app: mario spec: replicas: 1 selector: matchLabels: app: mario template: metadata: labels: app: mario spec: tolerations: - key: "node-role.kubernetes.io/control-plane" effect: "NoSchedule" nodeSelector: node-role.kubernetes.io/control-plane: "" containers: - name: mario image: pengbai/docker-supermario readinessProbe: exec: command: - cat - healthcheck --- apiVersion: v1 kind: Service metadata: name: mario spec: ports: - name: mario-webport port: 80 targetPort: 8080 nodePort: 30001 selector: app: mario type: NodePort externalTrafficPolicy: Local EOF
kubectl get pod,svc,ep,endpointslice -o wide

기존의 엔드포인트는 Probe 실패 시, IP 가 출력되지 않지만 엔드포인트 슬라이스는 IP 가 출력되는 차이점이 있습니다.