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