LabHub

블로그

containerd 컨테이너 생명주기 관리

한국어English日本語

containerd 컨테이너 생명주기 관리

containerd에서 컨테이너의 생성부터 종료까지의 전체 생명주기를 분석합니다. 컨테이너 메타데이터와 실행 프로세스(Task)의 분리, shim을 통한 프로세스 관리, 다양한 런타임 클래스의 통합 방식을 살펴봅니다.


1. 컨테이너와 Task의 분리

1.1 핵심 개념

containerd는 컨테이너의 메타데이터와 실행 상태를 분리합니다:

Container (메타데이터):
  - ID, 이미지 참조, 스냅샷 키
  - OCI 런타임 스펙
  - 레이블, 확장 데이터
  - BoltDB에 영구 저장

Task (실행 상태):
  - 실제 실행 중인 프로세스
  - PID, 상태 (created/running/stopped)
  - stdin/stdout/stderr
  - shim 프로세스가 관리

1.2 분리의 장점

분리 설계의 이점:

1. 컨테이너 메타데이터는 Task 없이 존재 가능
   - 컨테이너를 생성하고 나중에 시작 가능
   - 중지된 컨테이너의 메타데이터 보존

2. containerd 재시작에 독립적
   - Task는 shim이 관리하므로 containerd가 재시작되어도 유지
   - 재시작 후 기존 shim에 재연결

3. 다양한 런타임 지원
   - Container 객체는 런타임에 무관
   - Task 생성 시 런타임 선택

2. 컨테이너 생성

2.1 생성 과정

컨테이너 생성 플로우:

1. 이미지에서 OCI 스펙 생성
        |
        v
2. 스냅샷 준비
   - 이미지 스냅샷 체인에 Active 스냅샷 추가
   - 컨테이너의 쓰기 가능 레이어
        |
        v
3. 컨테이너 메타데이터 저장
   - BoltDB에 Container 레코드 생성
   - ID, 이미지, 스냅샷, 런타임, 스펙 저장
        |
        v
4. 컨테이너 객체 반환
   (아직 프로세스는 시작되지 않음)

2.2 OCI 런타임 스펙

containerd는 OCI 런타임 스펙을 생성하여 컨테이너 실행 환경을 정의합니다:

OCI 런타임 스펙 주요 섹션:

ociVersion: "1.0.2"

process:
  terminal: false
  user: uid=0, gid=0
  args: ["/bin/sh"]
  env: ["PATH=/usr/local/sbin:..."]
  cwd: "/"
  capabilities: ...
  rlimits: ...

root:
  path: "rootfs"
  readonly: false

hostname: "container-abc"

mounts:
  - destination: "/proc"
    type: "proc"
    source: "proc"
  - destination: "/dev"
    type: "tmpfs"
    source: "tmpfs"

linux:
  namespaces:
    - type: "pid"
    - type: "network"
    - type: "ipc"
    - type: "uts"
    - type: "mount"
  resources:
    memory:
      limit: 536870912
    cpu:
      shares: 1024
      quota: 100000
      period: 100000
  cgroupsPath: "/kubelet/pod-abc/container-xyz"

2.3 스펙 생성기 (Spec Opts)

containerd의 스펙 생성 패턴:

Spec Opts는 OCI 스펙을 점진적으로 구성하는 함수 체인입니다:

WithImageConfig(image)     -> 이미지의 CMD, ENV, WORKDIR 적용
WithHostNamespace(ns)      -> 호스트 네임스페이스 공유
WithMemoryLimit(limit)     -> 메모리 제한 설정
WithCPUs(cpus)             -> CPU 제한 설정
WithMounts(mounts)         -> 마운트 포인트 추가
WithProcessArgs(args)      -> 프로세스 인자 설정
WithRootfsPropagation(p)   -> rootfs 마운트 전파 설정
WithSeccompProfile(p)      -> Seccomp 프로파일 적용
WithApparmorProfile(p)     -> AppArmor 프로파일 적용

3. Task 실행

3.1 Task 생성

Task 생성 플로우:

1. 컨테이너의 런타임 타입 확인
   (예: io.containerd.runc.v2)
        |
        v
2. shim 바이너리 실행
   (containerd-shim-runc-v2 start)
        |
        v
