[KANS] 3기 4주 – Service : ClusterIP, NodePort

CloudNet@팀에서 진행하는 쿠버네티스 네트워크 스터디 3기 참가글입니다.

‘쿠버네티스’ 네트워크에 대해서 직접 실습을 통해 장애 시 해결 능력을 갖추고, 학습 내용을 전파 및 공유합니다!


1. 개요

클러스터에서 실행 중인 애플리케이션을 하나의 외부를 향하는 엔드포인트 뒤에 노출합니다. (워크로드가 여러 백엔드에 걸쳐 분할된 경우에도)

Expose an application running in your cluster behind a single outward-facing endpoint, even when the workload is split across multiple backends.

Service, k8s doc

쿠버네티스에서 서비스는 하나(또는 그이상의) 파드로 구성된 어플리케이션의 진입점 역할을 하는 오브젝트이다.

Using a Service to Expose Your App


위의 그림은 10.10.10.1~3과 10.10.10.5 의 ip를 갖는 4개의 파드가, 1.1.1.1인 서비스 리소스를 통해 접근될 수 있는 모습을 설명한 그림이다.

쿠버네티스상의 파드는 가변적인 존재이다. 배포(업데이트), 스케일링(노드변동), 장애(OOM)등으로 인해 언제든지 제거되고 재생성될 수 있다. 문제는 그때마다 파드의 ip의 변동이 일어나게되고, 해당 어플리케이션을 의존하는 다른어플리케이션들 또한 변경된 정보를 다시 읽어오거나, 경우에 따라 재시작 되어야한다.

이를 조금더 유연하게 사용하기 위해, 파드 앞단에 서비스 오브젝트를 배치할 수 있다. 서비스는 그 자체의 별도의 ip를 갖고, 뒷단의 워크로드의 정보를 알고있기 때문에 뒷단의 파드의 변화에 상관없이 안정적인 진입점 역할을 수행할 수 있다. 이번 챕터는 이러한 서비스 오브젝트의 설정과 그 구현에 대해 살펴볼것이다.

1.1. 파드를 클러스터 외부로 노출하는 3가지 방법

클러스터내에 존재하는 파드를 클러스터 외부로 노출하기에는 3가지 방법이 있다.

  1. Pod 직접 노출

첫번째는 파드를 직접 외부로 노출하는것이다.

그러나 파드는 가변적인 특징이있고, 재생성시마다 IP가 바뀐다는 문제가 존재한다.

2. 서비스(ClusterIP Type) 연결 : K8S 클러스터 내부에서만 접속

동일한 애플리케이션의 다수의 파드의 접속을 용이하게 하기 위한 서비스에 접속

고정 접속(호출) 방법을 제공 : 흔히 말하는 ‘고정 VirtualIP’ 와 ‘Domain주소’ 생성

다수의 파드를 하나의 서비스 오브젝트로 묶어 제공할 수 있다. 서비스가 제공하는 DNS이름에 따라 뒷단 pod들의 ip들이 유동적으로 업데이트가 되기때문에 고정진입점으로서의 역할을 수행할 수 있다

그러나 해당 DNS는 클러스터 내부의 자체적인 규칙에 의존하기때문에, 외부에서의 통신에서는 사용할 수 없다는 단점이 있다.

3. 서비스(NodePort Type) 연결 : 외부 클라이언트가 서비스를 통해서 클러스터 내부의 파드로 접속

서비스(NodePort Type)의 일부 단점을 보완한 서비스(LoadBalancer Type)

파드가 존재하는 노드에 별도의 포트를 개방한뒤, 해당 노드포트를 통해 클러스터내부의 서비스에 접근할 수 있다.

1.2. 서비스의 종류

서비스들의 종류는 아래 그림과 같이, 실제 노드에서 트래픽을 라우팅하는 iptables 룰이 어디에 존재하고, 어디로 지정하는가에 따라 다음의 3가지 유형으로 나누어 볼 수 있다.

1.2.1. ClusterIP 타입

1.2.2. NodePort 타입

1.2.3. LoadBalancer 타입

1.3. kube-proxy모드에 관하여

k8s 아키텍쳐에서, kube-proxy는 각 노드에 상주하는 네트워크 데몬이다. 해당 데몬은 데몬셋으로 배포되므로 모든 노드에 생성되며, userspace mode, iptables mode, IPVS mode 3가지 모드 중 하나의 설정을 갖는다 (default는 iptables)

https://kubernetes.io/docs/reference/networking/virtual-ips/#proxy-modes

1.3.1. Userspace Proxy 모드

geekforgeeks

  • Redirect, DNAT를 통해서 Service로 전송한 모든 요청 Packet은 kube-proxy로 전달 → 포트 하나 당 하나의 서비스가 맵핑
  • 즉 유저스페이스(kube-proxy)에서 커널 스페이스로 변환이 필요하여 그 만큼 비용이 듬!
  • kube-proxy 프로세스가 문제 시 SPOF 발생! ← 대응이 어렵다!
  • > 221118 제거됨, Removal of kube-proxy userspace modes

