들어가며

지난주에 이어 이번주에는 쿠버네티스에대해 간략하게 알아보고 KIND, PAUSE 컨테이너와 Flannel CNI에 대해 알아보겠습니다. KANS 3기 2주차 스터디를 시작하겠습니다.

쿠버네티스 소개

  • 쿠버네티스는 구글에서 오픈소스로 공개한 컨테이너화된 애플리케이션을 자동으로 배포, 스케일링 및 관리하는 오픈소스 플랫폼입니다.

20240907_kans_w2_1.gif 출처: https://blog.naver.com/love_tolty/222167051615

  • 쿠버네티스는 위의 그림과 같이 다양한 컴포넌트로 이루어져 있습니다. 각 요소를 살펴보면 아래와 같습니다.

  • Control Plane(마스터 노드) : 마스터는 단일 서버 혹은 고가용성을 위한 클러스터 마스터로 구축
    • kube-apiserver : 모든 요청을 받아 드리는 API 서버
    • etcd : 클러스터내 모든 메타 정보를 저장하는 key/value DB 서비스
    • kube-scheduler : 컨테이너를 워커 노드에 배치하는 스케줄러
    • kube-controller-manager : 현재 상태와 바라는 상태를 지속적으로 확인하며 특정 이벤트에 따라 특정 동작을 수행하는 컨트롤러 - 링크
    • cloud-controller-manager : (AWS, GCP, Azure 등 클라우드 플랫폼에 특화된 리소스를 제어하는 클라우드 컨트롤러 - 링크
  • 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, …
    • 기타 : 모니터링, 대시보드, 로깅 등등

kind 소개 및 설치

img.png

kind는 Kubernetes IN Docker의 약자로, 로컬 환경에서 쿠버네티스 클러스터를 쉽게 구성할 수 있도록 도와주는 도구입니다. 이름에서 알 수 있듯이 Kubernetes를 Docker 안에서 DIND(Docker in Docker) 방식으로 구동시켜주는 도구입니다. minikube나 k3s 등과 달리 Docker만 설치되어 있으면 손쉽게 쿠버네티스 클러스터를 구성할 수 있습니다.

설치

제가 사용중인 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 &quot;kind-kind&quot;
#    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가 없어서 파드가 배포되는 것을 확인할 수 있습니다.

img.png

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
# => [
#      {
#        &quot;Name&quot;: &quot;kind&quot;,
#        &quot;Driver&quot;: &quot;bridge&quot;,
#        ...
#        &quot;IPAM&quot;: {
#          &quot;Driver&quot;: &quot;default&quot;,
#          &quot;Options&quot;: {},
#          &quot;Config&quot;: [
#            {
#              &quot;Subnet&quot;: &quot;172.20.0.0/16&quot;,
#              &quot;Gateway&quot;: &quot;172.20.0.1&quot;
#            }
#          ]
#        },
#       ...
#      }
#    ]

# 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
# => {
#      &quot;kind&quot;: &quot;Status&quot;,
#      &quot;apiVersion&quot;: &quot;v1&quot;,
#      &quot;metadata&quot;: {},
#      &quot;status&quot;: &quot;Failure&quot;,
#      &quot;message&quot;: &quot;forbidden: User \&quot;system:anonymous\&quot; cannot get path \&quot;/\&quot;&quot;,
#      &quot;reason&quot;: &quot;Forbidden&quot;,
#      &quot;details&quot;: {},
#      &quot;code&quot;: 403
#    } # 호스트에서 접속이 됩니다! 왜 그럴까요?

$ docker ps # 포트 포워딩 정보 확인
# => CONTAINER ID   IMAGE                  COMMAND                  CREATED         STATUS         PORTS                                  NAMES
#    6228f280b992   kindest/node:v1.31.0   &quot;/usr/local/bin/entr…&quot;   3 minutes ago   Up 3 minutes   0.0.0.0:30000-30001-&gt;30000-30001/tcp   myk8s-worker
#    219113c36204   kindest/node:v1.31.0   &quot;/usr/local/bin/entr…&quot;   3 minutes ago   Up 3 minutes   127.0.0.1:58638-&gt;6443/tcp              myk8s-control-plane

# 도커에서 127.0.0.1:58638-&gt;6443/tcp 로 포트포워딩을 하기 때문인것을 확인할 수 있습니다. 

# apiserver 프로세스 확인
$ docker exec -it myk8s-control-plane ss -tnlp | grep 6443
# => LISTEN 0      4096               *:6443             *:*    users:((&quot;kube-apiserver&quot;,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   &lt;none&gt;           &lt;none&gt;
# 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    &lt;none&gt;        Debian GNU/Linux 12 (bookworm)   5.10.76-linuxkit   containerd://1.7.18
#    myk8s-worker          Ready    &lt;none&gt;          7m1s    v1.31.0   172.20.0.3    &lt;none&gt;        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   &lt;none&gt;           &lt;none&gt;
#    kube-system          etcd-myk8s-control-plane                      1/1     Running   0          10m   172.20.0.2   myk8s-control-plane   &lt;none&gt;           &lt;none&gt;
#    kube-system          kindnet-b5brb                                 1/1     Running   0          10m   172.20.0.3   myk8s-worker          &lt;none&gt;           &lt;none&gt;
#    kube-system          kube-apiserver-myk8s-control-plane            1/1     Running   0          10m   172.20.0.2   myk8s-control-plane   &lt;none&gt;           &lt;none&gt;
#    kube-system          kube-controller-manager-myk8s-control-plane   1/1     Running   0          10m   172.20.0.2   myk8s-control-plane   &lt;none&gt;           &lt;none&gt;
#    kube-system          kube-proxy-tbh7b                              1/1     Running   0          10m   172.20.0.3   myk8s-worker          &lt;none&gt;           &lt;none&gt;
#    kube-system          kube-scheduler-myk8s-control-plane            1/1     Running   0          10m   172.20.0.2   myk8s-control-plane   &lt;none&gt;           &lt;none&gt;
#    local-path-storage   local-path-provisioner-57c5987fd4-nfcv8       1/1     Running   0          10m   10.244.0.2   myk8s-control-plane   &lt;none&gt;           &lt;none&gt;
#    ...

# 네임스페이스 확인 
$ 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   &lt;none&gt;           &lt;none&gt;
#    nginx    1/1     Running   0          52s   10.244.1.2   myk8s-worker   &lt;none&gt;           &lt;none&gt;

# 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   &quot;/usr/local/bin/entr…&quot;   2 hours ago   Up 2 hours   0.0.0.0:30000-30001-&gt;30000-30001/tcp   myk8s-worker
#    219113c36204   kindest/node:v1.31.0   &quot;/usr/local/bin/entr…&quot;   2 hours ago   Up 2 hours   127.0.0.1:58638-&gt;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    &lt;none&gt;        Debian GNU/Linux 12 (bookworm)   5.10.76-linuxkit   containerd://1.7.18
#    myk8s-worker          Ready    &lt;none&gt;          119s    v1.31.0   172.20.0.3    &lt;none&gt;        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:             &lt;none&gt;

# 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   &quot;/usr/local/bin/entr…&quot;   6 minutes ago   Up 6 minutes   127.0.0.1:58498-&gt;6443/tcp              myk8s-control-plane
#    5b7fa2f98703   kindest/node:v1.31.0   &quot;/usr/local/bin/entr…&quot;   6 minutes ago   Up 6 minutes   0.0.0.0:31000-31001-&gt;31000-31001/tcp   myk8s-worker
$ docker port $CLUSTERNAME-worker
# => 31000/tcp -&gt; 0.0.0.0:31000
#    31001/tcp -&gt; 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   &lt;none&gt;        8080:31000/TCP   18s
#    
#    NAME                      ENDPOINTS   AGE
#    endpoints/kube-ops-view   &lt;none&gt;      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"

img.png

클러스터 구성시 노드의 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   &quot;/usr/local/bin/entr…&quot;   15 minutes ago   Up 15 minutes   0.0.0.0:31000-31001-&gt;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   &lt;none&gt;        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

img.png 31001 포트로 접속시 nginx 페이지가 나오는 것을 확인할 수 있습니다.

img_1.png 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개 이상의 컨테이너로 구성된 컨테이너의 집합입니다.

img.png

  • 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 컨테이너에는 두 가지 핵심 책임이 있습니다.
    1. 파드에서 Linux 네임스페이스 공유의 기반 역할을 합니다. (Network, IPC, UTS 네임스페이스)
    2. 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"

img.png

  • 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   &lt;none&gt;           &lt;none&gt;
$ 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 컨테이너가 보이지 않지만, pslsns와 같은 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가지 요구사항
    1. 파드와 파드 간 통신 시 NAT(Network Address Translation) 없이 통신이 가능해야 합니다.
    2. 노드의 에이전트(예) kubelet, 시스템 데몬)는 Pod와 통신이 가능해야 합니다.
    3. 호스트 네트워크를 사용하는 파드는 NAT 없이 파드와 통신이 가능해야 합니다.
    4. 서비스 클러스터 IP 대역과 파드가 사용하는 IP 대역은 중복되지 않아야 합니다.
  • 해결해야 하는 문제
    1. 파드 내 컨테이너는 Loopback을 통한 통신을 할 수 있도록 해야 합니다.
    2. 파드 간 통신을 할 수 있어야 합니다.
    3. 클러스터 내부에서 Service를 통한 통신을 할 수 있어야 합니다.
    4. 클러스터 외부에서 Service를 통한 통신을 할 수 있어야 합니다.

