4주차 [2] - Service: NodePort

1. 목적성

NodePort의 네트워크는 어떻게 구성이 되고

k8s에서 Cluster IP의 사용에 있어, 클러스터 외부에서 pod까지 어떻게 접속하는지 확인합니다.

이에따른, 네트워크의 흐름과 라우팅을 이해합니다.

  • 부하분산과 SessionAffinity를 구현합니다.

  • ExternalTrafficPolicy 를 활용해서 출발지 아이피 보정과 비효율적 hopping을 방지하는 방법을 알아봅니다.

  • NodePort의 단점을 알아보고 LoadBalancer가 해당 단점들을 극복하는것을 확인할 수 있습니다.

2. NodePort

2.1 통신 흐름

  • 외부 클라이언트가 nodeIP:NodePort 접속할 때, 노드의 iptables 룰으로 SNAT/DNAT 되어서 목적지 파드와 통신후에 리턴 트래픽은 인입 노드를 경유해서 외부로 돌아갑니다.

  • SNAT, DNAT 되는 과정을 확인할 수 있습니다.

2.2 [실습] 서비스 접속확인 및 네트워크 흐름

  • 실습 구성

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


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

// NODE PORT 를 생성합니다.
kubectl apply -f echo-deploy.yaml,svc-nodeport.yaml

(⎈|kind-myk8s:N/A) root@kind:~# kubectl apply -f echo-deploy.yaml,svc-nodeport.yaml
deployment.apps/deploy-echo created
service/svc-nodeport created
(⎈|kind-myk8s:N/A) root@kind:~# kubectl get deploy,pod -o wide
NAME                          READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS    IMAGES                    SELECTOR
deployment.apps/deploy-echo   0/3     3            0           7s    kans-websrv   mendhak/http-https-echo   app=deploy-websrv

NAME                               READY   STATUS              RESTARTS   AGE   IP          NODE                  NOMINATED NODE   READINESS GATES
pod/deploy-echo-5c689d5454-b7gng   0/1     ContainerCreating   0          6s    <none>      myk8s-worker3         <none>           <none>
pod/deploy-echo-5c689d5454-cjpfp   0/1     ContainerCreating   0          6s    <none>      myk8s-worker2         <none>           <none>
pod/deploy-echo-5c689d5454-m4hd9   0/1     ContainerCreating   0          6s    <none>      myk8s-worker          <none>           <none>
pod/net-pod                        1/1     Running             0          29m   10.10.0.5   myk8s-control-plane   <none>           <none>
pod/webpod1                        1/1     Running             0          29m   10.10.3.3   myk8s-worker          <none>           <none>
pod/webpod2                        1/1     Running             0          29m   10.10.2.2   myk8s-worker2         <none>           <none>

위의 그림의 방식으로 snat 과 dnat 되는 것을 확인할 수 있습니다.

  • 서비스 접속 확인

# 현재 k8s 버전에서는 포트 Listen 되지 않고, iptables rules 처리됨
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ss -tlnp; echo; done


(⎈|kind-myk8s:N/A) root@kind:~# for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ss -tlnp; echo; done
>> node myk8s-control-plane <<
State      Recv-Q     Send-Q          Local Address:Port            Peer Address:Port     Process
LISTEN     0          4096                127.0.0.1:2379                 0.0.0.0:*         users:(("etcd",pid=650,fd=8))
LISTEN     0          4096                127.0.0.1:10248                0.0.0.0:*         users:(("kubelet",pid=709,fd=24))
LISTEN     0          4096               172.18.0.3:2379                 0.0.0.0:*         users:(("etcd",pid=650,fd=10))
LISTEN     0          4096               172.18.0.3:2380                 0.0.0.0:*         users:(("etcd",pid=650,fd=7))
LISTEN     0          4096                127.0.0.1:45467                0.0.0.0:*         users:(("containerd",pid=111,fd=10))
LISTEN     0          4096               127.0.0.11:40627                0.0.0.0:*
LISTEN     0          4096                        *:6443                       *:*         users:(("kube-apiserver",pid=558,fd=3))
LISTEN     0          4096                        *:2381                       *:*         users:(("etcd",pid=650,fd=13))
LISTEN     0          4096                        *:10250                      *:*         users:(("kubelet",pid=709,fd=23))
LISTEN     0          4096                        *:10249                      *:*         users:(("kube-proxy",pid=877,fd=12))
LISTEN     0          4096                        *:10259                      *:*         users:(("kube-scheduler",pid=7081,fd=3))
LISTEN     0          4096                        *:10257                      *:*         users:(("kube-controller",pid=6364,fd=3))
LISTEN     0          4096                        *:10256                      *:*         users:(("kube-proxy",pid=877,fd=14))

