2.3.2 저수준(Low-level) 컨테이너 런타임

우리가 쿠버네티스를 통해 컨테이너를 다룰 때, ‘컨테이너를 실행한다’는 것은 구체적으로 어떤 과정을 의미할까요? 컨테이너 이미지를 준비하고, 네트워크를 설정하고, 필요한 저장 공간(볼륨)을 연결하는 등 다양한 준비 작업들이 필요합니다. 하지만 이 모든 준비가 끝난 후, 실제로 운영체제 위에서 격리된 공간을 만들고 그 안에서 우리가 원하는 애플리케이션 프로세스를 딱 실행시키는, 가장 근본적이고 핵심적인 역할은 누가 담당할까요? 바로 이 질문에 대한 답이 저수준 컨테이너 런타임(Low-level Container Runtime)입니다.

고수준 런타임이 오케스트라의 지휘자라면, 저수준 런타임은 각 악기(컨테이너 프로세스)를 직접 연주하는 연주자에 비유할 수 있습니다. 지휘자의 지시에 따라 정확한 타이밍에, 정확한 소리를 내는 것이 연주자의 역할인 것처럼, 저수준 런타임은 고수준 런타임의 지시를 받아 리눅스 커널의 기능을 직접 활용하여 컨테이너를 생성하고 실행하는 실질적인 작업을 수행합니다.

이 저수준 런타임들이 약속된 규격에 맞춰 개발될 수 있도록 기준을 제시하는 것이 바로 OCI(Open Container Initiative) 런타임 명세(Runtime Specification)입니다. 이 명세 덕분에 다양한 저수준 런타임들이 등장할 수 있었고, 우리는 필요에 따라 적합한 런타임을 선택하여 사용할 수 있게 되었습니다. 이는 클라우드 네이티브 생태계의 개방성과 유연성을 보여주는 좋은 예시입니다.

이제 OCI 런타임 명세를 충실히 구현한 대표적인 저수준 컨테이너 런타임 두 가지, runc와 crun에 대해 자세히 알아보겠습니다. 이들이 어떻게 컨테이너를 현실로 만드는지 이해하는 것은 컨테이너 기술의 근본 원리를 파악하는 데 매우 중요합니다.

runc와 crun 비교

비교 항목 runc (OCI 표준 구현체) crun (C로 작성된 런타임)
주요 목적/특징 OCI 런타임 명세의 참조 구현체이자 사실상의 표준. 성능과 효율성에 중점을 둔 OCI 호환 런타임.
구현 언어 Go 언어 C 언어
성능 안정적이고 검증된 성능. 일반적으로 runc 대비 더 빠른 컨테이너 시작 속도를 보임.
리소스 사용량 상대적으로 crun보다 메모리 사용량 및 바이너리 크기가 큼. 낮은 메모리 사용량과 작은 바이너리 크기가 특징.
표준 준수 OCI 런타임 명세를 충실히 따름. OCI 런타임 명세를 충실히 따름.
주요 사용 사례/강점 – 가장 널리 사용되고 검증된 안정성.
– 대부분의 표준 쿠버네티스 환경.
– 성능이 매우 중요한 환경 (예: 대규모 클러스터).
– 리소스 제약 환경 (예: IoT, 엣지 컴퓨팅).
– cgroup v2 등 최신 커널 기능 활용이 중요할 때.
생태계/성숙도 매우 성숙하고 광범위한 커뮤니티 지원. 빠르게 발전하고 있으며, 특히 Red Hat 계열 및 Podman 등에서 채택 증가.

참고:

  • 성능과 리소스 사용량은 실제 워크로드, 하드웨어, 커널 버전 등 다양한 요인에 따라 달라질 수 있습니다. 위의 비교는 일반적인 경향을 나타냅니다.
  • runc와 crun 모두 활발히 개발되고 있으며, 기능 및 성능은 계속해서 개선되고 있습니다. 따라서 특정 시점의 기능 차이는 시간이 지남에 따라 변할 수 있습니다.
  • 대부분의 사용자에게는 기본적으로 제공되고 널리 사용되는 runc가 충분히 좋은 선택이며, 특별한 요구사항이 있을 경우 crun을 고려해 볼 수 있습니다.

2.3.2.1 runc: OCI 표준 구현체

runc는 현재 가장 널리 알려지고 사용되는 저수준 컨테이너 런타임이라고 할 수 있습니다. 그 이름에서 알 수 있듯이(‘run’ + ‘c’ontainer), 컨테이너를 실행하는 데 집중하는 도구입니다. runc의 탄생 배경을 살펴보면 컨테이너 기술의 발전 과정을 엿볼 수 있는데요, 원래는 컨테이너 기술을 대중화시킨 도커(Docker) 프로젝트의 일부였습니다. 컨테이너 실행을 담당하는 핵심 로직이었죠.