3. shim이 ttrpc 소켓 주소 반환
        |
        v
4. containerd가 shim에 Create 요청
   - OCI 스펙 전달
   - 번들 경로 전달
        |
        v
5. shim이 runc create 실행
   - 네임스페이스 생성
   - cgroup 설정
   - rootfs 마운트
   - 프로세스 생성 (아직 시작 안 됨)
        |
        v
6. Task 상태: Created

3.2 Task 시작

Task 시작:

1. containerd가 shim에 Start 요청
        |
        v
2. shim이 runc start 실행
   - 컨테이너 프로세스의 init 프로세스 시작
   - exec.fifo를 통해 동기화
        |
        v
3. Task 상태: Running
   - PID 할당됨
   - stdin/stdout/stderr 연결됨

3.3 Task 상태 전이

Task 상태 머신:

  Created
     |
     | Start()
     v
  Running
     |
     +-- Kill(signal) -> 시그널 전송
     |
     +-- Pause()  -> Paused
     |                 |
     |                 +-- Resume() -> Running
     |
     +-- 프로세스 종료 -> Stopped
     |
     v
  Stopped
     |
     | Delete()
     v
  (삭제됨)

3.4 Exec (추가 프로세스)

Exec 동작:

기존 실행 중인 컨테이너에 새 프로세스를 추가:

1. ExecProcess 생성
   - 새 프로세스의 스펙 정의 (args, env, user)
   - execID 할당
        |
        v
2. shim에 Exec 요청
        |
        v
3. runc exec 실행
   - 기존 컨테이너의 네임스페이스에 진입
   - 새 프로세스 시작
        |
        v
4. 독립적으로 stdin/stdout/stderr 관리

사용 예: kubectl exec, docker exec

4. Shim 생명주기

4.1 Shim 시작

Shim 시작 과정:

1. containerd가 shim 바이너리를 fork/exec
   containerd-shim-runc-v2 -namespace k8s.io \
     -id container-abc \
     -address /run/containerd/containerd.sock \
     start
        |
        v
2. shim이 자신을 데몬화
   - 부모 프로세스에서 분리 (setsid)
   - containerd와 독립적으로 실행
        |
        v
3. ttrpc Unix 소켓 생성
   /run/containerd/s/abc123...
        |
        v
4. 소켓 주소를 stdout으로 출력
   containerd가 이 주소를 읽어 연결

4.2 Shim 역할 상세

Shim의 주요 책임:

1. 프로세스 관리:
   - 컨테이너 프로세스의 부모 역할
   - wait4()로 종료 상태 수집
   - OOM 이벤트 감지 및 보고

2. I/O 관리:
   - stdin/stdout/stderr FIFO 관리
   - 로그 드라이버와 연결
   - I/O 복사 (containerProcess <-> FIFO)

3. containerd와의 통신:
   - ttrpc를 통한 명령 수신
   - 이벤트 보고 (TaskExit 등)
   - 상태 조회 응답

4. containerd 재시작 대응:
   - containerd가 재시작되어도 계속 실행
   - 재시작된 containerd가 기존 shim에 재연결
   - 상태 복구

4.3 Shim 종료

Shim 종료:

1. Task Delete 요청 수신
        |
        v
2. 컨테이너 리소스 정리
   - cgroup 삭제
   - 네임스페이스 정리
   - rootfs 언마운트
        |
        v
3. ttrpc 소켓 종료
        |
        v
4. shim 프로세스 종료

5. Checkpoint/Restore

5.1 Checkpoint

Checkpoint 동작:

실행 중인 컨테이너의 상태를 스냅샷으로 저장:

1. CRIU (Checkpoint/Restore in Userspace) 호출
        |
        v
2. 프로세스 메모리 덤프
   - 메모리 페이지 저장
   - 파일 디스크립터 상태 저장
   - 네트워크 연결 상태 저장
        |
        v
3. 체크포인트 이미지 생성
   - CRIU 이미지 파일 세트
   - 컨테이너 스펙과 함께 저장
        |
        v
4. 선택적으로 컨테이너 중지

사용 사례:
  - 라이브 마이그레이션
  - 빠른 시작 (사전 워밍업된 상태에서 복원)
  - 디버깅 (특정 시점 상태 캡처)

