K8S 환경에서 클러스터내에 구성되어 있는 서비스를 외부에 노출하는 방법 중 하나인 NodePort 타입의 Service 를 구성하였을 경우 외부에서 접속하는 사용자의 요청이 어떤 처리과정을 거쳐 최종 목적지인 Pod 에 도달하게 되는지 간단하게 살펴 남겨보도록 하겠습니다.
K8S Service Networking(https://kubernetes.io/ko/docs/concepts/services-networking/service/) 에 대한 자세한 설명은 생력하고 여러가지 Service 옵션 중에 Node 에 특정 Port 를 Open 하여 서비스를 Open 하고자 하는 경우라면 아주 간단하게 아래와 같은 yaml 을 이용하여 NodePort 를 배포할 수 있습니다.
#ReplicaSet 배포
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: juiceshop
spec:
replicas: 3
selector:
matchLabels:
app: juiceshop
template:
metadata:
labels:
app: juiceshop
spec:
containers:
- name: juiceshop
image: bkimminich/juice-shop#Service NodePort 배포
apiVersion: v1
kind: Service
metadata:
name: juiceshop-nodeport
spec:
type: NodePort
ports:
- port: 80
targetPort: 3000
nodePort: 31001
selector:
app: juiceshop
이렇게 서비스를 배포하고 나면 아래의 그림과 같이 외부의 사용자나 Client Pod 에서 Node 의 IP 주소와 NodePort(31001) 를 이용하여 서비스에 접근할 수 있는 환경이 만들어집니다.
Client Pod 은 NodePort 가 아니더라도 ClusterIP 를 통해서 서비스에 접근할 수 있도록 구성할 수 있지만 NodePort 를 생성하면 ClusterIP 가 아닌 Node IP 를 이용해서도 접속할 수 있게 됩니다.
NodePort 는 기본적으로 Cluster 내의 서비스를 외부에 노출하기 위해서 사용하는 서비스이며 몇 가지 특징적인 내용들을 가지고 있습니다. 첫번째로, Node Port 는 외부에 노출할 수 있는 포트의 범위(30000~32767)가 정해져있습니다. 따라서, Node Port 를 생성할 때에는 이 범위 내에서 Port Number 를 정해줘야 합니다. 만일 별도로 Port Number 를 정하지 않는다면 이 범위내의 Port Number 가 임의로 할당되게 됩니다. 그리고 Node Port 는 배포를 하게되면 Cluster 내의 모든 Worker Node 에 동일한 설정이 반영이 되고 따라서 Pod 의 위치와 Worker Node 의 위치가 동일하지 않은 경우에도 사용자의 트래픽이 전달되는 상황이 발생할 수 있습니다. 이와 같은 상황은 방지하고자 한다면 즉, 동일한 Node 에 배포된 Pod 으로만 요청이 전달되도록 하고자 한다면 “externalTrafficPolicy” 를 “local” 로 해주시면 되겠습니다.
Node Port 를 배포하고 나면 실제로 외부의 사용자 요청을 Cluster 내의 Pod 으로 전달하는 역할은 iptables 가 수행을 하게 됩니다. K8S 는 관리자가 서비스를 배포하면 배포되는 서비스의 타입과 배포되는 Pod 의 현황에 따라 iptables 를 업데이트 하게 되는데요. 위와 같이 Node Port 서비스에 ReplicaSet = 3 으로 배포를 하게되면 실제로는 아래와 같은 iptables 가 생성되게 됩니다.
#kubectl 명령을 이용하여 Service 확인
admin-for-switch:~/environment $ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP Port
demo NodePort 10.100.92.88 <none> 80:31001/TCP #Worker Node 에서 31001 Port 를 Listen 하고 있는지 확인
[root@ip-192-168-53-148 ~]# netstat -na
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:31001 0.0.0.0:* LISTEN#iptables 중 Node Port 와 관련되어 있는 규칙 확인
-A KUBE-NODEPORTS -p tcp -m comment --comment “default/ecsdemo-frontend” -m tcp --dport 31001 -j KUBE-SVC-RLLYAW25GVFLXGYP# KUBE-SVC-RLLYAW25GVFLXGYP 와 관련한 규칙들 중 ClusterIP 와 관련한 규칙
:KUBE-SVC-RLLYAW25GVFLXGYP - [0:0]
-A KUBE-SERVICES -d 10.100.92.88/32 -p tcp -m comment --comment “default/ecsdemo-frontend cluster IP” -m tcp --dport 80 -j KUBE-SVC-RLLYAW25GVFLXGYP# KUBE-SVC-RLLYAW25GVFLXGYP 와 관련한 규칙들 중 Pod 으로 요청을 분기하기 위한
# 규칙들
-A KUBE-SVC-RLLYAW25GVFLXGYP -m comment --comment “default/ecsdemo-frontend” -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-TX2T7SUCBIUN3MWG
-A KUBE-SVC-RLLYAW25GVFLXGYP -m comment --comment “default/ecsdemo-frontend” -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-5ZY7MBULTQHAHDMO
-A KUBE-SVC-RLLYAW25GVFLXGYP -m comment --comment “default/ecsdemo-frontend” -j KUBE-SEP-IUZDMQ6R3VBLKQ53[# KUBE-SVC-RLLYAW25GVFLXGYP 의 각 규칙에 해당하는 KUBE-SEP-해쉬값 에 해당하는 # 규칙들
:KUBE-SEP-TX2T7SUCBIUN3MWG - [0:0]
-A KUBE-SEP-TX2T7SUCBIUN3MWG -s 192.168.19.88/32 -m comment --comment “default/ecsdemo-frontend” -j KUBE-MARK-MASQ
-A KUBE-SEP-TX2T7SUCBIUN3MWG -p tcp -m comment --comment “default/ecsdemo-frontend” -m tcp -j DNAT --to-destination 192.168.19.88:3000[root@ip-192-168-53-148 ~]# iptables-save | grep KUBE-SEP-5ZY7MBULTQHAHDMO
:KUBE-SEP-5ZY7MBULTQHAHDMO - [0:0]
-A KUBE-SEP-5ZY7MBULTQHAHDMO -s 192.168.32.150/32 -m comment --comment “default/ecsdemo-frontend” -j KUBE-MARK-MASQ
-A KUBE-SEP-5ZY7MBULTQHAHDMO -p tcp -m comment --comment “default/ecsdemo-frontend” -m tcp -j DNAT --to-destination 192.168.32.150:3000[root@ip-192-168-53-148 ~]# iptables-save | grep UBE-SEP-IUZDMQ6R3VBLKQ53
:KUBE-SEP-IUZDMQ6R3VBLKQ53 - [0:0]
-A KUBE-SEP-IUZDMQ6R3VBLKQ53 -s 192.168.94.6/32 -m comment --comment “default/ecsdemo-frontend” -j KUBE-MARK-MASQ
-A KUBE-SEP-IUZDMQ6R3VBLKQ53 -p tcp -m comment --comment “default/ecsdemo-frontend” -m tcp -j DNAT --to-destination 192.168.94.6:3000 (edited)
이 iptables 을 해석해보면 관리자가 NodePort 를 생성하게 되었을 때 사용자의 요청은 “KUBE-NODEPORTS, KUBE-SVC-해쉬값, KUBE-SEP-해쉬값”의 처리절차를 통해 최종적으로 Pod 에 전달된다는 것을 알 수 있습니다.
이와 같은 사용자 요청에 대한 처리절차를 그림으로 나타내면 아래와 같습니다.
아래 그림은 일반적인 개념을 나타낸 것이므로 위 iptables 의 IP 주소와 매칭되지 않습니다.
정리해보면 Node Port 서비스는 Cluster 의 서비스를 외부에 노출하기 위해 사용되는 서비스이지만 실제로 Node Port 서비스를 생성하면 Cluster IP 서비스도 기본적으로 생성이 된다는 것을 알 수 있고 또한 Node Port 로 유입되는 사용자의 요청은 여러 Worker Node 에 배포되어 있는 Pod 들로 분기될 수 있다는 것을 볼 수 있었습니다.
즉, Node Port 는 특정 Pod 와 Node 가 Listen 하고 있는 특정 Port 를 1:1 맵핑하는 것이 아니라 설정된 Node Port 에 유입되는 사용자의 요청을 여러 Worker Node 에 부하 분산하는 기능을 수행한다고 보시면 될 것 같습니다. 그리고 위에 언급드린 것처럼 여러 Work Node 가 아닌 특정 Node 에 한하여 부하를 분산하고자 한다면 externalTrafficPolicy = true 값을 선택해주시면 되겠습니다.
감사합니다.