[KANS] 3기 2주 – K8S Flannel CNI & Pause 컨테이너

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

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


1. Kubernetes 소개


단순히 소수의 컨테이너를 제어하기에는 docker 및 docker compose와 같은 cli 기반의 제어나 EC2/ECS 기반의 플랫폼만으로도 충분할것이다. 다른 container 실행환경 대비kubernete 환경의 장점은 무엇이고 왜 써야할까?


Kubernetes는 중앙집중화된 대규모 컨테이너 오케스트레이션 플랫폼이라고 정의할 수 있으며 다음과 같은 장점을 가지고 있다고 알려져있다. Overview

  • 서비스 디스커버리와 로드 밸런싱 
    • 쿠버네티스는 DNS 이름을 사용하거나 자체 IP 주소를 사용하여 컨테이너를 노출할 수 있다. 컨테이너에 대한 트래픽이 많으면, 쿠버네티스는 네트워크 트래픽을 로드밸런싱하고 배포하여 배포가 안정적으로 이루어질 수 있다.
  • 스토리지 오케스트레이션 
    • 쿠버네티스를 사용하면 로컬 저장소, 공용 클라우드 공급자 등과 같이 원하는 저장소 시스템을 자동으로 탑재할 수 있다
  • 자동화된 롤아웃과 롤백 
    • 쿠버네티스를 사용하여 배포된 컨테이너의 원하는 상태를 서술할 수 있으며 현재 상태를 원하는 상태로 설정한 속도에 따라 변경할 수 있다. 예를 들어 쿠버네티스를 자동화해서 배포용 새 컨테이너를 만들고, 기존 컨테이너를 제거하고, 모든 리소스를 새 컨테이너에 적용할 수 있다.
  • 자동화된 빈 패킹(bin packing) 
    • 컨테이너화된 작업을 실행하는데 사용할 수 있는 쿠버네티스 클러스터 노드를 제공한다. 각 컨테이너가 필요로 하는 CPU와 메모리(RAM)를 쿠버네티스에게 지시한다. 쿠버네티스는 컨테이너를 노드에 맞추어서 리소스를 가장 잘 사용할 수 있도록 해준다.
  • 자동화된 복구(self-healing) 
    • 쿠버네티스는 실패한 컨테이너를 다시 시작하고, 컨테이너를 교체하며, ‘사용자 정의 상태 검사’에 응답하지 않는 컨테이너를 죽이고, 서비스 준비가 끝날 때까지 그러한 과정을 클라이언트에 보여주지 않는다.
  • 시크릿과 구성 관리 
    • 쿠버네티스를 사용하면 암호, OAuth 토큰 및 SSH 키와 같은 중요한 정보를 저장하고 관리할 수 있다. 컨테이너 이미지를 재구성하지 않고 스택 구성에 시크릿을 노출하지 않고도 시크릿 및 애플리케이션 구성을 배포 및 업데이트할 수 있다.


이러한 장점은 왜 EC2/ECS같은 다른 플랫폼에서는 불가능하고, EKS와 같은 k8s 에서만 가능한가? 이는 kubernetes에서는 현재 상태를 감시하고 관리하는 별도의 주체가 존재하기 때문이다.

Kubernetes Components


k8s의 구조를 살펴보면 실제 컨테이너가 구동되는 Node부 외에 별도의 ControlPlane 영역이 별도로 존재함을 알 수 있다. 바로 이렇게 실제 워크로드의 작동을 담당하는 Node부(EC2,ECS 같은 타 플랫폼과 유사)외에 전체 클러스터의 작동을 관리하는 ControlPlane이 별도로 구성되고, 별도의 서버(api-server)와 데이터베이스(etcd)를 갖고 작동하기에 위와같은 장점들이 가능하다.


그러면 컨트롤플레인은 어떻게 같은 머신에 있지 않은 다른 노드들과 그 내부의 컨테이너을 관리하고 통신할 수 있을까? 이러한 컨테이너 관리(혹은 오케스트레이션)과 네트워크가 이번 스터디의 큰 주제라고 할 수 있겠다.

2. kind 소개 및 설치


2.1. 소개


Kind는 Kubernetes IN Docker 의 약자로, Kubernetes 테스트를 위한 로컬 클러스터를 제공해준다. k8s 클러스터 생성 방법을 설명한 k8s docs에 kubeadm/kubespray/minikube 외에 kind 또한 추가 되어있다. 컨테이너 노드 기반 Kubernetes 클러스터 구축 도구이며, 구성파일을 사용하여 생성 및 공유가 간편 / HA및 네트워크 등 설정할 수 있는 옵션이 많다는 특징이 있다.

kind, Design – Initial Design

2.2. 설치 및 기본 사용(Mac)


VM이 아닌 컨테이너로 생성된 Node를 사용하여 k8s클러스터가 생성되므로 컨테이너실행 환경을 구성해야한다.(Docker Desktop)

# https://formulae.brew.sh/cask/docker
brew install --cask docker


그 뒤, kind 및 추가 도구들을 설치한다.

kind 및 추가 도구 설치 스크립트

# Install Kind
brew install kind
kind --version

# Install kubectl : k8s client
brew install kubernetes-cli
kubectl version --client=true

# Install Helm : YAML manifest 관리도구
brew install helm
helm version

# Install Wireshark : 캡처된 패킷 확인
brew install --cask wireshark

# Install Krew : kubectl 플러그인 관리 도구
brew install krew 

# Install kube-ps1 : 현재 k8s 컨텍스트 및 ns 표시
brew install kube-ps1

# Install kubectx : k8s 클러스터 및 ns 간 쉬운 전환설정
brew install kubectx


이후 간단히 클러스터를 생성하고 몇가지 작동을 확인한다

kind 기본 사용

# 클러스터 배포 전 확인
docker ps

