Kubernetes

Container Runtime과 Docker

Operation CWAL 2021. 3. 9. 21:47

Container Runtime

Docker나 Kubernetes 사용자라면 컨테이너 런타임(Container Runtime)이라는 단어를 종종 들어봤을 것이다. 컨테이너의 개념은 상당히 명확하지만 이에 반해 컨테이너 런타임이 구체적으로 어떤 역할을 맡고 있는지 설명하기란 쉽지 않다. Docker 자체가 컨테이너 런타임인가? kubelet은 Docker와 어떤 방식으로 상호작용하여서 컨테이너를 실행하는가? 이번 시간엔 컨테이너 런타임의 정확한 개념과 기능에 대해 알아보고자 한다.

 

 

Docker와 Container 기술 표준

우선 Docker의 역사에 대해 짚고 넘어갈 필요가 있다. 2013년에 공개된 Docker는 그동안 어렵게만 여겨졌던 Linux 컨테이너 기술을 쉽게 다룰 수 있도록 아래와 같은 기능을 제공하였다.

  • 컨테이너 이미지 형식(union mount)
  • 이미지 빌드 방법(Dockerfile, docker build)
  • 이미지 공유(docker push, pull)
  • 이미지 관리(docker image ls, docker image rm, etc)
  • 컨테이너 인스턴스 관리(docker ps, docker rm, etc)
  • 컨테이너 실행(docker run)

 

이 당시, Docker는 Monolithic 시스템이었기 때문에 위에서 나열한 모든 기능이 한데 모여있는 구조였다. 하지만 Docker의 폭발적인 수요에 따라 개선의 목소리가 커지면서, 기능들을 분리하여 여러개의 Tool로 나누고, 서로간의 인터페이스를 통해 연계하는 방식의 리팩토링이 필요했다. 이를 위해 컨테이너 기술 표준 기구 Open Container Initiative(OCI)가 출범하였으며 Docker, Google, AWS 등 다양한 벤더가 여기에 참여하였다. 이후 대부분의 컨테이너 기술은 OCI에서 정한 런타임 및 이미지 명세를 준수하면서 발전해왔고, 2016년엔 외부(ex: Kubernetes, Mesos)에서 컨테이너 런타임에 접근하기 위한 표준 스펙인 Container Runtime Interface가 공개되었다.

 

모두 알다시피 Docker가 컨테이너 런타임으로서, 컨테이너를 실행하는 순서는 아래와 같다.

  1. 컨테이너 이미지 다운로드
  2. 이미지를 압축해제하여, 컨테이너의 파일시스템 'Bundle' 생성
  3. Bundle로부터 컨테이너 생성

 

Docker는 세번째 단계, 즉 컨테이너 생성 기술에 대한 표준을 정해 runC라는 툴을 OCI에 제공하였다. 다만 1,2단계의 경우 표준화 및 분리되지 않은 상태로 꽤 오랜 시간이 지났고, 대다수의 사람들은 모든 컨테이너 런타임이 Docker처럼 1,2,3 단계를 전부 지원하는 것으로 착각할 수 밖에 없었다. 그리고 컨테이너 시장 또한 1,2단계가 섞인 High Level Container Runtime 그리고 3단계만 담당하는 Low Level Container Runtime으로 나뉘어지는 계기가 되었다.

 

Low Level Container Runtime

Low Level Container Runtime은 오로지 컨테이너를 실행하는 기능만 제공하며, OCI Runtime으로 부르기도 한다. 일반적으로 간단한 CLI 형태의 실행파일 또는 라이브러리로 존재하며, 개발자가 디버깅 목적으로 사용하는 경우를 제외하곤 High Level Container Runtime에 의해 호출되는 것이 일반적인 케이스다.

 

컨테이너는 namespace와 cgroups를 통해 구현되는 것은 다들 알고 있을 것이다. namespace는 시스템 리소스(Filesystem, Network 등)의 가상화 및 격리를 가능케하고, cgroups는 컨테이너 안에서 사용할 수 있는 리소스의 양을 제한하는데 쓰인다. Low Level Container Runtime은 이러한 namespace와 cgroup의 설정 및 실행을 담당하고 있다.

 