위와 같은 요구사항과 문제를 해결하고 원활한 네트워크 통신을 위해 CNI(Container Network Interface)를 정의했습니다. CNI 플러그인들은 이러한 요구사항들을 기반으로 만들어졌습니다.

img.png 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  포트를 통해 노드 간 터널링 기법으로 통신하는 기술입니다. 

img.png 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'

img.png

  • 다음 명령을 통해 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           &lt;none&gt;          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         &lt;none&gt;           &lt;none&gt;
#    pod/kube-flannel-ds-67ftf   1/1     Running   0          100s   172.20.0.3   myk8s-worker          &lt;none&gt;           &lt;none&gt;
#    pod/kube-flannel-ds-87wv8   1/1     Running   0          100s   172.20.0.4   myk8s-control-plane   &lt;none&gt;           &lt;none&gt;
#    
#    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         &lt;none&gt;           &lt;none&gt;
#    kube-system          coredns-7db6d8ff4d-lfmzj                      1/1     Running   0          29m     10.244.1.3   myk8s-worker2         &lt;none&gt;           &lt;none&gt;
#    ...

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;">&lt;none&gt;</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;">&lt;none&gt;</span>           <span style="color:gray;">&lt;none&gt;</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;">&lt;none&gt;</span>           <span style="color:gray;">&lt;none&gt;</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;">&lt;none&gt;</span>           <span style="color:gray;">&lt;none&gt;</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: {&quot;VNI&quot;:1,&quot;VtepMAC&quot;:&quot;6e:0e:72:1e:72:1d&quot;}
#                        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: {&quot;VNI&quot;:1,&quot;VtepMAC&quot;:&quot;ca:de:a3:b8:65:d8&quot;}
#                        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: {&quot;VNI&quot;:1,&quot;VtepMAC&quot;:&quot;1e:1e:b1:15:9e:d3&quot;}
#                        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