# Create a cluster with kind
kind create cluster

# 클러스터 배포 확인
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
혹은
cat $KUBECONFIG # KUBECONFIG 변수 지정 사용 시

# 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
혹은
cat $KUBECONFIG # KUBECONFIG 변수 지정 사용 시

결과

  • 설치 도구들 버전
    • kind: 0.24.0
    • kubectl: 1.31.0
    • helm: 3.15.4
  • 기본 클러스터 정보 확인
  • 컨트롤플레인 노드지만, 별도 taint 설정이 없어 pod 배치가 자유로움
  • 해당 노드는 kindest/node 라는 이미지를 기반으로한 컨테이너 노드

2.3. 동작원리 확인 – Docker IN Docker

Keep Calm and Load Balance on KIND – Antonio Ojea & Benjamin Elder, Google


kind 도구로 생성된 클러스터의 node는 kindest/node 라는 이미지를 기반으로한 컨테이너 노드이다. dockerhub, github

클러스터 내부에 생성되고 동작하는 pod들은 호스트(맥북)이 아닌, 도커 컨테이너인 Node 내부에 새로 Pod로 생성되고 작성되는 Docker IN Docker 구조를 갖는다. 따라서 전체 컨테이너는 호스트(맥북)에서 동작하긴 하지만, k8s 상의 Pod들은 node 컨테이너 내부에 격리되어있어 호스트에서 조회할 수 없다.

  • host에서의 docker ps -a / node에서의 crictl ps -a
kind, Design – Initial Design

기본 2node 클러스터 정보 확인 스크립트

  • 기본 2node 구성
# 클러스터 배포 전 확인
docker ps

# kind 는 별도 도커 네트워크 생성 후 사용 : 기본값 172.18.0.0/16
docker network ls
docker inspect kind | jq

# Create a cluster with kind
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

# k8s api 주소 확인 : 어떻게 로컬에서 접속이 되는 걸까?
kubectl cluster-info
docker ps # 포트 포워딩 정보 확인
docker exec -it myk8s-control-plane ss -tnlp | grep 6443
kubectl get pod -n kube-system -l component=kube-apiserver -owide # 파드 IP 확인
kubectl describe  pod -n kube-system -l component=kube-apiserver
docker exec -it myk8s-control-plane curl -k https://localhost:6443/livez ;echo
docker exec -it myk8s-control-plane curl -k https://localhost:6443/readyz ;echo

# 노드 정보 확인 : CRI 는 containerd 사용
kubectl get node -o wide

# 파드 정보 확인 : CNI 는 kindnet 사용
kubectl get pod -A -owide

# 네임스페이스 확인 >> 도커 컨테이너에서 배운 네임스페이스와 다릅니다!
kubectl get namespaces

# 컨트롤플레인, 워커 컨테이너 각각 1대씩 실행 : 도커 컨테이너 이름은 myk8s-control-plane , myk8s-worker 임을 확인
docker ps
docker images

# 디버그용 내용 출력에 ~/.kube/config 권한 인증 로드
kubectl get pod -v6

# kube config 파일 확인
cat ~/.kube/config
혹은
cat $KUBECONFIG

# local-path 라는 StorageClass 가 설치, local-path 는 노드의 로컬 저장소를 활용함
# 로컬 호스트의 path 를 지정할 필요 없이 local-path provisioner 이 볼륨을 관리
kubectl get sc
kubectl get deploy -n local-path-storage

# 툴 설치
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 관련 정보 조사
# 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/
...

# 워커 노드(컨테이너) bash 진입
docker exec -it myk8s-worker bash
---------------------------------
whoami

# kubelet 상태 확인
systemctl status kubelet

# 컨테이너 확인
docker ps
crictl ps

# kube-proxy 확인
pstree
pstree -p
ps afxuwww |grep proxy
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

# 파드 확인
kubectl get pod -owide

# 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>
  • 컨트롤플레인에서의 정보 확인
# 도커 컨테이너 확인
docker ps
docker inspect myk8s-control-plane | jq
...
      "Entrypoint": [
        "/usr/local/bin/entrypoint",
        "/sbin/init"
      ],
...

# 컨트롤플레인 컨테이너 bash 접속 후 확인
docker exec -it myk8s-control-plane bash
-------------------------------------------
# CPU 정보 확인
arch
aarch64  # mac m1~m3
혹은
x86_64   # intel/호환 cpu

# 기본 사용자 확인
whoami
root

# 네트워크 정보 확인
ip -br -c -4 addr
ip -c route
cat /etc/resolv.conf

# Entrypoint 정보 확인
cat /usr/local/bin/entrypoint

# 프로세스 확인 : PID 1 은 /sbin/init
ps -ef

# 컨테이터 런타임 정보 확인
systemctl status containerd

# DinD 컨테이너 확인 : crictl 사용
crictl version
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
ff3d3a53905fd       9d6767b714bf1       12 minutes ago      Running             nginx-pod                 0                   20328fe63d512       nginx
bebe6b14d1ab3       eead9e442471d       13 minutes ago      Running             netshoot-pod              0                   28cd918f0561a       netpod
...

# 파드 이미지 확인
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
docker port myk8s-control-plane

# kubectl 확인 : k8s api 호출 주소 확인
kubectl get node -v6 
  • 클러스터 제거
# 클러스터 삭제
kind delete cluster --name myk8s
docker ps

결과

  • 기본 2node 구성
    • 컨테이너의 특정포트(56510)이 api 서버의 6443 포트포워딩
    • ~/.kube/config의 등록된 엔드포인트로 호출하여 동작
  • Static pod 동작
    • k8s의 일부 Pod는 /etc/kubernetes/manifests/에 명시함으로써 api-server/etcd 가 아닌 node의 kubelet이 직접 실행하도록 설정가능
  • 컨트롤플레인에서의 정보 확인
    • entrypoint에 지정된 내용에 따라 /sbin/init 가 PID 1로 실행
    • 그 하위 프로세스로 각 컨테이너 가동