하지만 컨테이너 기술이 특정 회사에 종속되지 않고 개방적인 표준으로 발전해야 한다는 공감대가 형성되면서, 도커는 이 핵심 실행 로직을 분리하여 OCI(Open Container Initiative)라는 표준화 기구에 기증했습니다. 이렇게 탄생한 것이 바로 runc입니다. 즉, runc는 특정 회사나 프로젝트가 아닌, OCI 런타임 명세의 참조 구현(Reference Implementation)이자 사실상의 표준(de facto standard)으로 자리매김하게 되었습니다.

그렇다면 runc는 구체적으로 어떤 일을 할까요? runc는 OCI 런타임 명세에 정의된 컨테이너 번들(bundle)을 입력으로 받습니다. 이 번들은 컨테이너의 설정 정보를 담은 config.json 파일과 컨테이너의 루트 파일 시스템(rootfs) 디렉토리로 구성됩니다. runc는 이 config.json 파일을 읽어서 다음과 같은 핵심적인 작업들을 수행합니다.

  1. 네임스페이스(Namespaces) 생성 및 설정: 컨테이너에게 격리된 환경을 제공하기 위해 리눅스 커널의 네임스페이스 기능을 사용합니다. 예를 들어, PID 네임스페이스를 통해 컨테이너 안에서는 자신만의 프로세스 트리(보통 1번 프로세스로 시작)를 갖게 하고, 네트워크 네임스페이스를 통해 독립적인 네트워크 환경을 제공하며, 마운트 네임스페이스를 통해 파일 시스템 뷰를 격리합니다. 이 외에도 UTS(호스트 이름), IPC(프로세스 간 통신), User(사용자 ID) 네임스페이스 등을 설정하여 컨테이너의 독립성을 보장합니다.
  2. 컨트롤 그룹(Control Groups, cgroups) 설정: 컨테이너가 사용할 수 있는 시스템 자원(CPU, 메모리, 디스크 I/O 등)을 제한하고 격리합니다. 예를 들어, 특정 컨테이너가 CPU를 50%까지만 사용하도록 제한하거나, 메모리 사용량을 1GB로 제한하는 등의 설정을 적용하여 특정 컨테이너가 시스템 전체 자원을 독점하는 것을 방지하고 안정적인 서비스 운영을 가능하게 합니다.
  3. 루트 파일 시스템 설정: config.json에 명시된 루트 파일 시스템 디렉토리를 컨테이너의 루트 디렉토리(/)로 마운트합니다. 필요에 따라 pivot_root나 chroot와 같은 시스템 콜을 사용하여 컨테이너 프로세스가 지정된 루트 파일 시스템 외부로는 접근할 수 없도록 격리합니다.
  4. 케이퍼빌리티(Capabilities), 보안 정책(Seccomp, AppArmor 등) 적용: 컨테이너 내부에서 실행되는 프로세스가 가질 수 있는 특수 권한(Capabilities)을 제한하고, Seccomp나 AppArmor 같은 리눅스 보안 모듈을 통해 허용되지 않는 시스템 콜 호출을 차단하거나 특정 행동을 제어하여 보안을 강화합니다.
  5. 컨테이너 프로세스 실행: 위의 모든 설정이 완료되면, config.json에 정의된 명령어(entrypoint 또는 cmd)를 컨테이너 내부의 첫 번째 프로세스로 실행합니다.

이 모든 과정을 거쳐 비로소 하나의 컨테이너 프로세스가 격리된 환경에서 실행되게 됩니다. runc는 이처럼 컨테이너 실행의 가장 본질적인 역할을 수행하며, Go 언어로 작성되어 비교적 이해하기 쉽고 확장성이 좋습니다.

여러분이 kubectl run mypod –image=nginx 와 같은 명령을 실행할 때, 또는 containerd나 CRI-O 같은 고수준 런타임을 사용할 때, 그 내부에서는 최종적으로 runc (또는 이와 유사한 저수준 런타임)가 호출되어 실제 컨테이너를 생성하고 실행하게 되는 것입니다. 따라서 runc의 역할을 이해하는 것은 쿠버네티스가 어떻게 컨테이너를 다루는지 근본적으로 이해하는 데 필수적이라고 할 수 있습니다. 비록 우리가 직접 runc 명령어를 사용할 일은 거의 없겠지만, 그 존재와 역할을 아는 것은 문제 해결이나 시스템 이해에 큰 도움이 될 것입니다.

