Work/개발 노트

[KANS] 2주차 - K8S Flannel CNI & Pause 정리

★용호★ 2024. 9. 7. 13:33

Kind

그림 출처: https://kind.sigs.k8s.io/

 

  • kubernetes in docker의 약자
  • 로컬환경에서 테스트용도로 사용하며 docker 기반으로 Kubernetes를 설치함
  • docker 컨테이너 안에서 docker를 사용해서 Kubernetes 실행에 필요한 각 컴포넌트들을 컨테이너로 실행
  • 멀티 클러스터에 대한 테스트를 해보기 좋음

아래 명령으로 설치 및 실행

# Install Kind
brew install kind
kind --version

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

# Install Helm
brew install helm
helm version

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

# 노드 정보 확인
kubectl get node -o wide

# 파드 정보 확인
kubectl get pod -A

kind 생성로 클러스터 생성 시 yaml 파일을 사용할 수도 있음

 

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로 워커 노드를 실행할 때 NodePort로 호스트의 특정 Port로 들어온 트래픽을 대상 Pod로 전달하기 위해 extraPortMapping을 사용함. 아래 yaml 파일에서 containerPort와 hostPort 정보를 추가해주면 Kind가 컨테이너 생성 시 해당 정보를 반영해서 생성함

# '컨트롤플레인, 워커 노드 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

  • extraPortMappings은 kind 노드 컨테이너와 호스트 시스템 간의 포트 매핑을 정의하고, 클러스터 생성 시점에 적용되기 때문에 실행 중인 클러스터의 노드에 직접 반영할 수 있는 방법이 없음
    • 클러스터를 제거하고 새로 생성해야함
    • 미리 Port 여러개를 점유해놓는 것도 방법

Tip) kind로 Control Plane만 설치가 됐는데 왜 Pod를 생성할 수 있을까?

  • Amazon EKS와 같이 관리형 Kubernetes 서비스를 사용할 때는 데이터플레인 영역에만 Pod를 실행할 수 있음
  • 기본적으로 컨트롤 플레인의 노드들에는 사용자가 생성하는 Pod를 해당 노드에 배포되지 않도록 Taint 설정이 default로 되어 있음
  • kind로 설치한 클러스터는 Taints 설정이 없기 때문에 컨트롤플레인 노드 조차 Pod가 배포할 대상이 될 수 있게됨

  • 만약 클러스터에 워커 노드가 별도로 추가되면 Kind가 자동으로 컨트롤 플레인의 Taints에 다른 Pod가 실행되지 않도록 설정을 추가함

Docker in Docker

  • Docker in Docker는 컨테이너 내부에 컨테이너가 실행되는 구조이며, 컨테이너 안에서 실행된 컨테이너 정보는 호스트의 docker ps 명령으로 확인할 수 없음
    • 가상화된 프로세스 안에서 다시 가상화된 프로세스를 생성하는 구조
  • Kind는 kubeadm을 사용해서 클러스터를 생성
    • Kind로 생성된 docker 컨테이너 안에서 컨테이너 리스트를 볼 때는 docker 명령이 아닌 crictl 명령 사용
    • crictl은 CRI(Container Runtime Interface) 표준을 준수하는 모든 컨테이너 런타임(ex: containerd, CRI-O 등)에서 사용 가능
    • crictl은 쿠버네티스 클러스터 관리자가 노드 레벨에서 컨테이너를 직접 관리하고 문제를 해결할 때 유용한 도구
  • Kind로 생성된 클러스터 내부의 노드는 모두 컨테이너로 되어 있음. 따라서 모든 노드가 Docker 네트워크의 IP Range에서 IP를 할당 받음
    • 쿠버네티스 클러스터 안에서는 기본적으로 kindnet이라는 경량의 CNI를 사용함
  • docker ps 명령을 실행해보면 호스트의 58321 포트로 트래픽이 컨트롤플레인 컨테이너의 6443 포트로 전달되고 있는 것을 확인할 수 있음
    • 참고로 호스트의 58321 포트 번호는 Kind로 Kubernetes를 설치할 때마다 랜덤으로 바뀜
  • 컨트롤 플레인의 6443 포트를 사용하는 컴포넌트는 kube-apiserver 임. 즉, kubectl 명령을 실행하면 config 파일에 적혀있는 API 서버로 트래픽을 보내는데 해당 트래픽은 Docker에 의해 DNAT 돼서 컨트롤 플레인에 있는 kube-apiserver로 전달됨

 

