[container] 컨테이너 런타임 표준화(OCI 표준)

2022. 8. 26. 20:27[ DevOps ]/[ k8s ]

 

https://containerd.io/

 

 

 

containerd 등장 배경 - OCI 표준

초기 docker engine은 Monolithic한 구조였고 이를 나누는 작업이 시작되었다. 이러한 Monolithic한 구조의 docker engine에는 API, CLI, 네트워크, 스토리지 등 여러 기능들이 한 곳에 담기게 되었고 Docker에 의존적이던 kubernetes에서는 docker 버전이 새로 나올 때마다 영향을 받을 수밖에 없었다.

 

이를 계기로 2015년에 Linux Foundation에서는 '컨테이너 기술에 대한 표준'을 정하는 프로젝트(OCI)가 시작되었다. 이 프로젝트는 2016년 12월에 Container Runtime에 대한 표준을 만들고 이를 실행할 수 있는 추상화된 인터페이스인 CRI(Container Runtime Interface) 스펙을 제공함으로써 docker 버전과 무관하게 containerd 표준을 준수하는 어떠한 이미지도 kubernetes에서 실행 가능하게 되었다.

 

정리하자면 기존의 Monolithic한 docker engine은 kubernetes입장에서 docker에 완전히 의존적일 수밖에 없다 보니, docker를 쪼개는 프로젝트가 시작되었으며 해당 프로젝트에서는 Container Runtime에 대한 표준(OCI 표준)을 만들었다. 그리고 docker에서 해당 표준을 기반으로 만든  Container Runtime이 containerd인 것이다. (d는 데몬 프로세스) docker 외에도 Red Hat, Intel, IBM에서 OCI 표준에 따라 만든 Container Engine CRI-O도 있다.

 

CRI-O와 containerd가 가장 널리 사용되는, OCI 표준을 기반으로 하는 Container Runtime이다. Docker에서는 Container Engine으로 containerd를 사용 중이며 Docker를 설치하면 자동으로 설치된다. 따라서 containerd가 OCI 표준을 기반으로 만들어졌기 때문에 kubernetes에서 무리 없이 관리될 수 있다.

 


좀 더 구체적으로,,

 

위는 이미지가 컨테이너로 실행되는 일반적인 과정이다.(Container Runtime) docker는 container runtime 표준화 단계에서 실제 컨테이너로 실행시키는 단계(3번째 단계, 실행 부분)만 표준화한다. 이로 인해 Container Runtime 표준화는 다음과 같이 두 단계로 나뉘게 되었다.

 

1) Low-Level Container Runtime: 실제 컨테이너로 실행하는 부분 (ex. runC)

2) High-Level Container Runtime: 이미지 전송, 이미지 관리, 이미지 압출 풀기, 실행 중인 컨테이너들의 모니터링 및 관리, 이미지 실행과 관련된 API 제공 등 (ex. containerd)

 

 

docker v1.11 이후부터 적용된 docker architecture

 

 

 

[1] Low-Level Container Runtime

Low-level Container Runtime은 컨테이너를 직접 실행하는 역할을 담당한다. Low-level Container Runtime에서 '저수준'이러는 이름을 붙인 이유는 오직 컨테이너를 실행시키고 실행 중인 컨테이너만을 관리하기 때문이다. 

 

컨테이너는 linux namespace와 cgroup(control group)을 사용하여 구현되는데, namespace는 각 컨테이너에 파일 시스템이나 네트워크와 같은 시스템 resource를 격리, 가상화하며 cgroup은 각 컨테이너가 사용할 CPU, 메모리, 네트워크, I/O, 디바이스 등의 자원을 제어한다. Low-Level Container Runtime은 이러한 namespace와 cgroup을 설정하고 namespace, cgroup 내에서 명령을 실행한다. cgroup과 namespace는 모두 kernel의 기능이며 이를 다루는 방법은 리눅스 배포판과 kernel 버전마다 다르다.

 

가정 널리 사용되는 Low-Level Container Runtime는 'runC'

runC의 발전과정은 아래와 같다.

 

1) cgroup, namespace은 리눅스 배포판과 kernel 버전마다 다루는 방법이 다르기 때문에 LXC(Linux Container)나 libvirt와 같은 '중간 매개체'를 통해 간접적으로 관리했다.

2) 이런 컨테이너 기술이 외부 솔루션에 의존적이다 보니, docker는 컨테이너 기술을 다루는 인터페이스를 직접 개발하게 되었고 이것이 libcontainer가 되었다. 그리고 docker는 0.9 버전부터 LXC를 대신하여 libcontainer를 도입한다.

