2.3.1 OCI (Open Container Initiative) 표준

앞서 컨테이너 런타임이 컨테이너를 실행하는 핵심 엔진이라고 말씀드렸죠? 그런데 만약 컨테이너 기술 회사마다 이미지를 만드는 방식, 컨테이너를 실행하는 방식이 제각각이라면 어떨까요? 마치 스마트폰 충전기가 제조사별로 달라 여행갈 때마다 여러 종류의 충전기를 챙겨야 했던 예전처럼, 컨테이너 기술을 사용하는 데 큰 불편함과 비효율이 발생할 겁니다. A라는 도구로 만든 컨테이너 이미지가 B라는 런타임에서는 돌아가지 않고, C라는 오케스트레이션 도구는 A의 이미지만 지원한다면, 기술 선택의 폭은 좁아지고 특정 기술에 종속될 수밖에 없겠죠.

이러한 혼란을 막고 컨테이너 생태계가 건강하게 발전할 수 있도록, 업계의 주요 기업들이 모여 만든 것이 바로 OCI(Open Container Initiative)입니다. OCI는 리눅스 재단(Linux Foundation) 산하의 프로젝트로, 컨테이너 기술의 핵심적인 두 가지 요소, 즉 컨테이너를 실행하는 방법(Runtime)과 컨테이너 이미지를 구성하는 형식(Image Format)에 대한 개방형 표준(Open Standard)을 정의하고 발전시키는 것을 목표로 합니다. 마치 전 세계적으로 통용되는 USB 규격이나 인터넷 프로토콜(TCP/IP)처럼, OCI 표준은 컨테이너 기술의 기본적인 약속이자 질서라고 할 수 있습니다.

이 표준 덕분에 우리는 다양한 도구와 기술들을 마치 레고 블록처럼 자유롭게 조합하여 사용할 수 있게 되었습니다. 이제 OCI가 정의하는 두 가지 핵심 표준 사양에 대해 좀 더 자세히 살펴보며, 이들이 어떻게 컨테이너 기술의 기반을 다지고 있는지 알아보겠습니다.

2.3.1 OCI (Open Container Initiative) 표준

2.3.1.1 OCI Runtime Specification

이제 OCI 표준의 첫 번째 기둥인 OCI 런타임 사양(Runtime Specification)에 대해 좀 더 깊이 파고들어 보겠습니다. 이 사양은 앞서 말씀드린 것처럼, 컨테이너라는 격리된 환경을 실제로 어떻게 만들고 실행할 것인가에 대한 매우 구체적이고 기술적인 약속입니다. 마치 아주 정교하게 작성된 기계 설계도나 상세한 요리 레시피와 같다고 생각하시면 이해가 쉬울 것 같습니다. 이 설계도나 레시피를 따르기만 하면, 누가 만들든 동일한 결과물(여기서는 실행 중인 컨테이너)을 얻을 수 있도록 하는 것이죠.

이 사양의 주된 사용자는 누구일까요? 바로 저수준 컨테이너 런타임(Low-level Container Runtime)입니다. 우리가 흔히 듣는 runc나 crun 같은 도구들이 이 사양을 충실히 구현하는 대표적인 예입니다. 쿠버네티스의 Kubelet이나 containerd, CRI-O와 같은 고수준 컨테이너 관리 도구들은 직접 컨테이너를 실행하는 복잡한 과정을 처리하는 대신, 이 OCI 런타임 사양을 준수하는 저수준 런타임에게 “여기 이 명세서(config.json)와 파일 시스템(rootfs)을 줄 테니, 이대로 컨테이너를 실행시켜 줘”라고 명령을 내립니다. 즉, OCI 런타임 사양은 고수준 관리 도구와 저수준 실행 엔진 사이의 명확한 계약서 역할을 수행하는 셈입니다.

그렇다면 이 중요한 계약서, OCI 런타임 사양은 구체적으로 어떤 내용을 담고 있을까요? 핵심 내용은 크게 두 가지 영역으로 나눌 수 있습니다. 바로 컨테이너의 구성(Configuration)과 생명주기 관리(Lifecycle Operations)입니다.

1. 컨테이너 구성 (Configuration): 컨테이너의 청사진, config.json

컨테이너를 실행하기 위해 필요한 모든 세부 설정은 config.json이라는 이름의 JSON(JavaScript Object Notation) 형식 파일에 담기도록 규정되어 있습니다. 이 파일은 컨테이너가 어떤 모습과 상태로 실행되어야 하는 지에 대한 완전한 명세, 즉 ‘청사진’입니다. 저수준 런타임은 이 config.json 파일을 읽어서 그대로 컨테이너 환경을 구성해야 합니다. 이 파일에는 다음과 같은 필수적이고 중요한 정보들이 아주 상세하게 기술됩니다.

OCI Runtime Specification: config.json 컨테이너 구성 항목 상세

구성 영역 관련 필드 (config.json 내) 상세 설명 주요 설정 내용 및 예시
기본 사양 및 식별 ociVersion 이 설정 파일이 준수하는 OCI 런타임 사양의 버전을 명시합니다. 런타임이 호환성을 확인하고 올바르게 해석하는 기준이 됩니다. “1.1.0”
  hostname 컨테이너 내부에서 사용할 호스트 이름을 지정합니다. UTS 네임스페이스를 통해 격리됩니다. “web-server-prod-01”
루트 파일 시스템 root 컨테이너의 근간이 되는 루트 파일 시스템(rootfs)의 위치와 속성을 정의합니다. path: “/path/to/container/rootfs” (필수)<br>readonly: true 또는 false (선택, 기본값 false)
프로세스 실행 process.args 컨테이너가 시작될 때 내부에서 실행될 명령어와 그 인자(arguments)들을 배열 형태로 지정합니다. 이것이 컨테이너의 주 목적이 됩니다. [“/usr/sbin/nginx”, “-g”, “daemon off;”]
  process.cwd 컨테이너 내 프로세스가 시작될 현재 작업 디렉토리(Current Working Directory)를 지정합니다. “/app”
  process.env 컨테이너 내 프로세스에 설정될 환경 변수 목록을 정의합니다. “KEY=VALUE” 형식의 문자열 배열로 제공됩니다. [“PATH=/usr/local/bin:/usr/bin:/bin”, “API_KEY=abcdef12345”]
프로세스 환경 process.terminal 프로세스를 위해 의사 터미널(pseudo-TTY)을 할당할지 여부를 지정합니다. 대화형 쉘을 실행할 때 주로 true로 설정합니다. true 또는 false
  process.consoleSize terminal이 true일 경우, 해당 터미널의 크기(높이, 너비)를 지정합니다. height: 25, width: 80
사용자 및 권한 process.user 컨테이너 내 프로세스를 실행할 사용자(UID)그룹(GID)를 지정합니다. 보안상 매우 중요하며, 최소 권한 원칙을 적용하는 데 사용됩니다. uid: 1000, gid: 1000,<br>additionalGids: [2000, 3000] (추가 그룹 ID)
  process.capabilities 리눅스 케이퍼빌리티(Capabilities) 설정을 통해 루트 권한을 세분화하여 프로세스에 필요한 최소한의 특권만 부여합니다. 보안을 강화하는 핵심 기능입니다. bounding, effective, inheritable, permitted, ambient 각 배열에 필요한 케이퍼빌리티 추가 (예: CAP_NET_BIND_SERVICE) 또는 제거 (예: CAP_SYS_ADMIN 제거 권장)
  process.noNewPrivileges 이 값을 true로 설정하면, 해당 프로세스 및 그 자식 프로세스가 execve 시스템 콜을 통해 새로운 권한(예: setuid, setgid 비트 사용)을 얻는 것을 방지합니다. true (보안 강화를 위해 강력히 권장)
