Kubernetes

[ Kans 3 Study - 4w ] 2. Service - NodePort

su''@ 2024. 9. 29. 04:21
CloudNetaStudy - Kubernets Networtk 3기 실습 스터디 게시글입니다.

 

NodePort

 

[  통신 흐름  ]

  • 요약 : 외부 클라이언트가 '노드IP:NodePort' 접속 시 해당 노드의 iptables 룰에 의해서 SNAT/DNAT 되어 목적지 파드와 통신 후 리턴 트래픽은 최초 인입 노드를 경유해서 외부로 되돌아감
    NodePort(노드포트)는 모든 노드(마스터 포함)에 Listen 됨! 외부 클라이언트의 출발지IP도 SNAT 되어서 목적지 파드에 도착함!, 물론 DNAT 동작 포함!
  • 외부에서 클러스터의 '서비스(NodePort)' 로 접근 가능 → 이후에는 Cluster IP 통신과 동일!
  • 모드 노드(마스터 포함)에 iptables rule 설정되므로, 모든 노드에 NodePort 로 접속 시 iptables rule 에 의해서 분산 접속이 됨
  • Node 의 모든 Loca IP(Local host Interface IP : loopback 포함) 사용 가능 & Local IP를 지정 가능
  • 쿠버네티스 NodePort 할당 범위 기본 (30000-32767) & 변경하기 - 링크
 

쿠버네티스 NodePort 할당 범위 변경하기

수정사항 2021.07.24 21:16 :: 제목 수정, 일부 잘못된 내용 삭제, 주석 수정 NodePort 쿠버네티스 기본 세팅에서 NodePort는 - 범위 내에서 할당된다. 1 이렇게 세팅된 이유는 다음 충돌이 예상되기 때문이

blog.frec.kr

 

 

실습 구성  ]

  • 목적지(backend) 디플로이먼트(Pod) 파일 생성 : echo-deploy.yaml
    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
  • 서비스(NodePort) 파일 생성 : svc-nodeport.yaml
    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
    
    # 모니터링
    watch -d 'kubectl get pod -owide;echo; kubectl get svc,ep svc-nodeport'
    
    # 확인
    kubectl get deploy,pod -o wide
    
    # 아래 31493은 서비스(NodePort) 정보!
    kubectl get svc svc-nodeport
    NAME           TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)          AGE
    svc-nodeport   NodePort   10.200.1.76   <none>        9000:31493/TCP   19s
    
    kubectl get endpoints svc-nodeport
    NAME           ENDPOINTS                                      AGE
    svc-nodeport   10.10.1.4:8080,10.10.2.3:8080,10.10.3.3:8080   48s
    
    # Port , TargetPort , NodePort 각각의 차이점의 의미를 알자!
    kubectl describe svc svc-nodeport

 

[ 서비스(NodePort) 접속 확인 ]

  • 외부 클라이언트(mypc 컨테이너)에서 접속 테스트 & 서비스(NodePort) 부하분산 접속 확인
    # NodePort 확인 : 아래 NodePort 는 범위내 랜덤 할당으로 실습 환경마다 다릅니다
    kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}'
    30353
    
    # NodePort 를 변수에 지정
    NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
    echo $NPORT
    
    # 현재 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
    ## (참고) 아래처럼 예전 k8s 환경에서 Service(NodePort) 생성 시, TCP Port Listen 되었었음
    root@k8s-m:~# ss -4tlnp | egrep "(Process|$NPORT)"
    State     Recv-Q    Send-Q        Local Address:Port        Peer Address:Port   Process
    LISTEN    0         4096                0.0.0.0:30466            0.0.0.0:*       users:(("kube-proxy",pid=8661,fd=10))
    
    # 파드 로그 실시간 확인 (웹 파드에 접속자의 IP가 출력)
    kubectl logs -l app=deploy-websrv -f
    
    
    # 외부 클라이언트(mypc 컨테이너)에서 접속 시도를 해보자
    
    # 노드의 IP와 NodePort를 변수에 지정
    ## CNODE=<컨트롤플레인노드의 IP주소>
    ## NODE1=<노드1의 IP주소>
    ## NODE2=<노드2의 IP주소>
    ## NODE3=<노드3의 IP주소>
    CNODE=172.18.0.A
    NODE1=172.18.0.B
    NODE2=172.18.0.C
    NODE3=172.18.0.D
    CNODE=172.18.0.2
    NODE1=172.18.0.4
    NODE2=172.18.0.5
    NODE3=172.18.0.3
    
    NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
    echo $NPORT
    
    # 서비스(NodePort) 부하분산 접속 확인
    docker exec -it mypc curl -s $CNODE:$NPORT | jq # headers.host 주소는 왜 그런거죠?
    for i in $CNODE $NODE1 $NODE2 $NODE3 ; do echo ">> node $i <<"; docker exec -it mypc curl -s $i:$NPORT; echo; done
    
    # 컨트롤플레인 노드에는 목적지 파드가 없는데도, 접속을 받아준다! 이유는?
    docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
    docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE1:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
    docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE2:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
    docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE3:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
    
    # 아래 반복 접속 실행 해두자
    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"
    
    
    # NodePort 서비스는 ClusterIP 를 포함
    # CLUSTER-IP:PORT 로 접속 가능! <- 컨트롤노드에서 아래 실행 해보자
    kubectl get svc svc-nodeport
    NAME           TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
    svc-nodeport   NodePort   10.111.1.238   <none>         9000:30158/TCP   3m3s
    
    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
    docker exec -it myk8s-control-plane curl -s $CIP:$CIPPORT | jq
    
    # mypc에서 CLUSTER-IP:PORT 로 접속 가능할까?
    docker exec -it mypc curl -s $CIP:$CIPPORT
    
    
    # (옵션) 노드에서 Network Connection
    conntrack -E
    conntrack -L --any-nat
    
    # (옵션) 패킷 캡쳐 확인
    tcpdump..
  • 외부 클라이언트 → 서비스(NodePort) 접속 시 : 3개의 목적지(backend) 파드로 랜덤 부하 분산 접속됨
  • 웹 파드에서 접속자의 IP 정보 확인(logs) 시 외부 클라이언트IP 가 아닌, 노드의 IPSNAT 되어서 확인됨
