후니의 IT인프라 사전

Pod와 Pause 컨테이너 본문

카테고리 없음

Pod와 Pause 컨테이너

james_janghun 2024. 9. 8. 07:55

 

다음은 가시다님의 KANS 3기의 포스팅으로 2주차 K8S Flannel CNI & PAUSE 내용을 정리합니다.

 

이번 포스팅은 pod와 pause 컨테이너의 내용에 대해서 알아보고자 합니다.

pod란?

pod는 쿠버네티스에서 가장 작은 단위로 리소스 제약이 있는 격리된 환경(네임스페이스)의 1개 이상의 컨테이너로 이루어진 컨테이너의 집합입니다. 또한 추가적으로 1개의 ip를 가지고 여러 컨테이너가 공유합니다.

 

 

pod의 특징

- 1개 이상의 컨테이너를 가질 수 있습니다 (sidecar 패턴 등)

sidecar 패턴은 datadog 등을 사용하신다면 쉽게 볼 수 있는데, 하나의 pod 내에서 애플리케이션 컨테이너의 로그를 감시, 전송 등을 하기 위해 추적하는 컨테이너를 별도로 넣는 방식으로 주로 사용합니다.

 

- pod 내에서 실행되는 컨테이너들은 반드시 동일한 노드에 할당되고 동일한 생명주기를 갖습니다. (Pod 삭제 시, Pod 내 모든 컨테이너가 삭제)

- 내부에서 실행되는 모든 컨테이너는 IP가 동일하며, 컨테이너들 끼리는 localhost를 통해 서로 접근하고 포트를 통해 구분합니다.

 

 

우리가 오늘 확인해 볼 내용은 특히 이 3번째 부분입니다. pod가 이렇게 ip가 동일하고 포트를 통해 구분되는 특징은 pause 컨테이너를 통해 이뤄집니다.

 

pause 컨테이너에 들어가기에 앞서 kubelet과 CRI의 개념을 알아야 합니다.

 

쿠버네티스에서 컨테이너의 그룹을 pod를 독립적인 실행을 보장하면서도 통신은 가능해야 하기 때문에 이 때 어떻게 동작하는지 확인하기 위해서 다음의 개념들이 쓰입니다.

 

Container Runtime

컨테이너를 실행/삭제 등의 관리를 담당하는 소프트웨어를 container runtime이라고 합니다. 이런 container runtime을 low-level과 high-level로 나눕니다. low-level은 대표적으로 runC, high-level은 우리가 흔히 아는 containerd, CRI-O 등이 있습니다.

https://velog.io/@mirrorkyh/Containerd%EC%99%80-runc-%EC%9D%98-%EC%B0%A8%EC%9D%B4

 

 

Low-Level Container Runtime

컨테이너의 생성, 실행, 중지 등 가장 핵심적인 저수준 기능을 담당하는 소프트웨어 입니다. OCI (Open Container Initiative) 표준에 따라 컨테이너 실행을 위한 핵심 기능을 수행합니다. runc, gVisor가 대표적입니다.

 

High-Level Container Runtime

Low-Level Container Runtime을 추상화해 더 사용하기 쉽도록 만들고, 컨테이너 관리, 네트워크 설정, 로깅, 컨테이너 생명주기 관리, 오케스트레이션 기능 등 고수준의 기능을 담당하는 소프트웨어이다. CRI 표준을 지원하고 쿠버네티스와 연동이 가능하다. 사용자는 따라서 High-Level Container Runtime만 다루면 Low-Level을 같이 컨트롤 할 수 있고 간단하게 명령어를 통해서 컨테이너를 관리할 수 있게 된다. containerd, CRI-O가 대표적입니다. 

 

참고로 docker 또한 containerd을 사용합니다. docker는 좀 더 상위에 존재하는 엔진이며, 이미지 패키징 등이 추가적으로 더 제공됩니다.

 

 

다만 이러한 컨테이너 런타임에 종속되지 않기 위해서 쿠버네티스에서는 CRI 표준 인터페이스를 만듭니다.

 

CRI(Container Runtime Interface)란?

