BookTakHae 프로젝트를 배포하기 위해서 Docker를 학습하던 중 이미지가 Layer로 구성된다는 것에 대해 알게 되었습니다. 여기서 말하는 Layer란 어떤 것인지 정확하게 알고 넘어가야겠다는 생각에서 해당 글을 작성하게 되었습니다 :)
관련 내용은 Docker Guide 공식문서를 참조했으며, 중간에 Union File System과 같이 처음 들어보는 개념들에 대해서도 함께 정리하겠습니다.
그럼 이제부터 Docker의 레이어 저장 방식에 대해서 낱낱이 파헤쳐보도록 하겠습니다.
Docker 공식 문서
Layer를 설명하기 앞서, Docker Image 개념에 대해서 복습해보겠습니다.
Docker 이미지란 컨테이너를 실행하는데 필요한 모든 파일, 바이너리, 라이브러리 및 구성을 포함하는 표준화된 패키지입니다. 이미지는 두 가지 주요 원칙을 따릅니다:
불변성 (Immutable): 이미지는 생성된 후에는 수정이 불가능합니다. 변경이 필요하면 새로운 이미지를 생성하거나 기존 이미지 위에 새로운 레이어를 추가합니다.
레이어 구성 (Layered): 이미지는 여러 레이어로 구성되며, 각 레이어는 파일 시스템 변경 사항을 나타냅니다.
이처럼 도커 컨테이너의 이미지는 레이어로 구성됩니다. 그리고 이러한 레이어는 일단 생성되면 변경이 불가능합니다. 그렇다면, 레이어는 컨테이너가 사용할 수 있는 파일 시스템을 만드는데 어떻게 사용될까요?
이미지의 각 레이어는 파일 시스템 변경 사항을 포함합니다. 예를 들어, 다음과 같은 단계로 이미지가 구성될 수 있습니다.
기본 레이어 : 운영체제와 기본 명령어, 패키지 관리자를 추가합니다.
런타임 레이어 : Python 런타임과 pip 같은 종속성 관리 도구를 설치합니다.
애플리케이션 설정 레이어 : 'requirements.txt' 파일을 복사하여 애플리케이션의 종속성을 정의합니다.
종속성 설치 레이어: requirements.txt에 명시된 종속성을 설치합니다.
소스 코드 레이어: 애플리케이션의 실제 소스 코드를 복사합니다.
위 과정을 예시로 들면 다음과 같습니다.
예를 들어, Python 애플리케이션을 위한 이미지를 만들 때, 위와 같은 단계로 이미지를 구성할 수 있습니다. 이러한 레이어링 덕분에 다른 Python 애플리케이션을 만들 때 기존의 Python 및 pip 관련 레이어를 재사용할 수 있습니다. 이는 빌드 시간을 단축하고 저장 공간과 대역폭을 절약하는 데 도움이 됩니다.
이는 레이어를 이미지 간에 재사용할 수 있기 때문에 유용합니다. 예를 들어, 다른 Python 애플리케이션을 만들고 싶다고 가정해 보겠습니다. 각 단계를 레이어로 나눴기 때문에 기존에 사용했던 Python과 pip 관련 레이어를 활용할 수 있습니다.
이처럼 기존의 레이어를 재사용하여 이미지를 확장해줌으로써, 빌드가 더 빨라지고 이미지를 배포하는데 필요한 저장 공간과 대역폭이 줄어듭니다.
레이어링은 이미지의 레이어를 쌓아 올리는 과정이며, 콘텐츠 주소 지정 스토리지와 유니온 파일 시스템을 통해 구현됩니다:
유니온 파일 시스템을 통해 컨테이너는 파일 시스템을 변경할 수 있으며, 원래 이미지 레이어는 그대로 유지됩니다. 이는 동일한 기본 이미지에서 여러 컨테이너를 실행할 수 있게 해줍니다.
이러한 구조는 개발자에게 효율적이고 일관된 환경을 제공하며, 이미지 빌드와 배포 과정을 최적화합니다.
Union File System이란?
여러 파일 시스템을 하나의 통합된 파일 시스템으로 마운트하는 기술입니다.
이 시스템은 주로 컨테이너 환경에서 사용되며, 여러 레이어를 쌓아 올려 하나의 파일 시스템처럼 동작하도록 해줍니다. UFS의 주요 특징은 다음과 같습니다:
Docker 공식 문서
Docker 빌드는 일련의 정렬된 빌드 명령어로 구성됩니다. Dockerfile에 정의된 명령어 순으로 레이어가 쌓이기 때문에 명령어 순서는 중요합니다. Dockerfile의 각 명령어는 컨테이너 이미지의 레이어 스택으로 변환됩니다.
이미지 빌드를 실행하면 빌더는 이전에 빌드된 레이어를 재사용합니다. 이미지의 레이어가 변경되지 않은 경우 빌더는 빌드 캐시에서 레이어를 가져옵니다. 마지막 빌드 이후 레이어가 변경된 경우 해당 레이어와 그 뒤를 따르는 모든 레이어를 다시 빌드해야 합니다.
FROM golang:1.20-alpine
WORKDIR /src
COPY . .
RUN go mod download
RUN go build -o /bin/client ./cmd/client
RUN go build -o /bin/server ./cmd/server
ENTRYPOINT [ "/bin/server" ]
위 Dockerfile은 모든 프로젝트 파일을 컨테이너에 복사한(COPY . .) 다음 단계에서 애플리케이션 종속성을 다운로드 합니다(RUN go mod download). 프로젝트 파일을 변경하면 COPY 레이어의 캐시가 무효화됩니다. 또한 이어지는 모든 레이어의 캐시도 무효화됩니다.
현재 Dockerfile 명령어 정의 순서로 인해서 빌더는 패키지가 마지막으로 변경된 이후로 아무것도 변경되지 않았음에도 불구하고 Go 모듈을 다시 다운로드해야 합니다.
Dockerfile 정의 순서를 수정하겠습니다. 기존에 Go 종속성 설치 명령어를 컨테이너에 복사하기 위한 명령어 전에 작성하겠습니다. 이처럼 수정하게되면, 소스 코드의 변경이 일어나도 종속성 레이어는 캐싱되어 재사용이 가능합니다.
Go 의존성을 설치를 위해서는 go.mod
와 go.sum
파일이 필요합니다. 따라서, Run go mod download
명령어 이전에 위 파일들을 컨테이너에 복사해야 합니다. 이외에 파일들은 Go 의존성 설치 이후에 복사하도록 정의하겠습니다.
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine
WORKDIR /src
- COPY . .
+ COPY go.mod go.sum .
RUN go mod download
+ COPY . .
RUN go build -o /bin/client ./cmd/client
RUN go build -o /bin/server ./cmd/server
ENTRYPOINT [ "/bin/server" ]
이렇게 정의해놓으면, 소스 코드를 수정하더라도 빌더에 의해서 동일한 종속성을 설치할 일이 없습니다. COPY . .
명령어 이전에 종속성을 설치하므로 해당 레이어는 캐싱되어 재사용이 가능하게 됩니다.
따라서, Dockerfile 명령어를 정의하는 순서가 효율성을 높이는데 중요한 역할을 합니다.