>> node myk8s-worker <<
State       Recv-Q      Send-Q           Local Address:Port            Peer Address:Port      Process
LISTEN      0           4096                127.0.0.11:43401                0.0.0.0:*
LISTEN      0           4096                 127.0.0.1:10248                0.0.0.0:*          users:(("kubelet",pid=227,fd=19))
LISTEN      0           4096                 127.0.0.1:43641                0.0.0.0:*          users:(("containerd",pid=111,fd=10))
LISTEN      0           4096                         *:10256                      *:*          users:(("kube-proxy",pid=376,fd=22))
LISTEN      0           4096                         *:10250                      *:*          users:(("kubelet",pid=227,fd=18))
LISTEN      0           4096                         *:10249                      *:*          users:(("kube-proxy",pid=376,fd=23))

>> node myk8s-worker2 <<
State       Recv-Q      Send-Q           Local Address:Port            Peer Address:Port      Process
LISTEN      0           4096                 127.0.0.1:42391                0.0.0.0:*          users:(("containerd",pid=111,fd=10))
LISTEN      0           4096                 127.0.0.1:10248                0.0.0.0:*          users:(("kubelet",pid=229,fd=9))
LISTEN      0           4096                127.0.0.11:45125                0.0.0.0:*
LISTEN      0           4096                         *:10256                      *:*          users:(("kube-proxy",pid=379,fd=16))
LISTEN      0           4096                         *:10249                      *:*          users:(("kube-proxy",pid=379,fd=22))
LISTEN      0           4096                         *:10250                      *:*          users:(("kubelet",pid=229,fd=16))

>> node myk8s-worker3 <<
State       Recv-Q      Send-Q           Local Address:Port            Peer Address:Port      Process
LISTEN      0           4096                 127.0.0.1:37225                0.0.0.0:*          users:(("containerd",pid=111,fd=10))
LISTEN      0           4096                127.0.0.11:35575                0.0.0.0:*
LISTEN      0           4096                 127.0.0.1:10248                0.0.0.0:*          users:(("kubelet",pid=230,fd=13))
LISTEN      0           4096                         *:10256                      *:*          users:(("kube-proxy",pid=383,fd=22))
LISTEN      0           4096                         *:10249                      *:*          users:(("kube-proxy",pid=383,fd=12))
LISTEN      0           4096                         *:10250                      *:*          users:(("kubelet",pid=230,fd=15))


# 외부 클라이언트(mypc 컨테이너)에서 접속 시도를 해보자
kubectl get nodes -owide
# 노드의 IP와 NodePort를 변수에 지정
## CNODE=172.18.0.3
## NODE1=172.18.0.5
## NODE2=172.18.0.2
## NODE3=172.18.0.4


# 아래 반복 접속 실행 해두자
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $CNODE:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
  "hostname": "172.18.0.3",d:~#
    "hostname": "deploy-echo-5c689d5454-b7gng"
    
    
mypc 컨테이너가 nodeport를 이용해 잘 접속하는것을 확인할 수 있습니다.

  • iptables 정책 확인

  • 1: PREROUTING : INPUT 트래픽이 IPTABLE RULE을 타게 됩니다.

  • 3: KUBE-NODEPORTS PREROUTING: CLUSTERIP나 NODEPORT에서 MARKER를 지정합니다.

  • 4: Service 부하분산에 대해 rule을 정의합니다.

  • 6: DNAT이 여기서 일어나게 됩니다.

  • 7: POSTROUTING으로 OUT TRAFFIC에 대한 IPTABLE RULE이 적용됩니다.

  • 8: KUBE-POSTROUTING으로 SNAT이 됩니다.