1.3.2. iptables proxy 모드

  • In this mode, kube-proxy configures packet forwarding rules using the iptables API of the kernel netfilter subsystem
    • For each endpoint, it installs iptables rules which, by default, select a backend Pod at random.
  • 1번 방식인 kube-proxy가 직접 proxy의 역할을 수행하지 않고 그 역할을 전부 netfilter에게 맡긴다
  • 이를 통해 service IP를 발견하고 그것을 실제 Pod로 전달하는 것은 모두 netfilter가 담당하게 되었고,
  • kube-proxy는 단순히 netfilter의 규칙을 알맞게 수정하는 것을 담당할 뿐이다. 즉 kube-proxy 는 rule 만 관리하고 실제 데이터 트래픽 처리를 직접 하지 않는다.
  • kube-proxy 가 데몬셋으로 동작하여, SPOF 발생 시 자동으로 다시 띄워 동작한다! 혹시 문제가 생겨도 룰은 이미 설정되어 있으니 데이터 통신은 된다. 물론 룰 추가 삭제는 안되지만.. 결론은 ‘User space’ 에 비해서 좀 더 안정적이고 성능 효율도 좋다!

1.3.3. IPVS 프록시 모드 (kernel IPVS , iptables APIs → netfilter subsystem)

IP Virtual Server

  • In ipvs mode, kube-proxy uses the kernel IPVS and iptables APIs to create rules to redirect traffic from Service IPs to endpoint IPs.
    • The IPVS proxy mode is based on netfilter hook function that is similar to iptables mode, but uses a hash table as the underlying data structure and works in the kernel space. That means kube-proxy in IPVS mode redirects traffic with lower latency than kube-proxy in iptables mode, with much better performance when synchronizing proxy rules. Compared to the iptables proxy mode, IPVS mode also supports a higher throughput of network traffic.
  • IPVS Mode는 Linue Kernel에서 제공하는 L4 Load Balacner인 IPVS가 Service Proxy 역할을 수행하는 Mode이다.
  • Packet Load Balancing 수행시 IPVS가 iptables보다 높은 성능을 보이기 때문에 IPVS Mode는 iptables Mode보다 높은 성능을 보여준다
  • IPVS 프록시 모드는 iptables 모드와 유사한 넷필터 후크 기능을 기반으로 하지만, 해시 테이블을 기본 데이터 구조로 사용하고 커널 스페이스에서 동작한다.
  • 이는 IPVS 모드의 kube-proxy는 iptables 모드의 kube-proxy보다 지연 시간이 짧은 트래픽을 리다이렉션하고, 프록시 규칙을 동기화할 때 성능이 훨씬 향상됨을 의미한다.
  • 다른 프록시 모드와 비교했을 때, IPVS 모드는 높은 네트워크 트래픽 처리량도 지원한다.

1.3.4. nftables 모드

nftables backend for kube-proxy (231213, k8s v1.29)

The default kube-proxy implementation on Linux is currently based on iptables. This was the preferred packet filtering and processing system in the Linux kernel for many years (starting with the 2.4 kernel in 2001). However, unsolvable problems with iptables led to the development of a successor, nftables. Development on iptables has mostly stopped, with new features and performance improvements primarily going into nftables instead.

This feature adds a new backend to kube-proxy based on nftables, since some Linux distributions already started to deprecate and remove iptables, and nftables claims to solve the main performance problems of iptables.

Linux의 기본 kube-proxy 구현은 현재 iptables를 기반으로 합니다. 이는 2001년 2.4 커널부터 시작하여 수년 동안 Linux 커널에서 선호되는 패킷 필터링 및 처리 시스템이었습니다. 그러나 iptables의 해결할 수 없는 문제로 인해 후속으로 nftables 이 개발되었습니다. iptables의 개발은 대부분 중단되었으며, 새로운 기능과 성능 개선은 주로 대체 nftables 으로 진행되었습니다.

일부 Linux 배포판에서 이미 iptables를 비활성화하고 제거하기 시작했으며, nftables이 iptables의 주요 성능 문제를 해결하였다고 주장하기 때문에 kube-proxy에 nftables에 기반한 새로운 백엔드를 추가합니다

nftables proxy mode

This proxy mode is only available on Linux nodes, and requires kernel 5.13 or later.

In this mode, kube-proxy configures packet forwarding rules using the nftables API of the kernel netfilter subsystem. For each endpoint, it installs nftables rules which, by default, select a backend Pod at random.

The nftables API is the successor to the iptables API and is designed to provide better performance and scalability than iptables. The nftables proxy mode is able to process changes to service endpoints faster and more efficiently than the iptables mode, and is also able to more efficiently process packets in the kernel (though this only becomes noticeable in clusters with tens of thousands of services).

As of Kubernetes 1.31, the nftables mode is still relatively new, and may not be compatible with all network plugins; consult the documentation for your network plugin


이 프록시 모드는 Linux 노드에서만 사용할 수 있으며, 커널 5.13 이상이 필요합니다.

