Search

Service: NodePort

Tags
k8s
network
service
nodeport
Created
2024/09/28 21:23
Created time
2024/09/28 12:23
category
kubernetes

개요

NodePort 타입으로 생성한 Service로 접속을 시도하면서, iptables 규칙과 부하 분산 동작을 확인
externalTrafficPolicy: ClientIP로 설정하고, 클라이언트의 IP가 보존되는 것을 직접 확인

실습 준비

실습은 이전 글에서 준비한 클러스터를 이용

외부 노드 생성

# 클러스터 외부에서의 접속을 가정하여 추가 노드 생성 # 클러스터는 kind라는 도커 브릿지 인터페이스를 이용 # 외부 노드도 이를 하도록 하고, 해당 대역의 IP를 직접 지정하여 생성 docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity # 노드 생성 확인 docker ps | grep mypc
Shell
복사

목적지 Deployment 생성

# Deployment 정의한 manifest 생성 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 # manifest 적용 kubectl apply -f echo-deploy.yaml
Shell
복사

NodePort 타입 Service 생성

# NodePort 타입 manifest 생성 cat <<EOT> svc-nodeport.yaml apiVersion: v1 kind: Service metadata: name: svc-nodeport spec: ports: - name: svc-webport port: 9000 targetPort: 8080 selector: app: deploy-websrv type: NodePort EOT # manifest 적용 kubectl apply -f svc-nodeport.yaml
Shell
복사

리소스 확인

# 생성 확인 watch -d 'kubectl get po -o wide ; echo ; kubectl get svc,ep svc-nodeport' # Service가 사용하는 노드의 포트 확인 kubectl get svc svc-nodeport
Shell
복사

접속 시도

부하 분산

클라이언트 Pod를 이용하여 Service 접속 시 부하 분산되어 접속됨을 확인
# 노드의 포트를 확인 kubectl get svc svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}' # 노드의 포트를 변수에 할당 NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}') echo $NPORT # 노드들의 IP를 확인 kubectl get no -o wide # 노드들의 IP를 변수에 할당 CNODE=... NODE1=... NODE2=... NODE3=...
Shell
복사
# 각 노드에 요청을 보내면, 해당 노드가 모두 요청을 받긴 했으나 요청 처리는 보두 고르게 분배 됨을 확인 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"
Shell
복사

ClusterIP

NodePort는 ClusterIP를 포함한다고 했었는데, 실제로 Control Plane에서 <cluster-ip>:<port>로 접속해보면 접속이 되는 것을 볼 수 있음
# <cluster-ip>:<port>에 해당하는 값들을 변수에 할당 CLUSTER_IP=$(kubectl get service svc-nodeport -o jsonpath="{.spec.clusterIP}") SERVICE_PORT=$(kubectl get service svc-nodeport -o jsonpath="{.spec.ports[0].port}") echo $CLUSTER_IP $SERVICE_PORT
Shell
복사
# 클러스터에 속한 Control Plane에서는 ClusterIP를 이용하여 접속 가능 docker exec -it myk8s-control-plane curl -s $CLUSTER_IP:$SERVICE_PORT | jq
Shell
복사
# 클러스터 외부 노드에서는 당연하게도 ClusterIP로는 접속 불가 docker exec -it mypc curl -s $CLUSTER_IP:$SERVICE_PORT
Shell
복사

sNAT

부하 분산을 확인하려던 위 요청을 로그를 켜둔 채로 요청하여 확인해보면, 외부 노드에서 요청했음에도 Control Plane의 IP를 출발지로 사용하는 것을 볼 수 있음에 따라 ClusterIP와는 달리 sNAT도 이용하는 것을 짐작 가능
# 좌측 터미널에는 로그를 출력 kubectl logs -f -l app=deploy-websrv # 우측 터미널에는 외부 노드를 이용하여 Control Plane 노드로 트래픽을 요청 docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
Shell
복사

iptables