img.png

  • 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
-----------------------------

img.png

통신 흐름 이해

  • Flannel 기반으로 네트워크를 구축할 때는 다음과 같이 세가지의 통신 시나리오가 생길 수 있습니다.
    1. 동일 노드에서 파드간 통신하는 경우 img.png
    2. 파드에서 외부와 통신하는 경우 img_1.png
    3. 서로다른 노드에서 파드간 통신하는 경우 img_2.png

파드 간 통신, 서로다른 노드의 파드간 통신, 외부 통신 여부에 대해 살펴보고, 통신이 일어나는 상황의 패킷을 캡처하면서 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

img.png

  • 서로다른 노드간 파드의 통신, 외부와의 통신 등이 모두 잘 통신 되는것을 확인할 수 있습니다.
  • 이번에는 각 노드의 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 관점)
    • 브릿지를 통해 각각 오가는 패킷이 잘 보입니다.

img.png

  • Pod-1 => 외부 (cni0 관점)
    • 같은 노드에서는 패킷이 외부로 나갔다 오는것이 잘 보입니다.
    • 하지만 다른 노드 (worker2)에서는 외부와 오가는 패킷이 보이지 않습니다.

img.png

  • Pod-1 => Pod-2 (flannel.1 관점)
    • flannel.1을 통해 각각 오가는 패킷이 잘 보입니다.

img.png

  • Pod-1 -> 외부 (flannel.1 관점)
    • 앞선 그림에서 보았듯 외부와 통신시 VTEP인 flannel.1을 거치지 않고 cni0에서 호스트의 eth0로 바로 나가기 때문에 패킷이 캡쳐되지 않습니다.

img.png

  • Pod 1 -> Pod 2 (eth0 관점)
    • eth0에서 봤을때는 캡쳐가 되지 않습니다. 이유는 flannel.1을 통해 tcp 패킷이 캡슐화 되어 udp 8472로 eth0를 통해 전달되기 때문에 -nn icmp 옵션으로 icmp 패킷 (ping)만 캡쳐할때는 보이지 않습니다.

img.png

  • Pod 1 -> Pod 2 (eth0 관점, udp port 8472 덤프)
    • udp 패킷을 캡쳐하여 wireshark로 확인해보겠습니다.

img.png

  • udp 8472 포트를 통해 icmp (ping)이 오가는 것을 확인할 수 있습니다. (8472는 VXLAN 포트가 아니기 때문에 옵션에서 VXLAN을 지정해야 보입니다.)

img_1.png

  • Pod 1 -> 8.8.8.8(외부) (eth0 관점)
    • eth0에서 외부와 통신하는 패킷이 같은 노드에서는 보이고, 다른 노드에서는 안 보입니다.

img.png


마치며

이번주에도 많은 내용들을 스터디해보았습니다. KIND를 알게되어서 참 좋았던것 같습니다. 기존에 사내 교육을 위해서 kubernetes를 설치하려면 갖은 어려움이 있었는데 docker만 있으면 간단하게 설치가 가능하다는 것이 참 좋은것 같습니다.

막연하게 알고 있었던 노드간의 파드의 통신에 대해서 알게되었고, 쿠버네티스의 운영중 네트워크 장애에 대해 1주차 스터디와 이번 스터디를 통해 자신감이 조금 생겼습니다. 남은 스터디도 열심히해서 쿠버네티스의 네트워크에 대해 조금 아는 사람이 되어보겠습니다.