5.2 Restore

Restore 동작:

1. 체크포인트 이미지 로드
        |
        v
2. 새 컨테이너 환경 준비
   - 네임스페이스 생성
   - rootfs 마운트
        |
        v
3. CRIU restore 실행
   - 메모리 페이지 복원
   - 프로세스 상태 복원
   - 파일 디스크립터 재연결
        |
        v
4. 프로세스 실행 재개

6. 런타임 클래스

6.1 다양한 런타임 지원

containerd는 shim 인터페이스를 통해 다양한 런타임을 지원합니다:

런타임 클래스별 비교:

+----------+------------+-----------+----------+----------+
| 런타임    | 격리 수준  | 오버헤드  | 시작시간 | 호환성   |
+----------+------------+-----------+----------+----------+
| runc     | 네임스페이스 | 최소     | 빠름    | 최고     |
| kata     | 경량 VM    | 중간      | 중간    | 높음     |
| gVisor   | 유저 커널  | 낮음      | 빠름    | 중간     |
| Wasm     | Wasm 샌드박스 | 최소   | 매우빠름 | 제한적   |
+----------+------------+-----------+----------+----------+

6.2 runc

runc:

- 기본 OCI 런타임
- Linux 네임스페이스와 cgroup 기반 격리
- 호스트 커널을 직접 사용
- 가장 낮은 오버헤드
- 모든 Linux 컨테이너 워크로드에 적합

shim: containerd-shim-runc-v2
config.toml:
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
    runtime_type = "io.containerd.runc.v2"

6.3 Kata Containers

Kata Containers:

- 경량 VM 내부에서 컨테이너 실행
- QEMU/Cloud-Hypervisor/Firecracker 사용
- 별도 게스트 커널로 강력한 격리
- 멀티테넌트 환경에 적합
- VM 오버헤드 존재

shim: containerd-shim-kata-v2
config.toml:
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
    runtime_type = "io.containerd.kata.v2"

6.4 gVisor

gVisor (runsc):

- 유저 스페이스 커널 (Sentry)
- 시스템 콜을 인터셉트하여 재구현
- 호스트 커널 공격 표면 축소
- ptrace 또는 KVM 기반 동작
- 일부 시스템 콜 미지원

shim: containerd-shim-runsc-v1
config.toml:
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
    runtime_type = "io.containerd.runsc.v1"

6.5 WebAssembly (Wasm)

Wasm 런타임:

- WebAssembly 바이너리를 컨테이너로 실행
- Wasmtime, WasmEdge 등 사용
- 매우 빠른 시작 시간 (밀리초 단위)
- 최소 메모리 사용
- 포터블 바이너리
- 제한된 시스템 접근 (WASI)

shim: containerd-shim-wasm
config.toml:
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasm]
    runtime_type = "io.containerd.wasm.v1"

6.6 RuntimeClass 선택

Kubernetes RuntimeClass 연동:

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

2. Pod에서 RuntimeClass 지정:
   spec:
     runtimeClassName: kata
     containers:
       - name: app
         image: nginx

3. containerd가 handler에 매칭되는 런타임 선택:
   handler "kata" -> containerd.runtimes.kata 설정
   -> containerd-shim-kata-v2 실행

7. ctr과 crictl로 Container와 Task를 직접 보기

1장의 분리는 개념 설명이 아니라 명령 출력에서 그대로 드러납니다. 두 개의 목록이 따로 있고, 두 목록의 줄 수가 다를 수 있다는 사실이 곧 분리의 증거입니다.

가장 먼저 걸리는 함정은 네임스페이스입니다. containerd는 클라이언트별로 격리된 네임스페이스에 메타데이터를 저장하고, kubelet이 만드는 모든 것은 k8s.io 네임스페이스에 들어갑니다. -n k8s.io를 빼고 명령을 실행하면 기본 네임스페이스를 보게 되므로 목록이 비어 있고, 그러면 컨테이너가 사라졌다는 잘못된 결론에 도달합니다. 노드에서 조사할 때 이 플래그는 선택이 아니라 필수입니다.

# 컨테이너(메타데이터) 목록 — 컬럼은 CONTAINER, IMAGE, RUNTIME
ctr -n k8s.io containers list