자원 제한 (Cgroups) linux.resources 컨트롤 그룹(cgroups) 설정을 통해 컨테이너가 사용할 수 있는 시스템 자원(CPU, 메모리, 디스크 I/O, 프로세스 수 등)을 제한하고 격리합니다. memory: { limit: 536870912 } (메모리 512MB 제한)<br>cpu: { shares: 1024, quota: 50000, period: 100000 } (CPU 상대적 가중치 및 절대적 제한)<br>pids: { limit: 100 } (최대 프로세스 수 100개 제한)<br>blockIO: (디스크 I/O 대역폭/IOPS 제한 설정)
  linux.cgroupsPath 컨테이너의 컨트롤 그룹이 생성될 호스트 시스템 상의 경로를 지정합니다. 고수준 런타임이 관리하는 경우가 많습니다. “/kubepods/besteffort/pod-<id>/<container-id>” (예시)
리눅스 네임스페이스 (격리) linux.namespaces 컨테이너 격리의 핵심 기술인 리눅스 네임스페이스 사용 여부 및 방식을 설정합니다. 각 타입별로 격리 여부(type)와 사용할 네임스페이스 파일 경로(path)를 지정할 수 있습니다. 배열 내 객체들:<br>{ type: “pid”, path: “” } (새 PID 네임스페이스 생성)<br>{ type: “network”, path: “/var/run/netns/my-netns” } (기존 네트워크 네임스페이스 사용)<br>주요 타입: pid, network, mount, ipc, uts, user, cgroup, time
마운트 포인트 mounts 컨테이너 내부에 마운트될 추가적인 파일 시스템이나 디렉토리를 정의합니다. 호스트 볼륨 연결, 임시 파일 시스템(tmpfs) 마운트 등에 사용됩니다. 배열 내 객체들:<br>{ destination: “/data”, type: “bind”, source: “/opt/host-data”, options: [“rbind”, “rw”] } (호스트 디렉토리 바인드 마운트)<br>{ destination: “/cache”, type: “tmpfs”, options: [“nosuid”, “size=64m”] } (tmpfs 마운트)
  linux.rootfsPropagation 루트 파일 시스템의 마운트 전파(Mount Propagation) 모드를 설정합니다. (예: private, shared, slave) “private”
보안 설정 (Linux) process.apparmorProfile AppArmor 보안 프로파일을 지정하여 프로세스의 행동(파일 접근, 네트워크 사용 등)을 강제적으로 제어합니다. “docker-default” (예시)
  process.selinuxLabel SELinux 보안 레이블(컨텍스트)을 지정하여 Mandatory Access Control(MAC) 정책을 적용합니다. “system_u:system_r:container_t:s0:c123,c456” (예시)
  process.seccomp Seccomp(Secure Computing mode) 프로파일을 통해 프로세스가 호출할 수 있는 시스템 콜(System Call)을 제한합니다. 커널 공격 표면을 줄이는 데 매우 효과적입니다. defaultAction: SCMP_ACT_ERRNO (허용되지 않은 콜은 에러 반환)<br>architectures: [“SCMP_ARCH_X86_64”]<br>syscalls: 허용/거부할 시스템 콜 목록 정의
  linux.maskedPaths 컨테이너 내부에서 특정 경로를 보이지 않도록 마스킹합니다. /proc 내의 민감한 정보 접근을 차단하는 데 사용될 수 있습니다. [“/proc/kcore”, “/proc/latency_stats”]
  linux.readonlyPaths 컨테이너 내부에서 특정 경로를 읽기 전용으로 만듭니다. 중요한 시스템 파일이나 설정을 보호하는 데 사용될 수 있습니다. [“/proc/sys”, “/proc/sysrq-trigger”]
  linux.devices 컨테이너가 접근할 수 있는 호스트의 장치 파일(/dev) 목록과 권한을 명시적으로 제어합니다. 기본적으로는 제한되며, 필요한 장치만 허용하는 방식(allowlist)입니다. 배열 내 객체들:<br>{ allow: true, type: “c”, major: 1, minor: 9, access: “rwm” } (/dev/urandom 허용 예시)<br>{ allow: false, access: “rwm” } (기본적으로 모든 장치 접근 거부)
  linux.sysctl 컨테이너의 네임스페이스 내에서 적용될 커널 파라미터(sysctl) 값을 설정합니다. 네트워크 스택 튜닝 등에 사용될 수 있습니다. { “net.ipv4.ip_forward”: “1” }
플랫폼 정보 platform 컨테이너 이미지가 의도된 운영체제(OS)와 CPU 아키텍처(Architecture) 정보를 명시합니다. 런타임은 이 정보를 확인하여 호환성을 검증할 수 있습니다. os: “linux”, arch: “amd64”
확장성 및 메타데이터 annotations OCI 표준 필드 외에, 특정 도구나 플랫폼(예: 쿠버네티스, CRI-O, containerd)에서 필요한 추가적인 메타데이터를 키-값 형태로 자유롭게 추가할 수 있습니다. { “io.kubernetes.cri.container-name”: “my-app”, “my.custom.tool/version”: “1.2.3” }
훅 (Hooks) hooks 컨테이너 생명주기의 특정 시점 (예: prestart, poststop)에 실행될 사용자 정의 스크립트나 명령어를 설정합니다. 네트워크 설정, 초기화 작업 등에 활용됩니다. prestart: [{ path: “/usr/local/bin/setup-network.sh”, args: [“setup-network.sh”, “eth0”], … }] (예시)

이 표는 config.json이 컨테이너의 거의 모든 측면 – 무엇을 실행할지, 어떤 환경에서 실행할지, 어떤 자원을 얼마나 사용할 수 있는지, 어떻게 격리되고 보호될지 – 을 얼마나 상세하고 정교하게 정의하는지를 보여줍니다. 저수준 컨테이너 런타임은 이 ‘설계도’를 충실히 따라서 일관되고 예측 가능한 컨테이너 실행 환경을 구축하는 것입니다.

2. 컨테이너 생명주기 연산 (Lifecycle Operations): 컨테이너를 다루는 표준 명령어

OCI 런타임 사양은 config.json과 파일 시스템 번들을 이용하여 컨테이너의 상태를 관리하기 위한 표준화된 명령어 집합(operations)도 정의합니다. 모든 OCI 호환 저수준 런타임은 이 명령어들을 동일한 방식으로 이해하고 처리해야 합니다. 이는 마치 VCR 플레이어의 ‘재생’, ‘정지’, ‘되감기’ 버튼처럼, 컨테이너를 제어하는 표준 인터페이스 역할을 합니다. 일반적으로 저수준 런타임은 이 명령어들을 처리하는 커맨드 라인 인터페이스(CLI)를 제공합니다. (예: runc create, runc start 등)

OCI Runtime Specification: 컨테이너 생명주기 명령어(Operations) 상세

