들어가며
이번 포스팅은 CloudNet@ 커뮤니티에서 주최하는 KANS 스터디 4주 차 주제인 "Service"에 대해서 정리한 내용입니다.
본문에서 설명드리겠지만, Service는 4가지 유형이 있습니다.
이번 주차에서는 그중에서 제일 기초인 ClusterIP와 NodePort에 대해서 스터디를 진행하였습니다.
실습 환경
K8S v1.31.0 , CNI(Kindnet, Direct 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
kindnet 기반 클러스터 설치
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
실습용 네트워크 툴 설치
# 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 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 bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done
실습을 위해 설치하는 도구들은 네트워크 진단, 프로세스 관리, 파일 및 디렉터리 탐색 등을 할 수 있습니다.
설치되는 주요 도구는 다음과 같습니다.
도구명 | 설명 |
tree | 파일 시스템의 디렉터리 구조를 트리 형태로 시각적으로 보여주는 도구입니다. 디렉터리와 파일의 계층 구조를 쉽게 확인할 수 있습니다. |
psmisc | killall, fuser, pstree와 같은 프로세스 관련 도구를 포함한 패키지입니다. 이 도구들은 프로세스 상태 확인 및 관리에 유용합니다. |
lsof | "List Open Files"의 약자로, 현재 열려 있는 파일들과 그 파일들을 열고 있는 프로세스를 나열하는 데 사용됩니다. 시스템에서 파일 핸들러를 추적하는 데 유용합니다. |
wget | 네트워크에서 파일을 다운로드하는 명령줄 도구입니다. HTTP, HTTPS, FTP 등의 프로토콜을 지원합니다. |
bridge-utils | Linux 브리지 인터페이스를 관리하기 위한 도구로, 네트워크 브리지의 상태를 확인하고 설정할 수 있습니다. |
net-tools | ifconfig, netstat, route 등 네트워크 관리와 모니터링에 사용하는 도구들을 포함한 패키지입니다. |
ipset | IP 주소 세트를 관리하기 위한 도구입니다. 주로 방화벽에서 네트워크 트래픽 필터링을 최적화할 때 사용됩니다. |
ipvsadm | IP Virtual Server (IPVS)에서 네트워크 트래픽을 로드밸런싱하는 데 사용하는 도구입니다. 클러스터 환경에서 중요한 역할을 합니다. |
nfacct | 네트워크 패킷 계정 정보 수집을 위한 도구입니다. 네트워크 트래픽 모니터링 및 분석에 사용됩니다. |
tcpdump | 네트워크 인터페이스에서 트래픽을 캡처하고 분석하는 데 사용하는 도구입니다. 네트워크 문제 해결에 필수적입니다. |
ngrep | 네트워크 패킷을 정규 표현식으로 검색할 수 있는 도구입니다. grep의 네트워크 버전으로, 패킷 데이터 안에서 특정 패턴을 찾는 데 유용합니다. |
iputils-ping | ping 명령어는 네트워크 연결 상태를 확인하기 위한 도구로, 주로 IP 주소로 대상 서버에 ICMP 패킷을 보내 응답을 확인합니다. |
arping | ARP(주소 결정 프로토콜)를 사용하여 특정 호스트에 도달할 수 있는지 확인하는 도구입니다. 주로 로컬 네트워크에서 호스트의 가용성을 확인할 때 사용됩니다. |
arp-scan | 네트워크에 연결된 모든 장치를 스캔하여 ARP를 통해 해당 장치들의 IP 주소 및 MAC 주소를 확인할 수 있는 도구입니다. |
초기 세팅 정보 확인
# 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 | grep mode:
# 노드 별 네트워트 정보 확인 : 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
# 통신 확인
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 확인
echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=1.5"
172.18.0.100은 쿠버네티스 외부 통신을 확인하기 위해 별도의 mppc라는 컨테이너로 구성했습니다.
해당 컨테이너에 접속하여 각각의 노드와 통신이 잘 되는 것을 확인했습니다.
Kubenretes의 "Service "란?
Kubernetes는 Declarative(선형형)으로 Workload를 관리하는 컨셉이기 때문에, Pod는 언제든지 재시작되거나 다시 생성될 수 있습니다. 이 과정에서 IP 주소도 변경될 수 있어, Pod의 IP 주소에 직접 의존하는 것은 신뢰성이 떨어지게 됩니다. Service는 Pod의 IP 변경과 상관없이 안정적인 네트워크 엔드포인트를 제공합니다.
서비스의 상세한 설명에 대해서는 가시다님이 집필하신 책의 내용을 기반으로 작성합니다.
서비스의 동작
1. 서비스를 통해 부하분산 접속이 가능합니다.
클라이언트가 서비스의 IP로 반복 접속하면, 서비스에 연결된 파드로 부하분산되어 접속됩니다.
2. 서비스와 파드간의 연결은 타겟포트와 레이블을 통해 이루어집니다.
서비스에 연동된 파드의 집합은 레이블과 셀렉터로 관리됩니다.
레이블은 관리의 목적으로 태그를 붙이는 것을 의미하는데 'app=web'과 같은 형태로 key, value로 구성되어 있습니다.
파드에 이러한 레이블이 설정되면 서비스의 셀렉터에서 해당 레이블을 가진 파드들을 엔드포인트로 인식하게 됩니다.
또한 서비스의 특정 포트를 엔드포인트에 있는 파드들의 포트와 연결시키는데, 이때 파드가 사용할 포트를 타겟 포트라고 합니다.
3. kube-proxy 모드
kube-proxy는 서비스 통신 동작에 대한 설정을 관리하는 역할을 합니다. kube-proxy는 클러스터에 속한 모든 노드들에 데몬셋으로 배포됩니다. kube-proxy 모드는 유저 스페이스 프록시 모드, iptables 프록시 모드, IPVS 프록시 모드, nftables 프록시 모드가 있습니다.
Kube-proxy 상세
1. 유저 스페이스 프록시 모드
유저 스페이스 프록시 모드는 클라이언트 요청 트래픽이 사용자 영역의 kube-proxy 까지를 거치는 형태로 동작합니다. 그렇기 때문에 커널 영역과 사용자 영역 전환 과정이 필요하기 때문에 비효율적인 성능 소모가 발생하기 때문에 사용하지 않는다고 합니다.
2. iptables 프록시 모드
iptables proxy 모드는 쿠버네티스 설치 시 기본 모드입니다. kube-proxy 통신 트래픽 전달과정에 직접 관여하지 않고, 서비스 설정 시 관련된 iptables 규칙이 커널영역의 넷필터에 적용됩니다. 클라이언트 요청 트래픽은 커널 영역의 넷필터 규칙에 의해 사용자영역을 걸치지 않고 바로 전달되기 때문에, 유저 스페이스 프록시 모드에 비해 오버헤드가 줄어듭니다.
하지만, iptables proxy 모드는 다양한 부하분산 알고리즘을 제공하지 않고, 규칙이 많아질 경우 모든 규칙 평가 및 추가로 인한 지연이 발생하기 때문에 수많은 파드를 관리가 쉽지 않습니다.
3. IPVS 프록시 모드
IPVS 프록시 모드는 가장 성능 효율적인 모드로써, 넷필터에서 동작하는 로드밸런서 입니다. iptables보다 높은 성능처리를 보여주고, 룰을 줄여줄 수 있습니다. 또한 다양한 부하분산 알고리즘을 제공합니다.
4. nftables 프록시 모드
nftables는 리눅스 커널에서 제공하는 방화벽 및 패킷 필터링 프레임워크입니다. 기존의 iptables를 대체하기 위해 설계되었으며, 더 효율적이고 유연한 방법으로 네트워크 패킷을 처리할 수 있습니다.
ftables 프록시 모드는 네트워크 계층 중 4 계층(Layer 4), 즉 전송 계층(Transport Layer)에서 동작합니다. 전송 계층은 TCP, UDP와 같은 프로토콜을 통해 통신 세션을 설정하고 데이터를 전달하는 역할을 합니다.
서비스 종류
서비스의 종류는 아래와 같이 Cluster IP, NodePort, LoadBalancer, ExternalName 4가지 종류가 있습니다.
서비스의 유형에 대하여 대해 간략히 설명하자면 다음과 같습니다.
ClusterIP
기본 서비스 타입으로, 클러스터 내부에서만 접근할 수 있는 IP 주소를 제공합니다.
외부에서 접근할 수 없으며, 같은 클러스터 내의 다른 파드들이 이 IP 주소를 통해 통신할 수 있습니다.
NodePort
각 노드의 특정 포트를 열어, 클러스터 외부에서 접근할 수 있게 해 줍니다.
노드의 IP 주소와 포트를 통해 외부에서 클러스터의 서비스를 이용할 수 있으며, 이 포트는 30000~32767 범위 내에서 할당됩니다.
사용자는 NodeIP:NodePort 형식으로 외부에서 서비스에 접근할 수 있습니다.
LoadBalancer
클라우드 환경(예: AWS, GCP, Azure)에서 주로 사용되며, 외부 로드 밸런서를 자동으로 프로비저닝 하여 외부 트래픽을 서비스로 전달합니다.
클라우드 제공자의 로드 밸런서를 통해 서비스가 자동으로 외부로 노출되며, 클러스터 외부에서도 쉽게 접근할 수 있습니다.
ExternalName
Kubernetes 서비스가 클러스터 외부의 서비스에 대한 DNS 이름을 매핑할 때 사용됩니다.
실제 IP 주소가 아니라 도메인 이름을 기반으로 외부 서비스와 연결됩니다. 예를 들어, 외부 데이터베이스나 API 서비스를 이 방식으로 설정할 수 있습니다.
*참고: 네트워크도식에서 자주 사용되는 약자는 다음과 같습니다.
약자
S.IP : Source IP , 출발지(소스) IP
D.IP : Destination IP, 도착지(목적지) IP
S.Port : Source Port , 출발지(소스) 포트
D.Port : Destination Port , 도착지(목적지) 포트
NAT : Network Address Translation , 네트워크 주소 변환
SNAT : Source IP를 NAT 처리, 일반적으로 출발지 IP를 변환
DNAT : Destination IP 를 NAT 처리, 일반적으로 목적지 IP와 목적지 포트를 변환
이제 서비스 별로 동작을 설명하겠습니다.
Cluster IP
Cluster IP의 동작은 클라이언트가 'CLUSTER-IP' 접속 시 해당 노드의 iptabhles 룰(랜덤 분산)에 의해서 DNAT 처리가 되어 목적지의 Pod와 통신하게 됩니다. 하지만 Cluster IP는 클러스터 내부에서만 사용가능합니다.(서비스 DNS로도 접속 가능)
서비스를 생성하면 apiserver에서 kube-proxy를 통해 Iptables에 룰이 추가됩니다. 모든 노드는 노드별로 iptables룰을 관리하고 있고, 모든 iptables의 정보는 모두 공유됩니다.
Cluster IP의 부족한 점
Cluster IP로 서비스가 구성되면 외부에서는 접속이 불가능합니다. 또한 iptables는 pod의 헬스체크 기능이 없기 때문에, 문제가 있는 파드에 연결될 수 있습니다. 또한 여러 분산 알고리즘을 사용할 수 없습니다.
실습 준비
# 목적지(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
서비스(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
모든 노드에 iptables 규칙이 동일하게 적용되어 있는 것을 확인할 수 있습니다.
dport가 설정되어도, tcp port는 열려있지 않습니다.
또한 ClusterIP가 설정되어 있어도 host의 routing table에는 라우팅이 등록되지 않습니다. 즉 iptable rule에 의해 라우팅이 되는 것입니다.
80 포트 tcpdump는 iptables 분산룰에 의해 목적지 주소가 NAT 됨
이미 노드 간 NAT 가 일어났기 때문에, 9000 포트 tcpdump는 확인되지 않습니다.
ngrep을 사용하면 tcp header 정보를 다음과 같이 확인할 수 있습니다.
control-plane에서 tcpdump를 wireshark를 이용하여 net-pod가 3개의 worker node상에 배포된 pod에 NAT 되어 통신되는 것을 확인했습니다.
IPTABLES 정책 확인
ClusterIP 서비스 생성 시 적용되는 주요 IP table NAT 테이블의 규칙은, iptable 체인에 의해 관리됩니다. 매칭되는 규칙에 따라 패킷을 어떻게 처리할 것인지 결정됩니다.
그림처럼 PREROUTING 체인에서 매칭되면 KUBE-SERVICES 체인으로 매칭됩니다. 매칭되면 트래픽은 해당 타겟으로 점프(-j 이하)합니다. 즉 내부에서 클러스터 IP로 접속 시, PREROUTE(nat)에서 DNAT(3개 파드) 되고, POSTROUTE(nat)에서 SNAT 되지 않고 나간다는 것입니다.
# 컨트롤플레인에서 확인 : 너무 복잡해서 리턴 트래픽에 대해서는 상세히 분석 정리하지 않습니다.
docker exec -it myk8s-control-plane bash
----------------------------------------
# iptables 확인
iptables -t filter -S
iptables -t nat -S
iptables -t nat -S | wc -l
iptables -t mangle -S
# iptables 상세 확인 - 매칭 패킷 카운트, 인터페이스 정보 등 포함
iptables -nvL -t filter
iptables -nvL -t nat
iptables -nvL -t mangle
# rule 갯수 확인
iptables -nvL -t filter | wc -l
iptables -nvL -t nat | wc -l
# 규칙 패킷 바이트 카운트 초기화
iptables -t filter --zero; iptables -t nat --zero; iptables -t mangle --zero
# 정책 확인 : 아래 정책 내용은 핵심적인 룰(rule)만 표시했습니다!
iptables -t nat -nvL
iptables -v --numeric --table nat --list PREROUTING | column -t
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
778 46758 KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
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
92 5520 KUBE-SVC-KBDEBIL6IU6WL7RF tcp -- * * 0.0.0.0/0 10.105.114.73 /* default/svc-clusterip:svc-webport cluster IP */ tcp dpt:9000
iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF | column
watch -d 'iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF'
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"
# SVC-### 에서 랜덤 확률(대략 33%)로 SEP(Service EndPoint)인 각각 파드 IP로 DNAT 됩니다!
## 첫번째 룰에 일치 확률은 33% 이고, 매칭되지 않을 경우 아래 2개 남을때는 룰 일치 확률은 50%가 됩니다. 이것도 매칭되지 않으면 마지막 룰로 100% 일치됩니다
Chain KUBE-SVC-KBDEBIL6IU6WL7RF (1 references)
pkts bytes target prot opt in out source destination
38 2280 KUBE-SEP-6TM74ZFOWZXXYQW6 all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport */ statistic mode random probability 0.33333333349
29 1740 KUBE-SEP-354QUAZJTL5AR6RR all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport */ statistic mode random probability 0.50000000000
25 1500 KUBE-SEP-PY4VJNJPBUZ3ATEL all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport */
iptables -v --numeric --table nat --list KUBE-SEP-<각자 값 입력>
Chain KUBE-SEP-6TM74ZFOWZXXYQW6 (1 references)
pkts bytes target prot opt in out source destination
38 2280 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport */ tcp to:172.16.158.3:80
iptables -v --numeric --table nat --list KUBE-SEP-354QUAZJTL5AR6RR | column -t
Chain KUBE-SEP-6TM74ZFOWZXXYQW6 (1 references)
pkts bytes target prot opt in out source destination
29 1500 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport */ tcp to:172.16.184.3:80
iptables -v --numeric --table nat --list KUBE-SEP-PY4VJNJPBUZ3ATEL | column -t
Chain KUBE-SEP-6TM74ZFOWZXXYQW6 (1 references)
pkts bytes target prot opt in out source destination
25 1740 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport */ tcp to:172.16.34.3:80
iptables -t nat --zero
iptables -v --numeric --table nat --list POSTROUTING | column; echo ; iptables -v --numeric --table nat --list KUBE-POSTROUTING | column
watch -d 'iptables -v --numeric --table nat --list POSTROUTING; echo ; iptables -v --numeric --table nat --list KUBE-POSTROUTING'
# POSTROUTE(nat) : 0x4000 마킹 되어 있지 않으니 RETURN 되고 그냥 빠져나가서 SNAT 되지 않는다!
Chain KUBE-POSTROUTING (1 references)
pkts bytes target prot opt in out source destination
572 35232 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 mark match ! 0x4000/0x4000
0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK xor 0x4000
0 0 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ random-fully
iptables -t nat -S | grep KUBE-POSTROUTING
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
...
exit
Pod 1개 장애 발생 시 동작 확인 실습
Pod에서 장애가 발생하면, 나머지 Pod들로 트래픽이 분산됩니다.
# 터미널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
정상 상황
장애 상황
Session Affinity 실습
서비스 접속 시 ClusterIP를 통해 랜덤 부하분산되어 접속 되는 상황을, 특정 파드로만 전달 하기 위해 Session Affinity를 사용합니다.
클라이언트가 서비스에 접속시 연결 상태 정보를 확인하여 최초 전달된 파드, 즉 동일한 파드로 전달할 수 있습니다.
# 기본 정보 확인
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"
iptables -t nat -S | grep recent
# 아래 10800초(=180분=3시간) 클라이언트에서 접속된 DNAT(파드)를 연결 유지 관련 설정이 추가됨!
## service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 에 최대 세션 고정 시간 설정 변경 가능함
IP테이블 적용규칙이 -m recent --rcheck가 추가되었습니다.
NodePort
NodePort는 외부 클라이언트가 '노드 IP:NodePort'접속 시 해당 노드의 iptables 룰에 의해 SNAT/DNAT 되어 목적지 파드와 통신 후 리턴 트래픽이 최초 인입 노드를 경유하여 외부로 되돌아가는 구조입니다.
외부에서 클러스터의 서비스로 접근한다는 점이 ClusterIP 타입과의 차이입니다. 또한 모든 노드에 iptables 룰이 설정되어 있기 때문에 어떤 노드의 NodePort로 접속해도 동일한 접근이 가능합니다.
별도의 로드밸런서를 사용할 경우, L4를 통해 여러 NodePort를 통해 통신되거나, Bypass를 통해 iptables 분산룰에 연결되어 통신이 됩니다.
NodePort의 할당 범위는 기본 30000~32767이고, 변경이 가능합니다.
실습구성
실습구성은 3개의 replicas를 가진 web server deployment를 생성한 다음, NodePort type의 service를 deployment에 연결합니다.
# 목적지(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
# 아래 30158은 서비스(NodePort) 정보!
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
kubectl get endpoints svc-nodeport
NAME ENDPOINTS AGE
svc-nodeport 172.16.158.5:8080,172.16.184.3:8080,172.16.34.4:8080 4m18s
# Port , TargetPort , NodePort 각각의 차이점의 의미를 알자!
kubectl describe svc svc-nodeport
디플로이먼트에 의해 pod 세 개가 생성되고, 서비스가 생성됩니다.
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주소>
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
NodePort를 설정하면, tcp 소켓에서는 포트가 등록되지 않습니다.
클러스터의 외부에 있는 mypc container가 nodePort를 이용해 내부에 잘 접속됩니다.
Cluster 내부의 모든 Node에 NodePort로 접근이 잘 됩니다.
노드포트를 설정하면, 내부 통신은 ClusterIP로, 외부는 NodePort를 통해 접근이 되고, 외부에서 ClusterIP를 이용하여 통신하면 통신이 안됩니다.
IPTABLE 정책 확인
NodePort의 기본 규칙은 ClusterIP 서비스 동작처리를 위한 규칙과 동일하지만, 몇 가지 다른 점이 있습니다.
KUBE_NODEPORTS, KUBE-MARK-MASQ, KUBE-POSTROUTING 체인이 추가되어 있다는 점이 다릅니다. NodePort에 매칭 시 마킹 후 출발지 IP를 해당 노드에 있는 네트워크 인터페이스의 IP로 변환하여 목적지 파드로 전달하는 구조입니다. 출발지 IP를 NAT 하는 이유는 iptables분산룰을 거쳐서 나가야지 클러스터에서 관리가 가능하기 때문입니다. 그래서 Mark가 된 것만 NAT 시킵니다.
NodePort IP IPTABLE 실습
컨트롤플레인 노드 - 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
----------------------------------------
externalTrafficPolicy 실습
externalTrafficPolicy를 Local로 설정하면, 특정노드 안의 Pod로 연결이 됩니다. 이때 SNAT 되지 않기 때문에 외부 클라이언트의 IP 그대로 트래픽을 받게 됩니다. 하지만, 특정 노드에 Pod가 없다면, 통신에러가 발생하게 됩니다.
# 기본 정보 확인
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 를 변수에 지정
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"
# 아래 반복 접속 실행
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"
# (옵션) 노드에서 Network Connection
conntrack -E
conntrack -L --any-nat
# 패킷 캡쳐 확인
정책 확인을 해보면 다음과 같이 자신의 노드에 생성된 파드만 DNAT 도는 것을 확인할 수 있습니다.
컨트롤플레인 노드 - iptables 분석 << 정책 확인 : 아래 정책 내용은 핵심적인 룰(rule)만 표시했습니다!
# (예시) 파드가 배포되어 있는 노드2에서 확인했습니다
docker exec -it myk8s-worker2 bash
---------------------------------------
iptables -t nat -S
iptables -t nat -S | grep <NodePort>
iptables -t nat -S | grep 31303
-A KUBE-NODEPORTS -d 127.0.0.0/8 -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 31303 -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 31303 -j KUBE-EXT-VTR7MTHHNMFZ3OFS
iptables -t nat -S | grep 'A KUBE-EXT-VTR7MTHHNMFZ3OFS'
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -s 10.10.0.0/16 -m comment --comment "pod traffic for default/svc-nodeport:svc-webport external destinations" -j KUBE-SVC-VTR7MTHHNMFZ3OFS
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -m comment --comment "masquerade LOCAL traffic for default/svc-nodeport:svc-webport external destinations" -m addrtype --src-type LOCAL -j KUBE-MARK-MASQ
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -m comment --comment "route LOCAL traffic for default/svc-nodeport:svc-webport external destinations" -m addrtype --src-type LOCAL -j KUBE-SVC-VTR7MTHHNMFZ3OFS
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -j KUBE-SVL-VTR7MTHHNMFZ3OFS
# 실습 환경에서는 아래처럼 2개의 파드 중 자신의 노드에 생성된 파드 1개만 DNAT 연결됨
iptables -t nat -S | grep 'A KUBE-SVL-VTR7MTHHNMFZ3OFS'
-A KUBE-SVL-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport -> 10.10.2.7:8080" -j KUBE-SEP-FBJG45W6XHLV2NA6
iptables -t nat -S | grep 'A KUBE-SEP-FBJG45W6XHLV2NA6'
-A KUBE-SEP-FBJG45W6XHLV2NA6 -s 10.10.2.7/32 -m comment --comment "default/svc-nodeport:svc-webport" -j KUBE-MARK-MASQ
-A KUBE-SEP-FBJG45W6XHLV2NA6 -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp -j DNAT --to-destination 10.10.2.7:8080
exit
NodePort의 부족한 점
NodePort를 이용하여 외부와 통신하려면 클러스터 내부의 노드 IP, Port를 외부로 노출해야 하기 때문에 보안에 취약합니다. 또한 클라이언트 IP 보존을 위해서, externalTrafficPolicy: local를 사용하게 되면 파드가 없는 노드 IP로 NodePort 접속 시 실패하게 됩니다.
차주에 스터디할 LoadBalancer 서비스 타입 사용을 통해 이러한 문제점들을 해결할 수 있습니다.
EndpointSlice
EndpointSlice는 Kubernetes에서 서비스를 더 효율적으로 확장하고 관리하기 위해 도입된 리소스 유형입니다.
Kubernetes 클러스터 내에서 서비스가 여러 개의 파드를 관리할 때, 이 파드들의 IP 주소와 관련된 정보를 관리하는 것이 중요한데, EndpointSlice는 이를 아래와 같은 특징을 가지고 더 효율적이고 유연하게 처리하도록 도와줍니다.
- 확장성: Endpoints 리소스는 파드가 많아질수록 모든 파드의 정보를 하나의 오브젝트로 저장하기 때문에 성능 문제를 일으킬 수 있습니다. 반면 EndpointSlice는 파드 목록을 여러 개의 Slice로 나누어 관리하여 대규모 환경에서의 성능을 향상합니다.
- 유연성: 한 EndpointSlice는 최대 100개의 엔드포인트(파드 IP)를 포함할 수 있으며, 여러 개의 EndpointSlice로 엔드포인트를 분할하여 필요한 만큼만 업데이트하고 사용할 수 있습니다.
- 다중 프로토콜 지원: EndpointSlice는 하나의 리소스에서 여러 네트워크 프로토콜을 지원할 수 있습니다(예: TCP, UDP, SCTP). 이는 다양한 네트워크 설정을 보다 쉽게 관리할 수 있게 해 줍니다.
- 기능 개선: EndpointSlice는 향후 Kubernetes 네트워크 개선 사항을 지원할 수 있도록 설계되었습니다. 예를 들어, 로드 밸런싱 및 장애 조치 메커니즘을 보다 세분화할 수 있습니다.
Endpoint Slice가 없으면 kube-proxy가 매번 넷필터에 endpoint 변경에 따라 업데이트를 수행해 줘야 합니다.
그렇기 때문에, Endpoint Slice를 사용하여 업데이트의 부하를 줄일 수 있습니다.
EndpointSlice 실습
kubectl get endpointslice
# 모니터링
watch kubectl get pod,svc,ep,endpointslice -owide
# 배포
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
# 모니터링
watch kubectl get pod,svc,ep,endpointslice -owide
# 상태 확인 : ‘Endpoints, EndpointSlice 출력 정보 차이’를 알아보자
kubectl get pod,svc,ep,endpointslice
kubectl describe pod
kubectl describe svc
kubectl describe ep
kubectl describe endpointslice
# 파드에 파일 생성
kubectl exec -it deployments/mario -- touch healthcheck
kubectl exec -it deployments/mario -- ls -l
# 상태 확인
kubectl get pod,svc,ep,endpointslice
kubectl describe pod
kubectl describe svc
kubectl describe ep
kubectl describe endpointslice
# 게임 접속
echo -e "Mario Game URL = http://localhost:30001" # macOS
echo -e "Mario Game URL = http://Y.Y.Y.Y:30001" # Windows
# 파드에 파일 삭제
kubectl exec -it deployments/mario -- rm healthcheck
kubectl exec -it deployments/mario -- ls
# 상태 확인
kubectl get pod,svc,ep,endpointslice
kubectl describe pod
kubectl describe svc
kubectl describe ep
kubectl describe endpointslice
# 삭제
kubectl delete deploy,svc mario
endpoint를 확인해 보면 ENDPOINT 정보가 등록되어 있지 않습니다. 하지만 endpoint slice에 확인해 보면 IP가 할당되어 있습니다.
Pod에 원격접속하여 ReadinessProve의 조건을 만족시키면, 다음과 같이 ENDPOINT가 등록됩니다.
마치며
이번 스터디에서는 k8s서 사용되는 service 중 ClusterIP와 NodePort에 대해 살펴보았습니다. 정말 맵군요.. 기존에는 서비스를 사용할 때 특징과 용례를 통해 배포하여 사용했었는데 이번 스터디에서 내부적인 동작원리를 확실히 파악할 수 있었습니다. 매번 사용할 때 블랙박스 느낌으로 사용하던 터라 문제가 생기진 않을까 염려되었었는데, 이제는 문제가 생겨도 원리를 통해 트러블 슈팅을 할 수 있을 것이라는 자신감이 생겼습니다.
다음 주 로드밸런서 챕터도 기대가 됩니다! 어느덧 스터디도 중반부를 달려가고 있네요..
끝까지 완주해 보겠습니다!
감사합니다 :)
'클라우드 컴퓨팅 & NoSQL > [KANS] 쿠버네티스 네트워크 심화 스터디' 카테고리의 다른 글
[6주차 - Ingress & Gateway API] KANS 스터디 (24.10.06) (7) | 2024.10.13 |
---|---|
[5주차 - Service : LoadBalancer] KANS 스터디 (24.09.29) (3) | 2024.10.05 |
[3주차(2/2) - k8s Calico CNI mode & 운영] KANS 스터디 (24.09.08) (0) | 2024.09.18 |
[3주차(1/2) - k8s Calico CNI] KANS 스터디 (24.09.08) (1) | 2024.09.18 |
[2주차(2/2) - Flannel CNI] KANS 스터디 (24.09.01) (4) | 2024.09.05 |