중요 서비스를 처리하는 경우 법적인 보안 요구사항으로, 최초 접속자(외부 클라이언트)의 IP를 수집해야한다.
현재 상태에서는 노드의 IP로 SNAT 되어서 웹서버(파드)에서 수집을 할 수 없다. 어떻게 해결할 수 있을까?

 

[  IPTABLES 정책 확인 ]

  • iptables 정책 적용 순서 : PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-EXT-#(MARK)KUBE-SVC-# → KUBE-SEP-# ⇒ KUBE-POSTROUTING (MASQUERADE)
    컨트롤플레인 노드 - iptables 분석 << 정책 확인 : 아래 정책 내용은 핵심적인 룰(rule)만 표시했습니다!
    
    docker exec -it myk8s-control-plane bash
    ----------------------------------------
    
    # 패킷 카운트 초기화
    iptables -t nat --zero
    
    
    PREROUTING 정보 확인
    iptables -t nat -S | grep PREROUTING
    -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
    ...
    
    
    # 외부 클라이언트가 노드IP:NodePort 로 접속하기 때문에 --dst-type LOCAL 에 매칭되어서 -j KUBE-NODEPORTS 로 점프!
    iptables -t nat -S | grep KUBE-SERVICES
    -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-NODEPORTS 에서 KUBE-EXT-# 로 점프!
    ## -m nfacct --nfacct-name localhost_nps_accepted_pkts 추가됨 : 패킷 flow 카운팅 - 카운트 이름 지정 
    NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
    echo $NPORT
    
    iptables -t nat -S | grep KUBE-NODEPORTS | grep <NodePort>
    iptables -t nat -S | grep KUBE-NODEPORTS | grep $NPORT
    -A KUBE-NODEPORTS -d 127.0.0.0/8 -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 30898 -m nfacct --nfacct-name localhost_nps_accepted_pkts -j KUBE-EXT-VTR7MTHHNMFZ3OFS
    -A KUBE-NODEPORTS -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 30898 -j KUBE-EXT-VTR7MTHHNMFZ3OFS
    
    # (참고) nfacct 확인
    nfacct list
    ## nfacct flush # 초기화
    
    
    ## KUBE-EXT-# 에서 'KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000' 마킹 및 KUBE-SVC-# 로 점프!
    # 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" 반복 접속 후 아래 확인
    watch -d 'iptables -v --numeric --table nat --list KUBE-EXT-VTR7MTHHNMFZ3OFS'
    iptables -t nat -S | grep "A KUBE-EXT-VTR7MTHHNMFZ3OFS"
    -A KUBE-EXT-VTR7MTHHNMFZ3OFS -m comment --comment "masquerade traffic for default/svc-nodeport:svc-webport external destinations" -j KUBE-MARK-MASQ
    -A KUBE-EXT-VTR7MTHHNMFZ3OFS -j KUBE-SVC-VTR7MTHHNMFZ3OFS
    
    
    # KUBE-SVC-# 이후 과정은 Cluster-IP 와 동일! : 3개의 파드로 DNAT 되어서 전달
    iptables -t nat -S | grep "A KUBE-SVC-VTR7MTHHNMFZ3OFS -"
    -A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-Q5ZOWRTVDPKGFLOL
    -A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-MMWCMKTGOFHFMRIZ
    -A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport" -j KUBE-SEP-CQTAHW4MAKGGR6M2
    
    
    POSTROUTING 정보 확인
    # 마킹되어 있어서 출발지IP를 접속한 노드의 IP 로 SNAT(MASQUERADE) 처리함! , 최초 출발지Port는 랜덤Port 로 변경
    iptables -t nat -S | grep "A KUBE-POSTROUTING"
    -A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN  # 0x4000/0x4000 되어 있으니 여기에 매칭되지 않고 아래 Rule로 내려감
    -A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
    -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
    
    
    # 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" 반복 접속 후 아래 확인
    watch -d 'iptables -v --numeric --table nat --list KUBE-POSTROUTING;echo;iptables -v --numeric --table nat --list POSTROUTING'
    
    exit
    ----------------------------------------
  • 서비스(NodePort) 생성 시 kube-proxy 에 의해서 iptables 규칙이 모든 노드에 추가되는지 확인
    #
    NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
    docker exec -it myk8s-control-plane iptables -t nat -S | grep KUBE-NODEPORTS | grep $NPORT
    ...
    
    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
    ...

 