2.4. Multi-node Cluster 실습

Multi-Node Cluster (Control-plane, Nodes) with kube-ops-view & Mapping ports

  • 생성 및 포트매핑 확인
# '컨트롤플레인, 워커 노드 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

# 배포 확인
kind get clusters
kind get nodes --name $CLUSTERNAME

# 노드 확인
kubectl get nodes -o wide

# 노드에 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>

# 컨테이너 확인 : 컨테이너 갯수, 컨테이너 이름 확인
# kind yaml 에 포트 맵핑 정보 처럼, 자신의 PC 호스트에 31000 포트 접속 시, 워커노드(실제로는 컨테이너)에 TCP 31000 포트로 연결
# 즉, 워커노드에 NodePort TCP 31000 설정 시 자신의 PC 호스트에서 접속 가능!
docker ps
docker port $CLUSTERNAME-worker
31000/tcp -> 0.0.0.0:31000
31001/tcp -> 0.0.0.0:31001

# 컨테이너 내부 정보 확인 : 필요 시 각각의 노드(?)들에 bash로 접속하여 사용 가능
docker exec -it $CLUSTERNAME-control-plane ip -br -c -4 addr
docker exec -it $CLUSTERNAME-worker  ip -br -c -4 addr
  • 로컬 접속확인을 위한 kube-ops-view 배포
# 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

# kube-ops-view 접속 URL 확인 (1.5 , 2 배율)
echo -e "KUBE-OPS-VIEW URL = http://localhost:31000/#scale=1.5"
echo -e "KUBE-OPS-VIEW URL = http://localhost:31000/#scale=2"
  • 로컬 접속확인을 위한 nginx 배포
# 디플로이먼트와 서비스 배포
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
# 5a7ab428d87e   kindest/node:v1.31.0   "/usr/local/bin/entr…"   4 minutes ago   Up 4 minutes   127.0.0.1:56929->6443/tcp              myk8s-control-plane
# b0d47e603eac   kindest/node:v1.31.0   "/usr/local/bin/entr…"   4 minutes ago   Up 4 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.135.223   <none>        80:31001/TCP   55s
# ...

# 자신의 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

결과

  • node Taints, Docker port 확인
  • local 동작확인

3. Pod & Pause 컨테이너

요약

  • 파드는 1개 이상의 컨테이너로 구성된 컨테이너의 집합
  • PAUSE 컨테이너가 Network/IPC/UTS 네임스페이스를 생성 및 유지/공유

3.1. CRI (Container Runtime Interface) 개요

쿠버네티스, 도커 지원 중단

흔히 위와같은 이미지로 자주 알려진 이야기의 상세 내용이다.

Container Runtime :

kubelet → CRI → High Level Runtime (containerd) ← OCI → Low Level Runtime (Runc)

노드의 kubelet은 컨테이너 런타임 인터페이스(Container Runtime Interface,CRI)를 준수하는 고수준 컨테이너 런타임 인터페이스 (Containerd)가, 공개컨테이너규약(Open Container Initiative,OCI)를 준수하는 저수준 컨테이너 런타임(Runc)와 상호작용하며 실제 컨테이너의 생성,중지,제거등의 작동을 수행한다.

CRI는 표준 인터페이스를 통해 kubelet의 재컴파일없이 다양한 컨테이너 런타임을 사용하기위해 필요하다. 각 컨테이너 런타임에는 장단점이 있어 이 플러그인 인터페이스를 사용하여 다양한 컨테이너 런타임을 사용할 수 있다.

  • CRI 아키텍처 개선과정 – k8s blog

최초 docker의 경우 CRI를 지원하지않아 kubelet이 통신하기위해서는 중간에 Dockershim 이라는 추가 레이어가 필요했다. 이에 반해 containerd는 CRI-contained 별도 데몬을 구현하여 dockershim+docker 를 CRI-Containerd로 간소화하였다. 그러나 여전히 CRI-Containerd와 Contaierd 2개의 다른데몬이 필요해 오버헤드가 발생하였다.

현재의 containerd는 CRI-Containerd를 플러그인화하여 내장해, 직접 kubelet과 상호작용할 수 있어 통신과정을 안정적이며 효율적으로 만들었고, 불필요한 중간 단계를 제거하였다.

Containerd Brings More Container Runtime Options for Kubernetes

CRI-Containerd는 containerd를 사용해 전체 컨테이너의 생애주기를 관리한다. 또한 CNI를 통해 파드 네트워크를 관리한다.

  • Kubelet이 CRI runtime service API를 통해 cri-containerd를 호출해서 파드를 생성
  • cri-containerd는 containerd를 사용해 pause 컨테이너(샌드박스 컨테이너)를 생성 및 시작. pause 컨테이너를 pod의 cgroups와 namespace에 배치
  • cri-containerd는 CNI를 사용해 Pod의 network namespace 설정
  • kubelet은 애플리케이션 컨테이너 이미지를 pull하기 위해 CRI 이미지 서비스 API를 통해 cri-containerd를 호출
  • cri-containerd는 이미지가 노드에 없으면 containerd를 사용해 이미지 pull
  • kubelet은 애플리케이션 컨테이너를 생성하고 시작하기 위해 CRI 런타임 서비스 API를 통해 cri-containerd를 호출
  • cri-containerd는 containerd를 호출해 애플리케이션 컨테이너를 생성하고, 파드의 cgroups와 namespace 안에 배치하고 컨테이너 시작

3.2. Pause container 개요