Tip) kube-apiserver, etcd, kube-scheduler 등 컨트롤 플레인을 구성하는 Pod는 누가 생성하는걸까?

  • 클러스터에 join 한 노드가 실행될 때 수동으로 실행하지 않아도 특정 디렉토리(일반적으로 /etc/kubernetes/manifests)에 매니페스트 파일을 작성해놓으면 kubelet이 자동으로 Pod를 실행함
  • 이를 Static Pod라고 하며, API 서버 없이 특정 노드의 kubelet 데몬에 의해 직접 관리되는 Pod를 의미
  • kubelet은 해당 디렉토리를 주기적으로 스캔하여 변경사항을 감지하고 Pod를 생성, 수정, 삭제함
  • 노드 레벨에서 항상 실행되어야 하는 중요한 시스템 컴포넌트를 배포하는 데 사용됨
  • 단, Pod만 생성 가능하고 다른 리소스(Deployment, Service 등)는 생성할 수 없음

 

CRI(Container Runtime Interface)

  • Kubernetes와 Kubelet, 컨테이너 런타임 사이의 통신을 위한 표준화된 API
    • kubelet과 CRI와는 unix domain socket으로 통신함

kubelet 실행 명령에서 컨테이너 런타임 연결 시 unix domain socket 사용

  • 쿠버네티스 초기에는 Docker를 컨테이너 런타임으로 사용했었고, Docker에 대한 의존성이 쿠버네티스 코드베이스에도 포함되어 있었음
  • 커뮤니티에서 다양한 컨테이너 런타임 사용에 대한 요구사항이 증가하면서 확장성과 유연성 개선이 필요해졌고, 1.5버전부터 CRI가 도입되었음
  • 컨테이너 런타임에 직접적인 의존성을 갖지 않고 표준화된 인터페이스를 추가함으로써 kubelet이 더이상 컨테이너 런타임 변경에 재빌드해야하는 의존성이 사라짐
  • CRI 구현체로 containerd와 CRI-O와 같은 새로운 컨테이너 런타임이 개발되었고, Docker도 내부적으로 containerd를 사용하는 걸로 변경됨
  • 쿠버네티스의 기본 런타임이었던 Docker 자체는 CRI 표준을 준수하지 않아서 쿠버네티스 내부에서는 Dockershim으로 호환성을 유지했었지만 의존성으로 인한 유지보수 문제로 인해 1.24부터는 Dockershim을 완전히 제거하고 containerd, CRI-O 등이 주요 컨테이너 런타임으로 자리 잡음

  • Pod는 리소스 제약이 있는 격리된 환경의 애플리케이션 컨테이너 그룹으로 구성되며, CRI에서 이 환경을 PodSandbox라고 함
    • PodSandbox의 목적은 Pod 내 컨테이너들을 위한 공유 네임스페이스와 리소스 제약을 제공하기 위함
    • 이렇게 되면 컨테이너 간 격리를 유지하면서도 리소스 공유를 가능하게 할 수 있음
    • kubelet이 Pod를 시작하기 전에 RuntimeService.RunPodSandbox를 호출해서 PodSandbox 환경을 생성하고, 이 과정에서 IP 할당과 같은 Pod를 위한 네트워크 설정도 이루어짐
    • pause 컨테이너를 사용해서 바로 이 PodSandbox를 구현함 

