KANS3 - k8s Advanced Networking Study

4주차 : Service : ClusterIP, NodePort

daniel00324 2024. 9. 26. 19:30
더보기

※ 본 게재 글은 gasida님의 KANS(Kubernetes Advanced Networking Study) 강의내용과 실습예제 및 kubernetes, Calico 공식 가이드 문서, "쿠버네티스입문" 서적 등을 참고하여 작성하였습니다. 

 

1. Kubernete Service란?

- Kubenetes node에 생성되는 POD 들은 Private IP가 할당 되는데, 이는 POD의 lifecycle에 따라

  생성,삭제를 반복하면서 변경된다. 따라서, node 외부에서 유동적인 POD에 고정적으로 접속하기 위해

  Service 란 개념이 생겨났다.

☞ 한마디로 정의하면, POD 집합에서 실행중인 Application을 외부에서 접속할 수 있도록 네트웍을

    외부로 노출시키는 추상화 기법이다.


1-1. 서비스의 종류

- Service 에는 3가지 대표 type이 존재한다.

 1) Cluster IP

  - 클러스터 안에 있는 다른 Pod들이 접근할 수 있도록 IP를 할당한다.

  - 내부 IP만을 할당하기 때문에 클러스터 외부에서는 접근이 불가능하다.

[ Cluster IP 내부 통신방식 ]

 2) Node Port

   - 고정 포트로 Pod이 배포된 노드들의 IP에 서비스를 노출시킨다.

   - NodePort 서비스는 ClusterIP 서비스를 자동으로 생성한다.

   - NodeIP:NodePort 형태로 요청하여 클러스터 외부에서 NodePort 서비스에 접근할 수 있다.

[ Node Port 의 통신방식 ]

 

 3) Load Balancer

   - 로드 밸런서는 서비스를 외부에 노출시키는 표준 방법이다.

   - 구성에 물리 장비가 필요하다.

[ LoadBalancer의 통신방식 ]

 


2. 서비스 동작상세

1) 로드밸런싱

 

 

♣ 외부 클라이언트에서 서비스를 통해 노드 내부의 POD

   들에 접속이 가능하며, 이때 그림과 같이 내부 iptables 의

   random 방식의 traffic 분산 connection이 맺어지게 된다.

 

 

 

2) 포트와 레이블, 셀렉터

 

 

♣ 서비스 생성 시, Port 와 Target Port, Label, Selector

   같이 생성되며, 이를 통해 외부에서 특정 application이

   떠 있는 POD를 선택하여 접속할 수 있다.

 


3. Kube-Proxy 모드

▼ 그림으로 보는 kube-proxy 동작방식의 이해 - Ref. Link

더보기

Phase I :

API ServerEndpoint 라는 추상화 계층을 생성하고, svc01 2개의 Endpoint , PODs 연동된다.

 

 

 

Phase II : 

이 모든 구성은 컨트롤 플레인의 일부분이며, 이것이 적용되었을 때 트래픽은 SVC01의 IP로 오는 트래픽을

EP01, EP02로 전달된다. 이 과정에서 각 노드에 있는 kubu-proxy가 연결 중계자 역할을 하게 된다.

 

Phase III : 

이제 SVC01로 향하던 트래픽은 DNAT 규칙을 따라가게 되고 파드로 연결되며,

EP01, EP02는 기본적으로 POD의 IP임을 잊지 말라!!

  

kube-proxydaemon-set 형태로 운영되며, 통신 동작의 설정을 관리합니다. 설치 후 API 서버와의 인증을 거쳐 새로운 서비스나

   엔드포인트가 추가/삭제되면, API 서버는 이러한 변화를 kube-proxy 에게 전달합니다. 이를 전달 받은 kube-proxy는 NAT 규칙에

   반영하고, 서비스가 해당 룰에 따라 내부 POD로 traffic을 전달하게 됩니다.

♣ User-Space 모드 ( 1주차 docker - 3.2.4. user space 상세참조) 에서 3가지 (Proxy, IPtables Proxy, IPVS Proxy) 방식으로 동작함 

 

