Skip to content
Published on

containerd CRI 구현: Kubernetes 런타임 통합

Authors

containerd CRI 구현: Kubernetes 런타임 통합

containerd는 Kubernetes CRI(Container Runtime Interface)를 내장 플러그인으로 구현합니다. 이 글에서는 CRI gRPC 서비스의 구현 상세, Pod Sandbox 관리, 컨테이너 스펙 변환, 스트리밍 API, RuntimeClass, NRI를 분석합니다.


1. CRI gRPC 서비스

1.1 서비스 구조

CRI는 두 개의 gRPC 서비스로 구성됩니다:

CRI gRPC 서비스:

RuntimeService:
  +-- PodSandbox 관리
  |     RunPodSandbox
  |     StopPodSandbox
  |     RemovePodSandbox
  |     PodSandboxStatus
  |     ListPodSandbox
  |
  +-- Container 관리
  |     CreateContainer
  |     StartContainer
  |     StopContainer
  |     RemoveContainer
  |     ListContainers
  |     ContainerStatus
  |     UpdateContainerResources
  |
  +-- 스트리밍
  |     ExecSync
  |     Exec
  |     Attach
  |     PortForward
  |
  +-- 런타임 정보
        Status
        Version

ImageService:
  +-- PullImage
  +-- ListImages
  +-- ImageStatus
  +-- RemoveImage
  +-- ImageFsInfo

1.2 소켓 구성

CRI 소켓:

containerd는 동일한 gRPC 소켓에서 CRI를 제공:
  /run/containerd/containerd.sock

kubelet 설정:
  --container-runtime-endpoint=unix:///run/containerd/containerd.sock

CRI 플러그인이 containerd 서버에 CRI 서비스를 등록:
  플러그인 ID: io.containerd.grpc.v1.cri

2. Pod Sandbox

2.1 Pod Sandbox 개념

Pod Sandbox는 Pod의 격리 환경을 나타냅니다:

Pod Sandbox 구성:

Pod Sandbox = Pause 컨테이너 + 공유 네임스페이스

공유 리소스:
  - 네트워크 네임스페이스 (같은 IP, 포트 공간)
  - IPC 네임스페이스 (프로세스 간 통신)
  - UTS 네임스페이스 (호스트명)
  - PID 네임스페이스 (선택적)

격리 리소스:
  - 마운트 네임스페이스 (컨테이너별)
  - cgroup (컨테이너별 리소스 제한)

2.2 RunPodSandbox 흐름

RunPodSandbox 처리:

1. Sandbox 메타데이터 생성
   - ID 생성
   - 로그 디렉토리 생성
        |
        v
2. Pause 이미지 풀
   - sandbox_image 설정에서 이미지 결정
   - 기본값: registry.k8s.io/pause:3.9
        |
        v
3. Pause 컨테이너 스냅샷 준비
        |
        v
4. OCI 스펙 생성
   - Pause 컨테이너용 최소 스펙
   - 호스트명, DNS 설정 포함
        |
        v
5. 네트워크 네임스페이스 생성
   - /var/run/netns/ 에 네임스페이스 파일 생성
        |
        v
6. CNI 플러그인 호출
   - 네트워크 인터페이스 생성
   - IP 할당
        |
        v
7. Pause 컨테이너 Task 생성 및 시작
        |
        v
8. Sandbox 상태를 SANDBOX_READY로 설정

2.3 Pause 컨테이너

Pause 컨테이너의 역할:

1. 네임스페이스 보유자:
   - 네트워크 네임스페이스의 첫 번째 프로세스
   - App 컨테이너가 종료되어도 네임스페이스 유지
   - 네임스페이스의 생명주기를 Pod에 바인딩

2. PID 1 역할:
   - Pod PID 네임스페이스의 init 프로세스
   - 좀비 프로세스 수거 (reap)
   - 최소 리소스 사용 (약 1MB)

3. 동작:
   - pause() 시스템 콜로 무한 대기
   - SIGTERM 수신 시 종료

3. 컨테이너 스펙 변환

3.1 CRI 요청에서 OCI 스펙으로

스펙 변환 과정:

CRI ContainerConfig:
  - Image
  - Command, Args
  - Envs
  - Mounts
  - Devices
  - SecurityContext
  - Resources
        |
        v
containerd CRI 플러그인이 변환
        |
        v
OCI Runtime Spec:
  - root (이미지 스냅샷 경로)
  - process (커맨드, env, capabilities)
  - mounts (볼륨, 특수 파일시스템)
  - linux.resources (cgroup 설정)
  - linux.namespaces (Sandbox와 공유)
  - hooks (OCI 훅)

3.2 리소스 변환

Kubernetes 리소스 -> OCI 리소스 변환:

