2.2.3 컨테이너 이미지와 레이어
우리가 클라우드 네이티브 환경에서 애플리케이션을 배포하고 운영할 때, 컨테이너는 핵심적인 역할을 담당합니다. 마치 물건을 담는 표준 규격의 상자처럼, 컨테이너는 애플리케이션과 그 실행에 필요한 모든 환경을 하나로 묶어 어디서든 동일하게 실행될 수 있도록 보장하죠. 이러한 컨테이너를 만드는 데 사용되는 설계도 또는 템플릿이 바로 컨테이너 이미지입니다. 이번 장에서는 이 컨테이너 이미지의 개념과 그 내부 구조, 그리고 이미지가 어떻게 효율적으로 관리되는지에 대해 자세히 알아보겠습니다. 이 개념들은 쿠버네티스가 컨테이너를 관리하는 방식을 이해하는 데 필수적인 기초가 됩니다.
2.2.3.1 이미지의 개념 및 구조 (Layered Filesystem)
클라우드 네이티브 환경의 핵심 구성 요소 중 하나인 컨테이너를 이해하기 위해서는 먼저 그 기반이 되는 컨테이너 이미지(Container Image)에 대한 깊이 있는 이해가 필수적입니다. 컨테이너 이미지를 가장 직관적으로 설명하자면, 특정 애플리케이션과 그 실행에 필요한 모든 종속성을 하나의 패키지로 묶어놓은 실행 가능한 청사진 또는 템플릿이라고 할 수 있습니다.
여기에는 단순히 개발자가 작성한 애플리케이션 코드뿐만 아니라, 해당 코드를 해석하고 실행할 런타임 환경(예: Node.js, Python 인터프리터, Java Virtual Machine), 운영체제 수준의 라이브러리 및 도구(예: C 라이브러리, 쉘 유틸리티), 필요한 설정 파일, 환경 변수 등 애플리케이션 구동에 필요한 모든 요소가 총망라되어 있습니다. 마치 특정 시점의 잘 구성된 시스템 상태를 그대로 복사해 둔 ‘스냅샷’과 같은 개념입니다.
이러한 개념은 가상 머신(Virtual Machine, VM)의 이미지와 유사하게 들릴 수 있습니다. VM 이미지 역시 운영체제, 애플리케이션, 설정 등을 포함하는 거대한 파일입니다. 하지만 컨테이너 이미지는 VM 이미지와 근본적인 차이점을 가지며, 이 차이점 때문에 훨씬 더 가볍고 효율적입니다. VM 이미지는 완전한 운영체제 커널과 시스템 전체를 포함하는 반면, 컨테이너 이미지는 호스트 운영체제의 커널을 공유하며 애플리케이션 실행에 필요한 파일 시스템과 종속성만을 패키징합니다. 이 구조적 차이가 컨테이너 이미지를 더 작고, 빠르며, 자원 효율적으로 만듭니다.
이러한 경량성과 효율성의 핵심 비결은 컨테이너 이미지가 채택하고 있는 독특한 구조, 바로 계층화된 파일 시스템(Layered Filesystem)에 있습니다. 컨테이너 이미지는 거대한 단일 파일 덩어리가 아니라, 여러 개의 얇고 독립적인 레이어(Layer)들이 마치 투명 필름처럼 겹겹이 쌓여 완성된 파일 시스템 뷰를 제공하는 형태입니다. 각 레이어는 파일 시스템의 특정 변경 사항(파일 추가, 수정, 삭제 등)을 담고 있습니다.
이 구조를 좀 더 자세히 살펴보겠습니다.