// 컨트롤 플레인의 IPTABLES 분석합니다.
docker exec -it myk8s-control-plane bash
iptables -t nat --zero
root@myk8s-control-plane:/# iptables -t nat -S | grep PREROUTING
-P PREROUTING ACCEPT
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A PREROUTING -d 172.18.0.1/32 -j DOCKER_OUTPUT

# 외부 클라이언트가 노드IP:NodePort 로 접속하기 때문에 --dst-type LOCAL 에 매칭되어서 -j KUBE-NODEPORTS 로 점프!
iptables -t nat -S | grep KUBE-SERVICES

root@myk8s-control-plane:/# iptables -t nat -S | grep KUBE-SERVICES
-N KUBE-SERVICES
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A KUBE-SERVICES -d 10.200.1.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-SVC-NPX46M4PTMTKRN6Y
-A KUBE-SERVICES -d 10.200.1.60/32 -p tcp -m comment --comment "default/svc-nodeport:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-VTR7MTHHNMFZ3OFS
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS


# KUBE-SVC-# 이후 과정은 Cluster-IP 와 동일! : 3개의 파드로 DNAT 되어서 전달
root@myk8s-control-plane:/# iptables -t nat -S | grep "A KUBE-SVC-VTR7MTHHNMFZ3OFS -"
-A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport -> 10.10.1.3:8080" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-XEXGJWEWSC2GPNPZ
-A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport -> 10.10.2.3:8080" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-2AEFSWYPQGZTCWEI
-A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport -> 10.10.3.4:8080" -j KUBE-SEP-C5MCUQGLTHD455UI

  • externalTrafficPolicy 설정

  • NodePort 로 접속 시 해당 노드에 배치된 파드로만 접속됨, 이때 SNAT 되지 않아서 외부 클라이언트 IP가 보존합니다 (요구사항일때)

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

# externalTrafficPolicy: local 설정 변경
kubectl patch svc svc-nodeport -p '{"spec":{"externalTrafficPolicy": "Local"}}'
kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'
	"externalTrafficPolicy": "Local",
  "internalTrafficPolicy": "Cluster",
  
# 서비스(NodePort) 부하분산 접속 확인 : 파드가 존재하지 않는 노드로는 접속 실패!, 파드가 존재하는 노드는 접속 성공 및 클라이언트 IP 확인!
docker exec -it mypc curl -s --connect-timeout 1 $CNODE:$NPORT | jq
for i in $CNODE $NODE1 $NODE2 $NODE3 ; do echo ">> node $i <<"; docker exec -it mypc curl -s --connect-timeout 1 $i:$NPORT; echo; done


DNAT만 보존되는것을 확인할 수 있습니다.
하지만, 파드가 살아있지 않은 NODE에 대한 트래픽은 실패하는것을 확인할 수 있습니다.

2.3 NodePort 부족한 점

  • 외부에서 노드 IP 와 포트로 직접 접속 해야함.

    • 내부망이 외부에 공개 되어 보안 취약 -> LoadBalancer로 공개 최소화 가능

  • 클라이언트 IP 보존을 위해 externalTrafficPolicy 사용시에 파드가 없는 노드IP로 NODEPORT 접속시 실패 -> 헬스체크 probe 사용으로 대응한다.

이 모든것이 장비 없이 IP만으로 라우팅을 하려고하니깐 IP 테이블의 복잡함이 생긴다.

2.4 Readiness Probe + Endpoints, EndpointSlice

  • kube-proxy가 모든 endpoint를 watch 해서 iptable 을 업데이트해주는데, 모든 endpoint를 업데이트 하기 힘들다.

  • Enpoint의 그룹 단위를 만들어서 해당 단위를 watch 하는것을 업데이트 하게끔한다.

  • Readiness Probe 사용해서 헬스 체크를 하는데 실패시에 Endpoint 에서 제거해줄 수 있다.

Last updated