CPU:
  requests.cpu: 250m
    -> linux.resources.cpu.shares = 256
       (1000m = 1024 shares 기준)

  limits.cpu: 500m
    -> linux.resources.cpu.quota = 50000
       linux.resources.cpu.period = 100000
       (500m/1000m * 100000us)

Memory:
  limits.memory: 512Mi
    -> linux.resources.memory.limit = 536870912
       (바이트 단위)

  requests.memory:
    -> 스케줄링에만 사용, OCI 스펙에 반영하지 않음

Hugepages:
  limits.hugepages-2Mi: 100Mi
    -> linux.resources.hugepageLimits:
         pageSize: "2MB"
         limit: 104857600

3.3 보안 컨텍스트 변환

SecurityContext -> OCI 스펙 변환:

runAsUser: 1000
  -> process.user.uid = 1000

runAsGroup: 1000
  -> process.user.gid = 1000

readOnlyRootFilesystem: true
  -> root.readonly = true

privileged: true
  -> 모든 capabilities 부여
  -> 모든 디바이스 접근 허용
  -> AppArmor/SELinux/Seccomp 비활성화

capabilities:
  add: ["NET_ADMIN"]
  drop: ["ALL"]
  -> process.capabilities 설정

seccompProfile:
  type: RuntimeDefault
  -> linux.seccomp 프로파일 적용

4. 스트리밍 API

4.1 ExecSync

ExecSync 동작:

동기적으로 컨테이너에서 명령 실행:

1. kubelet이 ExecSync(containerID, cmd, timeout) 호출
        |
        v
2. containerd가 shim에 Exec 요청
        |
        v
3. shim이 runc exec 실행
   - 컨테이너 네임스페이스에 새 프로세스 생성
        |
        v
4. stdout/stderr 캡처
        |
        v
5. 프로세스 종료 대기
        |
        v
6. exit code + stdout + stderr 반환

사용 사례: liveness/readiness probe, kubectl exec (동기)

4.2 Exec (비동기 스트리밍)

Exec 스트리밍 동작:

1. kubelet이 Exec(containerID, cmd, stdin, stdout, stderr) 호출
        |
        v
2. containerd가 스트리밍 URL 반환
   - 스트리밍 서버 주소: https://node:10250/exec/...
        |
        v
3. kubelet이 클라이언트에 URL 전달
        |
        v
4. 클라이언트가 WebSocket/SPDY로 스트리밍 서버 연결
        |
        v
5. 스트리밍 서버가 containerd에 실제 Exec 수행
        |
        v
6. stdin/stdout/stderr 양방향 스트리밍

스트리밍 프로토콜:
  - SPDY (레거시)
  - WebSocket (최신)

4.3 Attach

Attach 동작:

실행 중인 컨테이너의 메인 프로세스에 연결:

1. 스트리밍 URL 생성 (Exec와 유사)
        |
        v
2. 컨테이너의 stdin/stdout/stderr에 연결
   - 새 프로세스를 생성하지 않음
   - 기존 프로세스의 I/O에 직접 연결
        |
        v
3. 양방향 스트리밍

사용 사례: kubectl attach

4.4 PortForward

PortForward 동작:

Pod의 포트에 로컬 트래픽 전달:

1. 스트리밍 URL 생성
        |
        v
2. Pod의 네트워크 네임스페이스에서 socat/nsenter 실행
   - 지정 포트에 TCP 연결
        |
        v
3. 로컬 포트와 Pod 포트 간 양방향 데이터 전달

구현:
  containerd는 Pod의 네트워크 네임스페이스에 진입하여
  대상 포트에 TCP 연결을 생성합니다.

사용 사례: kubectl port-forward

5. RuntimeClass

5.1 RuntimeClass 매핑

RuntimeClass 처리:

1. Kubernetes RuntimeClass 리소스:
   apiVersion: node.k8s.io/v1
   kind: RuntimeClass
   metadata:
     name: kata
   handler: kata

2. kubelet이 CRI RunPodSandbox 호출 시
   runtime_handler = "kata" 전달

