새소식

Kubernetes

[Study 4주차] 쿠버네티스 Service - NodePort

  • -

CoudNet@ 팀의 가시다님께서 리딩하시는 KANS Study (Kubernetes Advanced Networking Study) 4주차 스터디 내용 정리

 

 

이번 글에는 Service 타입 중 NodePort 에 대해서 알아보겠습니다.

 

 

1. Service NodePort

 

출처 : 가시다님 쿠버네티스 책 원고

 

출처 : 가시다님 쿠버네티스 책 원고

 

 

k8s 클러스터 외부에서 k8s 내부 파드에 접근하기 위해 사용되는 NodePort 입니다.
말 그대로 k8s 노드의 포트를 하나 뚫어서 외부와 통신이 가능하게 만드는 구조입니다.

 

외부 클라이언트가 NodeIP:NodePort 로 접속 시,

해당 노드의 iptables 규칙에 의해 SNAT/DNAT 되어 목적지 파드와 통신 후 리턴 트래픽은 최초 인입 노드를 경유하여 외부로 되돌아갑니다.

 

NodePort 의 기본 할당 포트 범위는 30000 ~ 32767 입니다.

 

 

1.1. 실습 환경 구성

 

실습은 kind 클러스터를 사용했습니다.

kind 클러스터 생성

cat <<EOT> kind-svc-1w.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  "InPlacePodVerticalScaling": true
  "MultiCIDRServiceAllocator": true
nodes:
- role: control-plane
  labels:
    mynode: control-plane
    topology.kubernetes.io/zone: ap-northeast-2a
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        runtime-config: api/all=true
    controllerManager:
      extraArgs:
        bind-address: 0.0.0.0
    etcd:
      local:
        extraArgs:
          listen-metrics-urls: http://0.0.0.0:2381
    scheduler:
      extraArgs:
        bind-address: 0.0.0.0
  - |
    kind: KubeProxyConfiguration
    metricsBindAddress: 0.0.0.0
- role: worker
  labels:
    mynode: worker1
    topology.kubernetes.io/zone: ap-northeast-2a
- role: worker
  labels:
    mynode: worker2
    topology.kubernetes.io/zone: ap-northeast-2b
- role: worker
  labels:
    mynode: worker3
    topology.kubernetes.io/zone: ap-northeast-2c
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24
EOT

# k8s 클러스터 설치
kind create cluster --config kind-svc-1w.yaml --name myk8s --image kindest/node:v1.31.0

# 노드에 기본 툴 설치
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping git vim arp-scan -y'
for i in worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done

# mypc 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 직접 지정
docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity

 

서비스 생성

 

# Pod 생성
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

# Service 생성
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

 

 

 

1.2. NodePort 접속 테스트

 

외부 클라이언트 (mypc 컨테이너) 에서 접속 테스트 & 서비스(NodePort) 부하분산 접속을 확인해보겠습니다.

 

# NodePort 확인
kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}'

# NodePort 를 변수에 지정
NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
echo $NPORT

# 노드의 IP와 NodePort를 변수에 지정
CNODE=172.18.0.3
NODE1=172.18.0.2
NODE2=172.18.0.4
NODE3=172.18.0.5

# MyPC 컨테이너에서 서비스(NodePort) 부하분산 접속 확인
docker exec -it mypc curl -s $CNODE:$NPORT | jq

 

 

NodePort 를 사용할 경우, Pod 가 없는 노드에서도 접속이 가능합니다.
현재, Control Plane 에는 Pod 가 없지만 노드포트로 접근할 경우 파드와 통신이 됩니다.

 

# Control Plane 의 노드포트로 100번 호출
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

 

 

또한 NodePort 타입일지라도 서비스의 ClusterIP 로 접근 시 통신이 잘 되는 것을 알 수 있습니다.

 

# ClusterIP 와 Port 를 변수로 등록
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

# 서비스 ClusterIP 호출
docker exec -it myk8s-control-plane curl -s $CIP:$CIPPORT | jq

 

 

하지만, 클러스터 외부에서는 ClusterIP 로 호출이 되지 않습니다.

docker exec -it mypc curl -s $CIP:$CIPPORT

 

 

 

1.3. NodePort iptables 확인

 

출처 : 가시다님 쿠버네티스 책 원고

 

출처 : 가시다님 쿠버네티스 책 원고



ClusterIP 의 iptables 와 다른 점은 KUBE-NODEPORT, KUBE-MARK-MASQ, POSTROUTING 이 추가된 것입니다.

 

외부에서 NodePort 를 통한 서비스 접근 시에는 출발지 IP 가 접근한 노드의 IP 로 변환(SNAT) 되어 목적지 파드로 접속하게 됩니다.
그렇기 때문에 목적지 파드에서 응답할 경우, 무조건 진입한 노드를 거치게 되는 단점이 있습니다.

 