pod 생성시 최초 pause 컨테이너가 생성되어, parent처럼 작동해 네트워크, 리소스등을 관리한다.

  • kubernetes에서 컨테이너 어플리케이션은 Pod 단위로 관리됨
  • pod 내부에는 다수의 container가 존재할 수 있다. > sidecar 패턴 등
  • pod 최초 생성시 pause 컨테이너가 생성되고 이후 생성되는 컨테이너들에 대해 부모컨테이너 처럼 작동
    • network namespace 생성 및 공유
    • 각 pod의 PID 1번 역할로 좀비 프로세스 회수
    • volume을 공유하여 Filesystem기반 서로 파일 주고받기 가능
    • 다른 Pod와의 격리된 환경 구축

pause container 실습 환경 구성
Multi-Node Cluster (Control-plane, Nodes) with kube-ops-view

  • 클러스터 생성 및 필수 도구 설치
# '컨트롤플레인, 워커 노드 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 배율) : macOS 사용자
echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=1.5"
echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=2"

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

Pod 배포 및 격리 확인

  • 기본 정보 확인
# [터미널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
...

#
crictl ps

# 확인 : 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.31.0.2 --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
#   |   `-15*[{kubelet}]
#   |-containerd-shim -namespace k8s.io -id 05d78f4ef8f91d6c970fd761a8eb885998f0a94bab5962fc9b7188d3828fe133 -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 b6ce7a8fa99aecb1e01fe419857069537e581f9cc3fe2af3a2801b7657ed400d -address /run/containerd/containerd.sock
#   |   |-11*[{containerd-shim}]
#   |   |-pause
#   |   `-kindnetd
#   |       `-12*[{kindnetd}]
#   |-containerd-shim -namespace k8s.io -id 138ee1db3b4f60f070205bc982a0b4cb675b0ca7c633178acb9d4ebbd65e3d96 -address /run/containerd/containerd.sock
#   |   |-11*[{containerd-shim}]
#   |   `-pause
#   `-containerd-shim -namespace k8s.io -id 7c90395b0efde2bf9087bd17105a4b57b2c847e1618ebbf7a49a0aee196fc0af -address /run/containerd/containerd.sock
#       |-10*[{containerd-shim}]
#       |-pause
#       `-metrics-server --cert-dir=/tmp --secure-port=10250 --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --kubelet-use-node-status-port --metric-resolution=15s
#           `-12*[{metrics-server}]

# 확인 : 파드내에 pause 컨테이너와 metrics-server 컨테이너, 네임스페이스 정보
pstree -aclnpsS
# ...
# `-containerd-shim,1631 -namespace k8s.io -id 7c90395b0efde2bf9087bd17105a4b57b2c847e1618ebbf7a49a0aee196fc0af -address /run/containerd/containerd.sock
# |-{containerd-shim},1632
# ...
# |-pause,1650,ipc,mnt,net,pid,uts
# |-{containerd-shim},1662
# |-metrics-server,1736,cgroup,ipc,mnt,net,pid,uts --cert-dir=/tmp --secure-port=10250 --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --kubelet-use-node-status-port --metric-resolution=15s
# |   |-{metrics-server},1752
# ...

# 네임스페이스 확인 : lsns - List system namespaces
lsns -p 1
lsns -p $$
#         NS TYPE   NPROCS PID USER COMMAND
# 4026531834 time       17   1 root /sbin/init
# 4026531837 user       17   1 root /sbin/init
# 4026532799 mnt        10   1 root /sbin/init
# 4026532800 uts        14   1 root /sbin/init
# 4026532801 ipc        10   1 root /sbin/init
# 4026532802 pid        10   1 root /sbin/init
# 4026532803 net        14   1 root /sbin/init
# 4026532923 cgroup     15   1 root /sbin/init

# 해당 파드에 pause 컨테이너는 호스트NS와 다른 5개의 NS를 가짐 : mnt/pid 는 pasue 자신만 사용, net/uts/ipc는 app 컨테이너를 위해서 먼저 생성해둠
lsns -p 1650
#         NS TYPE   NPROCS   PID USER  COMMAND
# 4026531834 time       17     1 root  /sbin/init
# 4026531837 user       17     1 root  /sbin/init
# 4026532923 cgroup     15     1 root  /sbin/init
# 4026533599 net         2  1650 65535 /pause
# 4026533719 mnt         1  1650 65535 /pause
# 4026533720 uts         2  1650 65535 /pause
# 4026533721 ipc         2  1650 65535 /pause
# 4026533722 pid         1  1650 65535 /pause

# app 컨테이너(metrics-server)는 호스트NS와 다른 6개의 NS를 가짐 : mnt/pid/cgroup 는 자신만 사용, net/uts/ipc는 pause 컨테이너가 생성한 것을 공유 사용함
pgrep python3
lsns -p $(pgrep python3)

#         NS TYPE   NPROCS   PID USER  COMMAND
# 4026531834 time       17     1 root  /sbin/init
# 4026531837 user       17     1 root  /sbin/init
# 4026533599 net         2  1650 65535 /pause
# 4026533720 uts         2  1650 65535 /pause
# 4026533721 ipc         2  1650 65535 /pause
# 4026533723 mnt         1  1736 1000  /metrics-server --cert-dir=/tmp --secure-port=10250 --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --kubelet-use-node-status-port --metric-resolution=15
# 4026533724 pid         1  1736 1000  /metrics-server --cert-dir=/tmp --secure-port=10250 --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --kubelet-use-node-status-port --metric-resolution=15
# 4026533725 cgroup      1  1736 1000  /metrics-server --cert-dir=/tmp --secure-port=10250 --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --kubelet-use-node-status-port --metric-resolution=15

#
ls -l /run/containerd/containerd.sock

# 특정 소켓 파일을 사용하는 프로세스 확인
lsof /run/containerd/containerd.sock

#
ss -xl | egrep 'Netid|containerd'

