Search

CRI와 Pause 컨테이너

Tags
k8s
container
runtime
pause
Created
2024/09/06 08:54
Created time
2024/09/05 23:54
category
kubernetes

개요

CRI와 PodSandbox의 간단한 소개
Pause 코드를 통한 원리 파악과 Pause 컨테이너 간단 소개
선수적으로 KinD를 학습
결론 먼저 언급하면, Pause 컨테이너 내부에 컨테이너들은 net, uts, ipc를 공유하여 사용
파드 내에서 유연한 통신을 위해서 net 네임스페이스를 공유, 이후에 uts, ipc도 공유하도록 추가된 히스토리가 존재
이름도 네트워크 컨테이너 → 인프라 컨테이너 → Pause 컨테이너 순으로 변경

CRI

개념

CRI의 구체적인 내용을 확인 가능
컨테이너의 생성과 삭제는 컨테이너 런타임을 통해 이뤄지고, 컨테이너 런타임은 High-Level과 Low-Level로 구분
High-Level 컨테이너 런타임은 직접적으로 kubelet과 통신할 수 있도록 API (모니터링 및 관리) 를 제공하고, 컨테이너 조작의 논리적인 기능을 담당 (e.g. containerd)
Low-Level 컨테이너 런타임은 High-Level로부터 작업을 전달받아서, 보다 저수준에서 컨테이너를 직접적으로 조작하는 역할을 담당 (e.g. runC)
컨테이너 런타임은 필요성에 따라 다양하고 빠르게 발전해왔고, 쿠버네티스를 더 확장 가능하게 만들기 위해 이와 같은 컨테이너 런타임을 지원할 수 있도록 플러그인 API를 개발했는데 이를 CRI (Container Runtime Interface)라고 지칭

구조 변천사와 서비스

CRI 구조 변경 이력을 소개한 공식 문서
containerd 1.0 : CRI-Containerd (EOL)
Docker의 CRI 구현인 dockershim에 비해 하나의 홉을 제거하여개선
CRI-Containerd가 containerd와 여전히 구분되어 각 데몬으로 동작
containerd 1.1 : CRI Plugin (Current)
CRI 기능을 담당하는 플러그인이 containerd 내부에 존재하도록 리팩토링
CRI-Containerd처럼 grpc 홉이 아닌 함수 호출로 동작하여 오버헤드 제거
kubelet은 grpc 방식으로 UDS를 사용하여 컨테이너 런타임과 통신
kubelet - client / CRI shim - server의 역할을 수행
kubelet의 grpc 서비스에는 ImageService와 RuntimeService를 포함
ImageService : 레포지토리에서 이미지 풀, 검사, 제거 등
RuntimeService : 컨테이너의 라이프사이클 관리 등

Pause 컨테이너

Pod 개념과 특징

Pod는 컨테이너 어플리케이션의 기본 단위이며, 1개 이상의 컨테이너로 구성된 집합을 지칭하고 아래와 같은 특징 존재
Sidecar 패턴으로 1개 이상의 컨테이너를 가짐
Pod 내에서 실행되는 컨테이너들은 동일한 라이프사이클을 가짐
Pod의 IP는 노드와 별개이고, 클러스터 내에서 접근 할 수 있는 주소를 할당 받기에 다른 노드에 위치한 Pod로 통신도 NAT 없이 통신 가능
Pod 내의 컨테이너들은 IP를 공유하고, 컨테이너 간에는 포트로 구분
Pod 내의 컨테이너들은 볼륨을 공유하여 서로 파일을 주고 받을 수 있음
Pod는 리소스 제약이 있는 격리된 환경에 cgroup으로 구성되고, 이러한 환경을 PodSandbox라 지칭
PodSandbox는 Pod가 시작되기 전에 kubelet의 RuntimeService.RunPodSandbox를 통해 생성되는데, 이 때 Pod의 IP를 할당하는 등의 네트워트 설정도 수행

Pause 컨테이너 역할과 책임

Pod 내의 컨테이너들은 IP를 공유하고, 컨테이너 간에는 포트로 구분
이전 항목에서 위와 같은 특징을 소개 했는데, 이것이 가능한 것은 Pause 컨테이너 역할 덕분
Pause 컨테이너는 모든 컨테이너의 부모 컨테이너 역할을 수행하는데, 별도의 네트워크 네임스페이스를 구성하여 내부 컨테이너들이 Pause 컨테이너가 구성한 네트워크 네임스페이스를 공유할 수 있도록 동작
Pause 컨테이너는 다음과 같은 2가지 책임을 가짐
Pod에서 리눅스 네임스페이스 공유할 수 있는 기반을 제공
PID 네임스페이스가 공유되면, 각 Pod에 대해 PID 1 역할을 수행하여 좀비 프로세스를 거둠

pause.c