iptables 확인

# Control Plane 노드에 접근
docker exec -it myk8s-control-plane bash

# 1단계) PREROUTING 테이블 정보 확인
iptables -v --numeric --table nat --list PREROUTING

# 2단계) KUBE-NODEPORTS 테이블 정보 확인
iptables -v --numeric --table nat --list KUBE-NODEPORTS

# 3단계) KUBE-EXT 테이블 정보 확인
iptables -v --numeric --table nat --list KUBE-EXT-VTR7MTHHNMFZ3OFS

# 4단계) KUBE-SVC 테이블 정보 확인
iptables -v --numeric --table nat --list KUBE-SVC-VTR7MTHHNMFZ3OFS

# 5단계) KUBE-SEP 테이블 정보 확인
iptables -v --numeric --table nat --list KUBE-SEP-XEXGJWEWSC2GPNPZ

# 6단계) POST-ROUTING 테이블 정보 확인
iptables -v --numeric --table nat --list KUBE-POSTROUTING

 

 

 

1.3. externalTrafficPolicy

 

externalTrafficPolicy 설정을 통해 NodePort 로 접속 시 해당 노드에 배치된 파드로만 접속할 수 있습니다.

출처 : 가시다님 쿠버네티스 책 원고

 

# 기본 정보 확인
kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'

 

 

externalTrafficPolicy 적용

 

kubectl patch svc svc-nodeport -p '{"spec":{"externalTrafficPolicy": "Local"}}'
kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'

 

 

# 파드 3개를 2개로 줄임
kubectl scale deployment deploy-echo --replicas=2

# pod 확인
kubectl get pod -o wide

 

 

이제 파드는 노드1, 노드3 에만 배포되어 있습니다.

# 파드가 배포되지 않은 컨트롤 플레인의 노드 포트로 접속 -> 실패
docker exec -it mypc curl -s --connect-timeout 1 $CNODE:$NPORT | jq

## 각 노드의 포트에 접근
# 컨트롤플레인
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

# 노드1
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE1:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

# 노드2
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE2:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

# 노드3
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE3:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

 

 

externalTrafficPolicy 로 인해 다른 노드의 파드로 부하 분산이 되지 않는 것을 알 수 있습니다.

 

externalTrafficPolicy 에는 치명적인 단점이 존재합니다.
바로 파드가 배포되지 않은 노드에 접근할 경우, 서비스 연결이 불가능하다는 것입니다.
또한, 파드가 있는 노드의 연결 트래픽이 가중될 수 있다는 점이 있습니다.

 

출처 : 가시다님 쿠버네티스 책 원고

 

 

1.4. Endpoint Slices

 

기존 Endpoints 는 단일 리소스로 모든 엔드포인트를 관리했지만 대규모 백엔드 파드를 운영할 경우,

Endpoints 에 상당한 네트워크 트래픽이 발생하여 성능 저하와 CPU 비용 발생의 원인이 되었습니다.


Endpoint Slices 를 사용하게 되면서 Endpoints 를 여러 조각으로 나누어 관리함으로써 Endpoints 의 단점을 해결할 수 있었습니다.
Endpoint Slices 로 인해 Dual Stack Networking 및 Topology Aware Routing 기능이 가능하게 되었습니다.

 

Endpoint Slice 예시

apiVersion: discovery.k8s.io/v1beta1
kind: EndpointSlice
metadata:
  name: demo-slice-1
  labels:
    kubernetes.io/service-name: demo
addressType: IPv4
ports:
  - name: http
    protocol: TCP
    port: 80
endpoints:
  - addresses:
      - "10.0.0.1"
    conditions:
      ready: true

 

Endpoint Slices 실습

kubectl get endpointslice

# Endpoint Slices Service 배포
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mario
  labels:
    app: mario
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mario
  template:
    metadata:
      labels:
        app: mario
    spec:
      tolerations:
      - key: "node-role.kubernetes.io/control-plane"
        effect: "NoSchedule"
      nodeSelector:
        node-role.kubernetes.io/control-plane: ""
      containers:
      - name: mario
        image: pengbai/docker-supermario
        readinessProbe:
          exec:
            command:
            - cat
            - healthcheck
---
apiVersion: v1
kind: Service
metadata:
  name: mario
spec:
  ports:
    - name: mario-webport
      port: 80
      targetPort: 8080
      nodePort: 30001
  selector:
    app: mario
  type: NodePort
  externalTrafficPolicy: Local
EOF

 

kubectl get pod,svc,ep,endpointslice -o wide

 

 

기존의 엔드포인트는 Probe 실패 시, IP 가 출력되지 않지만 엔드포인트 슬라이스는 IP 가 출력되는 차이점이 있습니다.

 

Contents

포스팅 주소를 복사했습니다