#
findmnt -A
# TARGET                                                  SOURCE                 FSTYPE    OPTIONS
/                                                       overlay                overlay   rw,relatime,lowerdir=/var/lib/docker/overlay2/l/3SJ3TXYIFYY4ZMKIFSVG# TARGET                                                  SOURCE                 FSTYPE    OPTIONS
# /                                                       overlay                overlay   rw,relatime,lowerdir=/var/lib/docker/overlay2/l/3SJ3TXYIFYY4ZMKIFSVGEQRGAJ
# ...
# |-/sys                                                  sysfs                  sysfs     ro,nosuid,nodev,noexec,relatime
# | |-/sys/kernel/debug                                   debugfs                debugfs   rw,nosuid,nodev,noexec,relatime
# | |-/sys/kernel/tracing                                 tracefs                tracefs   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
# 
findmnt -t cgroup2
grep cgroup /proc/filesystems
stat -fc %T /sys/fs/cgroup/

----------------------------------
  • myweb pod 배포 및 확인
# [터미널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
kubectl describe pod myweb
kubectl get pod myweb -o json # status.conditions 에 Type 정보 확인 : 시간 정렬은 안되어 있음..

---

# [터미널1] myk8s-worker bash 진입 후 실행 및 확인
docker exec -it myk8s-worker bash
----------------------------------
crictl ps
pstree -aln
pstree -aclnpsS # 파드내에 pause 컨테이너와 app 컨테이너, 네임스페이스 정보

# 네임스페이스 확인 : lsns - List system namespaces
lsns -p 1
lsns -p $$
lsns -p <pstree -aclnpsS에서 출력된 pause 컨테이너 PID> 
lsns -p $(pgrep nginx) # app 컨테이너(metrics-server)

----------------------------------

# [터미널2] kubectl 명령 실행 및 확인
kubectl delete pod myweb

결과

  • root, pause, metrics-server linux namespace 확인
    • root (lsns -p 1)
      • root는 PID 1로 자신의 namespace를 갖음
    • pause (lsns -p 1650)
      • pause는 net/mnt/uts/ipc/pid 5개 별도의 namespace를 생성 및 점유
    • metrics-server ( lsns -p $(pgrep metrics-server) )
      • metrics-server(실제 어플리케이션)은
      • pause 컨테이너중 net, uts, ipc를 공유
      • 스스로 사용할 mnt, pid, cgroup 생성 및 할당

myweb2

  • 2개의 컨테이너를 갖는 myweb2
apiVersion: v1
kind: Pod
metadata:
  name: myweb2
spec:
  containers:
  - name: myweb2-nginx
    image: nginx
    ports:
    - containerPort: 80
      protocol: TCP

  - name: myweb2-netshoot
    image: nicolaka/netshoot
    command: ["/bin/bash"]
    args: ["-c", "while true; do sleep 5; curl localhost; done"] # 포드가 종료되지 않도록 유지합니다

  terminationGracePeriodSeconds: 0
  • 배포 후 NET IPC UTS namespace 공유 확인
# [터미널1] 파드 생성
kubectl apply -f https://raw.githubusercontent.com/gasida/NDKS/main/3/myweb2.yaml

# kind POD 정보 확인 : 컨테이너의 집합
kubectl explain pod
# KIND:       Pod
# VERSION:    v1
# DESCRIPTION:
#     Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts.
# ...

# 확인
# pod 정보 READY 에 2/2 를 확인 : pod 내 모든 컨테이너가 정상이여야지 status 가 Running 가 됨
kubectl get pod -owide

# Pod 상세 정보에 컨테이너 2개 정보가 보인다
kubectl describe pod myweb2
root@k8s-m:~# kubectl describe pod myweb2
Name:         myweb2
...
Containers:
  myweb2-nginx:
    Container ID: containerd://b3a420b8e09fdc75395761414ef0cd4d82b570260d437ab3c802f7dd435f70b1
    Image:          nginx
...
  myweb2-netshoot:
    Container ID: containerd://c1ee2fe4ca048a538ad19532e1acc7f0a02d718523a3be47a5517f42d18ea687
    Image:         nicolaka/netshoot
...

# 파드의 각각 컨테이너 IP 확인 >> IP가 같다!
kubectl exec myweb2 -c myweb2-netshoot -- ip addr
kubectl exec myweb2 -c myweb2-nginx -- apt update
kubectl exec myweb2 -c myweb2-nginx -- apt install -y net-tools
kubectl exec myweb2 -c myweb2-nginx -- ifconfig

# myweb2-netshoot 컨테이너 zsh 진입
kubectl exec myweb2 -c myweb2-netshoot -it -- zsh
----------------------------------
ifconfig
ss -tnlp
curl localhost # nginx 컨테이너가 아닌데, 로컬 접속 되고 tcp 80 listen 이다. 왜그럴까?
ps -ef # nginx 프로세스 정보가 안보이는데... 
exit
----------------------------------