이 모드에서는 kube-proxy가 커널 netfilter 서브시스템의 nftables API를 사용하여 패킷 포워딩 규칙을 구성합니다. 각 엔드포인트에 대해 기본적으로 랜덤으로 백엔드 Pod를 선택하는 nftables 규칙이 설치됩니다.

nftables API는 iptables API의 후속 버전으로, iptables보다 더 나은 성능과 확장성을 제공하도록 설계되었습니다. nftables 프록시 모드는 서비스 엔드포인트의 변경 사항을 iptables 모드보다 더 빠르고 효율적으로 처리할 수 있으며, 커널 내에서 패킷을 더 효율적으로 처리할 수 있습니다(하지만 이는 수만 개의 서비스가 있는 클러스터에서만 두드러지게 나타납니다).

Kubernetes 1.31 기준으로 nftables 모드는 여전히 비교적 새롭고 모든 네트워크 플러그인과 호환되지 않을 수 있습니다. 네트워크 플러그인에 대한 문서를 참조하십시오.


Benchmarking nftables

LATENCY AND THROUGHPUT COMPARISON BETWEEN IPTABLES AND NFTABLES AT DIFFERENT FRAME AND RULE-SET SIZES

8. Conclusions

연구 목표(3장)는 다양한 규칙 집합 크기와 프레임 크기에서 nftables와 iptables의 지연 시간 및 처리량을 비교하는 것이었습니다.
연구 질문(3.2장)은 구체적으로 서로 다른 크기의 규칙 집합과 프레임에서 nftables와 iptables 간의 지연 시간 및 처리량 성능에 통계적으로 유의미한 차이가 있는지 물었습니다.

특정 상황에서는 유의미한 차이가 발견되었고, 다른 상황에서는 성능이 동일한 것으로 나타났습니다. 일부 경우에는 지연 시간과 처리량 모두에서 미세하지만 통계적으로 유의미한 차이가 발견되었습니다.

IPset과 nftables의 집합 구조 형태로 인덱스화된 데이터 구조를 사용할 때 성능은 대부분의 경우 매우 유사했습니다. 때때로 미세하지만 통계적으로 유의미한 차이가 있었으며, 이는 테스트에 사용된 매개변수에 따라 nftables에 유리하거나 불리한 경우가 있었습니다. 그러나 이러한 차이는 전반적으로 매우 작았습니다. 대부분의 실제 응용에서 인덱스화된 데이터 구조를 사용하는 성능은 모든 경우에 동일한 것으로 간주할 수 있습니다.

선형 조회를 사용할 때 차이는 더 두드러졌습니다. 대체로 nftables는 iptables보다 성능이 떨어졌으며, 많은 경우에서 유의미하게 성능이 저하되었습니다. 실험 중 관찰된 일반적인 경향은 다음과 같습니다:

  • 지연 시간 측면에서 nftables는 작은 프레임 크기를 사용할 때 성능이 좋지 않으며, iptables에 비해 빠르게 뒤처지는 경향이 있습니다. 이는 때때로 20개의 규칙 집합에서도 발생합니다.
  • 지연 시간 측면에서 nftables는 큰 규칙 집합을 사용할 때 성능이 좋지 않습니다. nftables가 iptables에 뒤처지기 시작하는 시점은 사용된 프레임 크기에 따라 다르지만, 규칙 집합 크기가 1000개에 도달하면 프레임 크기에 상관없이 nftables의 성능이 크게 저하됩니다.
  • 처리량 측면에서 nftables는 작은 프레임 크기를 사용할 때 성능이 좋지 않으며, iptables에 비해 빠르게 뒤처지는 경향이 있습니다. 이는 때때로 40개의 규칙 집합에서도 발생합니다.
  • 처리량 측면에서 nftables는 큰 규칙 집합을 사용할 때 성능이 좋지 않습니다. nftables가 iptables에 뒤처지기 시작하는 시점은 사용된 프레임 크기에 따라 다르지만, 규칙 집합 크기가 1000개에 도달하면 프레임 크기에 상관없이 nftables의 성능이 크게 저하됩니다.
  • 큰 프레임 크기(1024바이트 이상)와 작은 규칙 집합(최대 100개까지)을 사용할 때 nftables는 잘 작동합니다. 지연 시간 측면에서 nftables는 iptables보다 약간 더 나은 성능을 보이며, 때때로 통계적으로 유의미한 차이를 보이기도 합니다. 처리량 측면에서는 nftables와 iptables의 성능이 거의 동일합니다.


기존 iptables이 오래되었고, 개선할 수 없는 문제점이 있어 이를 개선하기 위해 nftables가 등장하였다고 한다. 그러나.. 몇개 벤치마크 결과를 살펴보니 성능적인 개선이 있는가?는 현재상태에서는 다소 의문이 들긴한다

1.3.5. eBPF +XDP

기존 netfilter/iptables 기반 통신

eBPF + XDP 네트워킹 모듈

eBPF (extended Berkeley Packet Filter): 커널 내에서 안전하게 코드를 실행할 수 있도록 해주는 기술로, 네트워킹, 보안, 성능 모니터링 등 다양한 작업을 수행할 수 있다. eBPF는 커널 모듈을 작성하지 않고도 커널의 동작을 확장하거나 커스터마이징할 수 있게 해준다.