컨테이너 런타임과 쿠버네티스 표준 간의 인터페이스를 의미하는 것으로 쿠버네티스에서 다양한 컨테이너 런타임과 상호 작용하기 위해 CRI를 도입하게 되었습다. 이를 통해 쿠버네티스가 특정 런타임에 종속되지 않고, 여러 종류의 컨테이너 런타임을 쉽게 사용하고 교체할 수 있습니다. kubelet은 컨테이너 런타임마다 재 컴파일 할 필요가 없어졌고 CRI는 프로토콜 버퍼, gRPC API, 라이브러리로 구성되어 있습니다.

 

다음 링크에서 좀 더 구체적으로 확인할 수 있습니다.

 

kubernetes/docs/devel/container-runtime-interface.md at 242a97307b34076d5d8f5bbeb154fa4d97c9ef1d · kubernetes/kubernetes

Production-Grade Container Scheduling and Management - kubernetes/kubernetes

github.com

 

 

CRI에서는 이러한 격리된 환경의 컨테이너 그룹을 PodSandbox라고 합니다.

PodSandbox는 컨테이너 런타임이 내부적으로 작동하는 방식에 따라 의미가 달라질 수 있습니다. 하이퍼 바이저 기반의 런타임의 PodSandbox는 가상 머신을 나타낼 수 있고, 다른 경우 linux 네임스페이스 일 수 있습니다.

 

https://kubernetes.io/blog/2017/11/containerd-container-runtime-options-kubernetes/

kubelet이란?

kubelet은 쿠버네티스의 각 노드에서 실행되는 주요 에이전트(데몬)입니다. 노드의 상태를 관리하고 컨테이너가 정상적으로 실행하는 것을 관여합니다.

참고로 kubernetes의 daemonset으로는 동작하지 않습니다. 이는 추후에 포스팅해봅니다.

 

kubelet은 gRPC 프레임워크를 사용해 Unix 소켓을 통해 container runtime(또는 runtime용 CRI shim)과 통신합니다. 이 때 kubelet은 클라이언트, CRI shim은 서버 역할을 합니다. 

 

pod를 시작하려면 각 노드에 배치된 kubelet이 RuntimeService.RunPodSandbox 호출하여 환경을 만듭니다. 이 때 pod에 대한 네트워킹 설정 (ip할당 등)이 포함됩니다. PodSandbox가 활성화되면 kubelet은 개별 컨테이너를 독립적으로 생성/시작/중지/제거할 수 있게 됩니다.

 

주요 kubelet의 설정 정보는 다음과 같이 확인할 수 있습니다. 일단 서비스 데몬 정보를 다음과 같이 확인해보면 설정정보에 대한 파일이 상세하게 기록되고 있습니다.

/etc/systemd/system/kubelet.service.d/10-kubeadm.conf

 

# https://github.com/kubernetes/kubernetes/blob/ba8fcafaf8c502a454acd86b728c857932555315/build/debs/10-kubeadm.conf
# Note: This dropin only works with kubeadm and kubelet v1.11+
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
# This is a file that "kubeadm init" and "kubeadm join" generates at runtime, populating the KUBELET_KUBEADM_ARGS variable dynamically
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
# This is a file that the user can use for overrides of the kubelet args as a last resort. Preferably, the user should use
# the .NodeRegistration.KubeletExtraArgs object in the configuration files instead. KUBELET_EXTRA_ARGS should be sourced from this file.
EnvironmentFile=-/etc/default/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS

 

kubelet이 사용하는 kubeconfig 정보는 --kubeconfig=/etc/kubernetes/kubelet.conf 에서 기록됩니다. 

그리고 kubelet의 설정값은 /var/lib/kubelet/config.yaml에서 일부 확인할 수 있습니다.

apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
  anonymous:
    enabled: false
  webhook:
    cacheTTL: 0s
    enabled: true
  x509:
    clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
  mode: Webhook
  webhook:
    cacheAuthorizedTTL: 0s
    cacheUnauthorizedTTL: 0s
cgroupDriver: systemd
cgroupRoot: /kubelet
clusterDNS:
- 10.96.0.10
clusterDomain: cluster.local
containerRuntimeEndpoint: ""
cpuManagerReconcilePeriod: 0s
evictionHard:
  imagefs.available: 0%
  nodefs.available: 0%
  nodefs.inodesFree: 0%
evictionPressureTransitionPeriod: 0s
failSwapOn: false
fileCheckFrequency: 0s
healthzBindAddress: 127.0.0.1
healthzPort: 10248
httpCheckFrequency: 0s
imageGCHighThresholdPercent: 100
imageMaximumGCAge: 0s
imageMinimumGCAge: 0s
kind: KubeletConfiguration
logging:
  flushFrequency: 0
  options:
    json:
      infoBufferSize: "0"
    text:
      infoBufferSize: "0"
  verbosity: 0