# 터미널3 : nginx 컨테이너 웹 접속 로그 출력 : 접속자(myweb2-netshoot)의 IP 가 ::1(ipv6) 혹은 127.0.0.1(ipv4) 이닷!
kubectl logs -f myweb2 -c myweb2-nginx
::1 - - [01/Sep/2024:06:33:26 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.7.1" "-"
혹은
127.0.0.1 - - [16/Jun/2021:06:22:24 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.77.0" "-"


# [터미널2] 
docker exec -it myk8s-worker bash
----------------------------------
# 컨테이너 정보 확인 : POD 와 POD ID가 같음을 확인
crictl ps
CONTAINER           IMAGE               CREATED              STATE               NAME                ATTEMPT             POD ID              POD
c1ee2fe4ca048       e286c635d1232       About a minute ago   Running             myweb2-netshoot     0                   40f9a3021011a       myweb2
b3a420b8e09fd       195245f0c7927       About a minute ago   Running             myweb2-nginx        0                   40f9a3021011a       myweb2
...

# 워커 노드에서 컨테이너 프로세스 정보 확인
ps -ef | grep 'nginx -g' | grep -v grep
root       2214   2137  0 09:23 ?        00:00:00 nginx: master process nginx -g daemon off;

ps -ef | grep 'curl' | grep -v grep
root       2347   2137  0 09:23 ?        00:00:00 /bin/bash -c while true; do sleep 5; curl localhost; done

# 각각 프로세스를 변수에 지정
NGINXPID=$(ps -ef | grep 'nginx -g' | grep -v grep | awk '{print $2}')
echo $NGINXPID

NETSHPID=$(ps -ef | grep 'curl' | grep -v grep | awk '{print $2}')
echo $NETSHPID

# 한 파드 내의 각 컨테이너의 네임스페이스 정보 확인
## time, user 네임스페이스는 호스트와 같음, 격리하지 않음
## mnt, uts, pid 네임스페이스는 컨테이너별로 격리
## ipc, uts, net 네임스페이스는 파드 내의 컨테이너 간 공유 (IPC : 컨테이너 프로세스간 공유 - signal, socket, pipe 등)
## Pause 컨테이너는 IPC, Network, UTS 네임스페이스를 생성하고 유지 -> 나머지 컨테이너들은 해당 네임스페이스를 공유하여 사용
## 유저가 실행한 특정 컨테이너가 비정상 종료되어 컨터이너 전체에서 공유되는 네임스페이스에 문제가 발생하는 것을 방지

lsns -p $NGINXPID
#         NS TYPE   NPROCS   PID USER  COMMAND
# 4026531834 time       32     1 root  /sbin/init
# 4026531837 user       32     1 root  /sbin/init
# 4026533726 net        14  2156 65535 /pause
# 4026533847 uts        14  2156 65535 /pause
# 4026533848 ipc        14  2156 65535 /pause
# 4026533850 mnt        11  2214 root  nginx: master process nginx -g daemon off;
# 4026533851 pid        11  2214 root  nginx: master process nginx -g daemon off;
# 4026533852 cgroup     11  2214 root  nginx: master process nginx -g daemon off;

lsns -p $NETSHPID
#         NS TYPE   NPROCS   PID USER  COMMAND
# 4026531834 time       32     1 root  /sbin/init
# 4026531837 user       32     1 root  /sbin/init
# 4026533726 net        14  2156 65535 /pause
# 4026533847 uts        14  2156 65535 /pause
# 4026533848 ipc        14  2156 65535 /pause
# 4026533853 mnt         2  2347 root  /bin/bash -c while true; do sleep 5; curl localhost; done
# 4026533854 pid         2  2347 root  /bin/bash -c while true; do sleep 5; curl localhost; done
# 4026533855 cgroup      2  2347 root  /bin/bash -c while true; do sleep 5; curl localhost; done

# pause 정보 확인 : 
PAUSEPID=<각자 자신의 pause PID> (pstree -aclnpsS | grep pause)
PAUSEPID=2156
lsns -p $PAUSEPID
        NS TYPE   NPROCS   PID USER  COMMAND
4026531834 time       32     1 root  /sbin/init
4026531837 user       32     1 root  /sbin/init
4026532923 cgroup     17     1 root  /sbin/init # cgroup 호스트와 같은 이유는?
4026533726 net        14  2156 65535 /pause
4026533846 mnt         1  2156 65535 /pause     # app 컨테이너와 다른 이유는?
4026533847 uts        14  2156 65535 /pause
4026533848 ipc        14  2156 65535 /pause
4026533849 pid         1  2156 65535 /pause     # app 컨테이너와 다른 이유는?

# 개별 컨테이너에 명령 실행 : IP 동일 확인
crictl ps
crictl ps -q
crictl exec -its c1ee2fe4ca048 ifconfig
crictl exec -its b3a420b8e09fd ifconfig


# PAUSE 의 NET 네임스페이스 PID 확인 및 IP 정보 확인
lsns -t net
nsenter -t $PAUSEPID -n ip -c addr
nsenter -t $NGINXPID -n ip -c addr
nsenter -t $NETSHPID -n ip -c addr

# 2개의 네임스페이스 비교 , 아래 2112 프로세스의 정제는?
crictl inspect <myweb2-nginx    컨테이너ID> | jq
crictl inspect <myweb2-netshoot 컨테이너ID> | jq
crictl inspect c1ee2fe4ca048 | jq
crictl inspect b3a420b8e09fd | jq
...
        "namespaces": [
          {
            "type": "pid"
          },
          {
            "type": "ipc",
            "path": "/proc/2112/ns/ipc"
          },
          {
            "type": "uts",
            "path": "/proc/2112/ns/uts"
          },
          {
            "type": "mount"
          },
          {
            "type": "network",
            "path": "/proc/2112/ns/net"
          },
          {
            "type": "cgroup"
          }
        ],
...

결과

  • pod의 netshoot, nginx ip 확인 > 10.244.1.4로 동일
  • 한 pod 내의 각 2개의 컨테이너는 pause container의 net 및 uts,ipc namespace 공유

4. Flannel CNI

요약
쿠버네티스는 네트워크 모델의 요건을 만족하는 CNI 플러그인이 있고, 대표적으로 ‘Calico, Cilium 등’이 있습니다.

4.1. Kubernetes Network

The 4 Types of Kubernetes Cluster Networking Models Explained

쿠버네티스환경에서 네트워크는 크게 4가지 유형의 해결해야할 문제가 있다.

  1. 컨테이너 <> 컨테이너간 통신
  2. 파드 <> 파드간 통신
  3. 클러스터 내부) 파드 <> 서비스간 통신
  4. 클러스터 외부) 외부 <> 서비스간 통신


