無知

갈 길이 먼 공부 일기

기술 공부/쿠버네티스

쿠버네티스 (1-3) | 도커 이미지 멀티 스테이지 빌드 및 경량화

moozii 2022. 1. 18. 22:15

https://programmerlib.com/docker-multi-stage-build-with-example/

 

멀티 스테이지 빌드를 통한 도커 이미지 경량화

멀티 스테이지 빌드란?

대부분의 사람들은 한 프로젝트의 하나의 Dockerfile만 존재하는 것이 익숙했습니다. 여러 개의 Dockerfile은 유지보수의 편의성도 떨어뜨리고, 각각의 단계에 순서 의존성이 있는 만큼 관리도 복잡해졌죠. 예를 들어 바이너리 빌드가 끝나기 전에 실행 이미지를 만들 수는 없으니까요. 이후 많은 사람들이 불편함을 느끼자, 여러 단계의 이미지 빌드 과정을 하나의 Docerfile에서 관리하기 위해 멀티 스테이지 빌드가 등장했습니다.

https://lynlab.co.kr/blog/89

 

Docker Multi Stage란?

컨테이너 이미지를 만들면서 빌드 등에는 필요하지만 최종 컨테이너 이미지에는 필요 없는 환경을 제거할 수 있도록 단계를 나누어서 기반 이미지를 만드는 방법
하나의 Dockerfile로 빌드 이미지와 실행 이미지를 분리할 수 있게 되어 훨씬 간편하게 이미지를 줄일 수 있게 되었습니다. 뿐만 아니라 배포 이미지의 용량이 줄어 빌드 시간이 감소하게 되었습니다.

https://nesoy.github.io/articles/2020-11/Docker-multi-stage-build

 

[Docker] Dockerfile 만들기(Multi-stage build)

멀티스테이지 빌드를 사용하게 되면 컨테이너 실행 시에는 빌드에 사용한 파일 및 디렉토리과 같은 의존 파일들이 모두 삭제된 상태로 컨테이너가 실행되게 됩니다. 결론적으로 좀 더 가벼운 크기의 컨테이너를 사용할 수 있게 됩니다.

https://kimjingo.tistory.com/63
# 1. Build Image
FROM golang:1.13 AS builder

# Install dependencies
WORKDIR /go/src/github.com/asashiho/dockertext-greet
RUN go get -d -v github.com/urfave/cli

# Build modules
COPY main.go .
RUN GOOS=linux go build -a -o greet .

# ------------------------------
# 2. Production Image
FROM busybox
WORKDIR /opt/greet/bin

# Deploy modules
COPY --from=builder /go/src/github.com/asashiho/dockertext-greet/ .
ENTRYPOINT ["./greet"]
위의 Dockerfile은 두 개의 부분으로 구성되어 있습니다.
1. 개발 환경용 Docker 이미지
Go의 버전 1.13을 베이스 이미지로 하고 'builder'라는 이름을 붙입니다. 그리고 개발에 필요한 것들을 모두 설치하여 로컬환경의 소스코드를 컨테이너로 복사합니다. 이 후 이 소스코드를 go build를 통하여 'greet'이라는 실행 가능한 바이너리 파일을 생성합니다.

2. 제품 환경용 Docker 이미지
제품 환경용 Docker 이미지의 베이스 이미지는 'busybox'를 사용합니다. 이후 개발용 환경의 Docker 이미지로 빌드한 'greet'이라는 이름의 바이너리 파일(실행파일)을 제품 환경의 Docker 이미지로 복사합니다. 이 때 --from 옵션을 사용하여 'builder'라는 이름의 이미지로 부터 복사한다는 것을 선언합니다. 마지막으로 복사한 실행 파일을 실행하는 명령어를 적습니다.

컨테이너 실행
제품 환경용 Docker 이미지인 'greet'을 사용하여 컨테이너를 실행합니다.

https://kimjingo.tistory.com/63

 

Docker multi-stage build를 통해 이미지 경량화하기

Builder Pattern
빌드와 러닝 이미지를 나누는 Builder Pattern도 있습니다. 그래서 Builder image에선 앱 빌드에 필요한 디펜던시 설정, 빌드 후 바이너리를 만들고, 실제로 동작하는 러닝 이미지에선 빌더로 부터 바이너리만 받아서 사용하는 방식입니다. 이러한 과정을 거치면 결국 Build에만 필요한 불필요한 도구, 라이브러리, 이미지 내 파일들을 제외하고 아주 컴팩트한 이미지에서 바이너리만 가지고 동작시킬 수 있습니다.

