[Docker軽量化] マルチステージビルドのステージ間COPYでは絶対パスが無難。

k8sを勉強しようと本を開いたらDockerイメージ軽量化の内容に遭遇したので、備忘録として。

# マルチステージビルドとは
ざっくりまとめると、**1つのDockerfileで複数のベースイメージを段階的に使用できる**。

例えば、golangのアプリケーション用イメージを作成する場合、僕だったらこう書いてしまう…

```Dockerfile
# 通常(シングルステージビルド?)の書き方
FROM golang:1.14.1-alpine3.11

COPY ./main.go ./

RUN go build -o ./app ./main.go

ENTRYPOINT ["./app"]
```

にわかエンジニアなので、alpineイメージで軽量だぜ！(ドヤァ)みたいな。笑

```bash
# イメージビルド
$ docker build -t goapp1 .

# イメージサイズの確認
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
goapp1              latest              e44201621bca        6 seconds ago       377MB
alpine              3.11                e389ae589224        4 months ago        5.62MB
golang              1.14.1-alpine3.11   760fdda71c8f        17 months ago       370MB
```

でも、ここから更に軽量化できるらしい。

## 処理毎にベースイメージを分割し、必要なものだけを最終イメージに残して軽量化する。

一方で、マルチステージビルドを活用すると、ビルド用イメージと実行用イメージを分けられる。

```dockerfile
# マルチステージビルド

# 1つ目のベースイメージにbuilderという名前を付ける
FROM golang:1.14.1-alpine3.11 as builder
# ローカルファイルをbuilderイメージ内にコピー
COPY ./main.go ./
# ビルド先を指定
RUN go build -o /app ./main.go

# 計量イメージを2つ目のベースイメージとして指定
FROM alpine:3.11
# builderイメージからファイルをコピー
COPY --from=builder /app .
# 2つ目のイメージだけコマンド実行
ENTRYPOINT ["./app"]
```

処理内容としては、以下のように**2つのイメージ間でbuildとrunの各処理が連携・分担される**。

**1. builder(golang:1.14.1-alpine3.11)イメージでmain.goをビルド**

**2. builderイメージでビルドしたappバイナリファイルをalpine:3.11イメージ内にコピー**

**3. alpine:3.11イメージでappを実行**

したがって、最終的には2つ目のalpine:3.11イメージだけを使用するため、イメージが軽量化される。

```bash
# マルチステージビルドを使用したDockerfileからイメージビルド
$ docker build -t goapp2 .

# イメージサイズの確認
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
goapp2              latest              8fb96a4a5138        3 seconds ago       13.1MB
<none>              <none>              a8884f1d05c5        4 seconds ago       377MB
goapp1              latest              e44201621bca        13 minutes ago      377MB
alpine              3.11                e389ae589224        4 months ago        5.62MB
golang              1.14.1-alpine3.11   760fdda71c8f        17 months ago       370MB
```

**goapp1 (377MB) から、goapp2 (13.1MB) のように、イメージが軽量化されていることがわかる**。

※1つ目のbuilderイメージが\<none\>として残っているが、不要であれば削除しても問題ない。

```bash
# IMAGE IDを指定して, <none>イメージ(builderイメージ)を削除
$ docker image rmi a8884f1d05c5

# イメージ一覧を表示
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
goapp2              latest              8fb96a4a5138        3 minutes ago       13.1MB
goapp1              latest              e44201621bca        16 minutes ago      377MB
alpine              3.11                e389ae589224        4 months ago        5.62MB
golang              1.14.1-alpine3.11   760fdda71c8f        17 months ago       370MB
```

# ステージ間COPYの注意点

マルチステージビルドを試していてハマった部分として、**ステージ間のCOPY時に相対パスだとエラーになることがあった**。


```dockerfile
# [NG例!!!]

FROM golang:1.14.1-alpine3.11 as builder
COPY ./main.go ./
# "相対パス"でアウトプット先を指定
RUN go build -o ./app ./main.go

FROM alpine:3.11
# builderイメージから"相対パス"でファイルをコピー
COPY --from=builder ./app .
ENTRYPOINT ["./app"]
```

イメージのビルド結果

```bash
$ docker build -t ng-goapp .
Sending build context to Docker daemon    6.4MB
Step 1/6 : FROM golang:1.14.1-alpine3.11 as builder
 ---> 760fdda71c8f
Step 2/6 : COPY ./main.go ./
 ---> Using cache
 ---> 422b5182bade
Step 3/6 : RUN go build -o ./app ./main.go
 ---> Using cache
 ---> 524167b98ef9
Step 4/6 : FROM alpine:3.11
 ---> e389ae589224
Step 5/6 : COPY --from=builder ./app .
COPY failed: stat /var/lib/docker/overlay2/68c7926017fabca29e5aa3a12024d52b2e6e2f000dd08f046a8f698fc2079bf1/merged/app: no such file or directory
# ファイルが見つからず, エラーになる!
```

上記のエラーを解消したい。

## 各ベースイメージにおけるデフォルトのカレントディレクトリが同一とは限らない。

ステージ間のCOPY時に相対パスだとエラーになってしまう原因は、**各ベースイメージにおけるデフォルトのカレントディレクトリが異なっているため**だった。

```dockerfile
FROM golang:1.14.1-alpine3.11
# カレントディレクトリを表示
ENTRYPOINT ["pwd"]
```

1つ目の(builder)イメージにおけるデフォルトのカレントディレクトリは、**/go** らしい。

```bash
$ docker image build -t test1 .
$ docker run test1
/go
```

同様の手順で、2つ目のapp実行用イメージにおけるデフォルトのカレントディレクトリを調べる。

```dockerfile
FROM alpine:3.11
# カレントディレクトリを表示
ENTRYPOINT ["pwd"]
```

こちらは、**/** らしい。

```bash
$ docker image build -t test2 .
$ docker run test2
/
```

よって、main.goが1つ目のイメージで **/go/app** にビルドされたのに対し、2つ目のイメージでは **/app** からコピーしようとしているため、エラーになっていたのだ。

## [結論] ステージ間のCOPYには、絶対パスを使った方が無難。

総括として、ベースイメージ毎に相対パスの解釈はズレる可能性があるため、**マルチステージビルドでCOPYする時には絶対パスを使ったほうが余計なバグは減らせる**。

また、**そもそもmain.goのビルド先も絶対パスで指定した方が安全**と言える。

```dockerfile
# マルチステージビルド

FROM golang:1.14.1-alpine3.11 as builder
COPY ./main.go ./
# "絶対パス"でビルド先を指定
RUN go build -o /app ./main.go

FROM alpine:3.11
# builderイメージから"絶対パス"でファイルをコピー
COPY --from=builder /app .
ENTRYPOINT ["./app"]
```

Dockerイメージはビルド時のデバッグが見えにくい分、Dockerfileで処理を明確化させるのが肝らしい。勉強になった。

# 参考文献
書籍
[⻘⼭ 真也『Kubernetes完全ガイド 第2版』 インプレス社, 2020年](https://book.impress.co.jp/books/1119101148)

webサイト
[Dockerドキュメント「Dockerfile を書くベスト・プラクティス - WORKDIR](http://docs.docker.jp/v1.12/engine/userguide/eng-image/dockerfile_best-practice.html#workdir)
[Webエンジニアの雑記録「DockerfileのCOPYコマンドについて」(2019年12月20日)](https://fantasiabaetica.hatenablog.com/entry/2019/12/20/094516)