3) docker는 libcontainer를 OCI에 기부하게 되고 지속적인 리펙토링을 통해 runC로 재탄생되었다.

4) 이후 runC는 OCI 표준을 준수하는 대표적인 Low-level Container Runtime이 된다. (22년 8월 기준 최신 버전 1.1.4)

 

 

https://github.com/orgs/opencontainers/repositories

 

OCI의 GitHub Organization에서 볼 수 있듯이 OCI에서 runC를 직접 관리하고 있는 것 같다.

 

 

 

[2] High-Level Container Runtime

High-level Container Runtime은 컨테이너를 논리적으로 실행(실제 실행은 Low-level Container Runtime에서 진행), 이미지 전송, 이미지 관리, 이미지 압출 풀기, 실행 중인 컨테이너들의 모니터링 및 관리 등과 관련된 API를 제공하면서 데몬 프로세스로서 동작한다. High-level Container Runtime의 역할을 정리하면 다음과 같다.

 

- 컨테이너 이미지 push, pull, 압축 관리

- 컨테이너에 할당할 resources(CPU, Memory, Network, Storage)

- "컨테이너 간 네트워킹 관리"

- runC와 같은 Low-level Container Runtime에 명령 전달(논리적 컨테이너 실행)

- 컨테이너 간 라이프사이클 관리

 

containerd와 같은 고수준 컨테이너 런타임이 할 수 있는 역할은 아래와 같다.

 

1) 컨테이너가 사용할 최대 Memory, CPU share 관리

2) 호스트 프로세스와 컨테이너의 격리 관리 (파일 시스템 등이 외부에서 접근하지 못하도록)

3) 컨테이너가 호스트의 파일 시스템의 일부를 공유하기를 원한다면 이에 대한 접근 허용 관리

4) UID namespace 격리에 대한 관리

5) 컨테이너 실행 중 필요한 환경변수 설정 (일부는 컨테이너 이미지로 부터 설정됨)

6) 컨테이너가 시작될 때 네트워크 설정을 생성하고 적용(attach)

 

즉, 고수준 컨테이너 런타임은 저수준 컨테이너 런타임에서 직접적으로 격리하는(컨테이너를 생성하는) 사항들을 '관리(supervision)'한다. 쉽게 말해 고수준 컨테이너 런타임은 저수준 컨테이너 런타임에 대한 configuration을 진행하고 실제 이에 대한 실행은 저수준 컨테이너 런타임에서 수행되는 것이다.

 

가장 널리 사용되는 High-level Container Runtime은 2가지이다.

 

1) containerd

- docker에서 개발했으며 OCI 표준을 준수한다.

- docker engine에 기본적으로 탑재된다.

- docker build 커맨드로 생성된 이미지는 OCI image spec을 준수한다.

- docker는 containerd를 CNCF 재단에 기부하고 MS, Google, AWS 등과 함께 관리되고 있다.

- docker v1.11 이후부터 적용되었다.

- 'sudo systemctl status containerd.service'를 통해 containerd 상태를 확인할 수 있다. 이 커맨드에서 알 수 있듯이 containerd는 systemd의 자식 프로세스이다.

 

2) CRI-O

- Red Hat, Intel, IBM에서 개발되었으며 OCI 표준을 준수한다.

- 로컬 쿠버네티스인 minikube에서 기본으로 사용한다.

 

* OCI 표준을 준수하여 만들어진 containerd, CRI-O는 특정 벤더에 의존적이지 않다는 것(컨테이너 표준에 의해 정의)을 의미한다. 

 

 

cf. containerd-shim

- runC를 실행하고 컨테이너 프로세스를 제어하는 경령 데몬 프로세스이다.

- containerd와의 모든 통신은 contianerd-shim을 통해서 이루어진다.

- 컨테이너의 표준 에러, 표준 출력 스트림 제공함으로써 containerd에 문제가 생겨도 로그파일 작성이 가능하다.

- runC는 컨테이너 프로세스를 실행(fork)하고 부모 프로세스(runC)를 종료함으로써 컨테이너 프로세스가 고아 프로세스가 되도록 하여 컨테이너 프로세스를 데몬화하게 되는데, 이 경우 컨테이너 프로세스는 호스트의 init 프로세스에 붙게 되어 관리가 어렵다는 문제가 있었다. 이를 해결하고자 컨테이너 프로세스를 shim 데몬에 붙게 함으로써 shim이 컨테이너를 관리하도록 한다.

 

 