pause.c는 Pause 컨테이너 동작 원리가 되는 코드
전체 코드는 여기에서 확인 가능
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #define STRINGIFY(x) #x #define VERSION_STRING(x) STRINGIFY(x) #ifndef VERSION #define VERSION HEAD #endif /* 아래 코드 기반에선 SIGINT, SIGTERM 발생 시 동작 */ /* 등록된 시그널이 발생하면 표준 오류로 문구 출력 후 정상 종료 */ static void sigdown(int signo) { psignal(signo, "Shutting down, got signal"); exit(0); } /* 아래 코드 기반에선 SIGCHILD 발생 시 동작 */ /* 현재 프로세스의 모든 자식 프로세스를 확인 (-1) */ /* WNOHANG이므로 자식 프로세스의 종료와 무관하게 기다림 없이 자식 프로세스 상태를 즉시 반환 */ /* 자식 프로세스들이 종료되지 않은 상태라면 0을 반환하여 모든 자식 프로세스의 종료를 기다림 */ /* waitpid 아래 링크에서 검색하여 확인 */ /* https://www.jseo.cc/library/42/inner-circle/8#66a3e123-9a33-4b07-b41b-3b9100ce95ec */ static void sigreap(int signo) { while (waitpid(-1, NULL, WNOHANG) > 0) ; } int main(int argc, char **argv) { /* 인자가 버전 확인인 -v로 기재되었는지 확인 */ /* -v가 있다면 버전 표출 후 즉시 종료 */ int i; for (i = 1; i < argc; ++i) { if (!strcasecmp(argv[i], "-v")) { printf("pause.c %s\n", VERSION_STRING(VERSION)); return 0; } } /* pid 1 프로세스가 아니면 경고 문구 트리거 */ /* 컨테이너에서 실행하는 것을 가정 */ if (getpid() != 1) /* Not an error because pause sees use outside of infra containers. */ fprintf(stderr, "Warning: pause should be the first process\n"); /* 시그널 핸들러 등록 */ if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0) return 1; if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0) return 2; if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap, .sa_flags = SA_NOCLDSTOP}, NULL) < 0) return 3; /* 프로그램 종료까지 무한히 대기 */ /* 시그널 발생할 떄까지 대기 */ for (;;) pause(); /* 종료 시 문구 출력 후 정상 종료 */ fprintf(stderr, "Error: infinite loop terminated\n"); return 42; }
C
복사
SIGINT, SIGTERM
→ 사용자의 종료 명령 트리거링 등으로 실행
→ pause 상태에서 깨어남
→ sigdown 핸들러 동작
→ 0번 코드로 정상 종료
SIGCHLD
→ 자식 프로세스가 죽는 등의 행위로 실행
→ pause 상태에서 깨어남
→ sigreap 핸들러 동작
→ pause 하위의 다른 모든 자식 프로세스의 종료를 무한 루프로 대기
→ 42번 코드로 정상 종료

네임스페이스 inode 확인

Pause 컨테이너가 생성한 네임스페이스의 공유를 확인하고, 다중 컨테이너가 상주하는 Pod에서는 해당 컨테이너들의 IP가 동일함을 확인

클러스터 환경 구축

# control-plane, worker 노드 각 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 # control-plane과 worker 노드에 툴 설치 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'
Shell
복사
# 노드 확인 kubectl get no -o wide # Docker 컨테이너 목록 조회 docker ps # worker 노드 별 포트 정보 조회 docker port myk8s-worker # control-plane 노드 IP 주소 조회 docker exec -it myk8s-control-plane ip -br -c -4 addr # worker 노드 IP 주소 조회 docker exec -it myk8s-worker ip -br -c -4 addr
Shell
복사

kube-ops-view 설치

# Helm Chart 레포지토리를 추가 helm repo add geek-cookbook https://geek-cookbook.github.io/charts/ # kube-ops-view 설치 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 # 설치된 Helm Chart 목록 조회 (kube-system 네임스페이스) helm list -n kube-system
Shell
복사
# 설치된 kube-ops-view 리소스 목록 확인 kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view # 접속 URL 확인 echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=1.5" echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=2"
Shell
복사

Pause 컨테이너 네임스페이스 공유 확인

현재 항목은 worker 노드에 접속한 상태로 진행
# worker 노드 접속 docker exec -it myk8s-worker bash # enable된 유닛 서비스 확인 systemctl list-unit-files | grep 'enabled enabled' # 노드 내에서 실행 중인 컨테이너 목록 확인 crictl ps
Shell
복사
# kubelet 실행 설정 조회 pstree -aln
Shell
복사
# pause 컨테이너, kube-ops-view 컨테이너 등의 네임스페이스 조회 pstree -aclnpsS
Shell
복사
# 시스템의 네임스페이스 inode 확인 lsns -p 1 lsns -p $$ # kube-ops-view에서 사용하는 pause 컨테이너의 네임스페이스 inode 확인 # 8개중 5개가 시스템과는 다른 네임스페이스를 이용 # mnt/pid는 pause 컨테이너만 이용 # net/uts/ipc는 kube-ops-view의 컨테이너를 위해 미리 생성하여 공유됨
Shell
복사
# kube-ops-view의 네임스페이스 inode 확인 # 8개중 6개가 시스템과는 다른 네임스페이스를 이용 # mnt/pid/cgroup은 kube-ops-veiw만 이용 # net/uts/ipc는 pause 컨테이너가 생성한 것을 공유 받아 사용 lsns -p $(pgrep python3)
Shell
복사

