42서울/Inception

Multi-stage builds [42 inception 과제 개념 심화 1]

뜨거운 개발자 2023. 6. 24. 17:59

Builder pattern vs. Multi-stage builds in Docker
The Docker builder pattern just got a lot easier. Let's checkout some bleeding-edge PRs from the Docker project that are causing a stir.
https://blog.alexellis.io/mutli-stage-docker-builds/
Multi-stage builds
Keeping your images small with multi-stage builds
https://docs.docker.com/build/building/multi-stage/#name-your-build-stages
💡
위 게시물은 인셉션 과제에서는 전혀 사용되지 않음을 알려드립니다. 다만, 도커파일을 작성할 때 캐시를 사용해서 더 작은 배포파일을 만드는 과정을 다루고 있기에 도커에 대해서 더 자세히 공부하고 싶으시다면 한번 읽어보시는 것도 좋은 공부가 될 것 같습니다.

Multi-stage builds의 등장 전 상황

이미지를 빌드하고 게시할 때 때때로 해당 이미지의 크기가 상당히 커지는 문제가 있었습니다.

Multi-stage builds가 등장하기 전에는 이미지 크기를 작게 하려면 이미지에서 리소스를 수동으로 정리했어야 했습니다.

과거에는 하나의 Dockerfile을 개발용으로 사용하고 다른 하나는 배포용으로 사용하는 것이 일반적이었습니다.

개발버전의 경우 어플리케이션을 구축하는데 필요한 모든 것이 포함되어있었고, 배포용은 어플리케이션과 실행을 위한 종속성이 필요한 항목만 포함되어 있었습니다.

가장 효율적인 Dockerfile을 작성하려면, shell 명령어 꼼수를 이용하거나 난해한 솔루션을 사용해서, 각 layer에 필요한 아티펙트만을 포함하고 다른 것은 포함시키지 않게 했어야 했습니다.

이것은 builder pattern 이라고 불렀습니다. 이 패턴은 build.Dockerfile (개발 빌드용)와 Dockerfile (간소화된 배포 빌드용) 으로 나누어졌습니다.

흔히 사용하는 방법 중 쉘 명령어 꼼수로 && 를 사용해 두 명령을 압축하였습니다.

build.Dockerfile(개발용) 예시

# syntax=docker/dockerfile:1

# Golang 1.16 이미지를 기반 이미지로 사용합니다. 
# 이 이미지는 Golang 개발 환경을 구축하는 데 필요한 도구와 라이브러리를 포함합니다.
FROM golang:1.16

# 작업 디렉토리를 /go/src/github.com/alexellis/href-counter/로 설정합니다. 
# 이 디렉토리는 컨테이너 내에서 소스 코드를 저장하고 빌드할 위치를 지정합니다.
WORKDIR /go/src/github.com/alexellis/href-counter/

# 현재 디렉토리의 app.go 파일을 컨테이너의 작업 디렉토리로 복사합니다. 
# 이 파일은 Golang 프로젝트의 소스 코드일 것으로 예상됩니다.
COPY app.go ./

# Golang의 golang.org/x/net/html 패키지를 다운로드합니다. 
# -d 플래그는 패키지를 다운로드만 하고 설치는 하지 않도록 지정합니다.
#app 바이너리를 빌드합니다. CGO_ENABLED=0는 CGO(C Go Call)를 비활성화하고, 
#-a는 모든 종속성을 새로 빌드하도록 지정하며, 
#-installsuffix cgo는 CGO에 대한 설치 접미사를 추가합니다. 
# 마지막으로, -o app는 빌드된 실행 파일의 이름을 app로 지정합니다.
RUN go get -d -v golang.org/x/net/html \
  && CGO_ENABLED=0 go build -a -installsuffix cgo -o app .

이 방법은 추가적인 layer의 생성을 방지합니다. 다만 이런 방법은 실패할수도 있으며, 유지보수 측면에서는 상당이 안 좋습니다.

build.Dockerfile Golang 1.16을 기반으로 하는 alexellis/href-counter 이미지를 빌드하는 데 사용됩니다.

Dockerfile(배포용) 예시

# syntax=docker/dockerfile:1

# 최신 버전의 Alpine Linux를 기반 이미지로 사용합니다. 
# Alpine Linux는 경량 리눅스 배포판으로, 작은 크기와 빠른 속도로 알려져 있습니다.
FROM alpine:latest