Multi-stage build
빌드 패턴을 사용하기 위해선 Builder 와 실제 실행될 이미지, 이렇게 2개의 Dockerfile이 필요하게 됩니다. 이는 관리상 굉장히 불편할 것 같은데, 다행히 Muilti-stage build가 있어 한 파일에서 2개의 이미지를 빌드하고 사용할 수 있습니다. 사용법은 단순한데, 그냥 한 파일에서 Base 이미지를 바꿔서 사용하면 마치 2개 이상의 Dockerfile이 있는 것과 동일하게 빌드를 수행할 수 있습니다. Builder에서 빌드한 바이너리를 실행할 이미지로 전달해주기 위해선 COPY의 --from 옵션을 통해 실행 이미지로 전달해줄 수 있습니다. 

https://www.hahwul.com/2020/10/07/docker-multistage-build-for-optimazation/

 

MasayaAoyama  /  kubernetes-perfect-guide

* 멀티 스테이지를 적용하지 않은 버전

# Alpine 3.7 버전 golang 1.10.1 이미지를 사용
FROM golang:1.10.1-alpine3.7

# 8080 포트 오픈
EXPOSE 8080

# 빌드할 머신에 있는 main.go 파일을 컨테이너에 복사
COPY ./main.go ./

# 컨테이너 내부에서 명령어 실행
RUN go build -o ./go-app ./main.go

# 실행 계정을 nobody로 변경
USER nobody

# 컨테이너가 기동할 때 실행할 명령어 정의
ENTRYPOINT ["./go-app"]

* 멀티 스테이지를 적용한 버전

# Stage 1 컨테이너(애플리케이션 빌드)
FROM golang:1.10.1-alpine3.7 as builder
COPY ./main.go ./
RUN go build -o /go-app ./main.go

# Stage 2 컨테이너(빌드된 바이너리를 포함한 실행용 컨테이너 생성)
FROM alpine:3.7
EXPOSE 8080
# Stage 1에서 빌드된 결과물을 복사
COPY --from=builder /go-app .
USER nobody
ENTRYPOINT ["./go-app"]

* alpine 대신 scratch를 사용해 경량화하기

# Stage 1 컨테이너(애플리케이션 빌드)
FROM golang:1.10.1-alpine3.7 as builder
COPY ./main.go ./
RUN CGO_ENABLED=0 go build -o /go-app ./main.go

# Stage 2 컨테이너(빌드된 바이너리를 포함한 실행용 컨테이너 생성)
FROM scratch
EXPOSE 8080
# Stage 1에서 빌드된 결과물을 복사
COPY --from=builder /go-app .
ENTRYPOINT ["./go-app"]
 

GitHub - MasayaAoyama/kubernetes-perfect-guide: 『Kubernetes完全ガイド』の付録マニフェストのリポジトリ / "

『Kubernetes完全ガイド』の付録マニフェストのリポジトリ / "Kubernetes perfect guide" sample manifest repository - GitHub - MasayaAoyama/kubernetes-perfect-guide: 『Kubernetes完全ガイド』の付録マニフェストのリポジト

github.com

 

가벼운 Base Image 선택을 통한 경량화 - scratch, alpine

scratch official image description