memorySwap: {}
nodeStatusReportFrequency: 0s
nodeStatusUpdateFrequency: 0s
rotateCertificates: true
runtimeRequestTimeout: 0s
shutdownGracePeriod: 0s
shutdownGracePeriodCriticalPods: 0s
staticPodPath: /etc/kubernetes/manifests
streamingConnectionIdleTimeout: 0s
syncFrequency: 0s
volumeStatsAggPeriod: 0s

 

 

 

이제 pod 내에서 생성되는 컨테이너에 대해서 알아보고자 합니다.

해당 컨테이너가 어떻게 생성되고 동작되는지 자세하게 살펴봅니다.

 

Pause 컨테이너

Pause 컨테이너란 모든 pod에서 동작하는 컨테이너로 network, IPC, UTS 네임스페이스를 생성하고 공유 및 유지합니다. 따라서 일종의 부모 컨테이너 역할을 합니다.

pause 컨테이너의 코드는 https://github.com/kubernetes/kubernetes/blob/master/build/pause/linux/pause.c 에서 확인할 수 있습니다.

 

Pause 컨테이너의 역할

1. Linux의 네임스페이스 공유

2. PID 1역할

  PID 네임스페이스 공유가 활성화되면 각 포드에 대한 PID 1역할을 하고 좀비 프로세스를 거둠

 

확인

지금부터 확인할 내용은 pause 컨테이너의 정보를 확인하고, pod 내에서 어떻게 동작하는지 다른 컨테이너와 어떻게 namespace를 공유하는지 확인해 보겠습니다.

 

먼저 워커노드에서 pstree -aln 명령어를 통해서 프로세스 정보를 나열해봅니다.

pstree -aln

 

일단 상단에 containerd가 container runtime으로 동작하고 kubelet도 동작중입니다. 해당 kubelet이 containerd.sock을 엔드포인트로 활용하는 것을 볼 수 있습니다.

 

crictl ps를 통해서 현재 띄워진 컨테이너 정보를 보면 총 3개가 존재하는데 각각 모두 containerd-shim으로 프로세스에 잡히는 것을 확인할 수 있습니다. 또한 이것들은 전부 pause 컨테이너를 기반으로 시작하는 것을 확인할 수 있었습니다.

 

여기서 containerd-shim의 id 값이 아래 crictl에서 확인할 수 없는데, 다음과 같이 확인할 수 있었습니다.

현재 crictl ps 상의 kube-ops-view 컨테이너를 inspect 명령어로 세부적인 정보를 조회할 수 있습니다.

crictl inspect d7b9664b04864

 

너무 많은 정보가 나와서 info 부분만 캡쳐를 했는데 현재 pstree에서 표시되는 id는 sandboxID임을 확인할 수 있습니다.

 

 

조금 더 깊게 들어가 보겠습니다.

 

다음 명령어를 통해서 보면 PID 수준으로 나눠서 전부 보여주게 되는데요.

pstree -aclnpsS

 

kube-ops-view 컨테이너 부분만 보면 다음과 같습니다.

 

pause 컨테이너의 경우 PID 1083으로 운영하고 있고, ipc,mnt,net,pid,uts는 호스트와 다른 네임스페이스를 나타냅니다. 이와 같이 kube-ops-view 컨테이너의 경우 PID는 1148로 운영중이고, 호스트와 다른 네임스페이스를 사용하는 것을 볼 수 있습니다. (참고로 이 둘은 하나의 pod 내에 있는 겁니다. pod내에서 컨테이너는 분리되어 있고 프로세스만 다릅니다.)

 

 

이제 lsns 명령어를 통해서 각 프로세스별로 네임스페이스를 확인해 보겠습니다. 1083은 pause, 1148은 kube-ops-view라는 프로세스 컨테이너라고 이해해주시면 됩니다.

lsns는 리눅스에서 namespace 정보를 보여주는 명령어 입니다. 이 명령어를 사용하면 시스템에서 실행 중인 각 namespace(예: 프로세스, 네트워크, 사용자 등)에 대한 정보를 확인할 수 있습니다. 앞서 계속 이야기한 것처럼 namespace는 프로세스를 격리하는데 사용되는 개념입니다.

 