명령어 (Operation) 목적 및 설명 주요 입력 (Inputs) 예상 결과 (Outputs) 및 상태 변화 주요 고려사항 및 참고
state 특정 컨테이너의 현재 상태 정보를 조회합니다. 컨테이너의 ID, PID, 상태(creating, created, running, stopped 등), 번들 경로 등을 포함한 JSON 객체를 반환합니다. <container-id> JSON 형식의 상태 정보 반환.<br>상태 변화 없음. 컨테이너의 현재 상황을 파악하기 위해 고수준 런타임이 주기적으로 또는 필요시 호출합니다. 어떤 상태의 컨테이너에 대해서도 호출 가능해야 합니다.
create config.json과 파일 시스템 번들을 기반으로 컨테이너 실행 환경을 준비(Preparation)합니다. 네임스페이스, cgroups, 마운트 등을 설정하지만, 사용자 프로세스는 아직 시작하지 않습니다. <container-id>, -b <bundle-path> (번들 디렉토리 경로) 컨테이너 환경이 성공적으로 설정됨.<br>상태 변화: (없음) → creating → created 실제 컨테이너 프로세스를 시작하기 전 단계입니다. 설정 오류나 자원 부족 시 실패할 수 있습니다. OCI Hooks (createRuntime, createContainer)가 이 단계에서 실행될 수 있습니다.
start create 명령으로 준비된 컨테이너 내부에서 config.json에 명시된 사용자 프로세스를 시작합니다. <container-id> 컨테이너 내 사용자 프로세스가 시작됨.<br>상태 변화: created → running 반드시 create 명령이 성공적으로 완료된 후에 호출해야 합니다. 프로세스 시작 실패 시 컨테이너는 created 상태로 남거나 에러 처리될 수 있습니다. OCI Hooks (prestart, poststart)가 이 단계 전후로 실행될 수 있습니다.
kill 실행 중인 컨테이너 내부의 주요 프로세스에게 특정 유닉스 신호(Signal)를 보냅니다. 컨테이너를 정상적으로 또는 강제로 종료시키는 데 사용됩니다. <container-id>, [signal] (예: TERM, KILL, USR1) 지정된 신호가 컨테이너 프로세스에게 전달됨.<br>상태 변화: 직접적인 상태 변경은 없으나, 프로세스 종료를 유발하여 결과적으로 running → stopped 상태로 이어질 수 있음. 신호 전달 성공 여부만 반환하며, 프로세스가 실제로 종료되었는지는 state 명령으로 확인해야 합니다. 고수준 런타임은 보통 SIGTERM 후 유예 시간을 두고 SIGKILL을 보내는 종료 로직을 구현합니다.
delete 컨테이너와 관련된 모든 리소스(네임스페이스, cgroups, 마운트 포인트, 임시 파일 등)를 정리하고 컨테이너 실행 환경 자체를 제거합니다. <container-id> 컨테이너 리소스가 완전히 정리되고 제거됨.<br>상태 변화: stopped → (존재하지 않음 / 삭제됨) 반드시 컨테이너 프로세스가 종료된 후 (stopped 상태)에 호출해야 합니다. 실행 중인 컨테이너에 대해 호출하면 일반적으로 실패합니다. (강제 삭제 옵션은 OCI 표준 외 구현체별 기능) OCI Hooks (poststop)가 이 단계 전에 실행될 수 있습니다.

표 요약 및 중요 포인트:

  • 순서: 일반적인 컨테이너 생성 및 실행 순서는 create → start 입니다.
  • 종료: 컨테이너를 종료할 때는 kill (프로세스 종료 요청) → (프로세스 종료 확인) → delete (리소스 정리) 순서로 진행됩니다.
  • 상태 확인: state 명령은 전체 생명주기 동안 컨테이너의 현재 상태를 확인하는 데 사용됩니다.
  • 표준 인터페이스: 이 명령어들은 저수준 런타임이 제공해야 하는 표준화된 인터페이스로, 어떤 OCI 호환 런타임을 사용하든 고수준 런타임은 동일한 방식으로 컨테이너를 관리할 수 있습니다.
  • 저수준 vs 고수준: 사용자가 직접 runc create 같은 저수준 명령어를 사용하는 경우는 드물며, 보통 kubectl run, docker run, ctr run 등의 고수준 명령어를 사용하면 내부적으로 이러한 OCI 생명주기 명령어들이 호출됩니다.

이 표를 통해 컨테이너가 단순히 생성되고 사라지는 것이 아니라, OCI 런타임 사양에 정의된 명확한 상태들과 각 상태를 전환시키는 표준화된 명령어들을 통해 체계적으로 관리된다는 점을 이해하실 수 있기를 바랍니다. 이러한 표준화 덕분에 복잡한 컨테이너 오케스트레이션이 가능해지는 것입니다.

OCI 런타임 사양의 의미: 표준화가 가져온 혁신

이처럼 OCI 런타임 사양이 컨테이너 구성과 생명주기 관리에 대한 명확하고 상세한 표준을 제시함으로써 얻게 되는 이점은 매우 큽니다.

  • 런타임 구현의 다양성 촉진: runc는 OCI의 공식 참조 구현체로서 가장 널리 쓰이지만, 이 표준 덕분에 다른 목표를 가진 저수준 런타임들이 등장할 수 있었습니다. 예를 들어,
    • crun: C 언어로 작성되어 runc(Go 언어)보다 더 가볍고 빠르며 메모리 사용량이 적은 것을 목표로 합니다. 임베디드 환경이나 리소스가 제한적인 환경에 유리할 수 있습니다.
    • kata-containers: 컨테이너마다 경량 가상 머신(VM)을 사용하여 하드웨어 수준의 격리를 제공합니다. 보안이 매우 중요한 워크로드나 신뢰할 수 없는 코드를 실행해야 하는 경우 강력한 보안 경계를 제공합니다. 하지만 성능 오버헤드는 runc보다 클 수 있습니다.
    • gVisor: Google에서 개발한 것으로, 사용자 공간(User-space)에서 자체적인 커널을 구현하여 애플리케이션의 시스템 콜을 가로채 처리합니다. 이를 통해 호스트 커널과의 직접적인 상호작용을 최소화하여 보안을 강화합니다. kata-containers보다는 가볍지만 runc보다는 오버헤드가 있습니다.이처럼 다양한 특성을 가진 런타임들이 OCI 런타임 사양이라는 공통 인터페이스를 따르기 때문에, 사용자나 쿠버네티스는 필요에 따라 이들을 비교적 쉽게 교체하여 사용할 수 있습니다. 마치 표준 규격의 자동차 엔진 마운트 덕분에 가솔린 엔진, 디젤 엔진, 전기 모터 등 다양한 종류의 동력원을 동일한 차체에 탑재할 수 있는 것과 같습니다.
  • 고수준 도구와의 명확한 역할 분담: containerd, CRI-O, Docker Engine 같은 고수준 도구들은 이미지 관리, 네트워크 설정, 스토리지 볼륨 연결, 여러 컨테이너 관리 등 복잡한 작업에 집중하고, 실제 컨테이너 실행이라는 저수준 작업은 OCI 호환 런타임에게 위임합니다. 이 명확한 역할 분담과 표준 인터페이스는 전체 시스템의 모듈성을 높이고 각 컴포넌트의 발전을 용이하게 합니다.

결론적으로, OCI 런타임 사양은 눈에 잘 띄지 않는 낮은 수준의 기술 명세처럼 보일 수 있지만, 실제로는 컨테이너 기술의 상호 운용성을 보장하고, 다양한 런타임 혁신을 가능하게 하며, 쿠버네티스와 같은 복잡한 오케스트레이션 시스템이 안정적으로 동작할 수 있도록 하는 매우 중요한 기반 기술입니다. 이 표준화된 약속 덕분에 우리는 오늘날 강력하고 유연한 클라우드 네이티브 환경을 구축하고 운영할 수 있는 것입니다.

2.3.1.2 OCI Image Format Specification

OCI 이미지 사양은 “무엇을 실행할지”를 운반하는 표준 패키징 규격이며, 이미지가 어떻게 저장되고 전송되며 검증되는지까지 포함하는 합의입니다. 핵심은 콘텐츠 주소 가능성(Content-Addressable Storage), 표준 JSON 매니페스트, 레이어 기반 파일 시스템, 설정 메타데이터, 그리고 멀티 플랫폼 지원입니다. 아래를 읽으면 빌드 도구, 레지스트리, 런타임 간의 연결 고리가 한눈에 그려질 것입니다.