This image is most useful in the context of building base images (such as debian and busybox) or super minimal images (that contain only a single binary and whatever it requires, such as hello-world). As of Docker 1.5.0 (specifically, docker/docker#8827), FROM scratch is a no-op in the Dockerfile, and will not create an extra layer in your image (so a previously 2-layer image will be a 1-layer image instead).

From https://docs.docker.com/engine/userguide/eng-image/baseimages/:
You can use Docker’s reserved, minimal image, scratch, as a starting point for building containers. Using the scratch “image” signals to the build process that you want the next command in the Dockerfile to be the first filesystem layer in your image. While scratch appears in Docker’s repository on the hub, you can’t pull it, run it, or tag any image with the name scratch. Instead, you can refer to it in your Dockerfile. For example, to create a minimal container using scratch:

https://hub.docker.com/_/scratch/?tab=description

 

컨테이너 이미지 생성시 고려사항

경량화 컨테이너를 만들기 위해서는 이미지의 기반이되는 Base Image의 선택이 중요하다. 대표적인 경량화 이미지로 Alpine과 Debian이 있다. 반드시 이러한 이미지를 사용해야 하는 것은 아니지만, 가능한 Minimal 이미지를 기반으로 Base Image를 생성하는 것이 컨테이너 사이즈를 줄일 수 있는 방법이다. 다만, Alpine, Debian을 기반으로 이미지를 생성해 나가는 것은 생각보다 어려운 작업이 될 수 있다. CentOS나 Ubuntu와 같이 기본으로 설치되어 있는 라이브러리들을 하나씩 추가해 나가며 어플리케이션이 동작되게 만드는 것은 다소 시간이 걸리는 작업이기 때문이다. 이러한 작업에 익숙하지 않은 경우에는 CentOS나 Ubuntu를 기반으로 하되 이미지를 Mininal 또는 필수패키지 만 Install된 버전으로 선택하여 구성해 나가는 것이 좋다.

https://waspro.tistory.com/692

 

이미지 레이어 통합을 통한 경량화

docker 이미지 레이어(Docker Image Layer)

Docker 의 이미지를 이용해서 docker run 을 하면 Docker 는 도커가 관리하는 파일 시스템 영역에 이미지를 복사한다. 복사후 docker는 이미지의 최상단에 컨테이너 레이어 라고 불리는 하나의 얇은 레이어를 추가하여 컨테이너를 생성한다. 그리고 사용자에게 유니온 파일 시스템을 이용하여 마치 이러한 여러개의 파일 시스템(Image layer)으로 구성되어 있는 이미지 스택 구조가 하나의 파일 시스템 처럼 보이도록 하게 한다.

사용자가 컨테이너 안에서 읽고 쓰는 모든 작업 들은 이 컨테이너 레이어에 기록되고 이미지 레이어에는 적용되지 않는다. 쉽게 말해 Image layer는 변경 불가하고(read only layer), Container layer는 변경 가능하다(Readable/Writable layer). 다른 사용자가 같은 이미지를 이용해서 container 를 실행할 경우 다른 Image layer만 생성이 되지, 다른 사용자들이 생성한 Container layer는 복사되지 않는다.

Docker Layer를 사용하는 이유
이미지 레이어를 사용하는 이유는, 만약 사용자가 앱 source를 변경할 경우 전체 이미지를 다시 다운받는것은 매우 비효율 적이기 때문에 변경된 부분만 새로운 레이어로 추가/삭제를 해주면 되기 때문이다.

https://eqfwcev123.github.io/2020/01/30/%EB%8F%84%EC%BB%A4/docker-image-layer/

 

도커 이미지 레이어(Docker Image Layer)

분리된 데이터를 레이어(Layer) 라고 한다. 레이어는 도커 이미지가 빌드될 때 Dockerfile에 정의된 명령문(Instructions)을 순서대로 실행하면서 만들어진다. ubuntu:14.04 이미지의 layer는 3개임을 알 수 있다.

https://jenakim47.tistory.com/40

 

도커 이미지 잘 만드는 방법

도커 이미지를 pull 받게 되면 마치 여러개로 분리된 조각을 내려받는 것처럼 보인다. 이렇게 분리된 데이터를 레이어(Layer) 라고 한다. 레이어는 도커 이미지가 빌드될 때 Dockerfile에 정의된 명령문(Instructions)을 순서대로 실행하면서 만들어진다. 이 레이어들은 각각 독립적으로 저장되며 읽기 전용이기 때문에 임의로 수정할 수 없다.

도커 이미지 레이어가 중요한 이유는 이미지를 빌드할 때마다 이미 생성된 레이어가 캐시 되어 재사용 되기 때문에 빌드 시간을 단축할 수 있다. 하지만 Dockerfile에 정의된 모든 명령문(Instructions)이 레이어가 되는 것은 아니다. RUN, ADD, COPY 이 3가지 단계만이 레이어로 저장되고, CMD, LABEL, ENV, EXPOSE 등과 같이 메타 정보를 다루는 부분은 임시 레이어로 생성되지만 저장되지 않아 도커 이미지 사이즈에 영향을 주지 않는다.

이미지 레이어 개수를 줄이자
과거 도커 버전에서는 이미지 레이어 개수가 성능에 영향을 줬다고 한다. 하지만 이제는 그렇지 않다. 그래도 도커 레이어 개수를 줄이는 것은 최적화 측면에서 도움이 된다고 볼 수 있다. 레이어는 RUN, ADD, COPY 명령문에서만 생성되기 때문에 아래와 같이 여러 개로 분리된 명령을 체이닝(chaining) 으로 엮어보자. 레이어 개수가 적다고 도커 이미지/컨테이너 성능에 영향을 주진 않지만 Dockerfile 가독성과 유지 보수 관점에서 도움이 될 것이다.

https://jonnung.dev/docker/2020/04/08/optimizing-docker-images/

 

docker build를 실행할 때 --squash 옵션을 추가하면, 겹겹이 쌓인 파일시스템 레이어를 하나로 합쳐줍니다. --squash를 사용하면, 이미지 레이어를 최소화해서 얻는 장점도 있는 반면, 여러 이미지들이 공유하던 중간 레이어들을 공유할 수 없다는 단점도 생깁니다.

http://raccoonyy.github.io/whats-new-in-docker-1-13-korean/