[KANS 3기] K8S Flannel CNI & PAUSE
들어가며
지난주에 이어 이번주에는 쿠버네티스에대해 간략하게 알아보고 KIND, PAUSE 컨테이너와 Flannel CNI에 대해 알아보겠습니다. KANS 3기 2주차 스터디를 시작하겠습니다.
쿠버네티스 소개
- 쿠버네티스는 구글에서 오픈소스로 공개한 컨테이너화된 애플리케이션을 자동으로 배포, 스케일링 및 관리하는 오픈소스 플랫폼입니다.
출처: https://blog.naver.com/love_tolty/222167051615
-
쿠버네티스는 위의 그림과 같이 다양한 컴포넌트로 이루어져 있습니다. 각 요소를 살펴보면 아래와 같습니다.
- Control Plane(마스터 노드) : 마스터는 단일 서버 혹은 고가용성을 위한 클러스터 마스터로 구축
-
Worker Node(워커 노드) - 링크
- kubelet : 마스터의 명령에 따라 컨테이너의 라이프 사이클을 관리하는 노드 관리자
- kube-proxy : 컨테이너의 네트워킹을 책임지는 프록시, 네트워크 규칙을 유지 관리
- Container Runtime : 실제 컨테이너를 실행하는 컨테이너 실행 환경, (ContainerD, CRI-O, …) - 링크
-
Addon(애드온)
- CNI : Container Network Interface 는 k8s 네트워크 환경을 구성해줍니다.
- 예) Flannel, Calico, Weave Net, Cilium, …
- DNS : 클러스터 내부 DNS 서비스를 제공합니다.
- 예) CoreDNS, Kube-DNS, …
- 기타 : 모니터링, 대시보드, 로깅 등등
- CNI : Container Network Interface 는 k8s 네트워크 환경을 구성해줍니다.
kind 소개 및 설치
kind는 Kubernetes IN Docker의 약자로, 로컬 환경에서 쿠버네티스 클러스터를 쉽게 구성할 수 있도록 도와주는 도구입니다. 이름에서 알 수 있듯이 Kubernetes를 Docker 안에서 DIND(Docker in Docker) 방식으로 구동시켜주는 도구입니다. minikube나 k3s 등과 달리 Docker만 설치되어 있으면 손쉽게 쿠버네티스 클러스터를 구성할 수 있습니다.
- kind의 구조를 그림으로 표현하면 아래와 같습니다.
출처 : https://kind.sigs.k8s.io/docs/design/initial/
설치
제가 사용중인 macOS를 기준으로 작성하였습니다. macOS에서 테라폼을 설치하려면 Homebrew를 이용하여 설치할 수 있습니다. (홈브루 설치 방법은 https://whalec.io/homebrew-설치-및-사용-방법 를 참고하세요.)
- kind 설치 및 필수 툴 설치
# Install Kind
$ brew install kind
$ kind --version
# => kind version 0.24.0
# Install kubectl
$ brew install kubernetes-cli
$ kubectl version --client=true
# => Client Version: v1.31.0
# Kustomize Version: v5.4.2
# Install Helm
$ brew install helm
$ helm version
# => version.BuildInfo{Version:"v3.15.4", GitCommit:"fa9efb07d9d8debbb4306d72af76a383895aa8c4", GitTreeState:"clean", GoVersion:"go1.22.6"}
# Install Wireshark : 캡처된 패킷 확인
$ brew install --cask wireshark
# (선택) kubectl 출력 시 하이라이트 처리
$ brew install kubecolor
$ echo "alias kubectl=kubecolor" >> ~/.zshrc
$ echo "compdef kubecolor=kubectl" >> ~/.zshrc
1-Node 클러스터 구성 테스트
- 간단한 클러스터를 만들고 테스트 해보겠습니다.
# 클러스터 배포 전 확인
$ docker ps
# Create a cluster with kind
$ kind create cluster
# => Creating cluster "kind" ...
# <span style="color:green;">✓</span> Ensuring node image (kindest/node:v1.31.0) 🖼
# <span style="color:green;">✓</span> Preparing nodes 📦
# <span style="color:green;">✓</span> Writing configuration 📜
# <span style="color:green;">✓</span> Starting control-plane 🕹️
# <span style="color:green;">✓</span> Installing CNI 🔌
# <span style="color:green;">✓</span> Installing StorageClass 💾
# Set kubectl context to "kind-kind"
# You can now use your cluster with:
#
# kubectl cluster-info --context kind-kind
#
# Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/docs/user/quick-start/
# 클러스터 배포 확인
$ kind get clusters
$ kind get nodes
$ kubectl cluster-info
# 노드 정보 확인
$ kubectl get node -o wide
# 파드 정보 확인
$ kubectl get pod -A
$ kubectl get componentstatuses
# 컨트롤플레인 (컨테이너) 노드 1대가 실행
$ docker ps
$ docker images
# kube config 파일 확인
$ cat ~/.kube/config
# nginx 파드 배포 및 확인 : 컨트롤플레인 노드인데 파드가 배포 될까요?
$ kubectl run nginx --image=nginx:alpine
$ kubectl get pod -owide
# 노드에 Taints 정보 확인
$ kubectl describe node | grep Taints
# => Taints: <none>
# 클러스터 삭제
$ kind delete cluster
# kube config 삭제 확인
$ cat ~/.kube/config
- 테스트 중에 노드가 커트롤 플레인 하나 뿐인데도 파드가 배포되는것을 확인할 수 있습니다.
-
kubectl describe node | grep Taints
를 통해 확인해본 결과=> Taints: <none>
로 컨트롤 플레인에 보통 걸려있는 taint가 없어서 파드가 배포되는 것을 확인할 수 있습니다.
2-Node 클러스터 구성 테스트
- 이번에는 좀 더 나아가서 control-plane과 worker의 2개의 노드로 구성된 KIND 클러스터를 만들고, 클러스터 구성을 확인해보겠습니다.
# 클러스터 배포 전 확인
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# kind 는 별도 도커 네트워크 생성 후 사용 : 기본값 172.18.0.0/16
$ docker network ls
# => NETWORK ID NAME DRIVER SCOPE
# ...
# 3bbcc6aa8f38 kind bridge local
# ...
$ docker inspect kind | jq
# => [
# {
# "Name": "kind",
# "Driver": "bridge",
# ...
# "IPAM": {
# "Driver": "default",
# "Options": {},
# "Config": [
# {
# "Subnet": "172.20.0.0/16",
# "Gateway": "172.20.0.1"
# }
# ]
# },
# ...
# }
# ]
# KIND로 control-plane, worker라는 2개의 노드를 가진 클러스터 만들기
$ cat << EOT > kind-2node.yaml
# two node (one workers) cluster config
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
EOT
$ kind create cluster --config kind-2node.yaml --name myk8s
# 확인
$ kind get nodes --name myk8s
# => myk8s-worker
# myk8s-control-plane
# k8s api 주소 확인
$ kubectl cluster-info
# => <span style="color:green;">Kubernetes control plane</span> is running at <span style="color:olive;">https://127.0.0.1:58638</span>
# <span style="color:green;">CoreDNS</span> is running at <span style="color:olive;">https://127.0.0.1:58638/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy</span>
#
# To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
# 호스트에서 접속 테스트
$ curl -k https://localhost:58638
# => {
# "kind": "Status",
# "apiVersion": "v1",
# "metadata": {},
# "status": "Failure",
# "message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
# "reason": "Forbidden",
# "details": {},
# "code": 403
# } # 호스트에서 접속이 됩니다! 왜 그럴까요?
$ docker ps # 포트 포워딩 정보 확인
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 6228f280b992 kindest/node:v1.31.0 "/usr/local/bin/entr…" 3 minutes ago Up 3 minutes 0.0.0.0:30000-30001->30000-30001/tcp myk8s-worker
# 219113c36204 kindest/node:v1.31.0 "/usr/local/bin/entr…" 3 minutes ago Up 3 minutes 127.0.0.1:58638->6443/tcp myk8s-control-plane
# 도커에서 127.0.0.1:58638->6443/tcp 로 포트포워딩을 하기 때문인것을 확인할 수 있습니다.
# apiserver 프로세스 확인
$ docker exec -it myk8s-control-plane ss -tnlp | grep 6443
# => LISTEN 0 4096 *:6443 *:* users:(("kube-apiserver",pid=584,fd=3))
$ kubectl get pod -n kube-system -l component=kube-apiserver -owide # 파드 IP 확인
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# kube-apiserver-myk8s-control-plane 1/1 Running 0 5m39s 172.20.0.2 myk8s-control-plane <none> <none>
# kube-apiserver 파드 상세 정보 확인
$ kubectl describe pod -n kube-system -l component=kube-apiserver
# health check 주소 접속 확인
$ docker exec -it myk8s-control-plane curl -k https://localhost:6443/livez ;echo
# => ok
$ docker exec -it myk8s-control-plane curl -k https://localhost:6443/readyz ;echo
# => ok
# 노드 정보 확인 : CRI 는 containerd 사용
$ kubectl get node -o wide
# => NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# myk8s-control-plane Ready control-plane 7m15s v1.31.0 172.20.0.2 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# myk8s-worker Ready <none> 7m1s v1.31.0 172.20.0.3 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# 파드 정보 확인 : CNI 는 kindnet 사용
$ kubectl get pod -A -owide
# => NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# kube-system coredns-6f6b679f8f-9gxnw 1/1 Running 0 10m 10.244.0.4 myk8s-control-plane <none> <none>
# kube-system etcd-myk8s-control-plane 1/1 Running 0 10m 172.20.0.2 myk8s-control-plane <none> <none>
# kube-system kindnet-b5brb 1/1 Running 0 10m 172.20.0.3 myk8s-worker <none> <none>
# kube-system kube-apiserver-myk8s-control-plane 1/1 Running 0 10m 172.20.0.2 myk8s-control-plane <none> <none>
# kube-system kube-controller-manager-myk8s-control-plane 1/1 Running 0 10m 172.20.0.2 myk8s-control-plane <none> <none>
# kube-system kube-proxy-tbh7b 1/1 Running 0 10m 172.20.0.3 myk8s-worker <none> <none>
# kube-system kube-scheduler-myk8s-control-plane 1/1 Running 0 10m 172.20.0.2 myk8s-control-plane <none> <none>
# local-path-storage local-path-provisioner-57c5987fd4-nfcv8 1/1 Running 0 10m 10.244.0.2 myk8s-control-plane <none> <none>
# ...
# 네임스페이스 확인
$ kubectl get namespaces
# => NAME STATUS AGE
# default Active 11m
# kube-node-lease Active 11m
# kube-public Active 11m
# kube-system Active 11m
# local-path-storage Active 10m
# 디버그용 내용 출력에 ~/.kube/config 권한 인증 로드
$ kubectl get pod -v6
# => I0907 21:08:08.698472 40997 loader.go:395] Config loaded from file: /Users/psyche/.kube/config
# I0907 21:08:08.709347 40997 round_trippers.go:553] GET https://127.0.0.1:58638/api/v1/namespaces/default/pods?limit=500 200 OK in 8 milliseconds
# No resources found in default namespace.
# kube config 파일 확인
$ cat ~/.kube/config
# local-path 라는 StorageClass 가 설치, local-path 는 노드의 로컬 저장소를 활용함
# 로컬 호스트의 path 를 지정할 필요 없이 local-path provisioner 이 볼륨을 관리
$ kubectl get sc
# => NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
# standard (default) rancher.io/local-path Delete WaitForFirstConsumer false 12m
$ kubectl get deploy -n local-path-storage
쿠버네티스 관련 정보 조사
이번에는 KIND 내부의 쿠버네티스 관련 정보를 살펴보겠습니다. 원활한 테스트를 위해 필요한 툴을 설치하고, KIND 내부의 쿠버네티스 관련 정보를 살펴보겠습니다.
# 툴 설치
$ docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree jq psmisc lsof wget bridge-utils tcpdump htop git nano -y'
$ docker exec -it myk8s-worker sh -c 'apt update && apt install tree jq psmisc lsof wget bridge-utils tcpdump htop git nano -y'
# static pod manifest 위치 찾기
$ docker exec -it myk8s-control-plane grep staticPodPath /var/lib/kubelet/config.yaml
# => staticPodPath: /etc/kubernetes/manifests
# static pod 정보 확인 : kubectl 및 control plane 에서 관리되지 않고 kubelet 을 통해 지정한 컨테이너를 배포
$ docker exec -it myk8s-control-plane tree /etc/kubernetes/manifests/
# => /etc/kubernetes/manifests/
# |-- etcd.yaml
# |-- kube-apiserver.yaml
# |-- kube-controller-manager.yaml
# `-- kube-scheduler.yaml
$ docker exec -it myk8s-worker tree /etc/kubernetes/manifests/
# => /etc/kubernetes/manifests/
# 워커 노드(컨테이너) bash 진입
$ docker exec -it myk8s-worker bash
# ---------------------------------
$ whoami
# => root
# kubelet 상태 확인
$ systemctl status kubelet
# => ● kubelet.service - kubelet: The Kubernetes Node Agent
# Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; preset: enabled)
# Drop-In: /etc/systemd/system/kubelet.service.d
# └─10-kubeadm.conf, 11-kind.conf
# Active: active (running) since Sat 2024-09-07 11:56:44 UTC; 48min ago
# Docs: http://kubernetes.io/docs/
# Main PID: 236 (kubelet)
# Tasks: 15 (limit: 2254)
# Memory: 32.9M
# CPU: 1min 7.074s
# CGroup: /kubelet.slice/kubelet.service
# └─236 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/conf>
# ...
# 컨테이너 확인
$ docker ps
# => bash: docker: command not found
$ crictl ps
# => CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
# dd7ff0509e335 6a23fa8fd2b78 49 minutes ago Running kindnet-cni 0 99f981aced025 kindnet-b5brb
# 41b224e66bc3c c573e1357a14e 49 minutes ago Running kube-proxy 0 8880634c13ba5 kube-proxy-tbh7b
-
docker ps
했을때는command not found
가 나오고crictl ps
로 했을때 컨테이너 정보가 나오는 것을 보면, KIND는 이름과는 다르게 Docker 대신 CRI(Container Runtime Interface)를 사용하고 있기 때문에docker
명령어 대신crictl
명령어를 사용해야 합니다.
# kube-proxy 확인
$ pstree
# => systemd-+-containerd---15*[{containerd}]
# |-containerd-shim-+-kube-proxy---8*[{kube-proxy}]
# | |-pause
# | `-12*[{containerd-shim}]
# |-containerd-shim-+-kindnetd---11*[{kindnetd}]
# | |-pause
# | `-12*[{containerd-shim}]
# |-kubelet---14*[{kubelet}]
# `-systemd-journal
$ pstree -p
# kube-proxy 프로세스 정보
$ ps afxuwww |grep proxy
# => root 387 0.0 1.1 1290144 24112 ? Ssl 11:56 0:02 \_ /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=myk8s-worker
# 방화벽 설정 확인
$ iptables -t filter -S
$ iptables -t nat -S
$ iptables -t mangle -S
$ iptables -t raw -S
$ iptables -t security -S
# tcp listen 포트 정보 확인
$ ss -tnlp
# 빠져나오기
$ exit
# ---------------------------------
파드 생성 및 확인
# 파드 생성
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
name: netpod
spec:
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx-pod
image: nginx:alpine
terminationGracePeriodSeconds: 0
EOF
# => pod/netpod created
# pod/nginx created
# 파드 확인
$ kubectl get pod -owide
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# netpod 1/1 Running 0 52s 10.244.1.3 myk8s-worker <none> <none>
# nginx 1/1 Running 0 52s 10.244.1.2 myk8s-worker <none> <none>
# netpod 파드에서 nginx 웹 접속
$ kubectl exec -it netpod -- curl -s $(kubectl get pod nginx -o jsonpath={.status.podIP}) | grep -o "<title>.*</title>"
# => <title>Welcome to nginx!</title>
컨트롤 플레인 컨테이너 정보 확인
이번에는 컨트롤 플레인 컨테이너의 정보를 확인해보겠습니다. kind는 Docker IN Docker 방식으로 컨트롤 플레인을 구성하기 때문에 컨트롤 플레인에서 정보를 확인할 때와 호스트에서 docker 정보를 확인할때와 차이가 있습니다.
# 도커 컨테이너 확인
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 6228f280b992 kindest/node:v1.31.0 "/usr/local/bin/entr…" 2 hours ago Up 2 hours 0.0.0.0:30000-30001->30000-30001/tcp myk8s-worker
# 219113c36204 kindest/node:v1.31.0 "/usr/local/bin/entr…" 2 hours ago Up 2 hours 127.0.0.1:58638->6443/tcp myk8s-control-plane
$ docker inspect myk8s-control-plane | jq
...
"Entrypoint": [
"/usr/local/bin/entrypoint",
"/sbin/init"
],
...
# 컨트롤플레인 컨테이너 bash 접속 후 확인
$ docker exec -it myk8s-control-plane bash
-------------------------------------------
# CPU 정보 확인
$ arch
# => aarch64 # intel 호환 CPU인 경우 x86_64가 표시됩니다.
# 기본 사용자 확인
$ whoami
# => root
# 네트워크 정보 확인
$ ip -br -c -4 addr
# => lo UNKNOWN 127.0.0.1/8
# vethfbd4a037@if4 UP 10.244.0.1/32
# veth50a51781@if4 UP 10.244.0.1/32
# veth1822edcc@if4 UP 10.244.0.1/32
# eth0@if17 UP 172.20.0.2/16
$ ip -c route
# => 10.244.0.2 dev vethfbd4a037 scope host
# 10.244.0.3 dev veth50a51781 scope host
# 10.244.0.4 dev veth1822edcc scope host
# 10.244.1.0/24 via 172.20.0.3 dev eth0
# 172.20.0.0/16 dev eth0 proto kernel scope link src 172.20.0.2
$ cat /etc/resolv.conf
# => nameserver 192.168.65.2
# options ndots:0
# Entrypoint 정보 확인
$ cat /usr/local/bin/entrypoint
# 프로세스 확인 : PID 1 은 /sbin/init
$ ps -ef
# => UID PID PPID C STIME TTY TIME CMD
# root 1 0 0 11:56 ? 00:00:01 /sbin/init
# ...
# kind는 docker 안에서 docker를 운영하기 위해 OS를 흉내내기 위해 자체적으로 systemd를 사용하기 때문에
# 위와 같이 PID 1이 /sbin/init 가 되고, systemctl 명령도 사용할 수 있습니다.
# 컨테이터 런타임 정보 확인
$ systemctl status containerd
# DinD 컨테이너 확인 : crictl 사용
$ crictl version
# => Version: 0.1.0
# RuntimeName: containerd
# RuntimeVersion: v1.7.18
# RuntimeApiVersion: v1
$ crictl info
$ crictl ps -o json | jq -r '.containers[] | {NAME: .metadata.name, POD: .labels["io.kubernetes.pod.name"]}'
$ crictl ps
# => CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
# 075e7a9f5f7a3 2437cf7621777 2 hours ago Running coredns 0 5e7c18501fc65 coredns-6f6b679f8f-9gxnw
# 6f61738127a55 2437cf7621777 2 hours ago Running coredns 0 3b69c8d195b5d coredns-6f6b679f8f-fk27q
# ...
# 파드 이미지 확인
$ crictl images
# => IMAGE TAG IMAGE ID SIZE
# docker.io/library/nginx alpine 9d6767b714bf1 20.2MB
# docker.io/nicolaka/netshoot latest eead9e442471d 178MB
# ...
# kubectl 확인
$ kubectl get node -v6
$ cat /etc/kubernetes/admin.conf
$ exit
-------------------------------------------
# 도커 컨테이너 확인 : 다시 한번 자신의 호스트PC에서 도커 컨테이너 확인, DinD 컨테이너가 호스트에서 보이는지 확인
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 6228f280b992 kindest/node:v1.31.0 "/usr/local/bin/entr…" 2 hours ago Up 2 hours 0.0.0.0:30000-30001->30000-30001/tcp myk8s-worker
# 219113c36204 kindest/node:v1.31.0 "/usr/local/bin/entr…" 2 hours ago Up 2 hours 127.0.0.1:58638->6443/tcp myk8s-control-plane
$ docker port myk8s-control-plane
# kubectl 확인 : k8s api 호출 주소 확인
$ kubectl get node -v6
- KIND의 컨트롤 플레인에서
crictl ps
로 컨테이너를 확인할때와 호스트에서docker ps
로 확인할때의 차이가 나는것을 확인할 수 있습니다. -
이것은 앞에서도 docker 컨테이너 안에서 docker (정확히는 containerd)를 별도로 사용하기 때문에 발생하는 현상입니다. 이것이 DIND(Docker IN Docker)입니다. —만약 같은 컨테이너가 나온다면 Docker OUT Docker로 동작하기 때문일것입니다—
- 클러스터를 삭제하겠습니다.
# 클러스터 삭제
$ kind delete cluster --name myk8s
# => Deleting cluster "myk8s" ...
# Deleted nodes: ["myk8s-worker" "myk8s-control-plane"]
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Multi-Node 클러스터 with kube-ops-view & mapping ports
이번에는 KIND로 Multi-Node 클러스터를 구성하고, kube-ops-view를 설치하여 클러스터 정보를 시각화하고, 포트 매핑을 통해 호스트에서 접속할 수 있도록 설정해보겠습니다.
- 클러스터 구성 및 노드 정보 확인
# '컨트롤플레인, 워커 노드 1대' 클러스터 배포 : 파드에 접속하기 위한 포트 맵핑 설정
$ cat <<EOT> kind-2node.yaml
# two node (one workers) cluster config
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
extraPortMappings:
- containerPort: 31000
hostPort: 31000
listenAddress: "0.0.0.0" # Optional, defaults to "0.0.0.0"
protocol: tcp # Optional, defaults to tcp
- containerPort: 31001
hostPort: 31001
EOT
$ CLUSTERNAME=myk8s
$ kind create cluster --config kind-2node.yaml --name $CLUSTERNAME
# => Creating cluster "myk8s" ...
# ✓ Ensuring node image (kindest/node:v1.31.0) 🖼
# ✓ Preparing nodes 📦 📦
# ✓ Writing configuration 📜
# ✓ Starting control-plane 🕹️
# ✓ Installing CNI 🔌
# ✓ Installing StorageClass 💾
# ✓ Joining worker nodes 🚜
# Set kubectl context to "kind-myk8s"
# You can now use your cluster with:
#
# kubectl cluster-info --context kind-myk8s
#
# Have a nice day! 👋
# 배포 확인
$ kind get clusters
# => myk8s
$ kind get nodes --name $CLUSTERNAME
# => myk8s-control-plane
# myk8s-worker
# 노드 확인
$ kubectl get nodes -o wide
# => NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# myk8s-control-plane Ready control-plane 2m12s v1.31.0 172.20.0.2 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# myk8s-worker Ready <none> 119s v1.31.0 172.20.0.3 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# 노드에 Taints 정보 확인
$ kubectl describe node $CLUSTERNAME-control-plane | grep Taints
# => Taints: node-role.kubernetes.io/control-plane:NoSchedule
$ kubectl describe node $CLUSTERNAME-worker | grep Taints
# => Taints: <none>
# control-plane 노드에는 taints가 걸려있어서 스케쥴링이 되지 않고
# worker 노드에는 taints가 없어서 스케쥴링이 될 것을 예상할 수 있습니다.
# 컨테이너 확인 : 컨테이너 갯수, 컨테이너 이름 확인
# kind yaml 에 포트 맵핑 정보 처럼, 자신의 PC 호스트에 31000 포트 접속 시, 워커노드(실제로는 컨테이너)에 TCP 31000 포트로 연결
# 즉, 워커노드에 NodePort TCP 31000 설정 시 자신의 PC 호스트에서 접속 가능!
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 7724a7ff92bb kindest/node:v1.31.0 "/usr/local/bin/entr…" 6 minutes ago Up 6 minutes 127.0.0.1:58498->6443/tcp myk8s-control-plane
# 5b7fa2f98703 kindest/node:v1.31.0 "/usr/local/bin/entr…" 6 minutes ago Up 6 minutes 0.0.0.0:31000-31001->31000-31001/tcp myk8s-worker
$ docker port $CLUSTERNAME-worker
# => 31000/tcp -> 0.0.0.0:31000
# 31001/tcp -> 0.0.0.0:31001
# 각 노드들의 정보 확인을 docker를 통해서 확인해 볼 수도 있습니다.
$ docker exec -it $CLUSTERNAME-control-plane ip -br -c -4 addr
$ docker exec -it $CLUSTERNAME-worker ip -br -c -4 addr
-
이번에 KIND를 통해 만든 클러스터는 호스트에서 31000, 31001 포트로 접속시 워커노드(컨테이너)의 31000, 31001 포트로 연결되도록 설정되는데, 그 이유는 KIND 클러스터를 생성할때 아래와 같이 포트를 열것을 지정했기 때문입니다.
... - role: worker extraPortMappings: - containerPort: 31000 hostPort: 31000 listenAddress: "0.0.0.0" # Optional, defaults to "0.0.0.0" protocol: tcp # Optional, defaults to tcp - containerPort: 31001 hostPort: 31001 ...
추가적인 포트 매핑이 필요한 경우 위와 같이
extraPortMappings
에 추가하여 포트를 매핑할 수 있습니다. -
Kube-ops-view 설치 : Node port 31000
# kube-ops-view
# helm show values geek-cookbook/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=31000 --set env.TZ="Asia/Seoul" --namespace kube-system
# 설치 확인
$ kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view
# => NAME READY UP-TO-DATE AVAILABLE AGE
# deployment.apps/kube-ops-view 0/1 1 0 18s
#
# NAME READY STATUS RESTARTS AGE
# pod/kube-ops-view-657dbc6cd8-tmkl5 0/1 ContainerCreating 0 18s
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/kube-ops-view NodePort 10.96.212.51 <none> 8080:31000/TCP 18s
#
# NAME ENDPOINTS AGE
# endpoints/kube-ops-view <none> 18s
# kube-ops-view 접속 URL 확인 (1.5 , 2 배율)
$ echo -e "KUBE-OPS-VIEW URL = http://localhost:31000/#scale=1.5"
# => KUBE-OPS-VIEW URL = http://localhost:31000/#scale=1.5
$ echo -e "KUBE-OPS-VIEW URL = http://localhost:31000/#scale=2"
클러스터 구성시 노드의 31000포트를 호스트의 31000에 매핑시켜서 호스트에서 위와 같이 열 수 있습니다.
- nginx 설치 : NodePort 31001
# 디플로이먼트와 서비스 배포
$ cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-websrv
spec:
replicas: 2
selector:
matchLabels:
app: deploy-websrv
template:
metadata:
labels:
app: deploy-websrv
spec:
terminationGracePeriodSeconds: 0
containers:
- name: deploy-websrv
image: nginx:alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: deploy-websrv
spec:
ports:
- name: svc-webport
port: 80
targetPort: 80
nodePort: 31001
selector:
app: deploy-websrv
type: NodePort
EOF
# 확인
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 5b7fa2f98703 kindest/node:v1.31.0 "/usr/local/bin/entr…" 15 minutes ago Up 15 minutes 0.0.0.0:31000-31001->31000-31001/tcp myk8s-worker
# ...
$ kubectl get deploy,svc,ep deploy-websrv
# => ...
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/deploy-websrv NodePort 10.96.115.233 <none> 80:31001/TCP 47s
# ...
# 자신의 PC에 호스트 포트 31001 접속 시 쿠버네티스 서비스에 접속 확인
$ open http://localhost:31001
$ curl -s localhost:31001 | grep -o "<title>.*</title>"
# => <title>Welcome to nginx!</title>
# 디플로이먼트와 서비스 삭제
$ kubectl delete deploy,svc deploy-websrv
31001 포트로 접속시 nginx 페이지가 나오는 것을 확인할 수 있습니다.
kube-ops-view에서 deploy된 정보를 확인할 수 있습니다.
파드 & PAUSE 컨테이너
파드(pod)는 쿠버네티스에서 배포하는 최소 단위이며, 파드 내부에는 여러 컨테이너가 포함될 수 있습니다. 파드 내부에는 PAUSE 컨테이너가 존재하며, PAUSE 컨테이너는 Network/IPC/UTS 네임스페이스를 생성하고 유지/공유하는 역할울 합니다. 네임스페이스와 네트워크 등에 대해서는 1주차에서 학습한 내용이 많이 도움되었습니다. 1주차 링크
K8S CRI (Container Runtime Interface)
먼저 파드와 PAUSE 컨테이너에 대해 알아보기전에 앞선 실습에서 보았던 CRI(Container Runtime Interface)에 대해 알아보겠습니다. 쿠버네티스는 컨테이너를 관리하기 위해 CRI(Container Runtime Interface)를 사용합니다.
CRI의 탄생 배경은 먼저 Docker에서 부터 찾아볼 수 있습니다. Docker가 대성공하고 컨테이너 기술이 확산되면서, 쿠버네티스도 Docker를 기본 컨테이너 런타임으로 사용했습니다. 하지만 Docker Inc라는 회사에 종속되는것을 우려하여, 표준화된 인터페이스를 만들어서 다양한 컨테이너 런타임을 지원하고자 했습니다. 그래서 CRI가 탄생하게 되었습니다. CRI라는 표준 인터페이스만 지키면 어떤 컨테이너 런타임이라도 쿠버네티스에서 사용할 수 있게 되었습니다. 이 과정에서 아쉬운건 Docker는 CRI를 지원하지 않았기 때문에, Docker를 사용하는 경우에는 Docker shim이라는 프록시를 사용해야 했었습니다.
파드 (Pod)
컨테이너 애플리케이션의 기본 단위를 파드(Pod)라고 부르며, 파드는 1개 이상의 컨테이너로 구성된 컨테이너의 집합입니다.
- Pod는 1개 이상의 컨테이너를 가질 수 있습니다.
- Pod내에 실행되는 컨테이너들은 동일한 노드에 할당되며 동일한 생명 주기(Life-cycle)를 갖습니다.
- Pod는 노드 IP 와 별개로 클러스터 내에서 접근 가능한 IP를 할당 받으며, 다른 노드에 위치한 Pod 도 CNI를 통해 NAT 없이 Pod IP로 접근 가능합니다.
- Pod내에 있는 컨테이너들은 서로 IP를 공유합니다. 같은 Pod내의 컨테이너끼리는 localhost 통해 서로 접근가능 합니다.
- pause 컨테이너가 network ns 를 만들어 주고, 내부의 컨테이너들은 해당 net ns 를 공유하기 때문에 IP를 공유하게 됩니다.
- Pod 안의 컨테이너들은 동일한 볼륨과 연결이 가능하여 파일 시스템을 기반으로 서로 파일을 주고받을 수 있습니다.
- Pod는 리소스 제약이 있는 격리된 환경의 애플리케이션 컨테이너 그룹으로 구성됩니다.
- 포드를 시작하기 전에 kubelet은 RuntimeService.RunPodSandbox를 호출하여 환경을 만듭니다.
- Kubelet은 RPC를 통해 컨테이너의 수명 주기를 관리하고, 컨테이너 수명 주기 후크와 활성/준비 확인을 실행하며, Pod의 재시작 정책을 준수합니다
PAUSE 컨테이너
- 쿠버네티스에서 pause 컨테이너는 포드의 모든 컨테이너에 대한 “부모 컨테이너” 역할을 합니다. - Link
-
pause 컨테이너에는 두 가지 핵심 책임이 있습니다.
- 파드에서 Linux 네임스페이스 공유의 기반 역할을 합니다. (Network, IPC, UTS 네임스페이스)
- PID(프로세스 ID) 네임스페이스 공유가 활성화되면 각 포드에 대한 PID 1 역할을 하며 좀비 프로세스를 거둡니다.
- pause의 핵심 소스코드는 여기에서 확인할 수 있습니다. 매우 짧지만 중요한 코드입니다. 상세하게 코드분석한 분이 있어서 자세히 알고 싶으신 분은 다음 링크를 참고하세요. 한글 링크 영문 링크
Pause 컨테이너 실습
- Pause 컨테이너의 동작에 대해 실습하기 위한 환경을 구성해보겠습니다.
# '컨트롤플레인, 워커 노드 1대' 클러스터 배포 : 파드에 접속하기 위한 포트 맵핑 설정
$ cat <<EOT> kind-2node.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
extraPortMappings:
- containerPort: 30000
hostPort: 30000
- containerPort: 30001
hostPort: 30001
EOT
$ kind create cluster --config kind-2node.yaml --name myk8s
# 툴 설치
$ docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree jq psmisc lsof wget bridge-utils tcpdump htop git nano -y'
$ docker exec -it myk8s-worker sh -c 'apt update && apt install tree jq psmisc lsof wget bridge-utils tcpdump htop -y'
# 확인
$ kubectl get nodes -o wide
$ docker ps
$ docker port myk8s-worker
$ docker exec -it myk8s-control-plane ip -br -c -4 addr
$ docker exec -it myk8s-worker ip -br -c -4 addr
# 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
# 설치 확인
$ kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view
# kube-ops-view 접속 URL 확인 (1.5 , 2 배율)
$ echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=1.5"
$ echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=2"
- worker 노드에 진입 후 네임스페이스 격리를 확인해보겠습니다.
# [터미널1] myk8s-worker bash 진입 후 실행 및 확인
$ docker exec -it myk8s-worker bash
----------------------------------
$ systemctl list-unit-files | grep 'enabled enabled'
# => containerd.service enabled enabled
# kubelet.service enabled enabled
# ...
# 확인 : kubelet에 --container-runtime-endpoint=unix:///run/containerd/containerd.sock
$ pstree -aln
# => systemd
# |-systemd-journal
# |-containerd
# | `-15*[{containerd}]
# |-kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --container-runtime-endpoint=unix:///run/containerd/containerd.sock --node-ip=172.20.0.3 --node-labels= --pod-infra-container-image=registry.k8s.io/pause:3.10 --provider-id=kind://docker/myk8s/myk8s-worker --runtime-cgroups=/system.slice/containerd.service
# | `-13*[{kubelet}]
# |-containerd-shim -namespace k8s.io -id 3368a087d8af3e241201257993e178d6a7d8ea23d3148cdf2ec5392f9db49832 -address /run/containerd/containerd.sock
# | |-11*[{containerd-shim}]
# | |-pause
# | `-kindnetd
# | `-11*[{kindnetd}]
# |-containerd-shim -namespace k8s.io -id e983e9fcff0162dec6128e014ae9092454fe0fd748ba237320aec17fa85fb17b -address /run/containerd/containerd.sock
# | |-11*[{containerd-shim}]
# | |-pause
# | `-kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=myk8s-worker
# | `-8*[{kube-proxy}]
# `-containerd-shim -namespace k8s.io -id 9cdabafac520e373ff0efdbbbe8b87bdfd7a0d02bd897863b609eefc4ae21b36 -address /run/containerd/containerd.sock
# |-12*[{containerd-shim}]
# |-pause
# `-python3 /usr/local/bin/python3 -m kube_ops_view
# `-2*[{python3}]
# 확인 : 파드내에 pause 컨테이너와 kube_ops_view 컨테이너, 네임스페이스 정보
$ pstree -aclnpsS
# => ...
# `-containerd-shim,1089 -namespace k8s.io -id 9cdabafac520e373ff0efdbbbe8b87bdfd7a0d02bd897863b609eefc4ae21b36 -address /run/containerd/contai
# nerd.sock
# |-{containerd-shim},1090
# ...
# |-pause,1110,ipc,mnt,net,pid,uts
# |-python3,1173,cgroup,ipc,mnt,net,pid,uts /usr/local/bin/python3 -m kube_ops_view
# ...
# ...
# 네임스페이스 확인 : lsns - List system namespaces
$ lsns -p 1
$ lsns -p $$
# => NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 15 1 root /sbin/init
# 4026531837 user 15 1 root /sbin/init
# 4026532329 mnt 9 1 root /sbin/init
# 4026532330 uts 13 1 root /sbin/init
# 4026532338 ipc 9 1 root /sbin/init
# 4026532339 pid 9 1 root /sbin/init
# 4026532341 net 13 1 root /sbin/init
# 4026532417 cgroup 13 1 root /sbin/init
# 파드의 pause 컨테이너는 노드의 NS와 다른 5개의 NS를 가짐 : mnt/pid 는 pasue 자신만 사용, net/uts/ipc는 app 컨테이너를 위해서 먼저 생성해둠
$ lsns -p 1797 # kube_ops_view 파드의 pause 컨테이너
# => NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 15 1 root /sbin/init
# 4026531837 user 15 1 root /sbin/init
# 4026532417 cgroup 13 1 root /sbin/init
# 4026532805 net 2 1110 65535 /pause # Node NS와 다름
# 4026532883 mnt 1 1110 65535 /pause # Node NS와 다름
# 4026532884 uts 2 1110 65535 /pause # Node NS와 다름
# 4026532885 ipc 2 1110 65535 /pause # Node NS와 다름
# 4026532886 pid 1 1110 65535 /pause # Node NS와 다름
# app 컨테이너(kube_ops_view)는 호스트NS와 다른 6개의 NS를 가짐 : mnt/pid/cgroup 는 자신만 사용, net/uts/ipc는 pause 컨테이너가 생성한 것을 공유 사용함
$ pgrep -f kube_ops_view
# => 1173
$ lsns -p $(pgrep -f kube_ops_view)
# => NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 15 1 root /sbin/init
# 4026531837 user 15 1 root /sbin/init
# 4026532805 net 2 1110 65535 /pause # pause 컨테이너와 공유
# 4026532884 uts 2 1110 65535 /pause # pause 컨테이너와 공유
# 4026532885 ipc 2 1110 65535 /pause # pause 컨테이너와 공유
# 4026532887 mnt 1 1173 1000 /usr/bin/qemu-x86_64 /usr/local/bin/python3 -m kube_ops_view # 자신만 사용
# 4026532888 pid 1 1173 1000 /usr/bin/qemu-x86_64 /usr/local/bin/python3 -m kube_ops_view # 자신만 사용
# 4026532889 cgroup 1 1173 1000 /usr/bin/qemu-x86_64 /usr/local/bin/python3 -m kube_ops_view # 자신만 사용
- 위와 같이 파드 내부의 pause 컨테이너와 kube_ops_view 컨테이너는 net, uts, ipc 네임스페이스를 공유하고, mnt, pid, cgroup 네임스페이스는 각각의 컨테이너가 사용하는 것을 확인할 수 있습니다.
- 이렇게 pause 컨테이너가 파드 내부의 컨테이너들이 공유하는 네임스페이스를 생성하고 유지하는 역할을 하는것을 확인 할 수 있었습니다.
- 마지막으로 DIND를 위한 containerd.sock 과 cgroup2fs, sys, proc 등의 정보를 확인해보겠습니다.
# containerd.sock 정보 확인 (docker.sock과 비슷한 역할)
$ ls -l /run/containerd/containerd.sock
# => srw-rw---- 1 root root 0 Sep 7 15:20 /run/containerd/containerd.sock
# 특정 소켓 파일을 사용하는 프로세스 확인
$ lsof /run/containerd/containerd.sock
# => COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
# container 106 root 9u unix 0x0000000000000000 0t0 726698 /run/containerd/containerd.sock type=STREAM (LISTEN)
# container 106 root 11u unix 0x0000000000000000 0t0 734691 /run/containerd/containerd.sock type=STREAM (CONNECTED)
# container 106 root 12u unix 0x0000000000000000 0t0 738482 /run/containerd/containerd.sock type=STREAM (CONNECTED)
# container 106 root 14u unix 0x0000000000000000 0t0 736712 /run/containerd/containerd.sock type=STREAM (CONNECTED)
# /sys 디렉터리 확인
$ findmnt -A
# => TARGET SOURCE FSTYPE OPTIONS
# / overlay overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/5UMBJJ
# ...
# |-/sys sysfs sysfs ro,nosuid,nodev,noexec,relatime
# | |-/sys/kernel/tracing tracefs tracefs rw,nosuid,nodev,noexec,relatime
# | |-/sys/kernel/debug debugfs debugfs rw,nosuid,nodev,noexec,relatime
# | |-/sys/fs/fuse/connections fusectl fusectl rw,nosuid,nodev,noexec,relatime
# | |-/sys/kernel/config configfs configfs rw,nosuid,nodev,noexec,relatime
# | `-/sys/fs/cgroup cgroup cgroup2 rw,nosuid,nodev,noexec,relatime
# ...
# cgroup 정보 확인
$ findmnt -t cgroup2
# => TARGET SOURCE FSTYPE OPTIONS
# /sys/fs/cgroup cgroup cgroup2 rw,nosuid,nodev,noexec,relatime
$ grep cgroup /proc/filesystems
# => nodev cgroup
# nodev cgroup2
$ stat -fc %T /sys/fs/cgroup/
# => cgroup2fs
$ exit
----------------------------------
- 신규파드를 배포하고 확인해보겠습니다.
# [터미널2] kubectl 명령 실행 및 확인
# Pod 생성 : YAML 파일에 컨테이너가 사용할 포트(TCP 80)을 설정
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: myweb
spec:
containers:
- image: nginx:alpine
name: myweb-container
ports:
- containerPort: 80
protocol: TCP
terminationGracePeriodSeconds: 0
EOF
# Pod 정보 확인 : pause 컨테이너 정보가 보이는지 확인
$ kubectl get pod -o wide
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# myweb 1/1 Running 0 15s 10.244.1.3 myk8s-worker <none> <none>
$ kubectl describe pod myweb | grep -i pause
$ kubectl get pod myweb -o json | grep -i pause
---
# [터미널1] myk8s-worker bash 진입 후 실행 및 확인
$ docker exec -it myk8s-worker bash
----------------------------------
$ crictl ps
$ pstree -aln
$ pstree -aclnpsS # 파드내에 pause 컨테이너와 app 컨테이너, 네임스페이스 정보
# => `-containerd-shim,1673 -namespace k8s.io -id 482ba93ecf76d46938d60e58f363e63e681d8a779439f270ff9c8417ad25a641 -address /run/containerd/containerd.sock
# |-{containerd-shim},1674
# ...
# |-pause,1693,ipc,mnt,net,pid,uts
# |-nginx,1754,cgroup,ipc,mnt,net,pid,uts
# 네임스페이스 확인 : lsns - List system namespaces
$ lsns -p 1
$ lsns -p $$
# => NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 25 1 root /sbin/init
# 4026531837 user 25 1 root /sbin/init
# 4026532329 mnt 10 1 root /sbin/init
# 4026532330 uts 14 1 root /sbin/init
# 4026532338 ipc 10 1 root /sbin/init
# 4026532339 pid 10 1 root /sbin/init
# 4026532341 net 14 1 root /sbin/init
# 4026532417 cgroup 15 1 root /sbin/init
$ lsns -p 1693
# => NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 25 1 root /sbin/init
# 4026531837 user 25 1 root /sbin/init
# 4026532417 cgroup 15 1 root /sbin/init
# 4026532891 net 9 1693 65535 /pause
# 4026532969 mnt 1 1693 65535 /pause
# 4026532970 uts 9 1693 65535 /pause
# 4026532971 ipc 9 1693 65535 /pause
# 4026532972 pid 1 1693 65535 /pause
$ lsns -p $(pgrep -n nginx) # app 컨테이너(nginx)
# => NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 25 1 root /sbin/init
# 4026531837 user 25 1 root /sbin/init
# 4026532891 net 9 1693 65535 /pause
# 4026532970 uts 9 1693 65535 /pause
# 4026532971 ipc 9 1693 65535 /pause
# 4026532973 mnt 8 1754 root nginx: master process nginx -g daemon off;
# 4026532974 pid 8 1754 root nginx: master process nginx -g daemon off;
# 4026532975 cgroup 8 1754 root nginx: master process nginx -g daemon off;
----------------------------------
# [터미널2] kubectl 명령 실행 및 확인
$ kubectl delete pod myweb
- 위와 같이
kubectl
과 같은 high-level에서는pause
컨테이너가 보이지 않지만,ps
나lsns
와 같은 OS에 접근 가능한 명령으로는pause
컨테이너가 확인이 되었습니다. - 이는 pause 컨테이너는 공기와 같이 항상 파드에 존재하기 때문에 굳이 보여줘서 화면만 복잡하게 할 뿐 보여줄 필요가 없다 판단한것 같습니다.
- PAUSE Container가 ns를 공유하는 특성을 이용하여, 애플리케이션과 런타임 의존성만 포함하는 최소화된 이미지기반 컨테이너인 Distroless Container를 대상으로 Ephemeral Containers와 같은 형태로 디버깅에 활용할 수 있습니다. Link
CNI (Container Network Interface)
- 쿠버네티스는 CNI(Container Network Interface)를 사용하여 네트워크를 관리합니다. CNI는 컨테이너 런타임과 네트워크 플러그인을 연결하는 인터페이스로 네트워크 인터페이스를 생성하고, IP 주소를 할당하며, 네트워크 정책을 적용하는 역할을 수행합니다.
- CNI의 주요 기능
- 네트워크 인터페이스 생성: CNI 플러그인은 컨테이너에 네트워크 인터페이스(예: 가상 이더넷 장치)를 생성하고 컨테이너 네임스페이스에 이를 연결합니다.
- IP 주소 할당: 플러그인은 네트워크 인터페이스에 IP 주소를 할당하고, 필요에 따라 IP 주소 관리를 처리합니다.
- 네트워크 정책 적용: 네트워크 정책을 통해 트래픽을 제어할 수 있으며, CNI 플러그인은 이를 구현하는 데 사용됩니다.
- 다양한 네트워크 모드 지원: 다양한 네트워크 토폴로지와 요구 사항을 지원하기 위해 여러 네트워크 모드(예: 브리지, VLAN, 오버레이 네트워크 등)를 지원합니다.
- CNI의 작동 방식
- CNI는 기본적으로 플러그인 기반 구조를 따릅니다. 오케스트레이션 도구는 특정 이벤트가 발생할 때 CNI 플러그인을 호출하여 필요한 네트워크 설정을 수행합니다. 각 CNI 플러그인은 JSON 형식의 구성 파일로 정의되며, 이를 통해 플러그인의 동작을 제어할 수 있습니다.
- CNI 플러그인의 종류 : CNI 플러그인은 다양한 종류가 있으며, 각기 다른 네트워킹 요구 사항을 충족시킵니다. 대표적인 CNI 플러그인은 다음과 같습니다.
- Flannel: 간단하고 사용하기 쉬운 오버레이 네트워크 플러그인입니다.
- Calico: 네트워크 정책과 보안을 강조하는 플러그인으로, 네트워크 격리 및 정책 적용에 강점이 있습니다.
- Weave: 자동 메쉬 네트워크와 서비스 디스커버리를 제공하는 플러그인입니다.
- Cilium: 고성능 BPF 기반 네트워킹 및 보안을 제공하는 플러그인입니다.
- Multus: 여러 CNI 플러그인을 사용하여 컨테이너에 여러 네트워크 인터페이스를 지원하는 플러그인입니다.
4가지 요구사항과 4가지 문제
쿠버네티스의 네트워크 모델은 4가지 요구사항을 만족해아하며 4가지 문제를 해결해야 합니다.
- 4가지 요구사항
- 파드와 파드 간 통신 시 NAT(Network Address Translation) 없이 통신이 가능해야 합니다.
- 노드의 에이전트(예) kubelet, 시스템 데몬)는 Pod와 통신이 가능해야 합니다.
- 호스트 네트워크를 사용하는 파드는 NAT 없이 파드와 통신이 가능해야 합니다.
- 서비스 클러스터 IP 대역과 파드가 사용하는 IP 대역은 중복되지 않아야 합니다.
- 해결해야 하는 문제
- 파드 내 컨테이너는 Loopback을 통한 통신을 할 수 있도록 해야 합니다.
- 파드 간 통신을 할 수 있어야 합니다.
- 클러스터 내부에서 Service를 통한 통신을 할 수 있어야 합니다.
- 클러스터 외부에서 Service를 통한 통신을 할 수 있어야 합니다.
위와 같은 요구사항과 문제를 해결하고 원활한 네트워크 통신을 위해 CNI(Container Network Interface)를 정의했습니다. CNI 플러그인들은 이러한 요구사항들을 기반으로 만들어졌습니다.
CNI 플러그인 동작 (출처: 추가예정)
Kubelet을 통해 파드가 신규 생성될 때 네트워크 관련 설정 추가 필요합니다. CNI 플러그인은 전달되는 설정 정의서를 보고 실제 파드가 통신하기 위한 네트워크 설정들을 실행하게 됩니다. 또한 CNI 플러그인은 IPAM(IP Address Management), 즉 IP 할당 관리를 수행해야 하며, 파드 간 통신을 위한 라우팅 설정을 처리해야 합니다.
Flannel
- Flannel은 쿠버네티스의 네트워크 요구사항을 충족하는 가장 간단하고 사용하기 쉬운 오버레이 네트워크 플러그인입니다.
- Flannel은 가상 네트워크를 생성하여 파드 간 통신을 가능하게 하며, VXLAN, UDP, Host-GW 등의 백엔드를 지원합니다. 하지만 VXLAN 사용이 권장됩니다.
- VXLAN은 Virtual eXtensible Local Area Network의 약자로, 물리적인 네트워크 환경에서 논리적인 가상의 네트워크 환경을 만들어 주는 것으로, UDP 8472 포트를 통해 노드 간 터널링 기법으로 통신하는 기술입니다.
Flannel 구조 (출처: 추가예정)
- 위의 그림과 같이 파드의 eth0 네트워크 인터페이스는 호스트 네임스페이스의 veth 인터페이스와 연결되고, veth는 cni0와 연결됩니다.
- 이를 통해 같은 노드에서 통신시 cni0 브릿지를 통해서 통신하고, 다른 노드와 통신시 VXLAN을 통해 통신합니다.
- VXLAN으로 가는 과정은 cni0 브릿지를 통해 flannel.1 인터페이스로 가고, flannel.1은 호스트의 eth0을 통해 다른 노드에 전송을 합니다. 이때 flannel.1은 VTEP(Vxlan Tunnel End Point)라고 하며 패킷을 감싸서 목표 node의 IP로 전송하면, 목표 node에서 감싼 패킷을 풀어서 해당 파드의 IP로 다시 보내는 역할을 수행합니다.
- 각 노드마다 파드에 할당할 수 있는 IP 네트워크 대역이 있고, flannel을 통하여 ETCD나 Kubernetes API에 전달되어, 모든 노드는 해당 정보를 자신의 라우팅 테이블에 업데이트합니다. 이를 통해 각각 다른 노드의 파드끼리도 내부 IP 주소를 통해 통신이 가능하게 됩니다.
Kind 와 Flannel 설치
- Kind 클러스터에 Flannel을 설치해보겠습니다. kind는 기본 CNI로 kindnet을 사용하는데 실습을 위해 kindnet을 끄고 클러스터를 구축하겠습니다.
#
$ cat <<EOF> kind-cni.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
labels:
mynode: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 30000
- containerPort: 30001
hostPort: 30001
- containerPort: 30002
hostPort: 30002
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
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: worker
- role: worker
labels:
mynode: worker2
networking:
disableDefaultCNI: true
EOF
$ kind create cluster --config kind-cni.yaml --name myk8s --image kindest/node:v1.30.4
# 배포 확인
$ kind get clusters
$ kind get nodes --name myk8s
$ kubectl cluster-info
# 네트워크 확인
$ kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"
# 노드 확인 : CRI
$ kubectl get nodes -o wide
# 노드 라벨 확인
$ kubectl get nodes myk8s-control-plane -o jsonpath={.metadata.labels} | jq
...
"mynode": "control-plane",
...
$ kubectl get nodes myk8s-worker -o jsonpath={.metadata.labels} | jq
$ kubectl get nodes myk8s-worker2 -o jsonpath={.metadata.labels} | jq
# 컨테이너 확인 : 컨테이너 갯수, 컨테이너 이름 확인
$ docker ps
$ docker port myk8s-control-plane
$ docker port myk8s-worker
$ docker port myk8s-worker2
# 컨테이너 내부 정보 확인
$ docker exec -it myk8s-control-plane ip -br -c -4 addr
$ docker exec -it myk8s-worker ip -br -c -4 addr
$ docker exec -it myk8s-worker2 ip -br -c -4 addr
#
$ docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree jq psmisc lsof wget bridge-utils tcpdump iputils-ping htop git nano -y'
$ docker exec -it myk8s-worker sh -c 'apt update && apt install tree jq psmisc lsof wget bridge-utils tcpdump iputils-ping -y'
$ docker exec -it myk8s-worker2 sh -c 'apt update && apt install tree jq psmisc lsof wget bridge-utils tcpdump iputils-ping -y'
- 다음 명령을 통해 bridge 실행파일을 생성해서 각 인스턴스마다 배포해야합니다. 먼저 다음과 같이 bridge 파일을 생헝 후 로컬에 복사하겠습니다.
$ docker exec -it myk8s-control-plane bash
---------------------------------------
# 빌드환경 구성
$ apt update && apt install golang git -y
$ git clone https://github.com/containernetworking/plugins
$ cd plugins
$ chmod +x build_linux.sh
# 빌드
$ ./build_linux.sh
# 파일 권한 확인 755
$ ls -l bin
# => -rwxr-xr-x 1 root root 4471145 Sep 7 16:53 bridge
# ...
$ exit
---------------------------------------
# 자신의 PC에 복사 : -a 권한 보존하여 복사(755)
$ docker cp -a myk8s-control-plane:/plugins/bin/bridge .
$ ls -l bridge
# => .rwxr-xr-x root staff 4.3 MB Sun Sep 0 00:00:00 2024 bridge
- 이제 Flannel을 설치하겠습니다. 현재 기본 CNI 인 kindnet 없이 설치했기 때문에 node가 not ready 상태여서 스케쥴링이 안 되고 있습니다.
$ watch -d kubectl get pod -A -owide
#
$ kubectl describe pod -n kube-system -l k8s-app=kube-dns | grep Events: -A 6
# => Events:
# Type Reason Age From Message
# ---- ------ ---- ---- -------
# Warning FailedScheduling 19m default-scheduler 0/1 nodes are available: 1 node(s) had untolerated taint {node.kubernetes.io/not-ready: }. preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling.
# 기본 CNI 인 kindnet 없이 설치했기 때문에 node가 not ready 상태여서 스케쥴링이 안 되고 있습니다.
# Flannel cni 설치
$ kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
# => namespace/kube-flannel created
# clusterrole.rbac.authorization.k8s.io/flannel created
# clusterrolebinding.rbac.authorization.k8s.io/flannel created
# serviceaccount/flannel created
# configmap/kube-flannel-cfg created
# daemonset.apps/kube-flannel-ds created
# namespace 에 pod-security.kubernetes.io/enforce=privileged Label 확인
$ kubectl get ns --show-labels
# => ...
# kube-flannel Active 60s k8s-app=flannel,kubernetes.io/metadata.name=kube-flannel,pod-security.kubernetes.io/enforce=privileged
$ kubectl get ds,pod,cm -n kube-flannel -owide
# => NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE CONTAINERS IMAGES SELECTOR
# daemonset.apps/kube-flannel-ds 3 3 3 3 3 <none> 100s kube-flannel docker.io/flannel/flannel:v0.25.6 app=flannel
#
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# pod/kube-flannel-ds-2l9p6 1/1 Running 0 100s 172.20.0.2 myk8s-worker2 <none> <none>
# pod/kube-flannel-ds-67ftf 1/1 Running 0 100s 172.20.0.3 myk8s-worker <none> <none>
# pod/kube-flannel-ds-87wv8 1/1 Running 0 100s 172.20.0.4 myk8s-control-plane <none> <none>
#
# NAME DATA AGE
# configmap/kube-flannel-cfg 2 100s
# configmap/kube-root-ca.crt 1 100s
# kube-flannel-ds가 daemonset으로 노드마다 실행중인것을 확인할 수 있습니다.
$ kubectl describe cm -n kube-flannel kube-flannel-cfg
$ kubectl describe ds -n kube-flannel kube-flannel-ds
$ kubectl exec -it ds/kube-flannel-ds -n kube-flannel -c kube-flannel -- ls -l /etc/kube-flannel
# failed to find plugin "bridge" in path [/opt/cni/bin]
$ kubectl get pod -A -owide
$ kubectl describe pod -n kube-system -l k8s-app=kube-dns
# => Warning FailedCreatePodSandBox 2m16s kubelet Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox "b0023dea7d730b58cfea0163318641ff1d000d368f8ad3b552c53040b371388c": plugin type="flannel" failed (add): failed to delegate add: failed to find plugin "bridge" in path [/opt/cni/bin]
# Warning FailedCreatePodSandBox 2m15s kubelet Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox "0454cbf9ae3c434f163865f6cd261ef2fa298a2de107471600195ebffa0b44cc": plugin type="flannel" failed (add): failed to delegate add: failed to find plugin "bridge" in path [/opt/cni/bin]
- 현재 /opt/cni/bin/bridge 파일이 없어서 오류가 발생하고 있습니다. 이를 해결하기 위해 bridge 파일을 복사하겠습니다.
# bridge 파일 복사
$ docker cp bridge myk8s-control-plane:/opt/cni/bin/bridge
$ docker cp bridge myk8s-worker:/opt/cni/bin/bridge
$ docker cp bridge myk8s-worker2:/opt/cni/bin/bridge
# 권한 부여
$ docker exec -it myk8s-control-plane chmod 755 /opt/cni/bin/bridge
$ docker exec -it myk8s-worker chmod 755 /opt/cni/bin/bridge
$ docker exec -it myk8s-worker2 chmod 755 /opt/cni/bin/bridge
$ docker exec -it myk8s-control-plane chown root:root /opt/cni/bin/bridge
$ docker exec -it myk8s-worker chown root:root /opt/cni/bin/bridge
$ docker exec -it myk8s-worker2 chown root:root /opt/cni/bin/bridge
# bridge 파일이 잘 복사되었는지 확인합니다.
$ docker exec -it myk8s-control-plane ls -l /opt/cni/bin/
$ docker exec -it myk8s-worker ls -l /opt/cni/bin/
$ docker exec -it myk8s-worker2 ls -l /opt/cni/bin/
$ for i in myk8s-control-plane myk8s-worker myk8s-worker2; do echo ">> node $i <<"; docker exec -it $i ls /opt/cni/bin/; echo; done
bridge flannel host-local loopback portmap ptp
#
$ kubectl get pod -A -owide
# => NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# ...
# kube-system coredns-7db6d8ff4d-hjmjf 1/1 Running 0 29m 10.244.1.4 myk8s-worker2 <none> <none>
# kube-system coredns-7db6d8ff4d-lfmzj 1/1 Running 0 29m 10.244.1.3 myk8s-worker2 <none> <none>
# ...
Flannel 설치 확인
- Flannel 설치 후 coredns가 정상적으로 배포되었습니다. 이제 Flannel이 정상적으로 설치되었는지 확인해보겠습니다.
#
$ kubectl get ds,pod,cm -n kube-flannel -owide
# => <span style="font-weight:bold;">NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE CONTAINERS IMAGES SELECTOR</span>
# <span style="color:gray;">daemonset.apps/kube-flannel-ds</span> <span style="color:teal;">3</span> <span style="color:gray;">3</span> <span style="color:teal;">3</span> <span style="color:gray;">3</span> <span style="color:teal;">3</span> <span style="color:gray;"><none></span> <span style="color:teal;">13m</span> <span style="color:gray;">kube-flannel</span> <span style="color:teal;">docker.io/flannel/flannel:v0.25.6</span> <span style="color:gray;">app=flannel</span>
#
# <span style="font-weight:bold;">NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES</span>
# <span style="color:gray;">pod/kube-flannel-ds-2l9p6</span> <span style="color:teal;">1/1</span> <span style="color:green;">Running</span> <span style="color:teal;">0</span> <span style="color:gray;">13m</span> <span style="color:teal;">172.20.0.2</span> <span style="color:gray;">myk8s-worker2</span> <span style="color:teal;"><none></span> <span style="color:gray;"><none></span>
# <span style="color:gray;">pod/kube-flannel-ds-67ftf</span> <span style="color:teal;">1/1</span> <span style="color:green;">Running</span> <span style="color:teal;">0</span> <span style="color:gray;">13m</span> <span style="color:teal;">172.20.0.3</span> <span style="color:gray;">myk8s-worker</span> <span style="color:teal;"><none></span> <span style="color:gray;"><none></span>
# <span style="color:gray;">pod/kube-flannel-ds-87wv8</span> <span style="color:teal;">1/1</span> <span style="color:green;">Running</span> <span style="color:teal;">0</span> <span style="color:gray;">13m</span> <span style="color:teal;">172.20.0.4</span> <span style="color:gray;">myk8s-control-plane</span> <span style="color:teal;"><none></span> <span style="color:gray;"><none></span>
#
# <span style="font-weight:bold;">NAME DATA AGE</span>
# <span style="color:gray;">configmap/kube-flannel-cfg</span> <span style="color:teal;">2</span> <span style="color:gray;">13m</span>
# <span style="color:gray;">configmap/kube-root-ca.crt</span> <span style="color:teal;">1</span> <span style="color:gray;">13m</span>
$ kubectl describe cm -n kube-flannel kube-flannel-cfg
# 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
# flannel 정보 확인 : 대역, MTU
$ for i in myk8s-control-plane myk8s-worker myk8s-worker2; do echo ">> node $i <<"; docker exec -it $i cat /run/flannel/subnet.env ; echo; done
# => >> node myk8s-control-plane <<
# FLANNEL_NETWORK=10.244.0.0/16
# FLANNEL_SUBNET=10.244.0.1/24
# FLANNEL_MTU=1450
# FLANNEL_IPMASQ=true
# ...
# 노드마다 할당된 dedicated subnet (podCIDR) 확인
$ kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' ;echo
# => 10.244.0.0/24 10.244.2.0/24 10.244.1.0/24
# 노드 정보 중 flannel 관련 정보 확인 : VXLAN 모드 정보와, VTEP 정보(노드 IP, VtepMac) 를 확인
$ kubectl describe node | grep -A3 Annotations
# => Annotations: flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"6e:0e:72:1e:72:1d"}
# flannel.alpha.coreos.com/backend-type: vxlan
# flannel.alpha.coreos.com/kube-subnet-manager: true
# flannel.alpha.coreos.com/public-ip: 172.20.0.4
# --
# Annotations: flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"ca:de:a3:b8:65:d8"}
# flannel.alpha.coreos.com/backend-type: vxlan
# flannel.alpha.coreos.com/kube-subnet-manager: true
# flannel.alpha.coreos.com/public-ip: 172.20.0.3
# --
# Annotations: flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"1e:1e:b1:15:9e:d3"}
# flannel.alpha.coreos.com/backend-type: vxlan
# flannel.alpha.coreos.com/kube-subnet-manager: true
# flannel.alpha.coreos.com/public-ip: 172.20.0.2
# 각 노드(?) 마다 bash 진입 후 아래 기본 정보 확인 : 먼저 worker 부터 bash 진입 후 확인하자
$ docker exec -it myk8s-worker bash
$ docker exec -it myk8s-worker2 bash
$ docker exec -it myk8s-control-plane bash
----------------------------------------
# 호스트 네트워크 NS와 flannel, kube-proxy 컨테이너의 네트워크 NS 비교 => 모두 동일한 NS를 가집니다.
$ lsns -p 1
# => NS TYPE NPROCS PID USER COMMAND
# ...
# 4026532344 net 12 1 root /sbin/init
$ lsns -p $(pgrep flanneld)
# => NS TYPE NPROCS PID USER COMMAND
# ...
# 4026532344 net 12 1 root /sbin/init
$ lsns -p $(pgrep kube-proxy)
# => NS TYPE NPROCS PID USER COMMAND
# ...
# 4026532344 net 12 1 root /sbin/init
# 기본 네트워크 정보 확인
$ ip -c -br addr
# => lo UNKNOWN 127.0.0.1/8 ::1/128
# flannel.1 UNKNOWN 10.244.2.0/32
# eth0@if37 UP 172.20.0.3/16 fc00:f853:ccd:e793::3/64 fe80::42:acff:fe14:3/64
$ ip -c link | grep -E 'flannel|cni|veth' -A1
# => 4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
# link/ether ca:de:a3:b8:65:d8 brd ff:ff:ff:ff:ff:ff
$ ip -c addr
$ ip -c -d addr show cni0 # 네트워크 네임스페이스 격리 파드가 1개 이상 배치 시 확인됨
# => (공백)
# 현재 네트워크 네임스페이스에 격리된 파드가 없어서 cni0 인터페이스가 없습니다.
$ ip -c -d addr show flannel.1
# => 4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
# link/ether ca:de:a3:b8:65:d8 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535
# vxlan id 1 local 172.20.0.3 dev eth0 srcport 0 0 dstport 8472 nolearning ttl auto ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
# inet 10.244.2.0/32 scope global flannel.1
# valid_lft forever preferred_lft forever
$ brctl show
# => (공백)
# 라우팅 정보 확인 : 다른 노드의 파드 대역(podCIDR)의 라우팅 정보가 업데이트되어 있음을 확인
$ ip -c route
# => default via 172.20.0.1 dev eth0
# 10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink
# 10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
# 172.20.0.0/16 dev eth0 proto kernel scope link src 172.20.0.3
# flannel.1 인터페이스를 통한 ARP 테이블 정보 확인 : 다른 노드의 flannel.1 IP와 MAC 정보를 확인
$ ip -c neigh show dev flannel.1
# => 10.244.1.0 lladdr 1e:1e:b1:15:9e:d3 PERMANENT
# 10.244.0.0 lladdr 6e:0e:72:1e:72:1d PERMANENT
# 브리지 fdb 정보에서 해당 MAC 주소와 통신 시 각 노드의 enp0s8
$ bridge fdb show dev flannel.1
# => 6e:0e:72:1e:72:1d dst 172.20.0.4 self permanent
# 1e:1e:b1:15:9e:d3 dst 172.20.0.2 self permanent
# 다른 노드의 flannel.1 인터페이스로 ping 통신 : VXLAN 오버레이를 통해서 통신
$ ping -c 1 10.244.0.0
# => ...
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
$ ping -c 1 10.244.1.0
# => ...
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
$ ping -c 1 10.244.2.0
# => ...
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
# 다른 노드와 VXLAN을 통해서 잘 통신 됩니다.
# iptables 필터 테이블 정보 확인 : 파드의 10.244.0.0/16 대역 끼리는 모든 노드에서 전달이 가능
$ iptables -t filter -S | grep 10.244.0.0
# => -A FLANNEL-FWD -s 10.244.0.0/16 -m comment --comment "flanneld forward" -j ACCEPT
# -A FLANNEL-FWD -d 10.244.0.0/16 -m comment --comment "flanneld forward" -j ACCEPT
# iptables NAT 테이블 정보 확인 : 10.244.0.0/16 대역 끼리 통신은 마스커레이딩 없이 통신을 하며,
# 10.244.0.0/16 대역에서 동일 대역(10.244.0.0/16)과 멀티캐스트 대역(224.0.0.0/4) 를 제외한 나머지 (외부) 통신 시에는 마스커레이딩을 수행
$ iptables -t nat -S | grep 'flanneld masq' | grep -v '! -s'
# => -A POSTROUTING -m comment --comment "flanneld masq" -j FLANNEL-POSTRTG
# -A FLANNEL-POSTRTG -m mark --mark 0x4000/0x4000 -m comment --comment "flanneld masq" -j RETURN
# -A FLANNEL-POSTRTG -s 10.244.2.0/24 -d 10.244.0.0/16 -m comment --comment "flanneld masq" -j RETURN
# -A FLANNEL-POSTRTG -s 10.244.0.0/16 -d 10.244.2.0/24 -m comment --comment "flanneld masq" -j RETURN
# -A FLANNEL-POSTRTG -s 10.244.0.0/16 ! -d 224.0.0.0/4 -m comment --comment "flanneld masq" -j MASQUERADE --random-fully
----------------------------------------
- 파드 2개를 생성해서 CNI 네트워크 브리지의 정보를 확인해보겠습니다.
# [터미널1,2] 워커 노드1,2 - 모니터링
$ docker exec -it myk8s-worker bash
$ docker exec -it myk8s-worker2 bash
-----------------------------
$ watch -d "ip link | egrep 'cni|veth' ;echo; brctl show cni0"
-----------------------------
# [터미널3] cat & here document 명령 조합으로 즉석(?) 리소스 생성
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
name: pod-1
labels:
app: pod
spec:
nodeSelector:
kubernetes.io/hostname: myk8s-worker
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
name: pod-2
labels:
app: pod
spec:
nodeSelector:
kubernetes.io/hostname: myk8s-worker2
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
# 파드 확인 : IP 확인
$ kubectl get pod -o wide
- CNI 가 없었는데 파드 패포후 CNI 네트워크 브리지가 생성되는 것을 확인할 수 있습니다. 정보를 확인해보겠습니다.
$ docker exec -it myk8s-worker bash
$ docker exec -it myk8s-worker2 bash
-----------------------------
# 브리지 정보 확인
$ brctl show cni0
# 브리지 연결 링크(veth) 확인
$ bridge link
# 브리지 VLAN 정보 확인
$ bridge vlan
# cbr(custom bridge) 정보 : kubenet CNI의 bridge - 링크
$ tree /var/lib/cni/networks/cbr0
# 네트워크 관련 정보들 확인
$ ip -c addr | grep veth -A3
-----------------------------
통신 흐름 이해
- Flannel 기반으로 네트워크를 구축할 때는 다음과 같이 세가지의 통신 시나리오가 생길 수 있습니다.
- 동일 노드에서 파드간 통신하는 경우
- 파드에서 외부와 통신하는 경우
- 서로다른 노드에서 파드간 통신하는 경우
- 동일 노드에서 파드간 통신하는 경우
파드 간 통신, 서로다른 노드의 파드간 통신, 외부 통신 여부에 대해 살펴보고, 통신이 일어나는 상황의 패킷을 캡처하면서 Flannel network에 대해 이해해 보았습니다.
$ kubectl exec -it pod-1 -- zsh
-----------------------------
$ ip -c addr show eth0
# GW IP는 어떤 인터페이스인가? => cni0
$ ip -c route
$ route -n
$ ping -c 1 <GW IP>
$ ping -c 1 <pod-2 IP> # 다른 노드에 배포된 파드 통신 확인
$ ping -c 1 8.8.8.8 # 외부 인터넷 IP 접속 확인
$ curl -s wttr.in/Seoul # 외부 인터넷 도메인 접속 확인
$ ip -c neigh
$ exit
- 서로다른 노드간 파드의 통신, 외부와의 통신 등이 모두 잘 통신 되는것을 확인할 수 있습니다.
- 이번에는 각 노드의 cni0에서 패킷 캡쳐를 진행해 보겠습니다.
# [터미널1,2] 워커 노드1,2
$ docker exec -it myk8s-worker bash
$ docker exec -it myk8s-worker2 bash
-----------------------------
$ tcpdump -i cni0 -nn icmp
$ tcpdump -i flannel.1 -nn icmp
$ tcpdump -i eth0 -nn icmp
$ tcpdump -i eth0 -nn udp port 8472 -w /root/vxlan.pcap
# CTRL+C 취소 후 확인 : ls -l /root/vxlan.pcap
$ conntrack -L | grep -i icmp
-----------------------------
# [터미널3]
$ docker cp myk8s-worker:/root/vxlan.pcap .
$ wireshark vxlan.pcap
- Pod-1 => Pod-2 (cni0 관점)
- 브릿지를 통해 각각 오가는 패킷이 잘 보입니다.
- Pod-1 => 외부 (cni0 관점)
- 같은 노드에서는 패킷이 외부로 나갔다 오는것이 잘 보입니다.
- 하지만 다른 노드 (worker2)에서는 외부와 오가는 패킷이 보이지 않습니다.
- Pod-1 => Pod-2 (flannel.1 관점)
- flannel.1을 통해 각각 오가는 패킷이 잘 보입니다.
- Pod-1 -> 외부 (flannel.1 관점)
- 앞선 그림에서 보았듯 외부와 통신시 VTEP인 flannel.1을 거치지 않고 cni0에서 호스트의 eth0로 바로 나가기 때문에 패킷이 캡쳐되지 않습니다.
- Pod 1 -> Pod 2 (eth0 관점)
- eth0에서 봤을때는 캡쳐가 되지 않습니다. 이유는 flannel.1을 통해 tcp 패킷이 캡슐화 되어 udp 8472로 eth0를 통해 전달되기 때문에
-nn icmp
옵션으로 icmp 패킷 (ping)만 캡쳐할때는 보이지 않습니다.
- eth0에서 봤을때는 캡쳐가 되지 않습니다. 이유는 flannel.1을 통해 tcp 패킷이 캡슐화 되어 udp 8472로 eth0를 통해 전달되기 때문에
- Pod 1 -> Pod 2 (eth0 관점, udp port 8472 덤프)
- udp 패킷을 캡쳐하여 wireshark로 확인해보겠습니다.
- udp 8472 포트를 통해 icmp (ping)이 오가는 것을 확인할 수 있습니다. (8472는 VXLAN 포트가 아니기 때문에 옵션에서 VXLAN을 지정해야 보입니다.)
- Pod 1 -> 8.8.8.8(외부) (eth0 관점)
- eth0에서 외부와 통신하는 패킷이 같은 노드에서는 보이고, 다른 노드에서는 안 보입니다.
마치며
이번주에도 많은 내용들을 스터디해보았습니다. KIND를 알게되어서 참 좋았던것 같습니다. 기존에 사내 교육을 위해서 kubernetes를 설치하려면 갖은 어려움이 있었는데 docker만 있으면 간단하게 설치가 가능하다는 것이 참 좋은것 같습니다.
막연하게 알고 있었던 노드간의 파드의 통신에 대해서 알게되었고, 쿠버네티스의 운영중 네트워크 장애에 대해 1주차 스터디와 이번 스터디를 통해 자신감이 조금 생겼습니다. 남은 스터디도 열심히해서 쿠버네티스의 네트워크에 대해 조금 아는 사람이 되어보겠습니다.