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 가 아닌, 노드의 IP로 SNAT 되어서 확인됨
중요 서비스를 처리하는 경우 법적인 보안 요구사항으로, 최초 접속자(외부 클라이언트)의 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) 로 대응 가능!
'Kubernetes' 카테고리의 다른 글
[ Kans 3 Study - 5w ] 2. MetalLB (2) | 2024.10.05 |
---|---|
[ Kans 3 Study - 5w ] 1. LoadBalancer (0) | 2024.10.05 |
[ Kans 3 Study - 4w ] 1. Service - ClusterIP (1) | 2024.09.22 |
[ Kans 3 Study - 4w ] 실습 환경 구성 (0) | 2024.09.22 |
[ Kans 3 Study - 3w ] 4. Calico 네트워크 모드 (1) | 2024.09.22 |