◈ Tip & Tips : kube-proxy에 Round-Robin DNS를 사용하지 않는 이유?!

더보기
  • 역사적 관행 고려 (검증된 방식 선호)
    : 전통적으로 레코드 TTL을 고려하지 않고, 만료된 이름 검색 결과를 캐싱하는 DNS 구현을 해 왔다.
  • 일부 어플리케이션 동작 특성고려
    : 일부 앱은 DNS 검색을 한 번만 수행하고 결과를 무기한으로 캐시하는 경우가 있다.
  • 관리 측면의 효율성 고려
    : 앱과 라이브러리가 적절히 재확인을 했다고 하더라도, DNS 레코드의 TTL이 낮거나 0이면
      DNS에 부하가 높아 지면 관리하기가 어려워 질 수 있다.

1) User-Space Proxy 모드

 

♣ 외부 클라이언트로부터 layer-2 계층의 NIC를 거쳐

   커널영역 넷필터와 사용자 영역의 kube-proxy까지

   패킷이 전달되며, kube-proxy의 NAT 처리에 의해

   연동된 라우팅 규칙을 적용하여 목적지 파드까지 이동함 

2) IPtables Proxy 모드

커널 영역운영체제 실행을 위한 메모리 공간이며,

   사용자 영역어플리케이션이 동작하는 메모리 공간이다.

IPtables호스트의 방화벽/NAT 역활을 하며, 

   사용자 영역에서 지정하는 IPtables의 규칙을 커널영역의

   NetFilter를 통해서 Traffic 통제를 하게 된다.

※ IPtables로 인해 관리가 어려움

3) IPVS Proxy 모드

 

♣ IPVSNetFilter에서 동작하는 Layer4 로드벨런서

   이며, IPtables보다 이해가 쉽고, 규칙을 줄일 수 있다.

♣ 다양한 분산 알고리즘을 제공하여, 3가지 모드 중

   가장 실무에 적합한 모드이다.

 

4) nftables 프록시 모드

 - (nftables API → netfilter subsystem) - https://netfilter.org/projects/nftables/

 - 해당 모드는 리눅스 모드와 kernel ver.5.1.3 이상에서만 사용가능한 제약사항이 있다.

Traffic 처리 시, kube-proxythe kernel netfilter subsystemnftables API를 통해 forwarding 한다.

♣ 기본적인 부하분산은 random logic을 사용한다.

♣ Kubernetes 1.31 버전 이상에서 지원하며, 아직까지 사용 reference가 많지 않아 Beta version 정도로 이해하고

   좀 더 많은 적용 사례를 확보 후 실무에 적용할 것을 권장한다.

 

5) eBPF 모드 + XDP

 - 기존의 네트웍 모드에서 한 단계 진보한 형태로, 통신 시 오버헤드가 큰  kernel Structure 할당을 우회하여 

   패킷 traffic 처리가 빨라 차세대 기술에서 활용범위가 넓다.

 

 

 

♣ 외부에서 Packet 이 NIC를 통해 수신되면,

   L2-layer 에서 L4-layer를 거쳐 Socket layer

  까지 갔다가, 역으로 L3-layer를 거쳐 L2-layer(NIC)

  에 도달하는 경로를 통해 traffic이 처리된다.

 ( Packet Traffic 처리를 위한 effort-loss 가 많은 구조 )

 

 

 

 

 

 

♣ 기존 netfilter/iptables 통신과 달리 L2-layer 단에

    구성된 eBPF/XDP Filter를 통해 Traffic 이 처리되므로,

    상당히 빠른 통신 구현이 가능하다!!

 

 

 

 


4. 실습하기 (따라하며 배우기)

☆ 이제 실습을 통해 정리된 내용들에 대해 이해를 하도록 하자!!

 

[ 실습환경 구성 ]  

 - vagrant + kind 환경 구성 ( gasida 님 2주차 - 실습환경 구성 참조 )

 - vagrant 기동 실패 시, 2주차 Trouble-shooting 부분 참조

 

▶ 실습환경 개요 및 상세코드

더보기