1) 전반 구조 개요: Manifest ↔ Config ↔ Layers, 그리고 Index
  • Image Manifest: 단일 플랫폼(예: linux/amd64)용 이미지의 “목차”입니다. 어떤 설정(Config)을 쓰고 어떤 레이어(Layers)로 구성되는지를 기술합니다.
  • Image Configuration(JSON): 기본 실행 사용자, 환경 변수, Entrypoint, Cmd, ExposedPorts, Volumes, WorkingDir 등 런타임 기본값과 이미지 메타데이터를 담습니다. 또한 rootfs.diff_ids로 “압축 해제된” 레이어 해시를 기록합니다.
  • Layers: 변경 사항(changeset)을 담은 tar 아카이브들의 순서 배열입니다. OverlayFS 같은 유니온 파일시스템으로 겹쳐 최종 rootfs를 만듭니다.
  • Image Index(선택): 여러 플랫폼용 매니페스트를 하나로 묶는 상위 카탈로그입니다. 동일 태그로 arm64, amd64 등을 동시에 제공하는 멀티플랫폼 이미지를 가능하게 합니다.

간단 예시(요지는 구조 이해):

클립보드에 복사
2) 콘텐츠 주소 가능성(CAS)과 무결성
  • Digest가 진리의 원천: 매니페스트, 설정, 각 레이어는 내용 기반 해시(SHA-256 등)로 식별됩니다. “sha256:…” 다이제스트가 곧 ID입니다.
  • 신뢰 경로: 레지스트리는 클라이언트 요청 시 다이제스트를 키로 블롭(blob)을 반환합니다. 런타임은 내려받은 바이트를 다시 해시해 매니페스트/레이어의 무결성을 검증합니다.
  • 중복 제거와 캐싱: 동일 내용의 레이어는 어떤 이미지든 하나만 저장·전송하면 됩니다. 노드 로컬 캐시에도 다이제스트 키로 보관되어 풀 속도가 빨라집니다.
3) 레이어(mediaType, whiteout)와 전송 최적화
  • Media Types(대표):
    • Manifest: application/vnd.oci.image.manifest.v1+json
    • Config: application/vnd.oci.image.config.v1+json
    • Layer: application/vnd.oci.image.layer.v1.tar(+gzip|+zstd)
  • 변경 표현: 레이어 tarball은 파일 추가/변경/삭제를 담습니다. 삭제는 whiteout(.wh.<file>)과 디렉터리용 .wh..wh..opq로 표현합니다.
  • 압축: gzip이 일반적이나 zstd도 지원됩니다. zstd는 속도/압축률 모두 우수한 편이라 대형 이미지에 유리합니다.
  • Lazy pulling(고급): stargz/estargz, Nydus 등 포맷은 “필요한 파일만 먼저” 스트리밍 받아 콜드 스타트 시간을 크게 줄입니다. OCI는 이러한 최적화 포맷을 별도 mediaType으로 공존시킵니다.

4) 이미지 설정(Image Config)의 의미

  • 런타임 기본값: Entrypoint/Cmd, Env, User, WorkingDir, ExposedPorts, Volumes, StopSignal 등은 “기본 실행 의도”를 담습니다. 최종 실행값은 런타임 옵션으로 덮어쓸 수 있습니다.
  • rootfs.diff_ids: 레이어 tar를 “압축 해제한” 콘텐츠 해시 목록입니다. Manifest의 layers.digest(압축본 해시)와 1:1 매핑은 아니므로 혼동 주의.
  • history: 어떤 명령으로 레이어가 생겼는지(예: RUN, COPY)가 남습니다. 디버깅과 SBOM 생성에 유용합니다.

5) 멀티 플랫폼: Image Index로 하나의 태그에 여러 아키텍처

  • 구조: application/vnd.oci.image.index.v1+json 내 manifests 배열에 각 플랫폼(os, architecture, variant)을 명시한 디스크립터를 나열합니다.
  • 선택: 클라이언트(containerd, CRI-O)는 자신의 플랫폼과 일치하는 매니페스트를 자동 선택합니다. 사용자는 동일한 레퍼런스(예: myapp:1.2)를 쓰면 됩니다.

6) 배포(Distribution) 흐름 요약

  1. 빌드: Dockerfile/BuildKit, Buildah, Kaniko 등으로 레이어 생성 → Config/Manifest 작성 → 로컬 CAS에 저장.
  2. 푸시: 레지스트리 API v2로 Blob 업로드(레이어/설정) → Manifest 업로드 → (옵션) Index 업로드.
  3. 풀: 클라이언트가 레퍼런스(이름:태그 또는 @다이제스트)로 Manifest/Index 요청 → 일치 플랫폼 Manifest 수신 → Config/Layer 다이제스트로 블롭 요청 → 검증 후 로컬 캐시 저장 → 컨테이너 시작.

7) 보안과 신뢰: 서명, SBOM, 공급망 무결성

  • 서명(Signing):
    • Cosign(Sigstore): 키리스 키(키 관리 간소화)와 투명성 로그(Rekor)로 매니페스트 다이제스트를 서명합니다. 정책 엔진(Kyverno, Gatekeeper)과 연계해 “서명·검증 필수”를 강제 가능합니다.
    • Notary v2/OCI Artifacts: 서명을 이미지와 나란히 OCI 아티팩트로 저장·배포하는 표준화가 진행되어, 레지스트리 간 복제·보존이 쉬워집니다.
  • SBOM(Software Bill of Materials): SPDX, CycloneDX 형태의 SBOM을 빌드 시 생성해 OCI 아티팩트로 함께 저장하면, 취약점 스캐닝과 라이선스 컴플라이언스가 수월해집니다.
  • 취약점 스캔: Trivy, Clair 등과 연계해 레지스트리/CI에서 자동 스캔. “기준 이상 심각도 발견 시 배포 차단” 정책을 둡니다.
  • 공급망 보증: SLSA, in-toto 어태스테이션으로 “누가, 언제, 어떤 소스로, 어떤 빌더로 만들었나” 출처를 남깁니다. 매니페스트 다이제스트 기준으로 불변 참조가 핵심입니다.

8) 운영 베스트 프랙티스

  • 최소 베이스 이미지: distroless, alpine, wolfi 등을 사용해 공격면과 크기를 동시에 감소.
  • 레이어 정리: 한 RUN에 패키지 설치·캐시 삭제를 함께 묶고, 자주 바뀌는 파일(COPY 소스)은 아래 레이어에 두지 않습니다(캐시 보존 극대화).
  • 사용자/권한: config.User를 루트가 아닌 계정으로 설정하고, 캡빌리티 최소화, readOnlyRootFilesystem를 활용합니다.
  • 고정 참조: 배포 시 태그 대신 다이제스트(@sha256:…)를 써서 동일 아티팩트를 재현 가능하게 참조합니다.
  • 멀티플랫폼 태그: 동일 태그에 arm64/amd64를 함께 게시해 하이브리드 클러스터 운영을 단순화합니다.
  • 레지스트리 정책: 보존·삭제(GC), 서명·검증, 복제(geo-replication) 정책으로 가용성과 신뢰성을 확보합니다.

9) OCI 아티팩트 확장

OCI는 단지 컨테이너만이 아닙니다. Helm 차트, WASM 모듈, 모델 파일, SBOM, 서명 등 “이미지 아닌 것들”도 OCI Artifacts로 저장·배포할 수 있도록 확장되고 있습니다. 동일한 CAS, 동일한 보안·정책·복제 메커니즘을 재사용한다는 점이 강점입니다.