Pause 컨테이너

  • pause 컨테이너가 Pod 내 모든 컨테이너들의 부모역할을 한다고 볼 수 있음
    • 만약 Pod 내 컨테이너들의 프로세스 네임스페이스를 공유하는 설정을 한 경우 PID 1번으로 실행되고 애플리케이션 컨테이너가 자식 프로세스로 생성됨
    • 그렇지 않으면 같은 레벨에 위치한 두 컨테이너 중 하나는 PID 1번이 아니게 될테니 좀비 프로세스가 생겨날 수 있음
  • Pod가 실행되면 그 안에서는 애플리케이션 컨테이너와 함께 네임스페이스 공유를 위한 Pause 컨테이너가 생성되고, 호스트에서는 프로세스 리스트 중 containerd-shim 프로세스의 자식 프로세스로 확인할 수 있음

아래 명령을 실행하면 호스트와 다른 네임스페이스를 사용하는 리스트를 볼 수 있음

pstree -aclnpsS

아래와 같이 실제로 pause 컨테이너에 대해 호스트와 ipc, mnt, net, pid, uts 네임스페이스의 아이노드 값이 다른 것을 확인할 수 있음

추가로 애플리케이션의 네임스페이스 정보를 보면 아래와 같이 net, uts, ipc는 Pause 컨테이너의 네임스페이스를 공유하는 것을 확인할 수 있음

Flannel CNI

쿠버네티스 네트워크 모델의 4가지 요구사항

  1. 파드와 파드 간 통신 시 NAT 없이 통신이 가능해야함
  2. 노드의 에이전트(예. kubelet, 시스템 데몬)는 파드와 통신이 가능해야함
  3. 호스트 네트워크를 사용하는 파드는 NAT 없이 파드와 통신이 가능해야함
  4. 서비스 클러스터 IP 대역과 파드가 사용하는 IP 대역은 중복되지 않아야함

쿠버네티스 네트워크의 4가지 해결 과제

  1. 파드 내 컨테이너는 루프백을 통한 통신을 할 수 있어야함
  2. 파드 간 통신을 할 수 있어야함
  3. 클러스터 내부에서 서비스를 통한 통신을 할 수 있어야함
  4. 클러스터 외부에서 서비스를 통한 통신을 할 수 있어야함

  • kubelet을 통해 파드가 신규로 생성될 때 네트워크 관련 설정 추가가 필요한데 CNI 플러그인이 하는 역할은 전달되는 설정 정의서를 보고 실제 파드가 통신하기 위한 네트워크 설정들을 실행하는 것
  • CNI 플러그인은 IP 할당 관리를 해야하기 때문에 IPAM을 통해 파드 간 통신을 위한 라우팅 설정을 처리함

각 노드에 배치된 파드 간 통신을 위한 3가지 네트워크 환경

  • 네트워크 오버레이 기술을 구현해주는 대표적인 방법인 VXLAN을 지원
    • 물리적인 네트워크 환경 위에서 가상의 네트워크 환경을 만들어 줌
    • 터널링 기법을 사용하며 파드의 패킷을 감싸서 노드를 빠져나가고 목적지 노드에 도착해서 해당 패킷에 감싸진 부분을 제거하여 목적지 파드에 전달하게 됨
  • UDP 네트워크 오버레이 기법을 지원
    • VXLAN을 지원하지 않는 오래된 리눅스 커널 버전 운영 시 사용
    • 권장하지 않음
  • host-gw 모드 지원
    • 네트워크 오버레이 기법을 사용하지 않고 각 노드의 파드 네트워크 대역을 라우팅 테이블에 업데이트하여 직접 라우팅 수행
    • 네트워크 오버레이를 사용하지 않기 때문에 빠르지만 모든 노드가 동일 네트워크 대역에 배포되어 있어야 함
    • 실제 운영 환경에서는 사용하기 쉽지 않음

통신 흐름 이해

동일 노드에서 파드간 통신

  • 동일 노드에서 파드 간 통신을 할 때는 flannel.1까지 트래픽이 가지 않고 cni0 브리지 네트워크에서 통신이 수행됨

다른 노드 간 파드 통신

  • 다른 노드에 있는 파드로 트래픽을 보낼 때는 flannel.1 인터페이스를 거쳐서 패킷을 한번 wrapping함
  • wrapping 시 출발지, 목적지에 해당하는 노드의 IP가 추가됨