lmctfy, rkt, railcar 등 다양한 Low Level Container Runtime이 존재했지만, 현재 OCI 표준 스펙을 지키면서 살아남은 건 많지않으며 runC가 사실상 시장을 지배했다고 보면 된다.

 

High Level Container Runtime

High Level Container Runtime은 이미지 관리, 압축해제, Low Level Container Runtime으로 전달 등 조금 더 고수준 작업을 수행한다. 보통은 Daemon 방식으로 동작하며 Remote API를 제공하기 때문에, 외부에서 컨테이너를 실행하거나 모니터링이 가능하다.

Docker는 원래 Low Level, High Level 모두 구현하였으나, 이후 여러개의 프로젝트로 분리되었으며 이는 각각 runC와 containerd로 발전하였다.

 

Docker 구성요소

Docker는 위에서 설명한 것과 같이 더 이상 Monolithic 방식이 아닌 High Level/Low Level Container Runtime으로 분리되어 아래와 같은 모듈로 구성된다.

Docker Architecture

  • dockerd: Docker Daemon으로 REST API를 제공하며, Client로부터 컨테이너 관련 요청을 받는다. 대부분은 containerd로 위임하지만, 일부 작업(이미지 빌드, 네트워킹 등)은 직접 수행한다. 기본값은 Unix Domain Socket이지만, TCP 방식으로 변경 가능하다.
  • containerd: High Level Container Runtime 기능을 담당하며 Daemon 방식으로 REST API를 제공한다.
  • runc: Low Level Container Runtime 기능을 담당한다. runc의 원래 역할인 컨테이너 생성 후 바로 종료된다.
  • containerd-shim: runc는 컨테이너를 생성한 후 즉시 종료되므로 컨테이너 안에서 생성되는 프로세스의 부모가 될 프로세스가 필요하다. containerd-shimd은 컨테이너마다 배정되어 runc와 컨테이너 간의 가교 역할을 하는데, 예를 들어 컨테이너의 로그(stdout/stderr)을 가져오거나, 새로운 프로세스를 생성하는 등의 동작이 가능하다.

CRI(Container Runtime Interface)

Kubernetes 1.5부터 도입된 개념으로 kubelet이 컨테이너 런타임을 호출할 수 있는 인터페이스를 의미한다. High Level Container Runtime과 k8s가 연계되기 위해선 반드시 CRI를 구현해야 하며, Image 및 컨테이너 관리 및 Pod 생성이 가능해야 한다. 

현재 CRI를 지원하는 컨테이너 런타임은 containerd, cri-o 가 대표적이다. Docker는 dockerd가 CRI를 구현하지 않았기 때문에 kubelet과 dockerd 사이에 'dockerd-shim'를 구현하여 이 역할을 대신 담당해왔다. 하지만 이제 Docker도 containerd를 기본으로 사용하기 때문에 더 이상 dockerd-shim 존재 이유가 없어졌으며, K8s를 위해 Docker를 의무적으로 설치할 필요는 더더욱 없다. 이러한 사정으로 Kubernetes v1.20부터 Docker 지원을 중단하게 되었다.

CRI Spec

CRI Spec은 gRPC 방식의 Remote API와 Protocol Buffer로 명시되어 있다. 많은 RPC와 메시지 형식이 존재하며, 대표적인 RPC는 'PullImage', 'RunPodSandbox', 'CreateContainer' 등이 있다. 다음은 런타임 관련 API의 일부 snippet으로 CRI가 어떤 서비스를 제공해야 하는지 쉽게 이해할 수 있다.

service RuntimeService {
    rpc Version(VersionRequest) returns (VersionResponse) {}

    rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
 
    rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}

    rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
    ...
}

 

구체적인 내용은 Kubernetes에서 제공하는 protobuf 파일을 참고하기 바란다.

 

참고

 

'Kubernetes' 카테고리의 다른 글

ETCD Encryption  (0) 2021.04.21
Imperative Vs. Declarative  (0) 2021.04.08
Calico IPAM  (0) 2021.01.31
Calico 설치 및 Mode 변경  (2) 2021.01.29
StatefulSet  (0) 2021.01.25