10) 핵심 정리

  • Manifest는 “목차”, Config는 “기본 실행 메타데이터”, Layers는 “파일 시스템 변화”, Index는 “멀티플랫폼 선택자”.
  • 모든 구성 요소는 다이제스트로 식별되고 검증됩니다.
  • 표준화 덕분에 빌드·배포·실행 전 과정이 도구 중립적으로 이어지고, 보안·최적화 기법이 공통 기반 위에서 발전합니다.

이로써 OCI 이미지 사양의 구조와 동작, 보안·운영 관점의 실무 포인트를 모두 연결해 보았습니다. 이제 어떤 빌드/레지스트리/런타임 조합이어도 “다이제스트를 기준으로 동일한 아티팩트”를 재현·검증·배포할 수 있다는 감각이 잡히실 것입니다.

앞서 OCI 런타임 사양이 컨테이너를 ‘어떻게 실행할 것인가’에 대한 약속이었다면, 이번에 살펴볼 OCI 이미지 사양(Image Format Specification)은 컨테이너의 내용물, 즉 ‘컨테이너 이미지를 어떻게 만들고 포장(패키징)할 것인가’에 대한 표준 약속입니다. 우리가 애플리케이션과 그 실행에 필요한 모든 라이브러리, 의존성, 설정 파일 등을 하나로 묶어 배포하는 단위가 바로 컨테이너 이미지인데요, 이것이 단순히 파일들을 압축한 덩어리가 아니라, 모든 관련 도구들이 이해하고 사용할 수 있는 일관된 구조와 형식을 가져야만 합니다.

만약 이미지 형식이 표준화되어 있지 않다면 어떤 일이 벌어질까요? A라는 도구로 만든 이미지를 B라는 저장소(레지스트리)에 올릴 수 없거나, C라는 실행 환경(런타임)에서 내려받아 실행할 수 없는, 그야말로 기술적인 장벽과 파편화가 만연했을 것입니다. 마치 각기 다른 포맷의 비디오 파일을 재생하기 위해 여러 종류의 플레이어를 설치해야 했던 불편함과 비슷하죠.

OCI 이미지 사양은 이러한 혼란을 막고, 컨테이너 기술 생태계의 모든 참여자들 – 이미지를 만드는 빌드 도구(예: Docker Build, Buildah, Kaniko), 이미지를 저장하고 공유하는 레지스트리(예: Docker Hub, Harbor, Quay.io, Amazon ECR, Google Container Registry), 그리고 이미지를 가져와 컨테이너로 실행하는 런타임(예: containerd, CRI-O) – 이 마치 하나의 언어를 사용하듯 원활하게 소통하고 협력할 수 있도록 하는 기반을 제공합니다. 이는 마치 전 세계적으로 통용되는 표준 화물 컨테이너 규격(ISO 668) 덕분에 선박, 기차, 트럭 등 다양한 운송 수단 간의 환적이 용이해지고 물류 시스템 전체의 효율성이 극대화된 것과 같은 원리입니다.

그렇다면 OCI 이미지 사양이 정의하는 컨테이너 이미지의 핵심 구성 요소들은 구체적으로 무엇일까요? 크게 네 가지 중요한 요소로 나누어 살펴볼 수 있습니다.

1. 이미지 매니페스트 (Image Manifest): 이미지의 설계도이자 목차
  • 이미지 매니페스트(Image Manifest)는 특정 플랫폼(OS/아키텍처 조합)을 위한 컨테이너 이미지의 전체 구조를 설명하는 핵심적인 JSON 파일입니다. 마치 책의 목차나 제품의 상세 명세서처럼, 이 매니페스트 파일은 해당 이미지가 어떤 설정 정보(Image Configuration)를 가지고 있으며, 어떤 파일 시스템 레이어(Layers)들로 구성되어 있는지에 대한 정보를 담고 있습니다.

매니페스트 안에는 크게 두 가지 중요한 정보가 포함됩니다.

  • 설정 객체(Config Descriptor): 이미지의 메타데이터와 기본 실행 설정을 담고 있는 ‘이미지 설정(Image Configuration)’ 파일(이 역시 JSON 형식)을 가리키는 정보입니다. 이 정보에는 해당 설정 파일의 미디어 타입(media type, 예: application/vnd.oci.image.config.v1+json), 파일 크기(size), 그리고 가장 중요한 고유 식별자(Digest)가 포함됩니다. 다이제스트는 해당 파일 내용물의 암호화 해시값(보통 SHA256)으로, 파일의 무결성을 보장하고 고유하게 주소를 지정하는 데 사용됩니다.
  • 레이어 목록(Layers Array): 이미지를 구성하는 파일 시스템 레이어들을 순서대로 나열한 목록입니다. 각 레이어 항목 역시 해당 레이어 파일(보통 압축된 tar 아카이브)의 미디어 타입(media type, 예: application/vnd.oci.image.layer.v1.tar+gzip), 압축된 파일 크기(size), 그리고 고유 식별자(Digest) 정보를 포함합니다. 레이어는 반드시 순서대로 적용되어야 최종적인 컨테이너 루트 파일 시스템을 구성할 수 있습니다.

이 매니페스트 자체도 고유한 미디어 타입(예: application/vnd.oci.image.manifest.v1+json)을 가지며, 레지스트리와 런타임은 이 매니페스트를 통해 이미지의 전체 구조를 파악하고 필요한 구성 요소(설정 파일, 레이어 파일)들을 다이제스트를 이용해 정확하게 요청하고 내려받을 수 있습니다. 즉, 매니페스트는 이미지의 모든 구성 요소를 연결하는 중심 허브 역할을 하는 셈입니다.

2. 이미지 인덱스 (Image Index) (선택 사항): 여러 이미지를 묶는 카탈로그

때로는 동일한 이미지 이름과 태그(예: nginx:latest)를 사용하지만, 서로 다른 운영체제나 CPU 아키텍처(예: linux/amd64, linux/arm64, windows/amd64)를 지원해야 할 필요가 있습니다. 이런 경우를 위해 OCI 이미지 사양은 이미지 인덱스(Image Index)라는 개념을 제공합니다. (이는 “Fat Manifest”라고 불리기도 합니다.)

이미지 인덱스는 그 자체로 또 다른 JSON 파일이며, 여러 개의 이미지 매니페스트를 하나로 묶어 참조할 수 있게 해주는 상위 레벨의 매니페스트 역할을 합니다. 인덱스 파일 내부에는 manifests라는 배열이 있고, 이 배열의 각 항목은 특정 플랫폼용 이미지 매니페스트를 가리키는 설명자(Descriptor)입니다. 이 설명자에는 해당 이미지 매니페스트의 미디어 타입, 크기, 다이제스트 정보와 더불어, 어떤 플랫폼(os, architecture, 선택적으로 variant)을 위한 매니페스트인지를 명시하는 platform 객체가 포함되어 있습니다.

컨테이너 런타임이 레지스트리에 특정 이미지 태그(예: nginx:latest)를 요청했을 때, 레지스트리가 이미지 인덱스를 반환하면, 런타임은 자신의 실행 환경(OS, 아키텍처)과 일치하는 platform 정보를 가진 매니페스트 설명자를 인덱스 내에서 찾습니다. 그리고 해당 설명자에 포함된 다이제스트를 사용하여 실제 필요한 이미지 매니페스트를 다시 요청하는 방식으로 작동합니다.

