マルチステージビルドとdistrolessイメージを活用してGoイメージを極限まで軽量化する

**マルチステージビルド** は，Dockerイメージのビルドを複数のステージに分割する機能です．Goのソースコードをビルドするステージと実行時のステージを分離することで，実行時のイメージサイズを軽減できます

実行時のイメージには **Alpine** のような軽量Linuxが使われることが多いですが，[distrolessイメージ](https://github.com/GoogleContainerTools/distroless)も良く知られています．distrolessイメージは，Debianイメージに含まれるファイルを極限まで削減したもので，シェルすら含まれていません．

> Distroless images are very small. The smallest distroless image, gcr.io/distroless/static-debian11, is around 2 MiB. That's about 50% of the size of alpine (~5 MiB), and less than 2% of the size of debian (124 MiB).
https://github.com/GoogleContainerTools/distroless

今回は実行時のイメージサイズを alpine / distrolessとした場合で比較してみます

**ソースコード** ([Dockerfile](https://github.com/tf63/docker-images/blob/master/go/Dockerfile))

https://github.com/tf63/docker-images/tree/master/go

スターを頂けると励みになります


## builderイメージ
Goのソースコードをビルドするステージです．次の3つのステージで構成しています
- baseステージ: 各ステージ間で共通する処理を実行
- depsステージ: パッケージの依存関係を解決
- builderステージ: Goのソースコードをビルド

Goのビルド成果物 (実行ファイル) は`server`という名前にしています

```Dockerfile
FROM golang:1.19-alpine as base

# ワークディレクトリの指定
WORKDIR /app

# ----------------------------------------------------------------
# 依存関係の解決
# ----------------------------------------------------------------
FROM base as deps

# モジュールのダウンロード
COPY go.mod go.sum ./
RUN go mod download

# ----------------------------------------------------------------
# ビルド
# ----------------------------------------------------------------
FROM base as builder

COPY --from=deps /go/pkg /go/pkg
COPY . .

# 外部依存の無い実行ファイルを作る
ARG CGO_ENABLED=0
# 64bit linux用にビルドする
ARG GOOS=linux
ARG GOARCH=amd64
# ビルド成果物にデバック情報が含まれないようにする
RUN go build -ldflags '-s -w' -o ./server
```

## 実行時のイメージ (runner)

### alpineイメージを利用する場合
builderステージから実行ファイル`server`をコピーし，ENTRYPOINTで実行します
```Dockerfile
# イメージのタグを指定した方が良いが，面倒なので今回は省略
FROM alpine as runner

WORKDIR /app

RUN addgroup --system --gid 10001 nonroot
RUN adduser --system --uid 10001 nonroot

COPY --from=builder --chown=nonroot:nonroot /app/server .
ENTRYPOINT ["./server"]

USER nonroot

EXPOSE 8080
```

### distrolessイメージを利用する場合
こちらのリポジトリから利用可能なイメージを選びます．今回は最も軽量な`gcr.io/distroless/static-debian12`を選びました．python, java, nodejsのランタイムが含まれるイメージも利用できます

https://github.com/GoogleContainerTools/distroless

alpineの場合と同様に`server`をコピーし，ENTRYPOINTで実行します

```Dockerfile
FROM gcr.io/distroless/static-debian12:nonroot as runner-distroless

WORKDIR /app

USER nonroot

COPY --from=builder /app/server .

ENTRYPOINT ["./server"]

EXPOSE 8080
```

:::note warn
`ENTRYPOINT ./server`としてはいけません．この書き方ではシェルを介して`./server`を実行しますが，distrolessイメージにはシェルが含まれないので実行できません
https://kinsta.com/jp/blog/dockerfile-entrypoint/
:::

### イメージサイズの比較
それぞれのイメージを適当な名前でビルドします
```shell
    docker build . --target runner-alpine -t go-alpine
    docker build . --target runner-distroless -t go-distroless
```

イメージサイズを確認します
```shell
docker images | grep go-

go-alpine       latest     7034c38b2fcf   48 seconds ago      13.3MB
go-distroless   latest     e9c3cc892aff   About an hour ago   6.46MB
```

alpineイメージが **13.3MB** でdistrolessイメージが **6.46MB** となりました．およそ半分にまでイメージサイズを削減できます

## 開発環境 (おまけ)
おまけで開発環境も載せておきます．[こちらの記事](https://zenn.dev/awonosuke/articles/dfe3a44486feb9)を参考にしました

Goのホットリロードツールである**air**を利用します
```Dockerfile
FROM golang:1.19-alpine as dev

WORKDIR /app

RUN go install github.com/cosmtrek/air@v1.40.0

ENTRYPOINT ["air"]
```
:::note warn
airの最新バージョンは`github.com/air-verse/air`にリポジトリが変更されているので注意です
:::

開発環境ではdocker-composeでワークスペースをvolumeマウントしています．docker-composeではビルドステージを指定するために`build > args`へ`- target=dev`を指定します

```yaml
# docker-composeの例
version: "3.3"

services:
  go:
    container_name: go
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - target=dev
    volumes:
      - .:/app
    ports:
      - 8080:8080
```

以上です