lsns -p 1083
lsns -p 1148

 

puase 컨테이너가 생성한 ipc, net, uts 를 공유

바로 확인할 수 있듯 pause 컨테이너가 생성한 ipc, net, uts 네임스페이스는 다른 앱 컨테이너(kube-ops-view)가 공유하고 있습니다.

아까 crictl로 inspect 했던거 기억하시나요? 

crictl inspect d7b9664b04864

 

여기서도 그대로 확인이 가능합니다. namespace라는 항목이 있는데 pid, mount, cgroup은 자기 자신의 namespace를 사용하지만 ipc, net(network), uts에서는 /proc/1083(pause)/ns를 마운트해서 사용하는 것을 볼 수 있습니다.

 

 

앱 컨테이너가 2개 이상일 경우

다음은 앱컨테이너가 2개인 상황에서도 동일한지 확인해 보겠습니다.

 

그림과 같이 nginx, netshoot 컨테이너가 띄워진 상황에서 ip addr을 통해서 각 컨테이너의 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

 

 

정말 신기하게도 둘의 ip가 같은것을 알 수 있는데 이는 network 네임스페이스를 공유하기 때문입니다.

 

또한 재미있는 사실은 netshoot 컨테이너 내부에서 ss -tnlp로 열려진 포트를 조회하면 다음과 같이 80포트가 열려있습니다.

kubectl exec myweb2 -c myweb2-netshoot -it -- zsh
ss -tnlp

 

한 번 80포트를 접속해 볼까요?

curl localhost

 

이처럼 netshoot에서 80포트를 접속해보면 nginx 페이지가 보이는 것을 볼 수 있습니다. 다만 ps -ef를 통해서 프로세스 정보를 확인해보면 다음과 같이 nginx를 찾아볼 수는 없습니다. 왜냐면 프로세스는 격리되어있기 때문이죠.

ps -ef

 

 

최종적으로 3개의 컨테이너에 대해 lsns로 네임스페이스 PID를 확인해보고 nsenter를 통해서 네트워크 인터페이스 정보를 확인해보면 동일한 것을 확인할 수 있습니다.

 

lsns -t net
nsenter -t $PAUSEPID -n ip -c addr
nsenter -t $NGINXPID -n ip -c addr
nsenter -t $NETSHPID -n ip -c addr

 

 

여기서 들어나는 몇가지 의문점은 다음과 같다.

 

1. 왜 pause 컨테이너는 host와 cgroup을 공유하고 있는가?

 

- pod 내 리소스 관리

pod 내에서 여러 컨테이너가 실행될 수 있기 때문에 이 pod의 전체 리소스 제약을 관리하기 위해 host와 cgroup을 공유합니다.

 

- 간단한 오버헤드

리소스 측면에서도 pause 컨테이너는 init 작업을 위한 것으로 다른 컨테이너의 net, pid, ipc namespace 들을 설정하는 역할을 하고 더 이상의 임무가 없기 때문에 host의 네임스페이스를 일부 공유하여 오버헤드를 최소화합니다.

 

 

2. mnt와 pid 네임스페이스는 왜 다른 앱컨테이너들이 공유하지 않는가?

 

- 파일시스템 격리 (mnt)

mnt 네임스페이스는 파일 시스템의 마운트 포인트를 관리합니다. 각 컨테이너가 독립된 파일 시스템 구조를 가지고 동작하기 때문에 본인만의 독립된 파일시스템이 필요하여 해당 네임스페이스는 공유되지 않습니다. 또한 보안적으로도 안전합니다.

 

- 프로세스 격리 (pid)

pid 네임스페이스는 각 컨테이너에서 프로세스 ID를 관리합니다. 컨테이너는 각자 독립된 공간에서 개별 프로세스를 가지고 동작하여 프로세스 간의 간섭을 줄일 수 있어 안정성을 보장합니다.

 

 

 

맺음말

이번 포스팅을 통해서 어떻게 pod가 생성되고, pod내 컨테이너들은 네임스페이스를 공유하는지 명확하게 알 수 있었습니다. 막연하게 pod 내부의 컨테이너는 동일한 IP를 가지고 있다는 특징만 외웠지 내부적으로 pause 컨테이너를 통해 네임스페이스를 공유한다는 내용은 이번 포스팅을 통해서 명확하게 알 수 있었습니다.