이 이미지 인덱스 덕분에 사용자들은 자신의 환경을 신경 쓸 필요 없이 동일한 이미지 이름과 태그를 사용하여 이미지를 가져올 수 있고, 시스템은 자동으로 현재 환경에 맞는 최적의 이미지를 선택하여 제공할 수 있게 됩니다. 이는 멀티 아키텍처 지원을 매우 편리하게 만들어주는 중요한 기능입니다.

3. 이미지 레이어 (Image Layers): 효율적인 파일 시스템 구성 방식

  • 이미지 레이어(Image Layers)는 컨테이너의 루트 파일 시스템(rootfs)을 구성하는 변경 사항(changeset)들을 순차적으로 쌓아 올린 것입니다. 각 레이어는 이전 레이어 상태에서 파일이나 디렉토리가 추가, 수정, 또는 삭제된 내용을 담고 있습니다.
  • 형식: 일반적으로 각 레이어는 파일 시스템의 변경 내용을 담은 압축된 tar 아카이브(tarball) 형태로 표현됩니다. (예: application/vnd.oci.image.layer.v1.tar+gzip 또는 application/vnd.oci.image.layer.v1.tar+zstd 등). OCI 사양은 특정 압축 알고리즘을 강제하지는 않지만 gzip이 가장 흔하게 사용됩니다. 파일 삭제를 표현하기 위해서는 ‘whiteout’ 파일이라는 특수한 표식(.wh.<filename> 또는 .wh..wh..opq)을 사용합니다.
  • 효율성: 이 레이어 방식은 컨테이너 이미지의 저장 및 전송 효율성을 극대화하는 핵심적인 이유입니다.
    • 재사용 및 공유: 여러 이미지가 동일한 기반 이미지(Base Image)를 공유하는 경우가 많습니다. 예를 들어, Ubuntu 기반 위에 각기 다른 애플리케이션을 설치한 이미지들은 Ubuntu 기본 레이어들을 모두 공유할 수 있습니다. 레이어는 내용 기반의 다이제스트로 식별되기 때문에, 동일한 내용의 레이어는 시스템 내에 단 한 번만 저장되고, 여러 이미지에서 참조하여 사용할 수 있습니다. (Content-Addressable Storage)
    • 네트워크 효율성: 이미지를 내려받거나(pull) 올릴(push) 때, 이미 로컬 시스템이나 대상 레지스트리에 존재하는 레이어는 다시 전송할 필요 없이 건너뛸 수 있습니다. 변경된 레이어만 전송하면 되므로 네트워크 대역폭 사용량과 전송 시간을 크게 절약할 수 있습니다.
  • 루트 파일 시스템 구성: 컨테이너 런타임은 이미지 매니페스트에 명시된 순서대로 이 레이어들을 내려받아, OverlayFS와 같은 유니온 파일 시스템(Union Filesystem) 기술을 사용하여 마치 하나의 통합된 파일 시스템처럼 보이도록 겹쳐 쌓습니다(stacking). 아래쪽 레이어들은 일반적으로 읽기 전용(read-only)으로 마운트되고, 컨테이너가 실행될 때 가장 위에는 변경 사항을 기록하기 위한 쓰기 가능한(writable) 레이어가 추가됩니다.

OCI 이미지 사양은 이러한 레이어들이 어떻게 패키징되고, 어떤 미디어 타입을 가져야 하며, 어떻게 식별(다이제스트)되어야 하는지를 명확히 규정함으로써, 이러한 효율적인 메커니즘이 모든 호환 도구에서 일관되게 작동하도록 보장합니다.

4. 이미지 설정 (Image Configuration): 컨테이너 실행을 위한 기본값과 메타데이터

이미지 설정(Image Configuration)은 해당 이미지로부터 컨테이너를 실행할 때 필요한 기본 설정값들과 이미지 자체에 대한 메타데이터를 담고 있는 JSON 파일입니다. 이 파일은 이미지 매니페스트에 의해 참조되며, 이미지 레이어들과 함께 이미지의 중요한 구성 요소입니다.

OCI Image Specification: 이미지 설정(Image Configuration) JSON 상세