이에 다음의 방식으로 문제를 해결한다.

  1. 컨테이너간 통신: Loopback을 사용한 로컬 통신
  2. 파드간 통신: 포트가 아닌, 고유한 ip를 통한 구분 및 통신
  3. 클러스터 내부간 통신: service object를 활용한 ip기반 노출
  4. 클러스터 외부간 통신: service object를 활용한 ip기반 노출 + ingress, gateway등의 object


이러한 네트워크 모델은 각 노드의 컨테이너 런타임에 의해 구현된다. 일반적으로는 Container Network Interface(CNI)플러그인을 사용해 네트워크 및 보안 기능을 관리한다. 일반적인 CNI의 요건은 CNI spec 문서를 통해 확인할 수 있고, 이에따라 다양한 CNI 구현체들이 존재한다. 이 CNI 마다 세부적인 구현 및 작동방식은 다르다.

4.2. Flannel CNI

Flannel CNI는 Kubernetes 상에 간단한 L3 Fabric 네트워크를 구축한다. 각 노드에 flanneld라는 단일 바이너트 에이전트를 배포하여, 사전에 구성된 주소대역대에서 각 호스트에 적절한 서브넷 대역을 배정한다.

Flannel Architecture

VXLAN, host-gw, UDP 등의 네트워크 백엔드를 지원하며 VXLAN이 권장된다. 참고1, 참고2

  • VXLAN: 일반적으로 권장
  • host-gw: 성능개선이 필요하고, 기반 인프라(일반적인 클라우드 제외)가 지원하는 숙련된 사용자용
  • UDP: 디버그 또는 VXLAN을 지원하지않는 구 리눅스 커널용

kind & Flannel 배포 (mac)

  • 기본 CNI 비활성화 후 kind 클러스터(v1.30) 생성
#
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'
#
watch -d kubectl get pod -A -owide

#
kubectl describe pod -n kube-system -l k8s-app=kube-dns
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  57s   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.

# Flannel cni 설치
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

# namespace 에 pod-security.kubernetes.io/enforce=privileged Label 확인 
kubectl get ns --show-labels | grep privileged
# NAME                 STATUS   AGE     LABELS
# kube-flannel         Active   2m49s   k8s-app=flannel,kubernetes.io/metadata.name=kube-flannel,pod-security.kubernetes.io/enforce=privileged

kubectl get ds,pod,cm -n kube-flannel

kubectl describe cm -n kube-flannel kube-flannel-cfg
# cni-conf.json:
# ----
# {
#   "name": "cbr0",
#   "cniVersion": "0.3.1",
#   "plugins": [
#     {
#       "type": "flannel",
#       "delegate": {
#         "hairpinMode": true,
#         "isDefaultGateway": true
#       }
#     },
#     {
#       "type": "portmap",
#       "capabilities": {
#         "portMappings": true
#       }
#     }
#   ]
# }
# 
# 
# net-conf.json:
# ----
# {
#   "Network": "10.244.0.0/16",
#   "EnableNFTables": false,
#   "Backend": {
#     "Type": "vxlan"
#   }
# }
# 

kubectl describe ds -n kube-flannel kube-flannel-ds
...
    Mounts:
      /etc/cni/net.d from cni (rw)
      /etc/kube-flannel/ from flannel-cfg (rw)
    ...
    Mounts:
      /etc/kube-flannel/ from flannel-cfg (rw)
      /run/flannel from run (rw)
      /run/xtables.lock from xtables-lock (rw)
  Volumes:
   run:
    Type:          HostPath (bare host directory volume)
    Path:          /run/flannel
    HostPathType:  
   cni-plugin:
    Type:          HostPath (bare host directory volume)
    Path:          /opt/cni/bin
    HostPathType:  
   cni:
    Type:          HostPath (bare host directory volume)
    Path:          /etc/cni/net.d
    HostPathType:  
   flannel-cfg:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      kube-flannel-cfg
    Optional:  false
   xtables-lock:
    Type:               HostPath (bare host directory volume)
    Path:               /run/xtables.lock
    HostPathType:       FileOrCreate
  Priority Class Name:  system-node-critical
...

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  35s               kubelet            Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox "786e9caec9c312a0b8af70e14865535575601d024ec02dbb581a1f5ac0b8bb06": plugin type="flannel" failed (add): loadFlannelSubnetEnv failed: open /run/flannel/subnet.env: no such file or directory
  Warning  FailedCreatePodSandBox  23s               kubelet            Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox "6ccd4607d32cbb95be9ff40b97a436a07e5902e6c24d1e12aa68fefc2f8b548a": plugin type="flannel" failed (add): failed to delegate add: failed to find plugin "bridge" in path [/opt/cni/bin]

#
kubectl get pod -A -owide

#
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

#
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

Flannel 정보 확인

#
kubectl get ds,pod,cm -n kube-flannel -owide
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=65485 # MTU 지정
FLANNEL_IPMASQ=true # 파드가 외부(인터넷) 통신 시 해당 노드의 마스커레이딩을 사용
...

# 노드마다 할당된 dedicated subnet (podCIDR) 확인
kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' ;echo

# 노드 정보 중 flannel 관련 정보 확인 : VXLAN 모드 정보와, VTEP 정보(노드 IP, VtepMac) 를 확인
kubectl describe node | grep -A3 Annotations

# 각 노드(?) 마다 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 비교 : 파드의 IP와 호스트(서버)의 IP를 비교해보자!
lsns -p 1
lsns -p $(pgrep flanneld)
lsns -p $(pgrep kube-proxy)


# 기본 네트워크 정보 확인
ip -c -br addr
ip -c link | grep -E 'flannel|cni|veth' -A1
ip -c addr
ip -c -d addr show cni0     # 네트워크 네임스페이스 격리 파드가 1개 이상 배치 시 확인됨

