초경량 Golang Docker 이미지 만들기 (1)

Scratch 이미지를 이용해 Docker 이미지를 778MB에서 1MB로 경량화 시키는 방법!

2018. 09. 09. #development #docker #go

"Go with Docker" 시리즈 글

다음은 10초에 한 번씩 Hello, world! 를 출력하는 Go 코드입니다.

// main.go
package main

import(
    "fmt"
    "time"
)

func main() {
    for {
        fmt.Println("Hello, world!")
        time.Sleep(10 * time.Second)
    }
}

지금부터 이 코드를 Dockerize하여 봅시다.

 

전통적인 방법

대부분의 언어, 특히 Python같은 스크립트 언어나 Java처럼 VM 위에서 작동하는 언어는 다음과 같은 방법으로 Docker 이미지를 만듭니다.

  1. 해당 언어의 베이스 이미지를 사용
  2. 구동에 필요한 라이브러리를 설치
  3. 스크립트나 실행 파일을 이미지로 복사
  4. 명령어 등 실행 환경을 설정

이 방법을 이용하여 이미지를 만드는 Dockerfile는 다음과 같습니다.

# Run command below to build binary.
#   go build -o main . 

FROM golang:1.11
WORKDIR /usr/src/app
COPY . .
CMD ["main"]

이렇게 만들어진 이미지를 확인해보면...

REPOSITORY      TAG         IMAGE ID        CREATED         SIZE
golang-demo     latest      b9bf0ab8e5c3    50 seconds ago  778MB

778MB에 달합니다! 바이너리의 크기가 1.9MB에 불과함에도 이미지의 크기가 이렇게 큰 이유는 golang:1.11 베이스 이미지의 크기부터가 776MB이기 때문입니다. golang 이미지는 debian 이미지를 확장시킨 것으로, OS의 기본적인 컴포넌트를 담고있기 때문에 그 자체로 상당한 용량을 차지합니다.

하지만 Go 언어를 dockerize하기 위해서는 이렇게 큰 베이스 이미지를 이용할 필요가 없습니다. 바이너리만 있다면 독립적으로 실행이 가능하기 때문입니다.

 

Scratch 이미지를 이용한 경량화 방법

우선 Docker Hub에서 scratch 이미지의 설명을 읽어봅시다.

An explicitly empty image, especially for building images "FROM scratch".

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).

텅 비어있는 이미지로, 베이스 이미지 또는 super minimal image를 만들기에 유용하다고 합니다. 저희의 목적과 부합하는군요.

 

Scratch를 베이스 이미지로 삼기 위해서는, 우선 go build 명령어를 실행할 때 약간(?)의 변화를 주어야합니다.

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

굉장히 복잡해보이는데요, 차근차근 살펴보면

  • CGO_ENABLED=0 : cgo를 사용하지 않습니다. Scratch 이미지에는 C 바이너리조차 없기 때문에, 반드시 cgo를 비활성화 후 빌드해야합니다.
  • GOOS=linux GOARCH=amd64 : OS와 아키텍쳐 설정입니다.
  • -a : 모든(all) 의존 패키지를 cgo를 사용하지 않도록 재빌드합니다.
  • -ldflags '-s' : 바이너리를 조금 더 경량화하는 Linker 옵션입니다. (참고 링크)

이제 이 빌드 결과물로 이미지를 만들어봅시다.

# Run command below to build binary.
#   CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-w -s' -o main main.go

FROM scratch
WORKDIR /usr/src/app
COPY . .
CMD ["main"]

만들어진 이미지는 다음과 같습니다.

REPOSITORY      TAG         IMAGE ID        CREATED         SIZE
golang-demo     latest      1b5a4460900c    4 seconds ago   1.36MB

무려 1.36MB로 최적화 되었습니다. 이미지에 바이너리 외에는 아무것도 들어있지 않기 때문입니다.

 

하지만 이 방법은 번거롭게도 매번 빌드할 때마다 위 옵션들을 주어야하며, 빌드 환경에 따른 바이너리의 동일성을 보장하지 못한다는 문제가 있습니다.

이에 다음 포스트에서는 Docker 멀티 스테이지 빌드를 이용하여, 하나의 Dockerfile로 바이너리 빌드와 실행 이미지 빌드를 한 번에 실행하는 방법을 다뤄보도록 하겠습니다.

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

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

© 2011 - 2020 Do Hoerin, LYnLab