# Task(실행 중인 프로세스) 목록 — 컬럼은 TASK, PID, STATUS
ctr -n k8s.io tasks list

# 이 빌드가 실제로 들고 있는 플러그인과 스냅샷터
ctr plugins ls

두 목록을 나란히 놓고 보면 분리가 눈에 들어옵니다. containers에는 있는데 tasks에는 없는 항목이 곧 프로세스 없이 메타데이터만 남은 컨테이너입니다. 정상적인 노드에서도 이런 상태는 잠깐씩 존재합니다. 컨테이너가 생성되었지만 아직 시작되지 않았거나, 프로세스가 종료되었는데 아직 삭제되지 않은 경우입니다. 문제는 이 상태가 오래 지속될 때입니다. 그때는 종료 후 정리가 막혀 있다는 신호이고, 다음에 볼 곳은 9장의 진단 순서입니다.

컨테이너 하나를 깊이 들여다볼 때는 info를 씁니다. 2장에서 본 OCI 런타임 스펙이 실제로 어떤 값으로 채워졌는지가 여기서 나옵니다.

# 컨테이너 레코드 전체 (이미지, 스냅샷 키, 런타임, 레이블 포함)
ctr -n k8s.io containers info CONTAINER_ID

# OCI 스펙만 보고 싶을 때
ctr -n k8s.io containers info CONTAINER_ID --spec

# Task 안의 프로세스 목록과 리소스 지표
ctr -n k8s.io tasks ps CONTAINER_ID
ctr -n k8s.io tasks metrics CONTAINER_ID

--spec으로 뽑은 출력에서 확인할 값은 정해져 있습니다. linux.resources에 들어간 메모리 한도와 CPU quota가 Pod 스펙에 적은 값과 맞는지, linux.namespaces에 어떤 네임스페이스가 있고 어느 것이 샌드박스와 공유되는지, cgroupsPath가 기대한 경로인지입니다. Pod 스펙과 이 출력이 다르면 변환 과정 어딘가가 개입한 것이고, 그 지점은 대개 kubelet이거나 6장의 런타임 핸들러 설정입니다.

같은 컨테이너를 crictl로 보면 관점이 달라집니다. ctr은 containerd의 객체를 보여주고, crictl은 CRI가 정의한 모델, 즉 Pod와 컨테이너를 보여줍니다. Kubernetes 문맥에서 무언가를 확인할 때는 crictl 쪽이 맞습니다. 컨테이너가 어느 Pod에 속하는지, 재시도가 몇 번째인지 같은 정보는 ctr에는 없습니다.

crictl ps            # 실행 중인 컨테이너
crictl ps -a         # 종료된 것까지
crictl inspect CONTAINER_ID
crictl stats

8. 워크드 예제: containerd를 재시작해도 컨테이너는 살아 있다

4장에서 설명한 shim의 데몬화가 실제로 어떤 결과를 만드는지는 직접 재현해 보는 편이 빠릅니다. 스테이징 노드 하나에서 다음 순서를 그대로 밟으면 됩니다.

# 1) 기준선 — Task 목록과 그중 하나의 PID를 기억해 둔다
ctr -n k8s.io tasks list

# 2) shim 프로세스가 몇 개 떠 있는지 세어 둔다
pgrep -c containerd-shim-runc-v2

# 3) containerd만 재시작한다 (kubelet은 건드리지 않는다)
systemctl restart containerd

# 4) 다시 확인 — Task는 그대로이고 PID도 그대로다
ctr -n k8s.io tasks list
pgrep -c containerd-shim-runc-v2

3단계에서 containerd 프로세스는 실제로 죽었다가 새로 뜹니다. 그런데 4단계의 출력은 1단계와 같습니다. Task의 PID가 바뀌지 않았다는 것이 핵심입니다. 컨테이너 프로세스의 부모는 containerd가 아니라 shim이고, shim은 자신을 데몬화해서 containerd와 별도의 세션에서 돌기 때문에 containerd가 사라져도 영향을 받지 않습니다. 재시작한 containerd는 각 shim의 ttrpc 소켓 주소를 다시 찾아 연결하고, 그 시점부터 상태 조회와 명령 전달이 재개됩니다.