ip -c -d addr show flannel.1
	5: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
    link/ether 1e:81:fc:d5:8a:42 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535
    vxlan id 1 local 192.168.100.10 dev enp0s8 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 172.16.0.0/32 brd 172.16.0.0 scope global flannel.1
    
brctl show
bridge name	bridge id		STP enabled	interfaces
cni0		8000.663bf746b6a8	no		vethbce1591c
							vethc17ba51b
							vethe6540260

# 라우팅 정보 확인 : 다른 노드의 파드 대역(podCIDR)의 라우팅 정보가 업데이트되어 있음을 확인		
ip -c route
default via 172.18.0.1 dev eth0 
10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink 
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1 
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink 
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.3 

# flannel.1 인터페이스를 통한 ARP 테이블 정보 확인 : 다른 노드의 flannel.1 IP와 MAC 정보를 확인
ip -c neigh show dev flannel.1

# 브리지 fdb 정보에서 해당 MAC 주소와 통신 시 각 노드의 enp0s8 
bridge fdb show dev flannel.1

# 다른 노드의 flannel.1 인터페이스로 ping 통신 : VXLAN 오버레이를 통해서 통신
ping -c 1 10.244.0.0
ping -c 1 10.244.1.0
ping -c 1 10.244.2.0

# iptables 필터 테이블 정보 확인 : 파드의 10.244.0.0/16 대역 끼리는 모든 노드에서 전달이 가능
iptables -t filter -S | grep 10.244.0.0

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

----------------------------------------

결과

  • host (mac)에서의 정보 조회
    • 전체 클러스터는 “Network”: “10.244.0.0/16” 대역
    • 개별 노드마다 10.244.0~2.1/24 서브넷 할당됨
  • 노드(worker)에서의 정보 조회
    • 호스트, flannel, kube-proxy의 net namespace PID 동일
    • flanel.1의 mac주소와 노드의 VtepMAC(?) 동일
    • 클러스터내 다른 노드들과의 route 정보 확인



pod 2개 생성

  • pod 생성
# [터미널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

  • 생성 이후 정보 확인
# [터미널1,2] 워커 노드1,2
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
	/var/lib/cni/networks/cbr0
	├── 10.244.2.4.
	├── last_reserved_ip.0
	└── lock

# 네트워크 관련 정보들 확인
ip -c addr | grep veth -A3
	8: veth3060489e@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
	    link/ether ea:26:f0:2a:c9:cb brd ff:ff:ff:ff:ff:ff link-netnsid 0
	    inet6 fe80::e826:f0ff:fe2a:c9cb/64 scope link
	       valid_lft forever preferred_lft forever
...
-----------------------------

파드 생성이후 결과

  • cni0 생성 및 관련 결과 조회 가능

4.3. Flannel 통신 흐름 (동일 노드 / 다른 노드 간)

그림은 깔끔했는데 flannel.1 설명 별도로 존재 – 취소

  • 동일 노드간 파드 통신
Routing traffic on the same host


Pod 4(좌측)에서 Pod 6(우측)으로 통신할 경우 cni0 내부에서 처리된다

  1. 패킷은 pod4의 eth0 인터페이스를 통해 pod4의 netns를 나오고, node의 veth8f0b10dd 인터페이스를 통해 root의 netns로 진입
  2. 패킷은 veth8f0b10dd을 지나서 cni0에 도달한 후, 목적지(pod6)의 주소 질의
  3. 패킷은 cni0에서 veth9e82b6d0로 전달
  4. 패킷은 veth9e82b6d0을 통해 root(node)의 netns를 나오고, pod6의 eth0 도달
  • 다른 노드간 파드 통신
Routing traffic across different hosts

Node의 Pod1에서 Node2의 Pod6과 통신할경우

  1. 패킷은 Pod 1의 eth1 인터페이스를 통해 Pod 1의 netns를 나오고, node1의 veth1 가상 인터페이스를 통해 node1의 root netns로 진입.
  2. 패킷은 veth1을 지나 cni0에 도달한 후, 목적지(Pod 6)의 주소를 질의.
  3. 패킷은 cni0에서 eth0으로 전달.
  4. 패킷은 Node1의 eth0에서 나와 게이트웨이에 도달.
  5. 패킷은 게이트웨이를 지나 Node2의 eth0 인터페이스를 통해 node1의 root netns로 진입.
  6. 패킷은 eth0을 지나 cni0에 도달한 후, 목적지(Pod 6)의 주소를 질의.
  7. 패킷은 cni0에서 veth6 가상 인터페이스로 전달.
  8. 패킷은 veth6을 통해 root netns를 나오고, Pod 6의 eth6 인터페이스에 도달.

https://routemyip.com/posts/k8s/networking/flannel-cni-under-the-hood/

Pod Shell 접근 후 확인 및 패킷 캡쳐(mac)

  • 통신 확인
kubectl exec -it pod-1 -- zsh
-----------------------------
ip -c addr show eth0

# GW IP는 어떤 인터페이스인가? (1) flannel.1 (2) cni0
ip -c route
route -n
ping -c 1 <GW IP>
ping -c 1 <pod-2 IP>  # 다른 노드에 배포된 파드 통신 확인
ping -c 8.8.8.8       # 외부 인터넷 IP   접속 확인
curl -s wttr.in/Seoul # 외부 인터넷 도메인 접속 확인
ip -c neigh
exit
-----------------------------
  • 패킷 캡처 후 확인
# [터미널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
  • Wireshark) 옵션 설정 – Protocols – VXLAN : 4789 → 8472

결과

  • 통신 확인

gateway IP는 host node의 cni0

통신 정상 응답 수신 가능

  • 패킷 캡쳐 후 확인

ping 유형에 따라 (노드 내부/외부/퍼블릭) 해당하는 인터페이스에서 기록조회 가능

  • wireshark

Leave a Reply