Pod 내 다중 컨테이너 간 IP 주소 공유 확인

# 로컬 호스트 # 다중 컨테이너 Pod를 배포 # https://raw.githubusercontent.com/gasida/NDKS/main/3/myweb2.yaml kubectl apply -f https://raw.githubusercontent.com/gasida/NDKS/main/3/myweb2.yaml # 배포된 Pod 목록 조회 kubectl get po -o wide # 배포된 Pod 기술 kubectl describe po/myweb2
Shell
복사
# 로컬 호스트 # nginx 컨테이너를 대상으로 net-tools 설치 kubectl exec myweb2 -c myweb2-nginx -- apt update kubectl exec myweb2 -c myweb2-nginx -- apt install -y net-tools # 생성한 Pod 내부의 컨테이너들을 대상으로 IP 주소를 조회 # 그림 상 10.244.1.3으로 두 컨테이너의 IP가 동일 kubectl exec myweb2 -c myweb2-netshoot -- ifconfig kubectl exec myweb2 -c myweb2-nginx -- ifconfig
Shell
복사
# 로컬 호스트 # netshoot 컨테이너를 zsh로 진입 kubectl exec myweb2 -c myweb2-netshoot -it -- zsh # 리스닝 프로세스 조회 # 프로세스는 조회되지 않지만 80 포트가 LISTENING인 것을 볼 수 있음 ss -nlptu # 열려 있는 80 포트로 HTTP 요청 # nginx의 index.html이 출력 curl localhost # nginx 프로세스가 있는지 조회 # netshoot 컨테이너에는 nginx가 없는 것을 확인 가능 ps -ef | grep nginx | grep -v grep # netshoot 컨테이너에서 나가기 exit
Shell
복사
# 로컬 호스트 # nginx 컨테이너의 로그 출력 # netshoot 컨테이너의 IP가 nginx와 동일하기 때문에 netshoot에서 날린 요청이 loopback으로 기록된 것을 볼 수 있음 kubectl logs -f myweb2 -c myweb2-nginx
Shell
복사

Pod 내 다중 컨테이너 간 네임스페이스 격리 확인

현재 항목은 worker 노드에 접속한 상태로 진행
# worker 노드 접속 docker exec -it myk8s-worker bash # 노드 내에서 실행 중인 컨테이너 목록 확인 crictl ps
Shell
복사
# 개별 컨테이너로 IP 주소 조회 시, 이전에 kubectl로 확인한 것처럼 동일한 주소를 가짐 crictl -p crictl ps -q crictl exec -its f5c0b810491f8 ifconfig crictl exec -its 4384f05e63264 ifconfig
Shell
복사
# PID 정보 변수 할당 # pause 컨테이너 pid 조회 pstree -aclnpsS PAUSEPID=... 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
Shell
복사
# pause 컨테이너 네임스페이스 inode 조회 lsns -p $PAUSEPID # nginx의 네임스페이스 inode 조회 # time, user는 호스트의 것을 이용 (격리 X) # mnt, pid, cgroup은 컨테이너 별로 격리 # ipc, uts, net은 Pause 컨테이너가 격리한 것을 공유 lsns -p $NGINXPID # netshoot의 네임스페이스 inode 조회 # time, user는 호스트의 것을 이용 (격리 X) # mnt, pid, cgroup은 컨테이너 별로 격리 # ipc, uts, net은 Pause 컨테이너가 격리한 것을 공유 lsns -p $NETSHPID
Shell
복사
# nginx와 netshoot이 IP를 동일하게 사용하는 것은 이전에 확인 # pause 컨테이너와도 비교 # 3개의 IP 주소가 모두 동일 nsenter -t $PAUSEPID -n ip -c addr nsenter -t $NGINXPID -n ip -c addr nsenter -t $NETSHPID -n ip -c addr
Shell
복사
# 컨테이너 ID 조회 crictl ps # nginx 컨테이너 network 정보 확인 # path의 PID가 pause 컨테이너의 PID와 동일 # 네트워크 네임스페이스가 pause 컨테이너와 공유된다는 사실을 여기서도 확인 가능 crictl inspect f5c0b810491f8 | jq | grep network -A 10 # netshoot 컨테이너 network 정보 확인 # path의 PID가 pause 컨테이너의 PID와 동일 # 네트워크 네임스페이스가 pause 컨테이너와 공유된다는 사실을 여기서도 확인 가능 crictl inspect 4384f05e63264 | jq | grep network -A 10
Shell
복사

클러스터 삭제

# 로컬 호스트 # 리소스 및 클러스터 삭제 kubectl delete po/myweb2 kind delete cluster --name myk8s
Shell
복사

Reference