XDP (eXpress Data Path): 리눅스 커널에서 매우 빠른 패킷 처리를 가능하게 하는 기술로, 네트워크 인터페이스 카드(NIC)에서 패킷을 커널에 도달하기 전에 필터링, 드롭, 리디렉션 등의 처리를 수행할 수 있다. XDP는 eBPF를 활용하여 네트워크 성능을 크게 향상시킬 수 있다.

실습환경 구성 – kind cluster + mypc(netshoot)

  • 구성 과정
#
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 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
kubectl get nodes -o jsonpath="{.items[*].metadata.labels}" | jq | grep 'topology.kubernetes.io/zone'

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

2. ClusterIP

요약:

  • 클라이언트(TestPod)가 ‘CLUSTER-IP’ 접속 시,
  • 해당 노드의 iptables 룰(랜덤 분산)에 의해서 DNAT 처리가 되어,
  • 각 노드의 목적지(backend) 파드와 통신
  • iptables 분산룰(정책)은 모든 노드에 자동으로 설정됨

10.96.0.1 은 Cluster IP 유형의 Service 리소스의 주소

  • 클러스터 내부에서만 ‘CLUSTER-IP’ 로 접근 가능 ⇒ 서비스에 DNS(도메인) 접속도 가능!
  • 서비스(ClusterIP 타입) 생성하게 되면, apiserver → (kubelet) → kube-proxy → iptables 에 rule(룰)이 생성됨
  • 모드 노드(마스터 포함)에 iptables rule 이 설정되므로, 파드에서 접속 시 해당 노드에 존재하는 iptables rule 에 의해서 분산 접속이 됨