2.3.2.2 crun: C로 작성된 런타임

runc가 OCI 런타임 명세의 훌륭한 참조 구현체이자 사실상의 표준으로 자리 잡았지만, 기술의 세계는 늘 그렇듯 더 나은 성능, 더 낮은 리소스 사용량, 혹은 특정 환경에 더 적합한 대안을 추구하기 마련입니다. 이러한 배경 속에서 등장한 또 다른 주목할 만한 저수준 컨테이너 런타임이 바로 crun입니다.

crun은 이름에서 유추할 수 있듯이(C로 작성된 runtime), 프로그래밍 언어 C를 사용하여 개발된 OCI 호환 컨테이너 런타임입니다. 주로 Red Hat 엔지니어들이 주도하여 개발했으며, runc와의 가장 큰 차별점은 바로 이 구현 언어에 있습니다.

왜 C로 다시 런타임을 만들었을까요? 몇 가지 중요한 이유가 있습니다.

  1. 성능 및 효율성: C 언어는 Go 언어에 비해 일반적으로 더 가볍고 실행 속도가 빠르며, 메모리 사용량이 적습니다. 컨테이너는 가볍고 빠르게 생성 및 삭제되는 것이 특징인데, 특히 수많은 컨테이너가 동시에 실행되는 환경이나, IoT 또는 엣지 컴퓨팅처럼 리소스가 제한적인 환경에서는 저수준 런타임 자체의 오버헤드가 작을수록 유리합니다. crun은 이러한 요구사항에 부응하기 위해 C 언어의 장점을 살려 개발되었습니다. 실제 벤치마크에서도 runc 대비 컨테이너 시작 시간 단축이나 메모리 사용량 감소 효과를 보여주는 경우가 많습니다.
  2. 작은 바이너리 크기 및 의존성: C로 작성된 프로그램은 일반적으로 Go로 작성된 프로그램보다 최종 실행 파일(바이너리)의 크기가 작고, 외부 라이브러리 의존성도 적은 경향이 있습니다. 이는 배포 및 관리를 용이하게 하고, 잠재적인 보안 취약점 표면(attack surface)을 줄이는 데 도움이 될 수 있습니다.
  3. 유연성 및 기능: crun은 OCI 런타임 명세를 충실히 따르면서도, cgroup v2 지원 강화, 사용자 네임스페이스(user namespace)와의 통합 개선 등 runc보다 먼저 혹은 다르게 구현된 고급 기능들을 제공하기도 합니다. (물론, runc도 지속적으로 발전하고 있으므로 기능 차이는 시점에 따라 달라질 수 있습니다.)

crun 역시 runc와 마찬가지로 OCI 런타임 명세를 구현했기 때문에, 고수준 컨테이너 런타임인 containerd나 CRI-O와 함께 사용할 수 있습니다. 예를 들어, Podman 같은 컨테이너 관리 도구는 crun을 기본 저수준 런타임으로 사용하거나 선택적으로 사용할 수 있도록 지원하며, CRI-O 설정 변경을 통해 쿠버네티스 클러스터에서도 runc 대신 crun을 사용하도록 구성할 수 있습니다.

그렇다면 runc와 crun 중 무엇을 선택해야 할까요? 대부분의 일반적인 환경에서는 runc가 여전히 안정적이고 검증된 좋은 선택입니다. 하지만 앞서 언급했듯이, 컨테이너 시작 성능이 매우 중요하거나, 메모리 같은 시스템 리소스 사용량을 최소화해야 하는 특별한 요구사항이 있다면 crun을 고려해볼 수 있습니다.

crun의 등장은 OCI라는 표준화된 인터페이스 덕분에 다양한 구현체들이 경쟁하고 발전할 수 있음을 보여주는 좋은 사례입니다. 클라우드 네이티브와 쿠버네티스를 배우는 여러분께서는 runc가 유일한 저수준 런타임이 아니며, 성능이나 특정 요구사항에 따라 crun과 같은 대안이 존재한다는 사실을 알아두시는 것이 좋습니다. 이는 컨테이너 생태계의 다양성과 기술 선택의 폭을 이해하는 데 도움이 될 것입니다.

궁극적으로 runc든 crun이든, 이 저수준 런타임들은 보이지 않는 곳에서 컨테이너라는 마법을 현실로 만드는 핵심적인 역할을 수행하고 있다는 점을 기억해 주시기 바랍니다.