# Alpine 패키지 관리자인 apk를 사용하여 ca-certificates를 설치합니다. 
# --no-cache 플래그는 패키지 캐시를 사용하지 않도록 지정합니다.
RUN apk --no-cache add ca-certificates

# 작업 디렉토리를 /root/로 설정합니다. 
# 작업 디렉토리는 컨테이너 내에서 명령을 실행할 위치를 말합니다.
WORKDIR /root/

# 현재 디렉토리의 app 파일을 컨테이너의 /root/ 디렉토리로 복사합니다. 
# 이 파일은 컨테이너 내에서 실행될 응용 프로그램 파일일 것으로 예상됩니다.
COPY app ./

# 컨테이너가 시작되었을 때 실행될 명령을 지정합니다. 
# 여기서는 /root/app을 실행하는 것으로 지정되어 있으므로, 
# 컨테이너가 시작되면 /root/app이 실행될 것입니다.
CMD ["./app"]

이 Dockerfile은 Docker 컨테이너를 빌드하고 실행하기 위한 명령어를 정의하는 파일입니다.

  1. 첫번째 이미지를 빌드합니다.(도커빌드 파일)
  1. 아티펙트를 복사할 컨테이너를 만듭니다.
  1. 두 번째 이미지를 빌드합니다.

유틸리티 스크립트 이 두개의 도커파일을 이용해서 가장 작은 도커파일을 만듭니다.

#!/bin/sh
#빌드 중인 이미지의 정보를 출력합니다.
echo Building alexellis2/href-counter:build

# build.Dockerfile을 사용하여 alexellis2/href-counter:build 이미지를 빌드합니다.
# 현재 디렉토리(.)에 있는 소스 코드를 사용하여 이미지를 빌드합니다.
docker build -t alexellis2/href-counter:build . -f build.Dockerfile

#alexellis2/href-counter:build 이미지를 기반으로 extract라는 이름의 컨테이너를 생성합니다.
#즉 golang을 기반으로하는 컨테이너 생성합니다.
docker container create --name extract alexellis2/href-counter:build

# extract 컨테이너에서 /go/src/github.com/alexellis/href-counter/app 경로에 있는 파일을 
# 호스트의 현재 디렉토리로 복사합니다. 
# 이 파일은 alexellis2/href-counter:build 이미지에서 빌드된 애플리케이션 파일입니다.
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app 

#extract 컨테이너를 강제로 제거합니다. 
docker container rm -f extract

#최신 버전(latest)의 alexellis2/href-counter 이미지를 빌드하는 작업 정보를 출력합니다.
echo Building alexellis2/href-counter:latest\

#latest 태그를 가진 alexellis2/href-counter 이미지를 빌드합니다. 
#--no-cache 플래그를 사용하여 캐시를 사용하지 않고 빌드합니다.
docker build --no-cache -t alexellis2/href-counter:latest .

#호스트의 현재 디렉토리에서 app 파일을 삭제합니다. 
#이 파일은 이전에 extract 컨테이너에서 복사된 애플리케이션 파일입니다.
rm ./app

결과적으로 app 두 이미지 모두 시스템에서 공간을 차지하며 로컬 디스크에도 아티팩트가 남게 됩니다 .

Multi-stage builds는 이런 번거로운 작업을 하던 사용자를 위해 등장했습니다. Multi-stage builds는 이것을 단순하게 만들었습니다.

Multi-stage builds

Multi-stage buildsDockerfile 에서 FROM 문을 여러 번 사용합니다.

각각 FROM 문 은 각각 다른 base를 사용할 수 있으며 각 명령어의 빌드는 new stage로 시작합니다. 당신이 from이 한 스태이지 지날 때마다 선택적으로 artifacts를 복사할 수 있습니다. 만약 당신이 마지막 이미지에 원하지 않는 것들을 다 남겨둘 수 있습니다.

# syntax=docker/dockerfile:1

FROM golang:1.16
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 apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]

이것을 이해하기 위해서는 Dockerfile의 이전 단계에 대한 이해가 필요합니다.

Multi-stage buildsDockerfile만이 필요하고 ,별도의 스크립트도 필요없이 docker build. 를 통해 실행합니다.

docker build -t alexellis2/href-counter:latest .

결국 최종 이미지는 복잡성은 줄어들었지만 이전과 동일하게 작은 배포이미지가 탄생했다.