cf. 컨테이너 실행 시 발생하는 dockerd, containerd, shim, runC 간 상호작용

1) client는 docker 커맨드를 통해 dockerd에 요청을 전달

2) dockerd는 gRPC를 통해 containerd로 요청 전달

3) containerd는 exec system call을 통해 shim을 자식 프로세스로 생성

4) shim은 runC를 이용하여 컨테이너를 생성하고 실행

5) 컨테이너가 정상적으로 실행되면 runC는 종료

6) 컨테이너 프로세스는 shim의 자식 프로세스로 adopted

 

** containerd는 dockerd의 자식 프로세스가 아니다. 위 과정에서 볼 수 있듯이 dockerd는 containerd로 gRPC 기반 API를 호출함으로써 요청을 전달한다. 즉, dockerd와 containerd는 서로 분리되어 있다. 이렇게 docker와 containerd가 분리되어 있기 때문에 docker를 재시작하더라도 컨테이너는 죽지 않고 실행된다.

 

 

위 커맨드에서 볼 수 있듯이 containerd와 dockerd는 모두 systemd(PID 1)의 자식 프로세스이다.(PPID 1) 즉 완전히 분리되어 있다.

 


쿠버네티스와 containerd

https://www.openmaru.io/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%99%80-%EB%8F%84%EC%BB%A4/

 

쿠버네티스 v1.20부터 docker는 deprecated되었으며 v1.22부터는 완전히 docker 지원을 중단하고 컨테이너 런타임 인터페이스(CRI) 요구사항을 만족하는 런타임을 사용하도록 바뀌었다.

 

http://www.opennaru.com/kubernetes/containerd/

 

 

OCI 표준화의 장단점

1) 장점

- 기존 쿠버네티스는 docker에 의존적이었지만 보다 가벼운 Container Runtime인 containerd로 전환하게 됨으로써 Pod를 더욱 빠르게 실행시키고 CPU와 메모리 사용량을 줄일 수 있게 되었다.

-  ‘통일된 표준을 통해 어디서든 실행되는 이식성'을 제공한다.

 

2) 단점

- containerd를 사용하는 방법은 분명히 Docker에 비해 까다롭다. 쿠버네티스 클러스터를 운영하는 입장에서는 Docker로 한 번에 모든 것을 제어할 수 있었던 예전과는 달리, 이제는 runC, containerd 등과 익숙해져야 하는 부담이 생겼다.

 

 

 

 

 

docker의 기본 Container Runtime은 containerd이지만 CRI-O와 같이 OCI 표준을 준수하는 Container Runtime이라면 어떤 것이로든 변경할 수 있다. 쿠머네티스에서도 마찬가지이다.

 

 

최종 정리

- 과거에는 Docker가 컨테이너를 직접 만들고 관리했다.

- OCI 표준이 등장하고 여기서 컨테이너 런타임에 대한 표준을 정의했다.

- containerd와 docker는 완전히 분리되었다.

- dockerd, containerd 모두 systemd 프로세스의 자식 프로세스로 동일 레벨에 존재하게 된다.

- 즉, Docker와 containerd는 완전히 독립적인 주체가 되었으며 Docker를 중지하더라도 containerd는 docker와 무관하게 실행된다.

- 쿠버네티스 v1.20 이후부터는 Docker를 사용하지 않고 contianerd, CRI-O와 같은 High-Level Container Runtime을 사용하도록 변경되었다.

 

 

 

 

 

[참고]

- ubuntu 20.04,22.04에 containerd 설치하기: https://www.itzgeek.com/how-tos/linux/ubuntu-how-tos/install-containerd-on-ubuntu-22-04.html

- 컨테이너 런타임에 대한 kubernetes 공식문서: https://kubernetes.io/ko/docs/setup/production-environment/container-runtimes/

 

 

Reference

- OCI, CRI 개론, https://www.samsungsds.com/kr/insights/docker.html

- OCI github, https://github.com/opencontainers

- 컨테이너 생태계 변화과정, https://blog.siner.io/2021/10/23/container-ecosystem/

- docker 구조, https://kangwoo.kr/2020/07/26/%EB%8F%84%EC%BB%A4-%EA%B5%AC%EC%A1%B0/

- 컨테이너 런타임, https://insujang.github.io/2019-10-31/container-runtime/

- 쿠버네티스 도커 지원 중단 공식문서, https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/

- containerd의 구체적인 역할, https://earthly.dev/blog/containerd-vs-docker/

- OCI 스펙 문서, https://opencontainers.org/release-notices/overview/