이 실험이 알려주는 것은 안심이 아니라 진단 순서입니다. 컨테이너가 죽었다는 신고가 들어왔을 때 containerd를 재시작하는 것은 대부분 아무것도 고치지 못합니다. 컨테이너를 실제로 붙들고 있는 것은 shim이므로, containerd 재시작은 관리 평면만 새로 띄우는 일입니다. 반대로 이 성질 때문에 생기는 고유한 실패도 있습니다. 재연결이 실패하면 컨테이너 프로세스는 멀쩡히 돌고 있는데 containerd는 그것을 관리하지 못하는 상태가 됩니다. 증상은 ctr tasks list에는 보이지 않는데 pgrep containerd-shim-runc-v2로는 프로세스가 남아 있는 불일치이고, 이때는 소켓 경로가 남아 있는지와 containerd 로그의 재연결 오류를 확인해야 합니다.

9. 실패 사례와 진단 순서

Task가 Stopped에서 삭제되지 않는 것이 가장 자주 보고되는 증상입니다. crictl ps -a에는 Exited로 남아 있고 지워지지 않으며, 같은 Pod가 계속 재시작합니다. 3장의 상태 머신을 떠올리면 Stopped는 종점이 아니라 Delete를 기다리는 상태입니다. 삭제가 진행되지 못하는 이유는 대개 정리 단계가 막힌 것으로, cgroup이 지워지지 않거나 rootfs 언마운트가 실패하는 경우입니다. 확인 순서는 Task 상태, 남아 있는 마운트, 그리고 containerd 로그의 정리 오류입니다. 강제로 치워야 한다면 ctr -n k8s.io tasks delete --force CONTAINER_ID가 프로세스를 죽이고 삭제를 진행합니다. 다만 이것은 kubelet이 모르는 사이에 상태를 바꾸는 일이라, 진단이 끝난 뒤 마지막 수단으로만 써야 합니다.

두 번째는 고아 shim입니다. containerd가 정상 종료가 아니라 강제 종료된 뒤에 남고, 증상은 8장에서 말한 불일치와 같습니다. 노드의 shim 프로세스 수가 실제 컨테이너 수보다 눈에 띄게 많다면 이 상태를 의심합니다. shim 하나가 붙들고 있는 자원은 크지 않지만, 누적되면 cgroup과 마운트가 함께 남아 결국 새 컨테이너 생성이 실패합니다. 노드를 비우고 교체하는 것이 손으로 정리하는 것보다 거의 항상 안전합니다.

세 번째는 cgroup 드라이버 불일치입니다. 컨테이너가 시작은 되는데 불안정하게 재시작하고, 리소스 제한이 의도대로 걸리지 않으며, 노드를 갈아도 재현됩니다. containerd 문서 기준으로 runc 옵션의 SystemdCgroup 기본값은 config version 2와 version 3 모두 false입니다. 그런데 최근 배포판의 kubelet은 대체로 systemd 드라이버를 씁니다. 즉 기본값을 그대로 두면 어긋나는 조합이 되기 쉽습니다. 확인은 설정 파일의 해당 값과 kubelet의 cgroup 드라이버 설정을 대조하는 것이고, 여기서도 6.2절의 설정 경로가 config 버전에 따라 다르다는 점을 먼저 봐야 합니다.

# 이 빌드의 기본 설정을 그대로 뽑아 현재 파일과 비교한다
containerd config default > /tmp/containerd-default.toml
grep -n -i "systemdcgroup\|runtime_type\|version" /tmp/containerd-default.toml | head

네 번째는 존재하지 않는 런타임 핸들러입니다. 6.6절의 RuntimeClass는 Kubernetes 쪽 유효성 검사를 통과하지만, kubelet이 그 handler 이름을 CRI로 넘기는 순간 containerd가 대응하는 runtimes 항목을 찾지 못해 샌드박스 생성이 실패합니다. RuntimeClass의 handler와 설정의 runtimes 아래 키 이름은 문자 단위로 같아야 하고, 그 항목의 runtime_type에 해당하는 shim 바이너리가 PATH에 있어야 합니다. kata를 설치했는데 Pod가 뜨지 않는다면 이 세 가지를 차례로 확인하는 것이 가장 빠릅니다.

