지난 포스팅에서 의존성 설치 부분을 Dockerfile 상단에 배치해 빌드 속도를 최적화 하는 방안을 다루었다. 그러나 이 방법은 의존성에 아주 조그마한 변화만 생겨도 모든 의존성 설치 과정을 처음부터 다시 시작한다는 약점이 있다. 이번 포스팅에선 cache mount를 이용해 이런 상황을 방지하는 방법에 대해 다룬다.
지난 예제에서 godotenv 라이브러리의 의존성을 제거해보자. 코드에서 godotenv를 사용하는 부분을 지우고 go mod tidy
명령을 수행하면 go.mod 파일의 내용이 자동으로 변경된다. 이후 Dockerfile을 빌드해보면 go mod download
과정이 처음부터 다시 실행되는 것을 알 수 있다.
[+] Building 13.4s (14/14) FINISHED docker:orbstack
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 378B 0.0s
=> [internal] load metadata for docker.io/library/golang:1.18 0.8s
=> [internal] load metadata for docker.io/library/golang:latest 0.8s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [builder 1/6] FROM docker.io/library/golang:latest@sha256:969349b8121a56d51c74f4c273ab974c15b3a8ae246a5cffc1df7d28b66cf978 0.0s
=> CACHED [stage-1 1/2] FROM docker.io/library/golang:1.18@sha256:50c889275d26f816b5314fc99f55425fa76b18fcaf16af255f5d57f09e1f 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 16.15kB 0.0s
=> CACHED [builder 2/6] WORKDIR /app 0.0s
=> [builder 3/6] COPY go.mod go.sum ./ 0.0s
=> [builder 4/6] RUN go mod download 4.7s
=> [builder 5/6] COPY . . 0.0s
=> [builder 6/6] RUN go build -o myapp 7.6s
=> [stage-1 2/2] COPY --from=builder /app/myapp /myapp 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:3ab766554cf7b595b0dd9aad7d1d5b1992e64b5bb8ca0ce8bee23774a169e06d 0.0s
=> => naming to docker.io/library/docker-test 0.0s
만약 컨테이너가 아닌 베어메탈이나 VM 장비였다면 이미 설치한 의존성을 다시 다운로드 받는 일은 없었을 것이다. cache mount 기능을 사용한다면 이런 상황을 방지할 수 있다. 의존성을 설치하는 부분을 다음과 같이 바꿔보자
FROM golang AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=$GOPATH/pkg/mod/ \
go mod download
COPY . .
RUN --mount=type=cache,target=$GOPATH/pkg/mod/ \
go build -o myapp
# 실행 단계
FROM golang:1.18
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
의존성을 설치하는 부분에 --mount
라는 플래그가 추가 되었다. 컨테이너의 $GOPATH/pkg/mod
디렉토리를 호스트 머신에 캐싱 하겠다는 뜻이다. 똑같이 의존성을 하나 삭제하고 빌드를 재실행해보자.
[+] Building 8.5s (14/14) FINISHED docker:orbstack
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 332B 0.0s
=> [internal] load metadata for docker.io/library/golang:latest 0.9s
=> [internal] load metadata for docker.io/library/golang:1.18 0.9s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> CACHED [stage-1 1/2] FROM docker.io/library/golang:1.18@sha256:50c889275d26f816b5314fc99f55425fa76b18fcaf16af255f5d57f09e1f 0.0s
=> [builder 1/6] FROM docker.io/library/golang:latest@sha256:969349b8121a56d51c74f4c273ab974c15b3a8ae246a5cffc1df7d28b66cf978 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 16.15kB 0.0s
=> CACHED [builder 2/6] WORKDIR /app 0.0s
=> [builder 3/6] COPY go.mod go.sum ./ 0.0s
=> [builder 4/6] RUN --mount=type=cache,target=/go/pkg/mod/ go mod download 0.2s
=> [builder 5/6] COPY . . 0.0s
=> [builder 6/6] RUN --mount=type=cache,target=/go/pkg/mod/ go build -o myapp 7.1s
=> [stage-1 2/2] COPY --from=builder /app/myapp /myapp 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:0af43254eff03d44a963800bd707d79b55b51df9bea90c6c18db6d53e01597a9 0.0s
=> => naming to docker.io/library/docker-test 0.0s
레이어 전체가 캐싱되지 않았는데도 의존성 설치 시간이 7초에서 0.2초로 감소한 것을 알 수 있다.
이 방식은 캐싱을 위해 호스트 장비의 디스크를 추가적으로 사용하는 트레이드오프가 있다. 그러나 대체로 엔지니어의 시간은 디스크 보다 비싼 자원이므로 사용할만한 가치가 충분한 기능이다.