[ externalTrafficPolicy 설정 ]

  • externalTrafficPolicy: Local : NodePort 로 접속 시 해당 노드에 배치된 파드로만 접속됨, 이때 SNAT 되지 않아서 외부 클라이언트 IP가 보존됨!
    Node1:NodePort 접속시 Node1에 생성된 파드(Pod1)로만 접속됨 Node3에 파드가 없을 경우에 접속 시 연결 실패됨!
  • 외부 클라이언트의 IP 주소(아래 출발지IP: 50.1.1.1)가 노드의 IP로 SNAT 되지 않고 서비스(backend) 파드까지 전달됨!
  • 설정 및 파드 접속 확인
    # 기본 정보 확인
    kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'
      externalTrafficPolicy: Cluster
      internalTrafficPolicy: Cluster
    
    # 기존 통신 연결 정보(conntrack) 제거 후 아래 실습 진행하자! : (모든 노드에서) conntrack -F
    for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i conntrack -F; echo; done
    kubectl delete -f svc-nodeport.yaml
    kubectl apply -f svc-nodeport.yaml
    
    # externalTrafficPolicy: local 설정 변경
    kubectl patch svc svc-nodeport -p '{"spec":{"externalTrafficPolicy": "Local"}}'
    kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'
    	"externalTrafficPolicy": "Local",
      "internalTrafficPolicy": "Cluster",
    
    # 파드 3개를 2개로 줄임
    kubectl scale deployment deploy-echo --replicas=2
    
    # 파드 존재하는 노드 정보 확인
    kubectl get pod -owide
    
    # 파드 로그 실시간 확인 (웹 파드에 접속자의 IP가 출력)
    kubectl logs -l app=deploy-websrv -f
    
    
    # 외부 클라이언트(mypc)에서 접속 시도
    
    # 노드의 IP와 NodePort를 변수에 지정
    ## CNODE=<컨트롤플레인노드의 IP주소>
    ## NODE1=<노드1의 IP주소>
    ## NODE2=<노드2의 IP주소>
    ## NODE3=<노드3의 IP주소>
    CNODE=172.18.0.A
    NODE1=172.18.0.B
    NODE2=172.18.0.C
    NODE3=172.18.0.D
    CNODE=172.18.0.5
    NODE1=172.18.0.4
    NODE2=172.18.0.3
    NODE3=172.18.0.2
    
    ## NodePort 를 변수에 지정 : 서비스를 삭제 후 다시 생성하여서 NodePort가 변경되었음
    NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
    echo $NPORT
    
    # 서비스(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
    
    # 목적지 파드가 배치되지 않은 노드는 접속이 어떻게? 왜 그런가?
    docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
    docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE1:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
    docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE2:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
    docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE3:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
    
    # 파드가 배치된 노드에 NodePort로 아래 반복 접속 실행 해두자
    docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $NODE1:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
    혹은
    docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $NODE2:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
    혹은
    docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $NODE3:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
    
    # (옵션) 노드에서 Network Connection
    conntrack -E
    conntrack -L --any-nat
    # 패킷 캡쳐 확인
  • 외부 클라이언트 → 각각 노드2,3 접속 시 : 각각 노드에 생성된 파드로만 접속됨!
    for i in $CNODE $NODE1 $NODE2 $NODE3 ; do echo ">> node $i <<"; docker exec -it mypc curl -s --connect-timeout 1 $i:$NPORT; echo; done
  • 웹 파드에서 접속자의 IP 정보 확인(logs) 시 외부 클라이언트IP 가 그대로 확인됨
    $ kubectl logs -l app=deploy-websrv -f 확인 시 출력 내용, 외부 클라이언트 IP는 192.168.100.1
  • 다음 실습을 위해 오브젝트 삭제 kubectl delete svc,deploy --all

 

[ 서비스(NodePort) 부족한 점 ]

  • 외부에서 노드의 IP와 포트로 직접 접속이 필요함 → 내부망이 외부에 공개(라우팅 가능)되어 보안에 취약함LoadBalancer 서비스 타입으로 외부 공개 최소화 가능!
  • 클라이언트 IP 보존을 위해서, externalTrafficPolicy: local 사용 시 파드가 없는 노드 IP로 NodePort 접속 시 실패LoadBalancer 서비스에서 헬스체크(Probe) 로 대응 가능!