[Golang] Docker 멀티 스테이지 빌드로 한 큐에 이미지 만들기

멀티 스테이지 빌드 기능을 이용하면 Go 컴파일러가 없는 환경에서도 명령어 한 줄로 Docker 이미지를 만들 수 있습니다.

2020. 03. 06. #development #docker #go

"Go with Docker" 시리즈 글

이전 포스트에서는 778MB에 달하는 Go 언어의 docker 이미지를 1.36MB로 경량화하는 방법에 대해 다루었습니다. 그러나 해당 과정에는 Go 컴파일러가 필요하기 때문에 개발 환경 혹은 CI/CD 환경에 컴파일러를 설치해야 하는 번거로움이 있었습니다.

이번 포스트에서는 이러한 컴파일 과정조차 Docker에서 실행하여, Dockerfile만 있다면 어느 환경에서나 명령어 한 줄로 Docker 이미지를 만드는 방법에 대해 알아보겠습니다.

멀티 스테이지 빌드란?

특정 언어의 컴파일러를 컴퓨터에 직접 설치하는 것은 꽤나 피곤한 일입니다. GVM 같은 버전 매니저 도구를 사용하면 조금 나아지긴 하지만, 여러 서버 장비와 CI/CD 환경까지 언어를 설치하고 유지보수 하는 것은 여간 어려운 일이 아니죠. 그래서 사람들은 바이너리 파일을 빌드하는 과정조차 Docker를 이용하기 시작했습니다.

과거에는 이러한 행동을 하려면 두 개 이상의 Dockerfile이 필요했습니다. 바이너리를 빌드하기 위한 하나, 빌드한 바이너리를 실행하기 위한 경량화된 이미지 하나. 전자는 흔히 'builder' 이미지라고 불리고, 후자는 지난 포스트에서 우리가 만들었던 바로 그 이미지입니다. 이러한 관리 방법을 builder 패턴이라고 부릅니다.

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

이후 많은 사람들이 불편함을 느끼자, 여러 단계의 이미지 빌드 과정을 하나의 Docerfile에서 관리하기 위해 멀티 스테이지 빌드가 등장했습니다.

Dockerfile

백문이 불여일견, 바로 Dockerfile부터 살펴봅시다.

### Builder
FROM golang:1.13-alpine as builder

WORKDIR /usr/src/app
COPY . .

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-s' -o main .


### Make executable image
FROM scratch

COPY --from=builder /usr/src/app/main /main
CMD [ "/main" ]

(참고 : 이 파일에는 아직 몇 가지 문제가 있습니다. 최종 파일은 아래로 더 내려가 확인해주세요!)

일단 FROM 커맨드가 두 개인 것을 확인하실 수 있습니다. 각각의 FROM새로운 빌드 스테이지(단계)가 시작되었음을 나타냅니다. 따라서 이 Dockerfile에는 두 개의 스테이지가 있는 셈이죠.

builder 스테이지

첫 번째 FROM에 해당하는 builder 스테이지는 이름 그대로 Go 바이너리를 빌드하는 단계입니다. golang:1.13-alpine 이미지를 이용해 소스 코드를 바이너리 형태로 빌드하고, 그 결과물을 main 이라는 파일로 떨구게 됩니다.

executable 스테이지

두 번째 FROM에 해당하는 스테이지에는 특별히 이름이 있지는 않습니다. 그러나 마지막 스테이지는 일반적인 Dockerfile과 동일하게 컨테이너로 실행되는 결과물이 나오므로, 저는 executable 스테이지라고 이름을 붙였습니다.

자세히 보시면 COPY 명령어에 --from=builder 라는 옵션이 붙어있습니다. 이는 COPY 명령어를 도커 호스트가 아닌 builder 스테이지로부터 실행한다는 뜻입니다.

builder 스테이지에서 /usr/src/app/main 경로에 바이너리를 생성했는데요, 해당 파일을 최종 이미지의 /main으로 복사하게 됩니다.

이 Dockerfile을 이용해 이미지를 만들어봅시다. (코드는 지난 포스트의 것을 재활용하겠습니다.)

REPOSITORY     TAG       IMAGE ID        CREATED          SIZE
golang-demo    latest    cd8f99bb0468    2 seconds ago    1.44MB

경량화된 1.44MB의 이미지가 생성되었습니다.

용량으로 확인할 수 있듯이, builder 스테이지의 파일들은 최종 빌드된 이미지에 포함되어있지 않았습니다. 항상 마지막 스테이지가 최종 이미지의 결과물이 된다는 점을 참고해주세요.

Docker 멀티 스테이지 빌드에 관한 더 자세한 설명은 공식 문서에서 확인하실 수 있습니다.

Go 언어 이미지에서 조심해야 할 점

앞선 과정처럼 scratch 에서 바이너리만 추가하여 Docker 이미지를 만드는 경우, 조심해야 할 점이 몇 가지 있습니다.

  • Go는 의존성 관리에 git이 필요한 경우가 많습니다. 그러나 alpine 이미지에는 git이 포함되어있지 않습니다.
  • scratch 이미지에는 암호화된 통신을 위한 CA 인증서가 들어있지 않습니다. 때문에 HTTPS를 비롯한 SSL 통신을 하는 경우 오류가 발생하게 됩니다.
  • 간혹 유저 관리를 위해 /etc/passwd 바이너리 등이 필요한 때도 있습니다. 당연히 scratch 이미지에는 없구요.

이러한 문제를 고려하여, 최종적으로 완성된 Dockerfile 다음과 같습니다.

### Builder
FROM golang:1.13-alpine as builder
RUN apk update && apk add git && apk add ca-certificates

WORKDIR /usr/src/app
COPY . .

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-s' -o main .


### Make executable image
FROM scratch

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /usr/src/app/main /main

CMD [ "/main" ]

이렇게 빌드를 하여 최종적으로 우리가 만든 이미지의 용량은 1.67MB가 되었습니다.

멀티 스테이지 빌드를 이용하면 docker만 설치되어있다면 어느 환경에서나 여러분의 코드를 Docker 이미지로 만들 수 있게 됩니다. 나아가 GitHub Actions와 Docker Hub 빌드 시스템을 훨씬 편리하게 구성할 수 있게 되죠. 기존에 복잡한 빌드 과정을 겪고 있었다면 지금 바로 도전해보시기 바랍니다.

크리에이티브 커먼즈 라이선스

이 저작물은 크리에이티브 커먼즈 저작자표시-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.

© 2011 - 2020 Do Hoerin, LYnLab