3. containerd가 handler를 런타임 설정에 매핑:
   [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
     runtime_type = "io.containerd.kata.v2"

4. 해당 shim 바이너리로 Task 생성:
   containerd-shim-kata-v2

5.2 기본 런타임

기본 런타임 설정:

[plugins."io.containerd.grpc.v1.cri".containerd]
  default_runtime_name = "runc"

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
  runtime_type = "io.containerd.runc.v2"

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = true

Pod에 runtimeClassName이 없으면 기본 런타임(runc) 사용

5.3 RuntimeClass별 오버헤드

RuntimeClass 리소스 오버헤드:

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata
handler: kata
overhead:
  podFixed:
    memory: "160Mi"
    cpu: "250m"

오버헤드 처리:
  - kubelet이 Pod 리소스에 오버헤드를 추가
  - 스케줄러가 오버헤드를 포함하여 노드 선택
  - VM 기반 런타임의 고정 비용 반영

6. NRI (Node Resource Interface)

6.1 NRI 개요

NRI는 containerd의 플러그인 확장 메커니즘으로, 컨테이너 생명주기 이벤트에 훅을 등록할 수 있습니다:

NRI 아키텍처:

kubelet -> containerd
               |
               +-- NRI Plugin 1 (리소스 할당)
               +-- NRI Plugin 2 (토폴로지 인식)
               +-- NRI Plugin 3 (모니터링)

NRI 플러그인은 컨테이너 생명주기 이벤트를 수신하고
OCI 스펙을 수정할 수 있습니다.

6.2 NRI 훅 포인트

NRI 훅 포인트:

1. RunPodSandbox:
   - Pod 생성 시 호출
   - Pod 레벨 리소스 할당

2. CreateContainer:
   - 컨테이너 생성 시 호출
   - OCI 스펙 수정 가능
   - CPU 핀닝, 메모리 NUMA 할당 등

3. StartContainer:
   - 컨테이너 시작 시 호출

4. UpdateContainer:
   - 리소스 업데이트 시 호출

5. StopContainer:
   - 컨테이너 중지 시 호출
   - 리소스 해제

6. RemoveContainer:
   - 컨테이너 삭제 시 호출

6.3 NRI 사용 사례

NRI 활용 사례:

1. CPU/메모리 토폴로지 인식 할당:
   - NUMA 노드 인식 CPU 핀닝
   - 메모리를 특정 NUMA 노드에 할당
   - 토폴로지 매니저와 연동

2. 디바이스 리소스 관리:
   - GPU 할당 최적화
   - RDMA 리소스 관리
   - 디바이스 플러그인 보완

3. 보안 정책 적용:
   - 동적 Seccomp 프로파일
   - 런타임 보안 규칙 주입

4. 모니터링/감사:
   - 컨테이너 시작/종료 이벤트 로깅
   - 리소스 사용 추적

7. 이미지 서비스

7.1 이미지 Pull

CRI PullImage 처리:

1. kubelet이 PullImage(imageSpec, authConfig) 호출
        |
        v
2. containerd가 이미지 참조 해석
   - 태그 또는 다이제스트
   - 레지스트리 인증 정보 적용
        |
        v
3. 이미지 다운로드
   - Manifest, Config, Layers
   - k8s.io 네임스페이스에 저장
        |
        v
4. 레이어 언패킹
   - Snapshotter로 스냅샷 체인 생성
        |
        v
5. 이미지 참조(imageRef) 반환

7.2 이미지 캐싱

이미지 캐싱:

containerd의 이미지 캐싱:
  - Content Store에 레이어가 이미 존재하면 다운로드 건너뜀
  - Snapshotter에 스냅샷이 이미 존재하면 언패킹 건너뜀
  - 다이제스트 기반 정확한 중복 제거

kubelet의 이미지 정책:
  imagePullPolicy: Always
    -> 항상 레지스트리 매니페스트 확인 (레이어는 캐시 활용)
  imagePullPolicy: IfNotPresent
    -> 로컬에 없을 때만 Pull
  imagePullPolicy: Never
    -> 로컬 이미지만 사용

8. 모니터링과 디버깅

8.1 CRI 메트릭

containerd CRI 관련 메트릭:

container_runtime_cri_operations_total:    CRI 연산 수
container_runtime_cri_operations_errors_total: CRI 연산 오류 수
container_runtime_cri_operations_latency_seconds: CRI 연산 지연

containerd 내부 메트릭:
  containerd_task_count:                   실행 중인 Task 수
  containerd_container_count:              컨테이너 수
  containerd_image_pull_duration_seconds:   이미지 Pull 소요 시간

8.2 디버깅 도구

디버깅 도구:

1. crictl (CRI CLI):
   crictl ps              # 컨테이너 목록
   crictl pods            # Pod 목록
   crictl images          # 이미지 목록
   crictl inspect CONTAINER_ID  # 컨테이너 상세
   crictl logs CONTAINER_ID     # 컨테이너 로그
   crictl exec -it CONTAINER_ID /bin/sh  # exec

2. ctr (containerd CLI):
   ctr -n k8s.io containers list
   ctr -n k8s.io tasks list
   ctr -n k8s.io images list

3. containerd 로그:
   journalctl -u containerd -f

9. 정리

containerd의 CRI 구현은 Kubernetes와 컨테이너 런타임 사이의 핵심 인터페이스입니다. Pod Sandbox를 통한 Pod 수준의 격리, CRI 요청에서 OCI 스펙으로의 정확한 변환, WebSocket/SPDY 기반 스트리밍, RuntimeClass를 통한 다중 런타임 지원, NRI를 통한 유연한 확장이 주요 특징입니다. 이러한 계층화된 설계로 containerd는 Kubernetes의 안정적인 컨테이너 런타임으로 자리매김하고 있습니다.