다섯 번째는 Paused 상태에 갇힌 Task입니다. 3.3절의 상태 머신에 나오듯 Task는 Pause로 멈출 수 있고 Resume으로만 풀립니다. freezer cgroup으로 프로세스를 얼린 것이므로 프로세스는 살아 있고 PID도 그대로지만 아무 일도 하지 않습니다. 애플리케이션 관점에서는 완전히 멈춘 것처럼 보이는데 CPU 사용량은 0이고 로그도 더 이상 나오지 않습니다. 이 상태는 사람이 ctr tasks pause를 실행했거나 체크포인트 도구가 중간에 실패했을 때 남습니다. 확인은 ctr -n k8s.io tasks list의 STATUS 컬럼이고, 복구는 ctr -n k8s.io tasks resume CONTAINER_ID입니다.

# STATUS 컬럼에서 PAUSED를 찾는다
ctr -n k8s.io tasks list

# 다시 실행 상태로 되돌린다
ctr -n k8s.io tasks resume CONTAINER_ID

10. 언제 이 기능들을 쓰지 않나

5장의 checkpoint/restore는 설명만 들으면 매력적이지만 실제로 쓸 수 있는 자리가 좁습니다. CRIU가 복원하는 것은 프로세스의 메모리와 파일 디스크립터 상태이고, 그 상태가 바깥 세계와 맺고 있던 관계까지 복원해 주지는 않습니다. 열려 있던 TCP 연결의 상대편은 복원을 알지 못하고, 파일 잠금과 세션 토큰은 만료되어 있으며, 데이터베이스 커넥션 풀은 이미 끊긴 소켓을 붙들고 있습니다. 그래서 실제로 값을 하는 경우는 외부 상태 의존이 거의 없는 워크로드, 예를 들어 초기화가 오래 걸리는 계산 프로세스를 미리 워밍업해 두었다가 복원하는 식의 용도입니다. Kubernetes 수준의 컨테이너 체크포인트 지원은 릴리스마다 상태가 달라졌으므로, 정확한 필드는 사용 중인 버전의 문서에서 확인하세요.

6장의 대체 런타임도 마찬가지로 기본값이 아닙니다. Kata나 gVisor를 도입하는 이유는 성능이 아니라 격리이고, 격리를 사기 위해 지불하는 비용은 시작 시간과 메모리와 호환성입니다. 그 비용은 워크로드마다 다르므로 남의 벤치마크가 아니라 자기 워크로드로 재야 합니다. 재 보지도 않고 전체 클러스터의 기본 런타임을 바꾸는 것이 최악의 선택이고, 멀티테넌트 구간이나 신뢰할 수 없는 코드를 실행하는 네임스페이스에만 RuntimeClass로 붙이는 것이 시작점으로 적절합니다. Wasm은 격리보다 시작 시간과 이식성을 사는 선택이며, 시스템 접근이 제한되므로 기존 컨테이너 이미지를 그대로 옮길 수 있는 대상이 아닙니다.

마지막으로 ctr 자체가 운영 도구가 아니라는 점을 짚어야 합니다. ctr은 containerd 개발자를 위한 디버깅 클라이언트이고, 안정적인 인터페이스라는 약속이 없습니다. 상태를 읽는 용도로는 훌륭하지만, 컨테이너를 만들거나 지우는 용도로 쓰면 kubelet이 아는 세상과 실제 세상이 어긋납니다. kubelet은 그 불일치를 발견하면 스스로 복구를 시도하는데, 그 복구가 사람이 의도한 것과 같으리라는 보장은 없습니다. Kubernetes 노드에서는 읽기는 ctr과 crictl로, 쓰기는 kubectl로 한다는 경계를 지키는 편이 안전합니다.

11. 참고 자료

12. 정리

containerd의 컨테이너 생명주기 관리는 Container(메타데이터)와 Task(실행)의 분리, shim을 통한 프로세스 격리, 다양한 런타임 클래스 지원이 핵심입니다. shim의 데몬화 설계로 containerd 재시작에도 컨테이너가 유지되며, OCI 런타임 스펙 기반의 표준화된 인터페이스로 runc, Kata, gVisor, Wasm 등 다양한 런타임을 통합합니다. 다음 글에서는 containerd의 네트워킹과 스토리지를 분석합니다.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다