우리가 도커를 사용하는 중요한 이유 중 하나는 우리의 서비스가 설치되는 서버의 환경에 의존하지 않고 안정적으로 배포 및 운영되길 원해서일 것입니다. 도커 이미지는 변경 불가능하며, 읽기전용의 스냅샷이기 때문에 개발자는 안정적이고 균일한 조건에서 애플리케이션을 배포할 수 있습니다.
전 시간에는 Go, Makefile, Docker 로 프로젝트 셋팅 및 빌드정보 활용하기에 대해서 다뤄보았습니다만 이번에는 도커의 Layer 와 Multi Stage Build 에 대해서 알아보겠습니다.
Go Build 를 Local 에서 진행할 때의 버전 이슈
다른 분들은 프로젝트의 소스파일을 어디서 build 하시나요? golang 이나 java 같은 경우에는 컴파일을 진행 후 실행파일만 있으면 애플리케이션을 구동가능하기 때문에 소스파일을 운영서버에 올릴 필요가 없습니다. 때문에 팀에서도 로컬 환경에서 Makefile 로 bulid 후 실행파일을 복사하여 도커 이미지에 올렸습니다.
그러나 빌드를 하는 각 개발자들의 컴파일하는 golang version 이 다 달랐습니다. 프로젝트별 golang version 이 다양한 부분도 있고, golang 은 version 에 대한 호환성이 좋기 때문에 프로젝트 version 보다 높은 version 의 golang 으로 컴파일을 진행하고 있었습니다. 그래서 팀차원으로 go build 환경을 동일한 환경으로 구성하기 위해 고민하였습니다.
Multi-Stage Build
원래 Multi-Stage Build 는 이미지 경량화를 위해 고안된 개념입니다. 예시로 트랜스코딩할 때에는 ffmpeg, nvidia-cuda-toolkit 등 각종 외부 라이브러리가 필요하지만, 실제 Runtime 환경에서는 실행파일만 필요하기 때문입니다. 그렇다고 모든 서버에 외부 라이브러리를 매번 설치하는 것은 매우 번거롭고 일관성을 유지하기도 어려웠습니다.
과거에는 이를 여러 단계의 Dockerfile 과 build.sh 파일을 사용하여 운영하였지만 현재에는 하나의 Dockerfile 에 Multi Stage 가 가능해졌습니다.
Multi-Stage Dockerfile Example
FROM golang:1.16 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go ./
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o app .FROM alpine:latest
RUN
Learn more about the "RUN " Dockerfile command.
apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]
COPY — from=builder
- 이전에는 — from=0 인 인덱스로 지정했지만, 위의 첫번째 이미지처럼 AS builder 이미지의 이름을 지정하였을 때는 — from=builder 가 가능합니다.
docker build — target builder -t alexellis2/href-counter:latest .
- 이미지 타겟도 가능
- 특정 빌드 단계 디버깅이 가능
- 목적에 맞는 이미지 빌드가 가능
도커 이미지란?
$ docker pull nginx:latest
Using default tag: latest
latest: Pulling from library/nginx
c499e6d256d6: Already exists
74cda408e262: Pull complete
ffadbd415ab7: Pull complete
Digest: sha256:282530fcb7cd19f3848c7b611043f82ae4be3781cb00105a1d593d7e6286b596
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
우리는 도커 이미지를 pull 받을 때 위와 같은 내용을 확인할 수 있습니다. 하나의 파일을 다운로드 받는 형태가 아닌, 여러 조각들을 단계별로 다운로드 받는 것으로 보입니다. 이러한 데이터 조각들을 레이어(layer) 로 칭하며, 이는 Dockerfile 명령어를 통해 만들어지게 됩니다.
Docker Inspect 를 통해 Layer 정보 출력
도커 파일의 이미지 레이어에 대해서 알고 싶으면 docker inspect {image_id} 를 통해 아래와 같이 출력됨을 알 수 있습니다.
RootFS Layers 필드를 보면 도커 layer 는 7개의 레이어를 갖고있으며, Metadata 로 LastTagTime 을 갖고 있음을 알 수 있습니다.
이러한 도커 이미지는 읽기전용의 레이어들과 쓰기, 읽기가 모두 가능한 최종 레이어로 구성되어 있습니다. 아래에서부터 컨테이너 런타임, Base Image , Read Only Layer, Read-Write Layer 로 구분될 수 있습니다. 그럼 단계별로 레이어에 대해서 알아보겠습니다.
컨테이너 런타임
컨테이너 런타임은 Docker 에서 cgroups, namespace, kernel 등의 기능을 구현하는 레이어를 의미합니다. 컨테이너 런타임은 컨테이너화 기술을 지원하고, 컨테이너를 관리하고 실행하기 위해 필요한 주요 구성 요소들을 담당합니다.
cgroups
리눅스에서 프로세스 그룹을 관리하는 기능
그룹 내의 프로세스들에게 CPU, 메모리, I/O 등과 같은 시스템 자원을 할당하고 제한하는 기능을 제공
namespace
리눅스에서 프로세스들이 리소스를 격리하는데 사용되는 매커니즘
- PID (namespace:pid) : 프로세스 ID 공간을 격리하여 각 컨테이너가 독립된 PID 공간을 가지도록 함
- Network (namespace:net) : 네트워크 인터페이스를 격리하여 각 컨테이너가 독립된 네트워크 스택을 가지도록 함
- Mount (namespace:mnt) : 파일 시스템 마운트를 격리하여 각 컨테이너가 독립된 파일 시스템을 관리하게 함
- IPC (namespace:ipc) : System V IPC(inter-process Conmmunication) 객체를 격리하여 컨테이너 간의 통신을 분리함
Linux 커널
- 리눅스 커널은 하드웨어와 소프트웨어 간의 통신을 담당함
- Docker는 리눅스 커널의 cgroups와 namespace 기능을 활용하여 컨테이너화를 구현
- 컨테이너는 호스트의 리눅스 커널을 공유하며, 컨테이너 간에 격리를 유지하는 데에 이러한 기능을 이용합니다.
Docker Base Image
도커의 베이스 이미지(Base Image) 는 컨테이너의 시작점으로 사용되는 기본 이미지입니다. 베이스 이미지에는 운영체제와 필수 소프트웨어가 포함되어 있으나 Ubuntu, Alpine, CentOS 등 운영체제별로 설치된 필수 소프트웨어가 다릅니다.
Docker 의 Base Image 는 기본적으로 변경되지 않는 특성을 가지기 때문에 Docker Image 를 생성할 때에는 Base Image 위에서 Layer 별로 쌓아가며 생성하게 됩니다.
Ubuntu
- Ubuntu는 인기 있는 데비안(Debian) 기반의 리눅스 배포판입니다.
- 다양한 패키지 관리자인 APT(Advanced Package Tool)을 사용하며, 다양한 소프트웨어 패키지를 쉽게 설치할 수 있습니다.
- 일반적으로 개발자와 사용자들에게 친숙하며, 컨테이너 내에서 소프트웨어 설치와 설정이 용이합니다.
- 비교적 크기가 크기 때문에, 베이스 이미지로 사용할 경우 컨테이너 이미지가 상대적으로 커질 수 있습니다.
Alpine
- Alpine은 경량 리눅스 배포판으로, 작은 크기와 빠른 부팅 속도를 특징으로 합니다.
- Musl 라이브러리를 사용하고 있으며, glibc 대비 메모리 사용량이 작습니다.
- 도커 이미지의 크기를 최소화하고자 할 때 주로 선택되는 베이스 이미지입니다.
- 일반적인 리눅스 배포판보다 패키지가 적지만, 애플리케이션에 필요한 최소한의 라이브러리만을 포함시키기 때문에 크기가 작습니다.
CentOS
- CentOS는 Red Hat Enterprise Linux (RHEL)의 무료 클론 버전으로, 엔터프라이즈급 안정성을 제공합니다.
- RPM(Red Hat Package Manager)을 사용하며, 레드햇 계열의 배포판과 호환성이 높습니다.
- 기업 환경에서 자주 사용되며, RHEL과 거의 동일한 패키지와 라이브러리를 제공합니다.
Read Only Layer / Read Write Layer
읽기 전용 레이어와 읽기/쓰기 레이어는 도커의 Base Image 위에 올라가게 됩니다.
- 읽기 전용 레이어 (Read-Only Layer):
- 읽기 전용 레이어는 이미지의 하위 레이어로서, 변경되지 않고 불변성을 유지합니다.
- 이미지의 안정성과 보안을 증가시키며, 여러 컨테이너가 동일한 읽기 전용 레이어를 공유할 수 있습니다.
2. 읽기/쓰기 레이어 (Read-Write Layer):
- 컨테이너는 읽기/쓰기 레이어를 가집니다.
- 컨테이너는 이미지의 읽기 전용 레이어 위에 읽기/쓰기 레이어를 생성합니다.
- 컨테이너가 실행되면서 컨테이너 내부에서 발생하는 모든 파일 시스템 변경 사항은 이 읽기/쓰기 레이어에 저장됩니다.
- 컨테이너를 중지하면 읽기/쓰기 레이어는 삭제되지만, 변경 사항은 도커 데몬이 유지합니다. 이후 컨테이너가 다시 시작되면 해당 변경 사항이 반영됩니다.
- 이를 통해 여러 컨테이너가 하나의 이미지를 기반으로 실행되더라도 서로 독립적인 파일 시스템을 가질 수 있습니다.
Base Image 와 더불어 모든 레이어들은 Dockerfile 안의 명령어 구성으로 생성되게 됩니다.
- FROM:
- FROM 명령어는 베이스 이미지를 지정합니다. 이 명령어가 실행되면 읽기 전용 레이어가 생성됩니다. 베이스 이미지는 이후 모든 레이어들의 기반이 되는 레이어입니다.
2. RUN:
- RUN 명령어는 새로운 파일 시스템 레이어를 생성하고, 해당 레이어에서 지정된 커맨드를 실행합니다. 파일 시스템의 변경 사항은 읽기/쓰기 레이어에 저장됩니다.
3. COPY/ADD:
- **COPY**와 ADD 명령어는 호스트 머신의 파일을 이미지로 복사합니다. 이로 인해 새로운 레이어가 생성되며, 해당 파일들은 읽기/쓰기 레이어에 추가됩니다.
4. CMD / ENTRYPOINT:
- CMD 또는 ENTRYPOINT 명령어는 컨테이너가 실행될 때 실행될 기본 명령어를 지정합니다. 이 명령어는 이미지의 일부로 들어가지만, 런타임 환경에서만 적용되기 때문에 새로운 레이어를 생성하지는 않습니다.
5. VOLUME:
- VOLUME 명령어는 컨테이너 내부의 특정 디렉토리를 호스트 머신과 공유하도록 설정합니다. 이는 읽기/쓰기 레이어와는 관련이 없지만, 호스트와의 디렉토리 공유를 위한 설정을 해줍니다.
즉, 우리는 Dockerfile 을 구성할 때 RUN, COPY/ADD 로 이미지레이어가 생성될 수 있음을 인지하여 Dockerfile 을 작성해야 합니다. 도커 이미지는 기존에 생성되어있으면 다른 컨테이너에서 공유가 가능하고, cache 될 수 있습니다. 따라서 Layer 를 생성할 수 있는 명령어는 최소화하고, 자주 변경되지 않는 레이어는 상단에, 변경 사항이 많은 레이어는 하단에 위치시켜야 합니다.
또한 위에서 배운 Multi-Stage 빌드를 사용하여 빌드와 런타임 단계를 분리해야합니다. 빌드 시에는 빌드에 필요한 도구들을 사용하고, 런타임 시에는 실제 애플리케이션과 라이브러리만 포함한 최종 이미지만을 생성하여 이미지를 경량화시켜야 합니다.
도커 이미지 용량
도커 이미지는 컨테이너를 실행하기 위한 모든 정보를 가지고 있기 때문에 보통 용량이 수백 MB입니다.
기존 이미지에 파일 하나를 추가했다고 다시 수백 MB 를 다시 다운받는다면 매우 비효율적입니다.
도커는 이런 문제를 해결하기 위해 레이어(layer) 라는 개념을 사용하고, 유니온 파일 시스템을 이용하여 여러 개의 레이어를 하나의 파일시스템으로 사용할 수 있게 해줍니다.
이렇게 여러 이미지가 레이어를 공유할 수 있기 때문에 실제로 파일 시스템에서 차지하는 전체 용량이 감소합니다.
서로 같은 이미지를 공유한다고 판단되면, 레이어가 이미 존재하는 것으로 다운로드 하지 않고 넘어갑니다.
Ngnix Docker Pull Test (nginx:1.15.1)
$ docker pull nginx:1.15.1
be8881be8156: Pull complete
f2f27ed9664f: Pull complete
54ff137eb1b2: Pull complete
Digest: sha256:4a5573037f358b6cdfa2f3e8a9c33a5cf11bcd1675ca72ca76fbe5bd77d0d682
Status: Downloaded newer image for nginx:1.15.1
docker.io/library/nginx:1.15.1
Ngnix Docker Pull Test (nginx:1.15.2)
$ docker pull nginx:1.15.2
1.15.2: Pulling from library/nginx
be8881be8156: Already exists
32d9726baeef: Pull complete
87e5e6f71297: Pull complete
Digest: sha256:d85914d547a6c92faa39ce7058bd7529baacab7e0cd4255442b04577c4d1f424
Status: Downloaded newer image for nginx:1.15.2
docker.io/library/nginx:1.15.2
- 위의 로그를 보면 be8881be8156: Already exists 를 보고 layer 를 이미 존재한다고 판단함을 알 수 있다.