필드명 (JSON Key) 설명 예시 값 / 형식 주요 역할 및 목적
created 이미지 생성 시각을 나타내는 타임스탬프 문자열 (ISO 8601 형식 권장). “2023-10-27T10:30:00Z” 메타데이터: 이미지의 생성 시점을 기록하여 버전 관리나 추적에 활용됩니다.
author 이미지 작성자 또는 관리자 정보를 담는 문자열. “Your Name <your.email@example.com>” 메타데이터: 이미지의 출처나 책임자를 명시합니다.
architecture 이미지가 빌드된 대상 CPU 아키텍처를 나타내는 문자열. “amd64”, “arm64”, “ppc64le” 메타데이터 / 호환성: 런타임이 이미지를 실행할 수 있는 하드웨어 아키텍처인지 확인하는 데 사용됩니다. 이미지 인덱스와 함께 멀티 아키텍처 지원의 핵심 요소입니다.
os 이미지가 빌드된 대상 운영 체제를 나타내는 문자열. “linux”, “windows”, “darwin” 메타데이터 / 호환성: 런타임이 이미지를 실행할 수 있는 운영 체제인지 확인하는 데 사용됩니다.
config 컨테이너 실행 시 적용될 기본 설정값들을 담고 있는 객체(Object). 아래 필드들은 이 config 객체 내부에 위치합니다. { … } 기본 런타임 동작 정의: 아래 필드들을 통해 컨테이너의 기본 실행 방식을 지정합니다. (사용자가 런타임 시 옵션으로 덮어쓸 수 있음)
config.User 컨테이너 내에서 프로세스를 실행할 기본 사용자 및 그룹을 지정하는 문자열. (user, user:group, uid, uid:gid 형식) “1001”, “nginx”, “1000:1000” 기본 보안 설정: 컨테이너가 기본적으로 어떤 사용자 권한으로 실행될지 정의합니다. 최소 권한 원칙 적용에 중요합니다.
config.ExposedPorts 컨테이너 애플리케이션이 사용할 것으로 예상되는 네트워크 포트와 프로토콜(tcp/udp)을 명시하는 객체. (Key: “포트/프로토콜”, Value: 빈 객체 {}) {“80/tcp”: {}, “443/tcp”: {}} 메타데이터 / 정보 제공: 이미지가 어떤 포트를 사용할 가능성이 있는지 알려줍니다. 주의: 이 설정만으로는 실제 호스트 포트와 연결(publish)되지 않습니다. 실제 포트 매핑은 런타임 시 별도로 지정해야 합니다.
config.Env 컨테이너 내부에 기본적으로 설정될 환경 변수 목록을 정의하는 문자열 배열 (“KEY=VALUE” 형식). [“PATH=/usr/local/bin:/usr/bin”, “APP_MODE=production”] 기본 환경 설정: 애플리케이션 실행에 필요한 기본 환경 변수를 제공합니다.
config.Entrypoint 컨테이너 시작 시 실행될 주 명령어(실행 파일 경로)를 지정하는 문자열 배열. 비어 있거나 null일 수 있습니다. [“/usr/sbin/nginx”], [“java”, “-jar”, “/app/app.jar”] 기본 실행 진입점: 컨테이너의 주 실행 파일을 정의합니다. Cmd는 이 Entrypoint의 인자로 사용됩니다.
config.Cmd Entrypoint에 전달될 기본 인자 또는 Entrypoint가 없을 경우 실행될 전체 명령어를 지정하는 문자열 배열. 비어 있거나 null일 수 있습니다. [“-g”, “daemon off;”] (Entrypoint가 있을 경우), <br>[“/bin/sh”, “-c”, “echo hello”] (Entrypoint가 없을 경우) 기본 명령어 인자/명령어: Entrypoint의 기본 인자를 제공하거나, Entrypoint 부재 시 기본 실행 명령어를 정의합니다.
config.Volumes 컨테이너 실행 시 외부 스토리지를 마운트할 것으로 예상되는 컨테이너 내부 경로를 명시하는 객체. (Key: 경로, Value: 빈 객체 {}) {“/data”: {}, “/var/log/app”: {}} 메타데이터 / 정보 제공: 이미지가 상태 저장을 위해 어떤 디렉토리를 외부 볼륨으로 사용할 수 있는지 알려줍니다. 주의: 실제 볼륨 마운트는 런타임 시 별도로 설정해야 합니다.
config.WorkingDir 컨테이너 내에서 Entrypoint나 Cmd가 실행될 기본 작업 디렉토리 경로를 지정하는 문자열. “/app”, “/var/www/html” 기본 실행 위치: 명령어 실행의 기준 디렉토리를 설정합니다.
config.Labels 이미지에 대한 추가적인 메타데이터를 자유롭게 추가할 수 있는 키-값 쌍(문자열) 객체. {“maintainer”: “team@example.com”, “version”: “1.2.0”, “com.example.project”: “my-app”} 확장 가능한 메타데이터: 버전 정보, 라이선스, 관리자, 빌드 정보 등 다양한 부가 정보를 저장하고 조회하는 데 사용됩니다.
config.StopSignal 컨테이너를 정상적으로 종료시키기 위해 보낼 기본 유닉스 신호(Signal)를 지정하는 문자열. “SIGTERM”, “SIGQUIT” 기본 종료 방식: 컨테이너 종료 요청 시 사용할 기본 신호를 정의하여 애플리케이션이 정상적으로 종료(graceful shutdown)할 기회를 갖도록 합니다.
rootfs 이미지의 루트 파일 시스템을 구성하는 레이어 정보를 담고 있는 객체. {“type”: “layers”, “diff_ids”: [“sha256:…”, “sha256:…”]} 파일 시스템 구조: 이미지의 파일 시스템이 어떤 레이어들로 구성되었는지 식별합니다. diff_ids는 각 레이어의 압축 해제된 내용에 대한 해시값(Digest) 목록이며, 순서대로 적용되어야 합니다.
history 이미지의 각 레이어가 어떻게 생성되었는지에 대한 빌드 이력을 담고 있는 객체 배열. 각 객체는 생성 시각, 생성 명령어 등의 정보를 포함할 수 있습니다. [{“created”: “…”, “created_by”: “/bin/sh -c #(nop) ADD file:abc in /”}, {“created”: “…”, “created_by”: “/bin/sh -c #(nop) CMD […]”}] 빌드 추적성 / 정보 제공: 이미지가 어떤 과정을 거쳐 빌드되었는지 추적하거나 디버깅하는 데 유용한 정보를 제공합니다.

핵심 요약:

  • 기본값 제공: 이미지 설정 파일은 컨테이너 실행 시 사용될 환경 변수, 명령어, 사용자 등의 기본값을 정의합니다. 이는 사용자가 별도 옵션을 주지 않았을 때 적용됩니다.
  • 메타데이터 저장소: 이미지 자체에 대한 중요한 정보(생성자, 플랫폼, 빌드 이력, 레이블 등)를 담고 있어 이미지 관리와 활용에 필수적입니다.
  • 런타임 설정과의 관계: 여기에 정의된 값들은 컨테이너 실행 시 사용자가 제공하는 옵션(예: docker run -e VAR=new_value –user 1002 myimage new_command)에 의해 덮어쓰여지거나(Override) 보강될 수 있습니다. 최종 실행 설정은 OCI 런타임 사양의 config.json으로 조합됩니다.
  • 이미지 불변성: 한번 빌드된 이미지의 설정 파일 내용은 변경되지 않습니다. 변경이 필요하면 새로운 이미지를 빌드해야 합니다.

이 표를 통해 이미지 설정 파일이 단순한 설정 모음이 아니라, 이미지의 정체성, 기본 행동 방식, 빌드 과정 등을 담고 있는 중요한 ‘명세서’ 역할을 한다는 것을 이해하실 수 있기를 바랍니다.

이 표준 덕분에, 어떤 도구를 사용하여 이미지를 빌드하든(예: Dockerfile + docker build, buildah bud), 그 결과물은 OCI 호환 레지스트리(예: Harbor, Docker Hub)에 안정적으로 저장 및 배포될 수 있으며, OCI 호환 런타임(예: containerd + runc, CRI-O + crun)은 이 이미지를 문제없이 해석하고 내려받아 컨테이너로 실행할 수 있습니다. 이는 마치 전 세계 어디서나 통용되는 표준 화물 컨테이너 덕분에 복잡한 국제 물류 시스템이 원활하게 작동하는 것과 같은 이치입니다. OCI 이미지 사양은 컨테이너 기술 생태계 전반에 걸쳐 ‘빌드(Build) – 배포(Ship) – 실행(Run)’ 워크플로우를 매끄럽게 만들고 자동화하는 데 필수적인 기반을 제공하는 것입니다.

2.3.1.3 표준화의 중요성: 왜 OCI 표준이 필수적인가?

지금까지 우리는 OCI가 정의하는 컨테이너 이미지 형식과 실행 방법에 대한 두 가지 핵심 기술 사양(Specification)을 자세히 살펴보았습니다. 기술적인 내용들이 조금 복잡하게 느껴지셨을 수도 있지만, 이제 잠시 한 걸음 물러나서 “왜 이렇게까지 표준을 만드는 것이 중요했을까?”라는 근본적인 질문에 답해볼 시간입니다. 단순히 기술 명세를 정하는 것을 넘어, OCI 표준화는 오늘날 우리가 누리는 강력하고 유연한 클라우드 네이티브 환경을 가능하게 만든 결정적인 전환점이었습니다. 그 중요성을 몇 가지 핵심적인 측면에서 자세히 살펴보겠습니다.

1. 상호 운용성 (Interoperability): 기술의 장벽을 허물다

OCI 표준이 가져온 가장 중요하고 직접적인 이점은 바로 상호 운용성(Interoperability)의 확보입니다. OCI 이전, 컨테이너 기술 초기에는 특정 회사의 도구가 사실상의 표준처럼 여겨지면서도, 다른 여러 기술들이 각자의 방식으로 컨테이너를 다루려는 시도들이 있었습니다. 만약 이런 상황이 지속되었다면, 특정 도구 A로 만든 컨테이너 이미지는 오직 도구 A의 런타임에서만 실행 가능하고, 도구 B의 레지스트리와는 호환되지 않는 ‘기술의 사일로(Silo)’ 현상이 심화되었을 것입니다. 이는 마치 제조사마다 다른 충전 규격을 사용하던 초기의 휴대폰 시장처럼, 사용자에게 큰 불편과 비효율을 초래했을 겁니다.

하지만 2015년, Docker, CoreOS(이후 Red Hat에 인수), Google, Red Hat, Microsoft 등 컨테이너 기술 분야의 주요 기업들이 리눅스 재단(Linux Foundation) 산하에 모여 OCI를 출범시키면서 상황은 극적으로 변했습니다. 이들은 경쟁 관계를 넘어 컨테이너 이미지 형식과 런타임 실행 방식이라는 핵심 기술에 대한 공통의 약속, 즉 OCI 표준을 만들기로 합의했습니다.

이 OCI 표준 덕분에, 이제 서로 다른 회사나 오픈소스 커뮤니티에서 개발된 다양한 컨테이너 관련 도구들 – 이미지를 만드는 빌드 도구(예: Docker Build, Buildah, Kaniko), 이미지를 저장하고 공유하는 레지스트리(예: Docker Hub, Harbor, Quay.io, Google Container Registry, Amazon ECR), 이미지를 실행하는 컨테이너 런타임(예: containerd, CRI-O, 그리고 이들이 사용하는 runc, crun 등), 그리고 이 모든 것을 지휘하는 오케스트레이션 도구(예: 쿠버네티스) – 가 마치 원래부터 하나였던 것처럼 매끄럽게 함께 작동할 수 있게 되었습니다.

이는 사용자에게 엄청난 선택의 자유를 선사합니다. 개발자와 운영자는 더 이상 특정 회사의 기술 스택에 갇힐 필요 없이, 자신들의 요구사항과 환경에 가장 적합한 도구들을 마치 레고 블록처럼 자유롭게 선택하고 조합하여 시스템을 구성할 수 있습니다. 예를 들어, 이미지 빌드는 Buildah를 사용하고, 레지스트리는 Harbor를 구축하며, 쿠버네티스 클러스터의 런타임으로는 containerd와 runc 조합을 선택하는 것이 가능해진 것입니다. 이러한 유연성은 벤더 종속성(Vendor Lock-in)이라는 위험을 효과적으로 피하게 해주는 핵심적인 요소입니다. 특정 기술에 묶여 원치 않는 업그레이드를 강요받거나, 다른 더 나은 대안으로 전환하기 어려워지는 상황을 막을 수 있게 된 것이죠.

2. 혁신 촉진 (Fostering Innovation): 기반 위에 꽃피는 다양성

두 번째 중요한 이점은 OCI 표준이 기술 혁신을 가속화하는 강력한 촉매 역할을 한다는 점입니다. 기본적인 규칙(이미지 형식, 런타임 실행 방식)이 명확하게 정립되면서, 개발자들과 기업들은 더 이상 ‘바퀴를 재발명’하는 데, 즉 이미 해결된 기본적인 문제를 다시 구현하는 데 귀중한 시간과 노력을 낭비할 필요가 없어졌습니다. 대신, 이 안정적인 OCI 표준이라는 기반 위에서 더욱 새롭고 차별화된 가치를 창출하는 데 역량을 집중할 수 있게 되었습니다.

이는 다양한 영역에서의 혁신으로 이어졌습니다.

런타임 다양화: OCI 런타임 사양을 준수하면서도 각기 다른 장점을 가진 저수준 런타임들이 등장했습니다. runc가 표준 참조 구현체라면, C언어로 작성되어 더 가볍고 빠른 crun이 개발되었고, 보안 강화를 위해 가상 머신 기술을 활용하는 kata-containers나 사용자 공간 커널을 구현한 gVisor 같은 혁신적인 런타임들이 OCI 호환성을 유지하며 생태계에 합류했습니다. 사용자들은 워크로드의 특성(성능 중시, 보안 중시 등)에 따라 최적의 런타임을 선택할 수 있게 되었습니다.
빌드 도구 발전: OCI 이미지 사양 덕분에 Docker 데몬에 의존하지 않는 새로운 이미지 빌드 도구들(예: Buildah, Kaniko, img)이 등장하여, 더 빠르고 안전하며 유연한 빌드 파이프라인 구축을 가능하게 했습니다.
이미지 관리 혁신: 이미지 레이어 형식, 매니페스트 등의 표준화는 레지스트리 기술의 발전을 촉진했고, 이미지 서명(예: Notary, Sigstore)이나 취약점 스캔 같은 보안 기능과의 통합을 용이하게 만들었습니다.
특수 목적 컨테이너 기술: OCI 표준을 기반으로 WebAssembly 컨테이너(wasm), GPU 가속 컨테이너 등 특정 워크로드에 최적화된 컨테이너 기술들도 개발되고 있습니다.
이처럼 OCI 표준은 기술의 ‘최소 공통 분모’를 정의함으로써, 그 위에 다양한 아이디어와 기술들이 자유롭게 꽃피울 수 있는 비옥한 토양을 제공했습니다.

3. 건강한 생태계 조성 (Healthy Ecosystem): 함께 성장하는 커뮤니티

세 번째로, OCI 표준은 개방적이고 건강한 컨테이너 기술 생태계를 만드는 데 결정적인 기여를 했습니다. 명확하고 공개된 표준은 기술의 진입 장벽을 낮추는 효과를 가져옵니다. 새로운 기업이나 개발자들이 컨테이너 관련 기술을 개발하거나 기존 기술에 기여하고자 할 때, 따라야 할 명확한 가이드라인이 있으므로 훨씬 수월하게 생태계에 참여할 수 있습니다.

이는 자연스럽게 더 많은 플레이어들의 참여를 유도했고, 결과적으로 경쟁과 협력이 공존하는 활발한 생태계가 형성되었습니다. 다양한 기업과 커뮤니티가 OCI 표준을 중심으로 각자의 솔루션을 개발하고 개선하면서 경쟁하는 동시에, OCI 표준 자체의 발전이나 관련 오픈소스 프로젝트에는 함께 기여하며 협력합니다. 이러한 역동적인 환경은 결국 최종 사용자인 우리에게 더 많은 선택권, 더 높은 품질의 도구, 그리고 더 합리적인 비용이라는 혜택으로 돌아옵니다. 마치 다양한 자동차 제조사들이 공통의 도로 규범과 안전 기준을 따르면서도 각자의 기술력과 디자인으로 경쟁하여 소비자에게 더 좋은 자동차를 제공하는 것과 유사합니다. OCI는 컨테이너 기술 분야에서 이러한 선순환 구조를 만드는 데 핵심적인 역할을 했습니다.

4. 쿠버네티스와의 시너지: 오케스트레이션의 기반을 다지다

마지막으로, OCI 표준은 오늘날 컨테이너 오케스트레이션의 사실상 표준(de facto standard)으로 자리 잡은 쿠버네티스와의 강력한 시너지를 만들어냅니다. 쿠버네티스는 설계 초기부터 특정 컨테이너 런타임 기술에 종속되지 않도록 하는 것을 중요한 목표로 삼았습니다. 이를 위해 쿠버네티스는 CRI(Container Runtime Interface)라는 자체적인 추상화된 인터페이스를 정의했습니다. 쿠버네티스의 핵심 컴포넌트인 Kubelet은 이 CRI를 통해 컨테이너 런타임에게 컨테이너 및 이미지 관리를 요청합니다.

여기서 중요한 점은, containerd나 CRI-O와 같은 널리 사용되는 고수준 컨테이너 런타임들은 바로 이 CRI를 구현하는 동시에, 내부적으로는 OCI 표준을 철저히 준수한다는 사실입니다. 즉, 이들은 쿠버네티스(Kubelet)로부터 CRI를 통해 명령을 받으면, 이를 해석하여 OCI 이미지 사양에 맞는 이미지를 가져오고, OCI 런타임 사양에 따라 저수준 런타임(runc, crun 등)을 호출하여 컨테이너를 생성하고 관리합니다.

결국, OCI 표준은 쿠버네티스가 다양한 컨테이너 런타임 구현체들을 일관된 방식으로 지원하고, 사용자에게 런타임 선택의 유연성을 제공할 수 있도록 하는 근본적인 토대(Foundation) 역할을 수행하는 것입니다. 쿠버네티스가 직접 OCI 표준을 사용하는 것은 아니지만, 쿠버네티스 생태계의 핵심 구성 요소들이 OCI 표준 위에 구축되어 있기 때문에, OCI 없이는 오늘날과 같은 쿠버네티스의 유연성과 확장성을 상상하기 어렵습니다.