K8S v1.31.0 , CNI(KindnetDirect Routing mode) , IPTABLES proxy mode - 노드(실제로는 컨테이너네트워크 대역 : 172.18.0.0/16 - 파드 사용 네트워크 대역 : 10.10.0.0/16 ⇒ 각각 10.10.1.0/24, 10.10.2.0/24, 10.10.3.0/24, 10.10.4.0/24 - 서비스 사용 네트워크 대역 : 10.200.1.0/24

https://kubernetes.io/docs/concepts/cluster-administration/networking/
https://blog.naver.com/yu3papa

 [ 상세 코드 ]

#
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
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        runtime-config: api/all=true
- role: worker
  labels:
    mynode: worker1
- role: worker
  labels:
    mynode: worker2
- role: worker
  labels:
    mynode: worker3
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 ps

# 노드에 기본 툴 설치
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


# k8s v1.31.0 버전 확인
kubectl get node

# 노드 labels 확인
kubectl get nodes -o jsonpath="{.items[*].metadata.labels}" | grep mynode
kubectl get nodes -o jsonpath="{.items[*].metadata.labels}" | jq | grep mynode

# kind network 중 컨테이너(노드) IP(대역) 확인 : 172.18.0.2~ 부터 할당되며, control-plane 이 꼭 172.18.0.2가 안될 수 도 있음
docker ps -q | xargs docker inspect --format '{{.Name}} {{.NetworkSettings.Networks.kind.IPAddress}}'
/myk8s-control-plane 172.18.0.4
/myk8s-worker 172.18.0.3
/myk8s-worker2 172.18.0.5
/myk8s-worker3 172.18.0.2

# 파드CIDR 과 Service 대역 확인 : CNI는 kindnet 사용
kubectl get cm -n kube-system kubeadm-config -oyaml | grep -i subnet
      podSubnet: 10.10.0.0/16
      serviceSubnet: 10.200.1.0/24
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"

# feature-gates 확인 : https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/
kubectl describe pod -n kube-system | grep feature-gates
      --feature-gates=InPlacePodVerticalScaling=true
kubectl describe pod -n kube-system | grep runtime-config
      --runtime-config=api/all=true

# MultiCIDRServiceAllocator : https://kubernetes.io/docs/tasks/network/extend-service-ip-ranges/
kubectl get servicecidr
NAME         CIDRS           AGE
kubernetes   10.200.1.0/24   2m13s


# 노드마다 할당된 dedicated subnet (podCIDR) 확인
kubectl get nodes -o jsonpath="{.items[*].spec.podCIDR}"
10.10.0.0/24 10.10.4.0/24 10.10.3.0/24 10.10.1.0/24

# kube-proxy configmap 확인
kubectl describe cm -n kube-system kube-proxy
...
mode: iptables
iptables:
  localhostNodePorts: null
  masqueradeAll: false
  masqueradeBit: null
  minSyncPeriod: 1s
  syncPeriod: 0s
...


# 노드 별 네트워트 정보 확인 : CNI는 kindnet 사용
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ls /opt/cni/bin/; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i cat /etc/cni/net.d/10-kindnet.conflist; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c route; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c addr; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c -4 addr show dev eth0; echo; done

# iptables 정보 확인
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-control-plane  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker2 iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker3 iptables -t $i -S ; echo; done

# 각 노드 bash 접속
docker exec -it myk8s-control-plane bash
docker exec -it myk8s-worker bash
docker exec -it myk8s-worker2 bash
docker exec -it myk8s-worker3 bash
----------------------------------------
exit
----------------------------------------

# kind 설치 시 kind 이름의 도커 브리지가 생성된다 : 172.18.0.0/16 대역
docker network ls
docker inspect kind

# arp scan 해두기
docker exec -it myk8s-control-plane arp-scan --interfac=eth0 --localnet

# mypc 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 직접 지정
docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity
docker ps
## 만약 kind 네트워크 대역이 다를 경우 위 IP 지정이 실패할 수 있으니, 그냥 IP 지정 없이 mypc 컨테이너 기동 할 것
## docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity

# 통신 확인
docker exec -it mypc ping -c 1 172.18.0.1
for i in {1..5} ; do docker exec -it mypc ping -c 1 172.18.0.$i; done
docker exec -it mypc zsh
-------------
ifconfig
ping -c 1 172.18.0.2
exit
-------------

# kube-ops-view 설치
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=NodePort,service.main.ports.http.nodePort=30000 --set env.TZ="Asia/Seoul" --namespace kube-system

# myk8s-control-plane 배치
kubectl -n kube-system edit deploy kube-ops-view
---
spec:
  ...
  template:
    ...
    spec:
      nodeSelector:
        mynode: control-plane
      tolerations:
      - key: "node-role.kubernetes.io/control-plane"
        operator: "Equal"
        effect: "NoSchedule"
---

# 설치 확인
kubectl -n kube-system get pod -o wide -l app.kubernetes.io/instance=kube-ops-view

# kube-ops-view 접속 URL 확인 (1.5 , 2 배율) : macOS 사용자
echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=1.5"
echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=2"

# kube-ops-view 접속 URL 확인 (1.5 , 2 배율) : Windows 사용자
echo -e "KUBE-OPS-VIEW URL = http://192.168.50.10:30000/#scale=1.5"
echo -e "KUBE-OPS-VIEW URL = http://192.168.50.10:30000/#scale=2"

 

 

▶ 실행결과 한 눈에 보기

 


4-1. Cluster IP 실습

요약 : 클라이언트(TestPod)가 'CLUSTER-IP' 접속 시 해당 노드의 iptables 룰(랜덤 분산)에 의해서 DNAT 처리가 되어 목적지(backend) 파드와 통신이 가능하게 된다.

 

[ iptables 분산룰(정책)은 모든 노드에 자동으로 설정됨 ]

 

[ 10.96.0.1 은 CLUSTER-IP 주소 ]

 

♣  클러스터 내부에서만 'CLUSTER-IP' 로 접근 가능 ⇒ 서비스에 DNS(도메인) 접속도 가능

  서비스(ClusterIP 타입) 생성하게 되면, apiserver → (kubelet) → kube-proxy → iptables 에 rule(룰)이 생성

  모드 노드(마스터 포함)에 iptables rule 설정되므로, 파드에서 접속 시 해당 노드에 존재하는 iptables rule 에 의해서 분산 접속이 됨

 

▶ 실습환경 개요 및 상세코드

더보기

Step1. 목적지(backend) 파드(Pod) 생성 : 3pod.yaml

cat <<EOT> 3pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: webpod1
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: webpod2
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker2
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: webpod3
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker3
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
EOT

 

Step2. 클라이언트(TestPod) 생성 : netpod.yaml

cat <<EOT> netpod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: net-pod
spec:
  nodeName: myk8s-control-plane
  containers:
  - name: netshoot-pod
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOT

 

Step3. 서비스(ClusterIP) 생성 : svc-clusterip.yaml

 ※ spec.ports.port 와 spec.ports.targetPort 가 어떤 의미인지 꼭 이해하자!

cat <<EOT> svc-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  ports:
    - name: svc-webport
      port: 9000        # 서비스 IP 에 접속 시 사용하는 포트 port 를 의미
      targetPort: 80    # 타킷 targetPort 는 서비스를 통해서 목적지 파드로 접속 시 해당 파드로 접속하는 포트를 의미
  selector:
    app: webpod         # 셀렉터 아래 app:webpod 레이블이 설정되어 있는 파드들은 해당 서비스에 연동됨
  type: ClusterIP       # 서비스 타입
EOT

 

Step4. 생성 및 확인

# 모니터링
watch -d 'kubectl get pod -owide ;echo; kubectl get svc,ep svc-clusterip'

# 생성
kubectl apply -f 3pod.yaml,netpod.yaml,svc-clusterip.yaml

# 파드와 서비스 사용 네트워크 대역 정보 확인 
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"

# 확인
kubectl get pod -owide
kubectl get svc svc-clusterip

# spec.ports.port 와 spec.ports.targetPort 가 어떤 의미인지 꼭 이해하자!
kubectl describe svc svc-clusterip

# 서비스 생성 시 엔드포인트를 자동으로 생성, 물론 수동으로 설정 생성도 가능
kubectl get endpoints svc-clusterip
kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip

 

 [ 실행 결과 - 한 눈에 보기 ]

  

서비스(ClusterIP) 접속 확인

  - 클라이언트(TestPod) Shell 에서 접속 테스트 & 서비스(ClusterIP) 부하분산 접속 확인

더보기

 

# webpod 파드의 IP 를 출력
kubectl get pod -l app=webpod -o jsonpath="{.items[*].status.podIP}"

# webpod 파드의 IP를 변수에 지정
WEBPOD1=$(kubectl get pod webpod1 -o jsonpath={.status.podIP})
WEBPOD2=$(kubectl get pod webpod2 -o jsonpath={.status.podIP})
WEBPOD3=$(kubectl get pod webpod3 -o jsonpath={.status.podIP})
echo $WEBPOD1 $WEBPOD2 $WEBPOD3

# net-pod 파드에서 webpod 파드의 IP로 직접 curl 로 반복 접속
for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod; done
for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod | grep Hostname; done
for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod | grep Host; done
for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod | egrep 'Host|RemoteAddr'; done

# 서비스 IP 변수 지정 : svc-clusterip 의 ClusterIP주소
SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
echo $SVC1

# 위 서비스 생성 시 kube-proxy 에 의해서 iptables 규칙이 모든 노드에 추가됨 
docker exec -it myk8s-control-plane iptables -t nat -S | grep $SVC1
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i iptables -t nat -S | grep $SVC1; echo; done
-A KUBE-SERVICES -d 10.200.1.52/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF

## (참고) ss 툴로 tcp listen 정보에는 없음 , 별도 /32 host 라우팅 추가 없음 -> 즉, iptables rule 에 의해서 처리됨을 확인
docker exec -it myk8s-control-plane ss -tnlp
docker exec -it myk8s-control-plane ip -c route

# TCP 80,9000 포트별 접속 확인 : 출력 정보 의미 확인
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname

# 서비스(ClusterIP) 부하분산 접속 확인
## for 문을 이용하여 SVC1 IP 로 100번 접속을 시도 후 출력되는 내용 중 반복되는 내용의 갯수 출력
## 반복해서 실행을 해보면, SVC1 IP로 curl 접속 시 3개의 파드로 대략 33% 정도로 부하분산 접속됨을 확인
kubectl exec -it net-pod -- zsh -c "for i in {1..10};   do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
kubectl exec -it net-pod -- zsh -c "for i in {1..1000}; do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
혹은
kubectl exec -it net-pod -- zsh -c "for i in {1..100};   do curl -s $SVC1:9000 | grep Hostname; sleep 1; done"
kubectl exec -it net-pod -- zsh -c "for i in {1..100};   do curl -s $SVC1:9000 | grep Hostname; sleep 0.1; done"
kubectl exec -it net-pod -- zsh -c "for i in {1..10000}; do curl -s $SVC1:9000 | grep Hostname; sleep 0.01; done"


# conntrack 확인
docker exec -it myk8s-control-plane bash
----------------------------------------
conntrack -h
conntrack -E
conntrack -C
conntrack -S
conntrack -L --src 10.10.0.6 # net-pod IP
conntrack -L --dst $SVC1     # service ClusterIP
exit
----------------------------------------

# (참고) Link layer 에서 동작하는 ebtables
ebtables -L

 

 [ 실행 결과 - 한 눈에 보기 ]

 


4-3. 세션 Affinity 실습

개요 : 세션 Affinity 는 PODs 에 구성 된 application 접속 시, 최초 접속한 경로와 대상을 기록해 두었다가

          동일 Client가 접속 시 기존 접속한 Pod의 health 상태를 체크하여 정상일 경우, 해당 Pod로 세션

          연결해 준다. ( Cluster Service IP의 random 부하분산 방식을 효율적으로 처리하기 위한 로직 )   

 

 

▼ 설정 및 파드 접속 확인

더보기
# 기본 정보 확인
kubectl get svc svc-clusterip -o yaml
kubectl get svc svc-clusterip -o yaml | grep sessionAffinity

# 반복 접속
kubectl exec -it net-pod -- zsh -c "while true; do curl -s --connect-timeout 1 $SVC1:9000 | egrep 'Hostname|IP: 10|Remote'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"

# sessionAffinity: ClientIP 설정 변경
kubectl patch svc svc-clusterip -p '{"spec":{"sessionAffinity":"ClientIP"}}'
혹은
kubectl get svc svc-clusterip -o yaml | sed -e "s/sessionAffinity: None/sessionAffinity: ClientIP/" | kubectl apply -f -

#
kubectl get svc svc-clusterip -o yaml
...
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800
...

# 클라이언트(TestPod) Shell 실행
kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
kubectl exec -it net-pod -- zsh -c "for i in {1..1000}; do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

 

[ 실행 결과 - 한 눈에 보기 ]

바로 이전 Cluster IP 테스트 시, random 분산과 다르게 특정 webpod# 에 고정적(100%) 세션 할당되는 것을 볼 수 있다!!

  

※ 다음 실습을 위해 반드시 자원을 정리해 주세요!!! 

 - # kubectl delete svc, pods --all


4-3. Node Port 실습

요약 : 외부 클라이언트가 '노드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) & 변경하기 - 링크

 

▶ 실습환경 개요 및 상세코드

더보기

Step1. 목적지(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

 

Step2. 서비스(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

 

Step3. 생성 및 확인

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

 

 [ 결과 확인 - 한 눈에 보기 ]

  

▶ 설정 및 파드 접속 확인

더보기

Step1. 외부 클라이언트(mypc 컨테이너)에서 접속 테스트 & 서비스(NodePort) 부하분산 접속 확인

[ 노드 포트 Traffic flow 이해도 ]

 [ 테스트 시나리오 ]

 1. 출발지로 가상머신 (192.168.10.200) 을 생성하면 random Port 를 할당받는다.

     목적지인 마스터 노드는 192.168.10.10 이고, Nodeport 는 30286을 사용한다.

 

 2. 마스터 노드의 iptables 에 등록된 NAT 룰에 따라 목적지의 대상 IP/Port가 변환된다.

     부하 방식은 random load-balancing 이다. (목적지 IP는 'app=deploy-websrv' 레이블)

 

3.  부하 분산 시, workernode #1 에 할당된 Pod(172.16.158.1:8080) 로 선택이 된다.

     이 때, 최종 목적지 Pod에서 관찰되는 Client IP와 Port는 중간 마스터노드의 IP와

     Port가 된다.

 

[ 실습 환경 구성 - 개요도 ]

[ https://kschoi728.tistory.com/262

 

▼ 세부 실습 Source 코드

 - mypc 생성 추가

# kind 설치 시 kind 이름의 도커 브리지가 생성된다 : 172.18.0.0/16 대역
docker network ls
docker inspect kind

# arp Scan 걸어둔다.
docker exec -it myk8s-control-plane arp-scan --interfac=eth0 --localnet

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

## 만약 kind 네트워크 대역이 다를 경우 위 IP 지정이 실패할 수 있으니, 그냥 IP 지정 없이 mypc 컨테이너 기동 할 것
## docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity

 

# 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..

 

 [ 실행 결과 - 한 눈에 보기 ]

  


[ 마무리 ]

이번 과정을 통해 Cluster에서 사용하는 "Service"에 대한 개념과 이의 중심에 있는 kube-proxy, 대표적인 서비스 제공 entity 별 특성에

대해 이해 할 수 있었습니다.

네트웍 iptables 연동 내용을 따라가는 것은 아직까지 좀 힘든 부분이 있네요 ~ ^^;;

이번 돌아오는 수업과정에서 새로운 도전을 또 한번 기대 해 봅니다~


[ 도움이 되는 링크모음 ]

더보기