이 방법은 middle image를 만드는 과정이 없어도 되고 로컬 시스템의 아티팩트를 추출할 필요가 없어졌습니다.

이 도커파일의 핵심은 COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./ 이 명령이다. 이 명령은 이전 스태이지에서 빌트한 것을 복사해온다. 다만 중간 아티팩트들은 뒤에 캐쉬로 남아 있으며, 최종 이미지 상에서는 저장되지 않습니다.

Stage에 이름 붙히기

기본적으로 단계는 이름이 지정되지 않으며 첫 번째 FROM 명령에 대해 0으로 시작하는 정수로 참조됩니다. 그러나 FROM에 AS <NAME>을 추가하여 단계 이름을 지정할 수 있습니다. 그래서 이전에 사용했던 Dockerfile에서 이전 스태이지를 0으로 지정했지만 이름을 붙혀서 사용이 가능합니다.

# syntax=docker/dockerfile:1

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 apk --no-cache add ca-certificates
WORKDIR /root/
#여기를 보면 from=스태이지 이름 으로 카피해온다.
COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]

이렇게 해주면 나중에 Dockerfile의 순서가 바뀌는 경우에도 Copy 대상이 정확하게 지정되어있기 때문에 더 좋은 방법 입니다.

특정 빌드 단계에서 중지

이미지를 빌드할 때 모든 단계를 포함하여 전체 Dockerfile을 반드시 빌드할 필요는 없습니다. 대상 빌드 단계를 지정할 수 있습니다.

다음 명령은 사용자가 이전 도커 파일을 사용하고 있지만, builder라는 단계에서 중지됩니다:

docker build --target builder -t alexellis2/href-counter:latest .

이것은 특정 빌드 단계에서 디버깅할 때 사용할 수 있습니다.

또는, testing앱이 테스트 데이터로 채워지는 단계를 사용 하지만 실제 데이터를 사용하는 다른 단계를 사용해서 특정 빌드단계에서 중단 하는 경우 입니다.

외부 이미지를 Stage로 사용

Multi-stage builds 는 Dockerfile 내에서 만든 이미지만을 복사할 수 있는게 아닙니다.

COPY --from명령을 사용하여 로컬 이미지이름(태그ID 등)을 사용해서, 별도의 이미지에서 복사가 가능합니다.

다음은 예시입니다.

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

이전 스태이지를 FROM에 사용

# syntax=docker/dockerfile:1

FROM alpine:latest AS builder
RUN apk --no-cache add build-base

FROM builder AS build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp

FROM builder AS build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp

BuildKit 활성화

이전에 사용하는 빌드는 도커파일의 모든 부분을 다 사용합니다. --target선택한 대상이 해당 단계에 의존하지 않더라도 단계를 구축합니다.

하지만 BuildKit 을 사용하면, 대상 단계가 의존하는 단계만 빌드합니다.

예를 들어 다음과 같은 도커파일이 있다고 보겠습니다.

# syntax=docker/dockerfile:1
FROM ubuntu AS base
RUN echo "base"

FROM base AS stage1
RUN echo "stage1"

FROM base AS stage2
RUN echo "stage2"

BuildKit을 사용하도록 설정한 상태에서,stage2base만 빌드가 되고, stage 1 은건너뛰는 모습을 볼 수 있습니다.

아래는 BuildKit으로 빌드한 모습입니다.

 DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile --target stage2 .
[+] Building 0.4s (7/7) FINISHED
 => [internal] load build definition from Dockerfile                                            0.0s
 => => transferring dockerfile: 36B                                                             0.0s
 => [internal] load .dockerignore                                                               0.0s
 => => transferring context: 2B                                                                 0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                0.0s
 => CACHED [base 1/2] FROM docker.io/library/ubuntu                                             0.0s
 => [base 2/2] RUN echo "base"                                                                  0.1s
 => [stage2 1/1] RUN echo "stage2"                                                              0.2s
 => exporting to image                                                                          0.0s
 => => exporting layers                                                                         0.0s
 => => writing image sha256:f55003b607cef37614f607f0728e6fd4d113a4bf7ef12210da338c716f2cfd15    0.0s

하지만 BuildKit이 없이 빌드하면 전부 다 빌드가 됩니다.

DOCKER_BUILDKIT=0 docker build --no-cache -f Dockerfile --target stage2 .


Uploaded by N2T

728x90