ClusterIP를 지원하기 때문에 대체로 동작이 비슷하나, ClusterIP와 달리 sNAT도 포함하기 때문에 KUBE-NODEPORTS, KUBE-MARK-MASQ, KUBE-POSTROUTING이 동작
결론적으론 PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-EXT-XXX → KUBE-MARK-MASQ → KUBE-SVC-XXX → KUBE-SEP-(#POD1|#POD2|#POD3) → POSTROUTING → KUBE-POSTROUTING 으로 정책이 적용되면서 부하를 분산
# Control Plane 접속 docker exec -it myk8s-control-plane bash
Shell
복사
# PREROUTING 체인의 규칙을 확인 # KUBE-SERVICE 체인으로 점프하는 규칙이 존재 iptables -t nat -nvL PREROUTING | column -t
Shell
복사
# KUBE-SERVICES 체인의 규칙을 확인 # ClusterIP는 첫 번째 규칙에 매칭되어 KUBE-SVC-XXX로 전달 # 그 외 NodePort는 KUBE-NODEPORTS 체인으로 매칭되어 점프 iptables -t nat -nvL KUBE-SERVICES | column -t
Shell
복사
# KUBE-NODEPORTS 체인의 규칙을 확인 # 노드의 지정된 포트로 인입되는 경우 KUBE-EXT-XXX로 전달 iptables -t nat -nvL KUBE-NODEPORTS | column -t
Shell
복사
# KUBE-EXT-XXX 체인의 규칙을 확인 # 패킷에 마킹을 위한 KUBE-MARK-MASQ로 점프 iptables -t nat -nvL KUBE-EXT-VTR7MTHHNMFZ3OFS | column -t
Shell
복사
# KUBE-MARK-MASQ 체인의 규칙을 확인 # 특별한 행동 없이 0x4000 마킹만 수행하고, KUBE-EXT-XXX에서 그 다음 규칙이었던 KUBE-SVC-XXX로 점프 iptables -t nat -nvL KUBE-MARK-MASQ | column -t
Shell
복사
# KUBE-SVC-XXX 체인의 규칙을 확인 # 여기서부터는 POSTROUTING 전까진 ClusterIP와 동일하게 동작하여 dNAT을 수행 iptables -t nat -nvL KUBE-SVC-VTR7MTHHNMFZ3OFS | column -t
Shell
복사
# KUBE-SEP-(#POD1|#POD2|#POD3) 체인의 규칙을 확인 # 각 엔드포인트들은 dNAT 체인으로 이어지는 것을 볼 수 있음 iptables -t nat -nvL KUBE-SEP-XEXGJWEWSC2GPNPZ | column -t iptables -t nat -nvL KUBE-SEP-R5FVJKGMGG2F5UME | column -t iptables -t nat -nvL KUBE-SEP-GEQNJ6BO5AOHB6LH | column -t
Shell
복사
# sNAT도 수행하는 것을 확인 # POSTROUTING 체인을 확인하여 KUBE-POSTROUTING 체인이 존재함을 확인 # KUBE-POSTROUTING에는 mark match ! 0x4000/0x4000 # 마킹을 거쳐가면서 RETURN 구문을 건너뛰면서 MASQUERADE를 수행하고, source의 포트는 랜덤하게 교체 iptables -t nat -nvL POSTROUTING | column -t iptables -t nat -nvL KUBE-POSTROUTING | column -t
Shell
복사

externalTrafficPolicy: Local

Service를 소개하는 글에서 externalTrafficPolicy에 대해 언급했었는데, 클라이언트 IP 주소의 수집이 필요한 경우 해당 설정을 이용하여 sNAT를 수행하지 않게 해서 이를 보존하는 것이 가능

초기화

# 연결 초기화 및 Service 재생성 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 # 노드의 포트를 확인 kubectl get svc svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}' # 노드의 포트를 변수에 할당 NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}') echo $NPORT # 노드들의 IP를 확인 kubectl get no -o wide # 노드들의 IP를 변수에 할당 CNODE=... NODE1=... NODE2=... NODE3=...
Shell
복사

설정 확인

# externalTrafficPolicy 및 internalTrafficPolicy를 확인 kubectl get svc svc-nodeport -o json | grep TrafficPolicy
Shell
복사

설정 변경

# externalTrafficPolicy의 값을 변경 kubectl patch svc svc-nodeport -p '{"spec":{"externalTrafficPolicy": "Local"}}'
Shell
복사
# Pod를 하나 삭제 kubectl scale deployment deploy-echo --replicas=2 # Pod 조회 kubectl get po -o wide
Shell
복사

접속 확인

# 좌측 터미널에는 로그를 출력 kubectl logs -f -l app=deploy-websrv # 우측 터미널에는 외부 노드를 이용하여 Control Plane 노드로 트래픽을 요청 # Pod가 존재하지 않는 Control Plane에서의 요청은 실패 docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr" # 우측 터미널에는 외부 노드를 이용하여 Pod가 존재하는 그 외 노드로 트래픽을 요청 # Pod가 존재하는 노드에서의 요청은 도달하며, sNAT 없이 클라이언트의 IP가 보존 docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE1:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
Shell
복사

iptables 확인

externalTrafficPolicy가 Cluster일 때와 달리 PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-EXT-XXX → KUBE-SVL-XXX → KUBE-SEP-XXX 로 구성
KUBE-MARK-MASQ 과정과 POSTROUTING 이후 과정이 삭제되고, KUBE-SVC-XXX 대신 KUBE-SVL-XXX가 단일 KUBE-SEP-XXX과 연결되어 있음
# Pod가 존재하는 노드에 접속하여 확인 docker exec -it myk8s-worker bash
Shell
복사
Pod가 존재하지 않는 노드는 externalTrafficPolicy가 Cluster인 일반적인 iptables 체인과 유사한데, Pod가 존재하지 않기 때문에 KUBE-MARK-DROP 후 KUBE-FIREWALL로 차단되는 절차를 가짐
# PREROUTING 체인의 규칙을 확인 # KUBE-SERVICE 체인으로 점프하는 규칙이 존재 iptables -t nat -nvL PREROUTING | column -t
Shell
복사
# KUBE-SERVICES 체인의 규칙을 확인 # ClusterIP는 첫 번째 규칙에 매칭되어 KUBE-SVC-XXX로 전달 # 그 외 NodePort는 KUBE-NODEPORTS 체인으로 매칭되어 점프 iptables -t nat -nvL KUBE-SERVICES | column -t
Shell
복사
# KUBE-NODEPORTS 체인의 규칙을 확인 # 노드의 지정된 포트로 인입되는 경우 KUBE-EXT-XXX로 전달 iptables -t nat -nvL KUBE-NODEPORTS | column -t
Shell
복사
# KUBE-EXT-XXX 체인의 규칙을 확인 # 패킷 마킹 단계 없이 KUBE-SVL-XXX로 바로 점프 iptables -t nat -nvL KUBE-EXT-VTR7MTHHNMFZ3OFS | column -t
Shell
복사
# KUBE-SVL-XXX 체인의 규칙을 확인 # 단일 KUBE-SEP-XXX 규칙만 존재 iptables -t nat -nvL KUBE-SVL-VTR7MTHHNMFZ3OFS | column -t
Shell
복사
# KUBE-SEP-XXX 체인의 규칙을 확인 # 엔드포인트는 dNAT 체인으로 이어지는 것을 볼 수 있음 iptables -t nat -nvL KUBE-SEP-R5FVJKGMGG2F5UME | column -t
Shell
복사
# dNAT만을 이용하는 것을 확인 # POSTROUTING 체인을 확인하여 KUBE-POSTROUTING의 체인이 존재함을 확인 # KUBE-POSTROUTING에는 mark match ! 0x4000/0x4000 # 마킹되어 있지 않은 상태임에 따라 RETURN 되어 sNAT은 수행되지 않음 iptables -t nat -nvL POSTROUTING | column -t iptables -t nat -nvL KUBE-POSTROUTING | column -t
Shell
복사

리소스 삭제

kubectl delete svc,deploy --all
Shell
복사