1svc-3pod 및 클라이언트(netshoot) 구성

  • 목적지 (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
  • 클라이언트 (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

  • 서비스 (ClusterIP 생성) > svc-clusterip.yaml
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
  • 생성 및 확인
# 모니터링
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

결과

  • 파드와 서비스의 네트워크 대역 조회 > 실제 해당 대역으로 svc, pod 생성
  • 서비스는
    • 서비스의 port로 클라이언트의 요청을 수신
    • 서비스의 targetport로 받은 요청을 전달

  • 서비스의 endpoints, endpointslices로 백엔드 파드의 ip 등록 확인

2.1. ClusterIP의 부하분산 동작확인

k8s service 오브젝트는 기본적으로 받은 요청에 대해, 뒷단의 대상에 대해 무작위로 부하를 분산하여 전달한다. 그 과정을 확인할 것이다

클라이언트(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.73/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

각 워커노드에서의 패킷덤프 확인

# 방안1 : 1대 혹은 3대 bash 진입 후 tcpdump 해둘 것
docker exec -it myk8s-worker bash
docker exec -it myk8s-worker2 bash
docker exec -it myk8s-worker3 bash
----------------------------------
# nic 정보 확인
ip -c link
ip -c route
ip -c addr

# tcpdump/ngrep : eth0 >> tcp 9000 포트 트래픽은 왜 없을까? iptables rule 동작 그림을 한번 더 확인하고 이해해보자
## ngrep 네트워크 패킷 분석기 활용해보기 : 특정 url 호출에 대해서만 필터 등 깔끔하게 볼 수 있음 - 링크
tcpdump -i eth0 tcp port 80 -nnq
tcpdump -i eth0 tcp port 80 -w /root/svc1-1.pcap
tcpdump -i eth0 tcp port 9000 -nnq
ngrep -tW byline -d eth0 '' 'tcp port 80'

# tcpdump/ngrep : vethX
VETH1=<각자 자신의 veth 이름>
tcpdump -i $VETH1 tcp port 80 -nn
tcpdump -i $VETH1 tcp port 80 -w /root/svc1-2.pcap
tcpdump -i $VETH1 tcp port 9000 -nn
ngrep -tW byline -d $VETH1 '' 'tcp port 80'

exit
----------------------------------

# 방안2 : 노드(?) 컨테이너 bash 직접 접속하지 않고 호스트에서 tcpdump 하기
docker exec -it myk8s-worker tcpdump -i eth0 tcp port 80 -nnq
VETH1=<각자 자신의 veth 이름> # docker exec -it myk8s-worker ip -c route
docker exec -it myk8s-worker tcpdump -i $VETH1 tcp port 80 -nnq

# 호스트PC에 pcap 파일 복사 >> wireshark 에서 분석
docker cp myk8s-worker:/root/svc1-1.pcap .
docker cp myk8s-worker:/root/svc1-2.pcap .

결과

  • 서비스 접근 확인
    • 기본포트(80)으로 접근시 접근되지 않으며, 타겟포트 9000로만 가능
    • 요청마다 응답하는 파드의 hostname이 다름을 확인할 수 있다

  • 부하분산 결과 조회
    • 완전히 동일하진 않지만, 들어오는 요청에 대해 가능한 균등하게 요청
  • packet dump
    • 워커 및 컨트롤플레인 노드에서 dump시, 이미 NAT 처리되어있기에 서비스(포트9000)은 보이지 않는다
    • 컨트롤플레인에서 tcpdump -i any -w /root/cp-any.pcap any로 잡을경우 뭔가 찍히긴 하는데.. 잘모르겠습니다..

2.2 iptables 정책 확인

쿠버네티스에서, 각 파드는 각 노드의 컨테이너로 구현되어있다. 서비스는 각 노드의 iptables rule로 구현되어있다. 따라서 service의 동작과정을 확인하려면, 각노드의 iptables 규칙을 확인하여야 할 것이다.

서비스는 다음의 체인?의 흐름에 따라 처리된다.

  • PREROUTING
  • KUBE-SERVICES
  • KUBE-SVC-###
  • KUBE-SEP-#<파드1> , KUBE-SEP-#<파드2> , KUBE-SEP-#<파드3>

iptables rule 확인

 # 컨트롤플레인에서 확인하겠습니다.
  $ docker exec -it myk8s-control-plane bash
  ----------------------------------------
        
  # iptables 확인
  $ iptables -t filter -S
  $ iptables -t nat -S
  $ iptables -t nat -S | wc -l
  # => 97
  $ iptables -t mangle -S
        
  # iptables 상세 확인 - 매칭 패킷 카운트, 인터페이스 정보 등 포함
  $ iptables -nvL -t filter
  $ iptables -nvL -t nat
  $ iptables -nvL -t mangle
        
  # rule 갯수 확인
  $ iptables -nvL -t filter | wc -l
  # => 47
  $ iptables -nvL -t nat | wc -l
  # => 158
        
  # 규칙 패킷 바이트 카운트 초기화
  $ iptables -t filter --zero; iptables -t nat --zero; iptables -t mangle --zero
        
  # 정책 확인 : 아래 정책 내용은 핵심적인 룰(rule)만 표시했습니다!
  $ iptables -t nat -nvL
  # => Chain PREROUTING (policy ACCEPT 121 packets, 7260 bytes) <<1. PREROUTING>>
  #     pkts bytes target     prot opt in     out     source               destination         
  #      121  7260 KUBE-SERVICES  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
  #    ...
  #    
  #    Chain INPUT (policy ACCEPT 121 packets, 7260 bytes)
  #    
  #    Chain OUTPUT (policy ACCEPT 392 packets, 23520 bytes)
  #     pkts bytes target     prot opt in     out     source               destination         
  #      392 23520 KUBE-SERVICES  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
  #    ...
  #    
  #    Chain KUBE-MARK-MASQ (18 references)
  #     pkts bytes target     prot opt in     out     source               destination         
  #        0     0 MARK       0    --  *      *       0.0.0.0/0            0.0.0.0/0            MARK or 0x4000
  #    
  #    Chain KUBE-SERVICES (2 references) <<2. SERVICES>>
  #     pkts bytes target     prot opt in     out     source               destination         
  #        0     0 KUBE-SVC-KBDEBIL6IU6WL7RF  6    --  *      *       0.0.0.0/0            10.200.1.96          /* default/svc-clusterip:svc-webport cluster IP */ tcp dpt:9000
  #    ...
  #    
  #    Chain KUBE-SVC-KBDEBIL6IU6WL7RF (1 references) <<3. KUBE-SVC-YYY>>
  #     pkts bytes target     prot opt in     out     source               destination         
  #        0     0 KUBE-MARK-MASQ  6    --  *      *      !10.10.0.0/16         10.200.1.96          /* default/svc-clusterip:svc-webport cluster IP */ tcp dpt:9000
  #        0     0 KUBE-SEP-T7YVH2JOMUTQFUDU  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport -> 10.10.1.4:80 */ statistic mode random probability 0.33333333349
  #        0     0 KUBE-SEP-SZHENXPAXVOCHRDA  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport -> 10.10.2.3:80 */ statistic mode random probability 0.50000000000
  #        0     0 KUBE-SEP-X47GKN7LA32LZ4H7  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport -> 10.10.4.3:80 */
  #    
  #    Chain KUBE-SEP-X47GKN7LA32LZ4H7 (1 references) <<4. KUBE-SEP-#WEBPOD1>>
  #     pkts bytes target     prot opt in     out     source               destination         
  #        0     0 KUBE-MARK-MASQ  0    --  *      *       10.10.4.3            0.0.0.0/0            /* default/svc-clusterip:svc-webport */
  #        0     0 DNAT       6    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ tcp to:10.10.4.3:80
  #    
  #    Chain KUBE-SEP-T7YVH2JOMUTQFUDU (1 references) <<4. KUBE-SEP-#WEBPOD2>>
  #     pkts bytes target     prot opt in     out     source               destination         
  #        0     0 KUBE-MARK-MASQ  0    --  *      *       10.10.1.4            0.0.0.0/0            /* default/svc-clusterip:svc-webport */
  #        0     0 DNAT       6    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ tcp to:10.10.1.4:80
  #
  #    Chain KUBE-SEP-SZHENXPAXVOCHRDA (1 references) <<4. KUBE-SEP-#WEBPOD3>>
  #     pkts bytes target     prot opt in     out     source               destination         
  #        0     0 KUBE-MARK-MASQ  0    --  *      *       10.10.2.3            0.0.0.0/0            /* default/svc-clusterip:svc-webport */
  #        0     0 DNAT       6    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ tcp to:10.10.2.3:80
        
  $ iptables -v --numeric --table nat --list PREROUTING | column -t
  # => Chain  PREROUTING  (policy        ACCEPT  777  packets,  46620  bytes)
  #    pkts   bytes       target         prot    opt  in        out    source     destination
  #    777    46620       KUBE-SERVICES  0       --   *         *      0.0.0.0/0  0.0.0.0/0    /*  kubernetes  service  portals  */
  #    0      0           DOCKER_OUTPUT  0       --   *         *      0.0.0.0/0  172.23.0.1
        
  $ iptables -v --numeric --table nat --list KUBE-SERVICES | column
  # 바로 아래 룰(rule)에 의해서 서비스(ClusterIP)를 인지하고 처리를 합니다
  # => Chain  KUBE-SERVICES  (2                         references)
  #    pkts   bytes          target                     prot         opt  in  out  source     destination
  #    0      0              KUBE-SVC-KBDEBIL6IU6WL7RF  6            --   *   *    0.0.0.0/0  10.200.1.96   /*  default/svc-clusterip:svc-webport  cluster  IP          */     tcp   dpt:9000
        
  $ iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF | column
  # => Chain  KUBE-SVC-KBDEBIL6IU6WL7RF  (1                         references)
  #    pkts   bytes                      target                     prot         opt  in  out  source         destination
  #    0      0                          KUBE-SEP-T7YVH2JOMUTQFUDU  0            --   *   *    0.0.0.0/0      0.0.0.0/0    /*  default/svc-clusterip:svc-webport  ->       10.10.1.4:80  */  statistic  mode      random  probability  0.33333333349
  #    0      0                          KUBE-SEP-SZHENXPAXVOCHRDA  0            --   *   *    0.0.0.0/0      0.0.0.0/0    /*  default/svc-clusterip:svc-webport  ->       10.10.2.3:80  */  statistic  mode      random  probability  0.50000000000
  #    0      0                          KUBE-SEP-X47GKN7LA32LZ4H7  0            --   *   *    0.0.0.0/0      0.0.0.0/0    /*  default/svc-clusterip:svc-webport  ->       10.10.4.3:80  */
        
  # 패킷 전달 수를 확인 하기 위해 watch를 겁니다.
  $ watch -d 'iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF'
        
  # control-plane 에서 테스트 패킷을 보냅니다.
  $ SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
  $ kubectl exec -it net-pod -- zsh -c "for i in {1..100};   do curl -s $SVC1:9000 | grep Hostname; sleep 1; done"

결과

  • PREROUTING > KUBE-SERVICES > KUBE-SVC-KBDEBIL6IU6WL7RF > KUBE-SEP-*
    • SEP의 첫번째 규칙에서 33%로 매칭
    • SEP의 두번째 규칙에서, 나머지(77%)에 대해 50% 매칭
    • SEP의 세번째 규칙에서, 나머지(33%) 매칭
    • > 따라서 전체 33% 균등 분산

2.3. 파드 장애시 확인

svc는 뒷단의 파드의 ip를 endpoint에 등록하여 관리한다. 실제로 파드 제거/재생성 시 업데이트 되는지 확인해 볼것이다.

svc의 endpoint 변화 확인

  • 모니터링 설정
# 터미널1 >> ENDPOINTS 변화를 잘 확인해보자!
watch -d 'kubectl get pod -owide;echo; kubectl get svc,ep svc-clusterip;echo; kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip'

# 터미널2
SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
kubectl exec -it net-pod -- zsh -c "while true; do curl -s --connect-timeout 1 $SVC1:9000 | egrep 'Hostname|IP: 10'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
혹은
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"
  • 파드 제거 및 재생성
# (방안1) 파드3번 삭제 >> 서비스의 엔드포인트가 어떻게 변경되는지 확인 하자!, 지속적인 curl 접속 결과 확인!, for 문 실행 시 결과 확인!, 절체 시간(순단) 확인!
kubectl delete pod webpod3

# (방안1) 결과 확인 후 다시 파드 3번 생성 >> 서비스 디스커버리!
kubectl apply -f 3pod.yaml

---------------------------------
# (방안2) 파드3번에 레이블 삭제
kubectl get pod --show-labels

## 레이블(라벨)의 키값 바로 뒤에 하이픈(-) 입력 시 해당 레이블 삭제됨! >> 레이블과 셀렉터는 쿠버네티스 환경에서 매우 많이 활용된다!
kubectl label pod webpod3 app-
kubectl get pod --show-labels

# (방안2) 결과 확인 후 파드3번에 다시 레이블 생성
kubectl label pod webpod3 app=webpod

결과

  • 기존 서비스의 정보 > pod 3개 모두 등록
  • webpod3(10.10.2.4)제거 > 해당 ip가 엔드포인트에서 제거됨
  • webpod3 (10.10.2.5) 재생성 > 해당 ip로 svc 엔드포인트 재등록

2.4. sessionAffinity

2.2. 에서 확인하였듯, 서비스의 기본 동작은 트래픽을 뒷단으로 부하분산 해준다. 그러나 세션어피니티를 적용하면, 클라이언트가 접속한 목적지 파드에 대해 고정적인 접속을 가능하도록 할 수 있다.

세션 어피니티 설정

  • 설정 방법
# 기본 정보 확인
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"

결과

  • 기존 상태 > 세션 어피니티 없음, 부하 분산 O
  • 적용 이후 > 세션 어피니티 있음, 부하 분산 X > 동일한 파드에 계속 요청

2.5. ClusterIP 유형의 단점

위의 실습을 통해 ClusterIP 유형의 service의 동작에 대해 알아보았다. 그러나 해당 유형은 아래의 문제점을 갖는다

  1. 클러스터 외부에서는 서비스(ClusterIP)로 접속이 불가능
  2. IPtables 는 파드에 대한 헬스체크 기능이 없음 > 파드의 동작확인을 어떻게?
  3. 서비스에 연동된 파드 갯수 퍼센트(%)로 랜덤 분산 방식, 세션어피니티 이외에 다른 분산 방식 불가능


위의 단점에 대해, 다음의 방식으로 개선이 가능하다.

  1. NodePort 등 다른 유형을 사용해 외부 접근 가능하도록 설정
  2. 파드의 Readiness Probe 설정 등을 통해 문제시 svc의 ep에서 제거 (+a필요)
  3. IPVS 모드의 경우 다양한 분산 알고리즘 적용 가능

3. NodePort

요약:

  • 외부 클라이언트가 ‘노드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) & 변경하기


1svc(nodeport)-3pod 구성

  • 목적지(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
  • 서비스(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
  • 동작 확인
# 생성
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

3.1. NodePort의 부하분산 동작확인

기존 ClusterIP의 경우 내부 iptables rule을 찔러야했기 때문에 외부에서 접근이 불가능하였다. NodePort의 경우 외부에서 노드IP:노드포트로 찌를 수 있어 외부에서 접근이 가능하다. 이하 내부 iptables 규칙에 따라 SNAT/DNAT 되어 목적지 파드와 통신 한다.

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

  • 노드포트 확인
# 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..

결과

  • 노드포트는 iptables로 처리되며, 해당 노드에 Listen되지않는다
    • Listen 포트에 노드포트 (32658) 없음
  • 노드포트로 접근시 호스트는 클라이언트(mypc)가 아닌 해당 노드로 변경되어 보인다
  • 노드포트는 클러스터ip처럼 동작할수도 있다. 단 svc의ip로 요청시 외부에서는 접근이 불가능하다

3.2. iptables 정책 확인

NodePort 유형의 iptables 정책 순서는 다음과 같다

  • PREROUTING
  • KUBE-SERVICES
  • KUBE-NODEPORTS
  • KUBE-EXT-#(MARK)
  • KUBE-SVC-#
  • KUBE-SEP-#
  • KUBE-POSTROUTING (MASQUERADE)

iptables rule 확인

컨트롤플레인 노드 - iptables 분석 << 정책 확인 : 아래 정책 내용은 핵심적인 룰(rule)만 표시했습니다!

docker exec -it myk8s-control-plane bash
----------------------------------------

# 패킷 카운트 초기화
iptables -t nat --zero


PREROUTING 정보 확인
iptables -t nat -S | grep PREROUTING
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
...


# 외부 클라이언트가 노드IP:NodePort 로 접속하기 때문에 --dst-type LOCAL 에 매칭되어서 -j KUBE-NODEPORTS 로 점프!
iptables -t nat -S | grep KUBE-SERVICES
-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-NODEPORTS 에서 KUBE-EXT-# 로 점프!
## -m nfacct --nfacct-name localhost_nps_accepted_pkts 추가됨 : 패킷 flow 카운팅 - 카운트 이름 지정 
NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
echo $NPORT

iptables -t nat -S | grep KUBE-NODEPORTS | grep <NodePort>
iptables -t nat -S | grep KUBE-NODEPORTS | grep $NPORT
-A KUBE-NODEPORTS -d 127.0.0.0/8 -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 30898 -m nfacct --nfacct-name localhost_nps_accepted_pkts -j KUBE-EXT-VTR7MTHHNMFZ3OFS
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 30898 -j KUBE-EXT-VTR7MTHHNMFZ3OFS

# (참고) nfacct 확인
nfacct list
## nfacct flush # 초기화


## KUBE-EXT-# 에서 'KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000' 마킹 및 KUBE-SVC-# 로 점프!
# 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" 반복 접속 후 아래 확인
watch -d 'iptables -v --numeric --table nat --list KUBE-EXT-VTR7MTHHNMFZ3OFS'
iptables -t nat -S | grep "A KUBE-EXT-VTR7MTHHNMFZ3OFS"
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -m comment --comment "masquerade traffic for default/svc-nodeport:svc-webport external destinations" -j KUBE-MARK-MASQ
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -j KUBE-SVC-VTR7MTHHNMFZ3OFS


# KUBE-SVC-# 이후 과정은 Cluster-IP 와 동일! : 3개의 파드로 DNAT 되어서 전달
iptables -t nat -S | grep "A KUBE-SVC-VTR7MTHHNMFZ3OFS -"
-A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-Q5ZOWRTVDPKGFLOL
-A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-MMWCMKTGOFHFMRIZ
-A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport" -j KUBE-SEP-CQTAHW4MAKGGR6M2


POSTROUTING 정보 확인
# 마킹되어 있어서 출발지IP를 접속한 노드의 IP 로 SNAT(MASQUERADE) 처리함! , 최초 출발지Port는 랜덤Port 로 변경
iptables -t nat -S | grep "A KUBE-POSTROUTING"
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN  # 0x4000/0x4000 되어 있으니 여기에 매칭되지 않고 아래 Rule로 내려감
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully


# 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" 반복 접속 후 아래 확인
watch -d 'iptables -v --numeric --table nat --list KUBE-POSTROUTING;echo;iptables -v --numeric --table nat --list POSTROUTING'

exit
----------------------------------------

결과

  • PREROUTING > KUBE-SERVICES > KUBE-NODEPORTS > KUBE-EXT-#(MARK) > KUBE-SVC-# > KUBE-SEP-# > KUBE-POSTROUTING (MASQUERADE)

3.3. externalTrafficPolicy 설정

Using Source IP- Source IP for Services with Type=NodePort

nodePort의 기본동작(externalTrafficPolicy:Cluster)의 경우 위에서 살펴보았듯, SNAT를 통해 요청한 클라이언트 대신, 요청을 받은 노드의 IP로  source IP가 변경된다.

이를 방지하기 위해서는, externalTrafficPolicy:Local로 설정하여 해당노드안에서 요청이 처리되도록하면, 원본 IP를 보존할 수 있다.


externalTrafficPolicy: Local > NodePort 로 접속 시 해당 노드에 배치된 파드로만 접속됨, 이때 SNAT 되지 않아서 외부 클라이언트 IP가 보존됨!

외부 클라이언트의 IP 주소(아래 출발지IP: 50.1.1.1)가 노드의 IP로 SNAT 되지 않고 서비스(backend) 파드까지 전달됨!

  1. Client가 파드가 있는 특정 노드의 노드IP:노드포트로 접근을 시도할경우
  2. iptables 규칙에 따라 D.IP는 변경되지만 S.IP는 보존됨

externalTrafficPolicy 설정

  • 설정 및 파드 접속 확인
# 기본 정보 확인
kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'
  externalTrafficPolicy: Cluster
  internalTrafficPolicy: Cluster

# 기존 통신 연결 정보(conntrack) 제거 후 아래 실습 진행하자! : (모든 노드에서) conntrack -F
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

# externalTrafficPolicy: local 설정 변경
kubectl patch svc svc-nodeport -p '{"spec":{"externalTrafficPolicy": "Local"}}'
kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'
	"externalTrafficPolicy": "Local",
  "internalTrafficPolicy": "Cluster",

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

# 파드 존재하는 노드 정보 확인
kubectl get pod -owide

# 파드 로그 실시간 확인 (웹 파드에 접속자의 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.5
NODE1=172.18.0.4
NODE2=172.18.0.3
NODE3=172.18.0.2

## NodePort 를 변수에 지정 : 서비스를 삭제 후 다시 생성하여서 NodePort가 변경되었음
NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
echo $NPORT

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

# 목적지 파드가 배치되지 않은 노드는 접속이 어떻게? 왜 그런가?
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"

# 파드가 배치된 노드에 NodePort로 아래 반복 접속 실행 해두자
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $NODE1:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
혹은
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $NODE2:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
혹은
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $NODE3:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"

# (옵션) 노드에서 Network Connection
conntrack -E
conntrack -L --any-nat
# 패킷 캡쳐 확인

결과

  • 파드가 존재하는 노드(워커1,워커2)에만 요청이 성공
  • 파드가 존재하지않는 노드 (컨트롤플레인,워커3)에는 요청 실패
  • 웹서버 로그에서 172.18.0.100로 시작한 mypc의 원본 IP 확인가능

3.5 NodePort 유형의 단점

위의 실습을 통해 NodePort 유형의 service의 동작에 대해 알아보았다. 그러나 해당 유형은 아래의 문제점을 갖는다

  1. 외부에서 노드의 IP와 포트로 직접 접근이 필요함 > 내부망이 외부에 공개되어 보안취약
  2. 클라이언트IP 보존을 위해 externalTrafficPolicy 사용시 파드가 없는 노드로 요청이 전달되면 실패


위의 단점에 대해, 추후에 소개될 LoadBalancer 유형을 사용하여 외부공개를 최소화하고, Probe 설정등으로 헬스체크를 수행해 정상적으로 응답하는 파드로만 요청을 전달할 수 있다.

Leave a Reply