- 기본 이미지 (Base Image) 레이어: 가장 아래층에는 모든 것의 기초가 되는 레이어가 위치합니다. 이는 보통 리눅스 배포판(예: Ubuntu, CentOS, Alpine Linux 등)의 최소한의 루트 파일 시스템을 포함하는 기본 이미지입니다. Alpine Linux처럼 극도로 경량화된 배포판을 사용하거나, 심지어 운영체제 구성 요소 없이 특정 언어 런타임만 포함하는 ‘Distroless’ 이미지를 사용하여 이미지 크기를 더욱 최적화하기도 합니다. 이 기본 이미지는 후속 레이어들이 기반으로 삼는 운영 환경의 토대를 제공합니다.
- 중간 레이어 (Intermediate Layers): 기본 이미지 위에는 애플리케이션 실행 환경을 구축하기 위한 다양한 레이어들이 순차적으로 쌓입니다. 예를 들어,
- 패키지 매니저(apt, yum, apk 등)를 사용하여 필요한 시스템 라이브러리나 도구(예: curl, git)를 설치하는 레이어.
- 특정 프로그래밍 언어의 런타임(예: Node.js, Python) 및 관련 패키지 관리자(npm, pip)를 설치하는 레이어.
- 애플리케이션의 종속성(예: npm 모듈, Python 패키지)을 설치하는 레이어.
- 애플리케이션 소스 코드를 이미지 내부의 특정 경로로 복사하는 레이어.
- 환경 변수를 설정하거나 기본 실행 명령을 지정하는 레이어.
- 레이어 생성 과정: 이러한 레이어들은 일반적으로 컨테이너 이미지 빌드 명세서(예: Dockerfile)의 각 명령어(Instruction)에 대응하여 생성됩니다. 예를 들어, RUN apt-get update && apt-get install -y nginx 명령어는 Nginx 패키지와 관련 파일들을 포함하는 새로운 레이어를 생성하여 이전 레이어 위에 추가합니다.
이 레이어 구조의 가장 핵심적인 특징은 각 레이어가 한 번 생성되면 절대 변경되지 않는 읽기 전용(Read-Only) 속성을 가진다는 점입니다. 이는 이미지의 불변성(Immutability)을 보장하는 중요한 요소입니다. 만약 이미지 빌드 과정에서 기존 파일이 수정되거나 삭제되어야 한다면, 해당 파일이 포함된 이전 레이어를 직접 수정하는 것이 아닙니다. 대신, 변경 사항(예: 수정된 파일의 새 버전, 또는 파일 삭제를 나타내는 표식)만을 담은 새로운 레이어가 그 위에 추가됩니다. 컨테이너가 실행될 때, 파일 시스템은 이 레이어들을 위에서부터 아래로 순차적으로 확인하여 최종적인 파일 뷰를 구성합니다. 만약 상위 레이어에 파일이 존재하면 하위 레이어의 동일한 경로는 가려지게 됩니다. 삭제의 경우, 상위 레이어에 해당 파일이 삭제되었다는 ‘화이트아웃(whiteout)’ 마커를 남겨 하위 레이어의 파일이 보이지 않도록 합니다. 이러한 방식을 Copy-on-Write (CoW) 전략의 기반으로 활용합니다.
이러한 계층화된 파일 시스템 구조는 다음과 같은 실질적인 이점들을 제공하며, 클라우드 네이티브 환경에서의 중요성을 부각시킵니다.
- 획기적인 저장 공간 효율성: 여러 컨테이너 이미지가 동일한 기본 이미지(예: Ubuntu 22.04)나 중간 레이어(예: 특정 버전의 Node.js 런타임 설치 레이어)를 공유하는 경우가 많습니다. 계층화된 파일 시스템에서는 이러한 공통 레이어를 시스템(호스트 또는 레지스트리)에 단 한 번만 저장하고, 여러 이미지가 이를 참조하여 공유합니다. 예를 들어, 동일한 Ubuntu 베이스 이미지를 사용하는 10개의 서로 다른 애플리케이션 이미지가 있다면, Ubuntu 레이어는 디스크에 단 하나만 존재하게 됩니다. 이는 중복 데이터 저장을 최소화하여 디스크 공간 사용량을 크게 절약합니다.
- 빠른 이미지 빌드 및 배포 속도:
- 빌드 캐싱: 이미지를 빌드할 때, 빌드 도구는 각 단계(명령어)마다 이전 빌드에서 생성된 레이어가 있는지 확인합니다. 만약 해당 단계의 명령어와 그 기반이 되는 레이어가 변경되지 않았다면, 이전 빌드에서 생성된 레이어를 재사용(캐시 히트)합니다. 변경된 부분부터의 레이어만 새로 빌드하면 되므로, 특히 코드 변경이 잦은 개발 과정에서 빌드 시간을 극적으로 단축할 수 있습니다.
- 네트워크 효율성: 이미지를 다른 시스템으로 전송(컨테이너 레지스트리에서 pull)하거나 다른 노드로 배포할 때, 대상 시스템에 이미 존재하는 레이어는 다시 다운로드할 필요가 없습니다. 전체 이미지를 통째로 받는 대신, 누락된 레이어만 네트워크를 통해 전송받으면 됩니다. 이는 이미지 배포 속도를 높이고 네트워크 대역폭 소모를 줄여줍니다. 이는 특히 대규모 클러스터 환경에서 수많은 노드에 이미지를 배포해야 하는 쿠버네티스와 같은 시스템에서 매우 중요합니다.
- 용이한 버전 관리 및 변경 추적: 각 레이어는 이전 상태로부터의 명확한 변경 사항을 나타냅니다. 이미지의 전체 레이어 스택은 해당 이미지가 어떻게 구성되었는지에 대한 상세한 이력을 제공합니다. 이는 이미지 버전 간의 차이를 쉽게 파악하고, 문제가 발생했을 때 특정 변경 사항이 도입된 레이어를 식별하는 데 도움을 줍니다. 또한, 특정 버전의 이미지는 그 레이어들의 고유한 해시(Digest) 값으로 식별되므로, 정확한 버전의 이미지를 참조하고 배포하는 것이 가능합니다. 문제가 발생하면 이전 버전의 이미지로 롤백하는 것도 간단히 해당 이미지 태그나 다이제스트를 사용하면 됩니다.
- 일관성 및 재현성: 이미지 레이어는 읽기 전용이므로, 일단 빌드된 이미지는 변경되지 않습니다. 동일한 이미지를 사용하여 컨테이너를 실행하면, 개발 환경, 테스트 환경, 운영 환경 어디에서든 항상 동일한 파일 시스템과 종속성을 가진 환경이 보장됩니다. 이는 “내 컴퓨터에서는 됐는데…”와 같은 고질적인 문제를 해결하고 애플리케이션 배포의 일관성과 재현성을 크게 향상시킵니다.
결론적으로, 컨테이너 이미지는 애플리케이션과 그 실행 환경 전체를 불변의 상태로 패키징한, 읽기 전용의 청사진입니다. 그 핵심에는 여러 개의 읽기 전용 레이어가 겹쳐진 계층화된 파일 시스템 구조가 있으며, 이는 저장 공간, 빌드/배포 속도, 버전 관리 측면에서 뛰어난 효율성을 제공합니다. 이러한 특성 덕분에 컨테이너 이미지는 클라우드 네이티브 패러다임 하에서 애플리케이션을 빠르고, 안정적이며, 일관성 있게, 그리고 확장 가능한 방식으로 배포하고 관리하는 데 있어 필수 불가결한 기술로 자리 잡았습니다. 쿠버네티스는 바로 이러한 컨테이너 이미지들을 기반으로 애플리케이션의 배포, 확장, 관리를 자동화하는 강력한 플랫폼입니다.
2.2.3.2 Copy-on-Write (CoW) 메커니즘
앞서 우리는 컨테이너 이미지가 불변성을 지닌 여러 개의 읽기 전용 레이어(Read-Only Layers)로 구성된다는 사실을 확인했습니다. 이는 이미지의 일관성과 재현성을 보장하는 강력한 특징이지만, 한 가지 의문을 남깁니다. 애플리케이션은 실행 중에 로그를 기록하고, 임시 파일을 생성하며, 때로는 설정을 동적으로 변경하는 등 지속적으로 ‘쓰기’ 작업을 수행해야 합니다. 그렇다면 이 읽기 전용 이미지 구조 위에서 어떻게 이러한 변경 가능한, 살아있는 컨테이너 환경을 구현할 수 있을까요?
이 질문에 대한 해답이자, 컨테이너 기술의 효율성을 뒷받침하는 핵심 원리가 바로 Copy-on-Write (CoW) 메커니즘입니다. 우리말로는 “쓰기 시 복사” 전략으로 번역될 수 있으며, 이름 자체가 그 동작 방식을 함축적으로 설명합니다. 즉, 원본 데이터는 최대한 공유하여 읽기 작업에 사용하되, 누군가 해당 데이터를 수정하려고 할 때(쓰기 작업 발생 시) 비로소 그 데이터를 복사하여 수정 작업을 수행하는 최적화 기법입니다. 이는 메모리 관리나 파일 시스템 등 다양한 컴퓨팅 영역에서 자원 효율성을 높이기 위해 사용되는 고전적인 방식입니다.
컨테이너의 맥락에서 CoW 메커니즘이 어떻게 적용되는지 구체적으로 살펴보겠습니다.
사용자가 컨테이너 이미지를 기반으로 새로운 컨테이너 실행을 요청하면, 컨테이너 런타임(쿠버네티스 환경에서는 CRI 표준을 따르는 containerd, CRI-O 등이 해당)은 다음과 같은 준비 작업을 수행합니다.
- 읽기 전용 레이어 스택 준비: 먼저, 명시된 컨테이너 이미지에 속한 모든 읽기 전용 레이어들을 순서대로 쌓아 올립니다. 이 레이어들은 이미지의 근간을 이루며, 모든 컨테이너 인스턴스 간에 공유될 수 있는 불변의 기반입니다.
- 쓰기 가능 레이어 추가: 그 다음, 이 읽기 전용 레이어 스택의 가장 위에 새로운 쓰기 가능한 레이어(Writable Layer)를 추가합니다. 이 레이어는 특별히 컨테이너 레이어(Container Layer)라고도 불리며, 해당 컨테이너 인스턴스만이 독점적으로 사용하는 공간입니다. 즉, 동일한 이미지를 기반으로 10개의 컨테이너를 실행한다면, 10개의 독립적인 쓰기 가능 레이어가 각각 생성됩니다. 컨테이너가 실행되는 동안 발생하는 모든 파일 시스템 변경 사항(새 파일 생성, 기존 파일 수정, 파일 삭제)은 바로 이 쓰기 가능한 컨테이너 레이어에 기록됩니다.
- 통합 파일 시스템 뷰 제공 (Union Mount): 마지막으로, 컨테이너 런타임은 유니온 파일 시스템(Union File System) 기술(예: OverlayFS, AUFS – 비록 오래되었지만 초기 컨테이너 기술에서 중요했음)을 활용하여 이 여러 개의 레이어(읽기 전용 이미지 레이어들 + 쓰기 가능 컨테이너 레이어)를 하나의 단일하고 일관된 디렉토리 구조처럼 보이도록 통합(mount)합니다. 컨테이너 내부에서 실행되는 프로세스들은 복잡한 레이어 구조를 인지할 필요 없이, 마치 일반적인 파일 시스템을 사용하는 것처럼 상호작용할 수 있습니다.
이제 이 구조 위에서 CoW 메커니즘이 실제 파일 작업 시나리오별로 어떻게 동작하는지 자세히 알아보겠습니다.
- 파일 읽기 (Read Operation): 컨테이너 내부의 애플리케이션이 특정 파일(/app/config.yaml 등)을 읽으려고 시도합니다.
- 컨테이너 런타임은 유니온 파일 시스템을 통해 해당 파일을 찾습니다. 검색은 가장 상위 레이어인 쓰기 가능 컨테이너 레이어부터 시작하여, 파일을 찾으면 즉시 그 파일을 반환합니다.
- 만약 쓰기 가능 레이어에 파일이 없다면, 바로 아래의 읽기 전용 이미지 레이어를 검색합니다. 이런 식으로 레이어 스택을 위에서 아래로 순차적으로 내려가며 파일을 찾습니다.
- 가장 먼저 해당 경로에 파일이 발견되는 레이어에서 파일을 읽어 애플리케이션에 전달합니다. 이 과정 덕분에, 상위 레이어에 수정된 파일이 있다면 하위 레이어의 원본 파일은 자연스럽게 가려지게(override) 됩니다.
- 기존 파일 수정 (Modify Operation – CoW 발동): 컨테이너가 읽기 전용 이미지 레이어에 존재하는 기존 파일(예: 기본 설정 파일 /etc/nginx/nginx.conf)을 수정하려고 합니다.
- 이 ‘쓰기’ 시점에서 CoW 메커니즘이 발동됩니다.
- 컨테이너 런타임은 실제 수정 작업을 수행하기 전에, 먼저 원본 파일이 위치한 읽기 전용 이미지 레이어에서 해당 파일 전체 또는 파일의 필요한 블록(구현에 따라 다름)을 최상단의 쓰기 가능한 컨테이너 레이어로 복사합니다. 이것이 바로 ‘Copy-on-Write’의 ‘Copy’ 단계입니다.
- 일단 복사가 완료되면, 실제 파일 수정 작업은 쓰기 가능 레이어에 복사된 파일 사본에 대해 이루어집니다.
- 이제부터 해당 컨테이너가 /etc/nginx/nginx.conf 파일을 읽으려고 하면, 검색 과정에서 쓰기 가능 레이어에 있는 수정된 버전을 먼저 발견하게 되므로, 수정된 내용을 읽게 됩니다.
- 매우 중요한 점은, 이 모든 과정 동안 원본 이미지를 구성하는 읽기 전용 레이어의 /etc/nginx/nginx.conf 파일은 전혀 변경되지 않고 그대로 유지된다는 것입니다. 이미지의 불변성이 철저히 지켜지는 것입니다.
- 기존 파일 삭제 (Delete Operation): 컨테이너 내부에서 읽기 전용 레이어에 존재하는 파일(예: /usr/bin/unnecessary-tool)을 삭제하려고 합니다.
- 읽기 전용 레이어의 파일은 물리적으로 삭제할 수 없습니다.
- 대신, 컨테이너 런타임은 쓰기 가능한 컨테이너 레이어에 해당 경로의 파일이 삭제되었음을 나타내는 특수한 종류의 표식, 즉 ‘화이트아웃(whiteout)’ 파일 또는 메타데이터를 생성합니다.
- 유니온 파일 시스템은 이 화이트아웃 표식을 인식하고, 하위 레이어에 실제 파일 데이터가 존재하더라도 컨테이너 내부에서는 해당 파일이 보이지 않도록(가려지도록) 합니다. 마치 파일이 정말로 삭제된 것처럼 보이는 효과를 주는 것입니다. 하지만 근본 이미지 레이어에는 파일이 그대로 남아 있습니다.
- 새 파일 생성 (Create Operation): 컨테이너가 실행 중에 새로운 파일(예: 로그 파일 /var/log/app.log, 임시 데이터 /tmp/data.bin)을 생성합니다.
- 이 경우는 간단합니다. 새로운 파일은 다른 레이어에 존재하지 않으므로, 직접적으로 최상단의 쓰기 가능한 컨테이너 레이어에 생성됩니다. CoW 과정(복사)이 필요 없습니다.
이처럼 정교하게 작동하는 Copy-on-Write 메커니즘 덕분에 컨테이너 기술은 다음과 같은 결정적인 이점들을 확보할 수 있습니다.
- 이미지 불변성(Immutability)의 철저한 보장: CoW는 컨테이너 실행 중 발생하는 모든 변경 사항을 각 컨테이너의 고유한 쓰기 가능 레이어 안에 격리시킵니다. 여러 컨테이너가 동일한 베이스 이미지를 공유하더라도, 한 컨테이너에서의 작업이 다른 컨테이너나 원본 이미지 자체에 전혀 영향을 미치지 않습니다. 이는 애플리케이션 배포의 예측 가능성, 안정성, 그리고 보안성을 크게 향상시키는 근본적인 토대가 됩니다. 문제가 발생하면 언제든 깨끗한 원본 이미지로부터 새로운 컨테이너를 다시 시작할 수 있습니다.
- 극대화된 자원 효율성:
- 디스크 공간 절약: 수십, 수백 개의 컨테이너가 동일한 운영체제 베이스 이미지(수백 MB 또는 GB 단위)와 애플리케이션 런타임 레이어를 사용하더라도, 디스크 상에는 해당 읽기 전용 레이어들이 단 한 벌만 저장됩니다. 각 컨테이너는 오직 자신만의 변경 사항을 담는, 상대적으로 매우 작은 크기의 쓰기 가능 레이어만 추가로 소유합니다. 이는 특히 고밀도 컨테이너 환경에서 스토리지 비용을 획기적으로 절감시킵니다.
- 빠른 컨테이너 시작 시간: 컨테이너를 시작할 때마다 거대한 OS 이미지 전체를 복사할 필요가 없습니다. 단순히 기존의 읽기 전용 레이어들 위에 얇은 쓰기 가능 레이어 하나만 추가하고 유니온 마운트를 수행하면 즉시 컨테이너를 가동할 수 있습니다. 이는 VM 부팅 시간에 비해 훨씬 빠른, 거의 즉각적인 컨테이너 시작을 가능하게 합니다.
- 컨테이너 간 격리 강화: 각 컨테이너의 쓰기 레이어는 서로 완전히 분리되어 있으므로, 파일 시스템 수준에서의 변경 사항이 다른 컨테이너에 영향을 줄 염려가 없습니다.
요약하자면, Copy-on-Write (CoW)는 읽기 전용의 계층화된 컨테이너 이미지를 기반으로 하면서도, 각 컨테이너가 독립적으로 파일 시스템을 수정하고 상태를 변경할 수 있도록 매끄럽게 연결해주는 핵심적인 다리 역할을 하는 기술입니다. 이는 이미지의 불변성이라는 중요한 원칙을 훼손하지 않으면서도, 실행 중인 컨테이너에 필요한 유연성과 쓰기 기능을 제공합니다. 또한, 자원 사용(디스크 공간, 시작 시간)의 효율성을 극대화하여 컨테이너가 가볍고 빠르게 동작할 수 있도록 보장합니다. 따라서 CoW는 클라우드 네이티브 환경에서 컨테이너 기술이 성공적으로 자리 잡고 대규모로 활용될 수 있게 만든 근본적인 메커니즘 중 하나라고 할 수 있습니다.
2.2.3.3 컨테이너 이미지 빌드 프로세스 상세 이해: 레시피에서 실행 가능한 패키지까지
이제 우리는 컨테이너 이미지가 계층화된 파일 시스템 구조를 가지며, Copy-on-Write 메커니즘을 통해 실행 중인 컨테이너 환경을 제공한다는 것을 알게 되었습니다. 그렇다면 이 정교한 구조물인 컨테이너 이미지는 과연 어떻게 탄생하는 것일까요? 바로 이미지 빌드 프로세스를 통해 만들어집니다. 클라우드 네이티브 환경에서는 애플리케이션을 직접 컨테이너화하거나, 기존 이미지를 특정 요구사항에 맞게 수정하고 최적화해야 하는 경우가 빈번하게 발생합니다. 따라서 이 빌드 과정을 깊이 이해하는 것은 컨테이너 기술을 효과적으로 활용하기 위한 필수적인 지식입니다.
컨테이너 이미지 빌드의 가장 일반적인 시작점은 컨테이너 빌드 명세서(Container Build Specification)라고 불리는 텍스트 파일입니다. 가장 널리 알려진 예시가 바로 Dockerfile이지만, 다른 빌드 도구들은 다른 형식의 명세서를 사용할 수도 있습니다. 본질적으로 이 파일은 최종 이미지를 구성하기 위해 필요한 단계별 지침(instructions)들을 순서대로 나열한 레시피와 같습니다. 마치 요리 레시피처럼, 어떤 재료(기본 이미지)를 사용하고, 어떤 조리 과정(소프트웨어 설치, 파일 복사)을 거쳐, 최종적으로 어떤 요리(실행 가능한 컨테이너 이미지)를 완성할지를 명확하게 정의합니다.
주요 명세서 명령어들의 역할은 다음과 같습니다.
- FROM: 빌드의 기초가 될 기본 이미지(Base Image)를 지정합니다. 모든 빌드 명세서는 일반적으로 FROM 명령어로 시작하며, 여기서 지정된 이미지가 첫 번째 레이어 세트가 됩니다. 이는 Ubuntu, CentOS, Alpine과 같은 운영체제 이미지일 수도 있고, Python, Node.js, Java 등 특정 언어 런타임이 미리 설치된 이미지일 수도 있으며, 심지어 아무것도 없는 상태에서 시작하는 scratch 이미지일 수도 있습니다.
- COPY 또는 ADD: 호스트 시스템(빌드가 실행되는 환경)의 파일이나 디렉토리를 이미지 내부의 지정된 경로로 복사합니다. ADD는 COPY의 기능 외에도 URL에서 파일을 다운로드하거나 압축 파일(tar)을 자동으로 해제하는 추가 기능을 제공하지만, 명확성을 위해 단순 파일 복사에는 COPY를 사용하는 것이 권장됩니다. 이 명령어들은 이미지의 파일 시스템에 변경을 가하므로 일반적으로 새로운 레이어를 생성합니다.
- RUN: 지정된 셸 명령어를 이미지 내부에서 실행합니다. 이는 주로 패키지 설치(예: apt-get install, yum install, apk add), 소스 코드 컴파일, 디렉토리 생성, 권한 변경 등 이미지의 상태를 변경하는 작업에 사용됩니다. RUN 명령어 실행 결과로 파일 시스템에 변경이 발생하면, 해당 변경 사항을 담은 새로운 레이어가 생성됩니다.
- CMD 또는 ENTRYPOINT: 컨테이너가 이 이미지로부터 시작될 때 기본적으로 실행될 명령어를 정의합니다. ENTRYPOINT는 컨테이너를 특정 실행 파일처럼 작동하도록 고정하는 데 사용되고, CMD는 ENTRYPOINT의 기본 인자를 제공하거나 ENTRYPOINT가 없을 경우 실행될 기본 명령을 지정합니다. 이 명령어들은 주로 이미지의 메타데이터(metadata)를 수정하며, 파일 시스템 자체를 변경하지 않기 때문에 일반적으로는 새로운 레이어를 생성하지 않거나, 생성하더라도 매우 작은 메타데이터 레이어를 만듭니다.
- 기타: WORKDIR (작업 디렉토리 변경), EXPOSE (노출할 포트 지정), ENV (환경 변수 설정), USER (명령 실행 사용자 지정) 등 다양한 명령어들이 이미지 구성과 메타데이터 설정을 위해 사용됩니다. 이들 중 일부(예: ENV, USER 등 메타데이터 변경)는 레이어를 생성하지 않을 수 있습니다.
컨테이너 빌드 도구(예: Docker 엔진의 빌드 컴포넌트, Buildah, Kaniko 등)는 이 명세서 파일을 위에서부터 아래로 한 줄씩 순차적으로 읽어 들이며 각 명령어를 실행합니다. 이 과정에서 앞서 설명한 레이어(Layer) 개념이 핵심적인 역할을 수행합니다.
명세서의 각 명령어, 특히 파일 시스템에 실질적인 변경을 가하는 명령어(RUN, COPY, ADD 등)가 실행될 때마다, 빌드 도구는 이전 상태(바로 직전 레이어)를 기반으로 변경 사항만을 담은 새로운 읽기 전용 레이어를 생성하여 기존 레이어 스택 위에 쌓아 올립니다. 각 레이어는 이전 레이어와의 차이점(diff)만을 효율적으로 저장합니다.
예시로 제시된 간단한 빌드 명세서를 다시 살펴보며 이 과정을 구체화해 보겠습니다.
- FROM ubuntu:latest: 빌드 도구는 ubuntu:latest 이미지를 로컬 저장소나 원격 레지스트리에서 찾습니다. 이 이미지는 이미 여러 개의 레이어(Ubuntu 기본 파일 시스템, 기본 패키지 등)로 구성되어 있습니다. 이 레이어들이 빌드의 기초가 됩니다.
- RUN apt-get update && apt-get install -y nginx: 빌드 도구는 ubuntu:latest 이미지의 최상위 레이어를 기반으로 임시 컨테이너를 실행하고, 그 안에서 apt-get update && apt-get install -y nginx 명령을 실행합니다. 이 명령 실행 결과로 Nginx 패키지 및 관련 의존성 파일들이 이미지 파일 시스템에 추가/변경됩니다. 빌드 도구는 이 변경 사항들만을 캡처하여 새로운 레이어(레이어 2)를 생성하고, ubuntu:latest의 레이어 스택 위에 추가합니다.
- COPY myapp /app: 빌드 도구는 빌드 컨텍스트(일반적으로 명세서 파일이 위치한 디렉토리)에서 myapp 디렉토리의 내용을 가져와, 이전 단계에서 생성된 레이어(레이어 2) 위에 /app 경로로 복사합니다. 이 파일 추가로 인한 파일 시스템 변경 사항이 또 다른 새 레이어(레이어 3)로 생성되어 레이어 2 위에 쌓입니다.
- CMD [“nginx”, “-g”, “daemon off;”]: 이 명령어는 컨테이너가 시작될 때 실행될 기본 명령(nginx -g ‘daemon off;’)을 이미지의 메타데이터에 설정합니다. 이는 파일 시스템 자체를 변경하는 것이 아니므로, 일반적으로는 새로운 레이어를 생성하지 않거나, 이미지 설정 변경을 나타내는 아주 작은 메타데이터 레이어를 추가할 수 있습니다.
이렇게 명세서의 각 단계를 순차적으로 처리하며 레이어를 차곡차곡 쌓아 올리면, 최종적으로 모든 레이어가 통합된 완전한 컨테이너 이미지가 완성됩니다. 완성된 이미지는 고유한 ID(보통 내용 기반 해시값)를 가지며, 사람이 식별하기 쉬운 이름과 태그(예: my-web-app:v1.1)를 붙여 관리할 수 있습니다.
이제 이미지 빌드 프로세스의 핵심적인 성능 최적화 기능이자 개발 생산성에 큰 영향을 미치는 빌드 캐시(Build Cache)에 대해 자세히 알아보겠습니다.
컨테이너 빌드 도구는 매우 영리하게 작동합니다. 각 명령어를 실행하고 새로운 레이어를 생성할 때마다, 해당 명령어와 그 결과물인 레이어를 내부 캐시에 저장합니다. 다음에 동일한 이미지(또는 다른 이미지라도 동일한 부분까지)를 다시 빌드할 때, 빌드 도구는 각 명령어를 실행하기 전에 캐시를 확인합니다.
캐시 확인 로직은 대략 다음과 같습니다. “이전에 동일한 부모 레이어를 기반으로 동일한 명령어를 실행한 적이 있는가? 만약 COPY나 ADD 명령어라면, 복사되는 파일들의 내용(체크섬)까지 동일한가?”
- 만약 이 모든 조건이 충족된다면(즉, 캐시 히트(Cache Hit)), 빌드 도구는 해당 명령어를 실제로 다시 실행하는 대신, 캐시에 저장된 기존 레이어를 즉시 재사용합니다. 이는 시간과 자원을 크게 절약해 줍니다.
- 만약 조건 중 하나라도 다르다면(즉, 캐시 미스(Cache Miss)), 빌드 도구는 해당 명령어를 실행하여 새로운 레이어를 생성해야 합니다. 중요한 점은, 일단 캐시 미스가 발생하면 그 이후의 모든 명령어들은 캐시를 사용할 수 없고 무조건 다시 실행되어야 한다는 것입니다. 왜냐하면 이후 명령어들은 변경된 새로운 부모 레이어를 기반으로 실행되어야 하기 때문입니다.
이 빌드 캐시 메커니즘은 이미지 빌드 속도를 극적으로 향상시킬 수 있습니다. 예를 들어, 위의 Nginx 예제에서 애플리케이션 코드(myapp)만 변경하고 다시 빌드한다고 가정해 봅시다.
- FROM ubuntu:latest: 변경 없으므로 캐시 히트.
- RUN apt-get update && apt-get install -y nginx: 명령어와 부모 레이어 변경 없으므로 캐시 히트.
- COPY myapp /app: myapp 디렉토리의 내용(파일 체크섬)이 변경되었으므로 캐시 미스! 빌드 도구는 파일을 다시 복사하여 새로운 레이어를 생성합니다.
- CMD […]: 이전 단계에서 캐시 미스가 발생했으므로, 이 단계도 캐시를 사용하지 못하고 다시 실행됩니다 (비록 실행 비용은 매우 저렴하지만).
만약 CMD 명령어만 수정했다면, FROM, RUN, COPY 단계까지 모두 캐시 히트가 발생하고 마지막 CMD 단계만 빠르게 처리되어 빌드가 거의 즉시 완료될 것입니다.
따라서 효율적인 컨테이너 이미지를 빠르고 효과적으로 빌드하기 위해서는 이 캐시 메커니즘을 잘 이해하고 빌드 명세서를 전략적으로 작성하는 것이 중요합니다. 다음은 몇 가지 유용한 팁입니다.
- 명령어 순서 최적화: 변경될 가능성이 낮은 명령어(예: FROM, 운영체제 설정, 기본 라이브러리 설치 RUN)를 명세서의 앞부분에 배치하고, 자주 변경될 가능성이 높은 명령어(예: 애플리케이션 소스 코드 COPY, 빌드 스크립트 RUN)를 뒷부분에 배치하십시오. 이렇게 하면 캐시 히트율을 극대화하여 빌드 시간을 단축할 수 있습니다.
- 불필요한 레이어 생성 최소화: 여러 개의 연속적인 RUN 명령어를 가능하면 하나의 RUN 명령어로 통합하십시오. 각 RUN 명령어는 잠재적으로 레이어를 생성하므로, 이를 하나로 묶으면 레이어 수를 줄일 수 있습니다. 특히, 패키지 설치 후에는 불필요한 캐시 파일 등을 정리하는 명령(rm -rf /var/lib/apt/lists/* 등)을 동일 RUN 명령어 내에서 &&로 연결하여 실행하는 것이 좋습니다. 이는 최종 이미지 크기를 줄이는 데 도움이 됩니다.
- 빌드 컨텍스트(Build Context) 최소화: COPY나 ADD 명령어가 파일을 복사할 원본 위치는 빌드 컨텍스트 내에 있어야 합니다. 빌드 시작 시, 빌드 도구는 이 빌드 컨텍스트 전체를 빌드 환경(데몬 등)으로 전송하는 경우가 많습니다. 따라서 빌드에 불필요한 파일이나 디렉토리(예: .git 폴더, 임시 파일, 로컬 의존성 폴더 등)가 컨텍스트에 포함되지 않도록 .dockerignore (또는 유사한 무시 파일)를 사용하여 명시적으로 제외하는 것이 매우 중요합니다. 이는 빌드 시작 시간을 단축하고 불필요한 데이터 전송을 막아줍니다.
- 멀티 스테이지 빌드(Multi-stage Builds) 활용: (고급 최적화 기법) 컴파일이 필요한 언어(Java, Go, C++)나 빌드 시에만 필요한 도구(Node.js 개발 의존성 등)가 있는 경우, 멀티 스테이지 빌드를 사용하는 것이 매우 효과적입니다. 이는 하나의 명세서 파일 내에서 여러 개의 빌드 단계(FROM … AS builder)를 정의하는 방식입니다. 첫 번째 단계(‘빌더 스테이지’)에서 소스 코드를 컴파일하거나 필요한 아티팩트를 생성하고, 최종 단계(‘파이널 스테이지’)에서는 빌더 스테이지에서 생성된 결과물(실행 파일, jar 파일 등)만을 깨끗하고 최소화된 기본 이미지(예: alpine 또는 distroless) 위로 COPY –from=builder 명령을 사용하여 복사합니다. 이렇게 하면 최종 이미지에는 빌드 도구나 중간 산출물, 소스 코드 등이 전혀 포함되지 않아 이미지 크기가 극적으로 작아지고 보안성도 향상됩니다.
결론적으로, 컨테이너 이미지 빌드 프로세스는 단순히 명세서의 명령어를 순차적으로 실행하는 것을 넘어, 계층화된 레이어 생성과 지능적인 빌드 캐싱 메커니즘을 통해 효율성과 속도를 추구하는 정교한 과정입니다. 이 프로세스를 깊이 이해하고 최적화 기법들을 적용함으로써, 개발자와 운영자는 더 작고, 빌드가 빠르며, 배포가 효율적인 컨테이너 이미지를 만들 수 있습니다. 이는 결과적으로 클라우드 네이티브 환경에서 애플리케이션 개발 주기 단축, 배포 안정성 향상, 운영 비용 절감에 크게 기여하며, 쿠버네티스와 같은 오케스트레이션 플랫폼의 이점을 최대한 활용하는 기반이 됩니다.
