【Docker】既存イメージを使って、サイズを小さくするマルチステージビルド

# はじめに

この記事ではマルチステージビルドを使って、Dockerイメージを作成する方法を考えます。Terraformについてのイメージを使います。
本当はKubernetesでTerraformを使う方法を調べたかったのですが、イメージ作成のところで詰まったのでまとめることにしました。

# 記事の概要

Terraformは既存イメージがあるので、手間を省くために使います。
https://hub.docker.com/r/hashicorp/terraform/

この記事では既存リソース（Kubernetesリソース、AWS）がある状態で、新しくTerraformを使い始める場合を考えます。この場合、既存リソースをTerraformに取り込む機能があったらいいなと思います。そこで、今回はTerraformerに触ってみることにしました。

https://dev.classmethod.jp/articles/terraformer-aws-check/

DockerからTerraformerを使う場合、自分でイメージを使う必要があります。他記事でも紹介されていますが、加えてマルチステージビルドでイメージのサイズを小さくする方法を考えます。

https://qiita.com/mziyut/items/e84d3e9f3646507c0b2a

https://docs.docker.jp/develop/develop-images/multistage-build.html

記事の流れとしては以下の通りです。

- DockerにてTerraformを使う
- Terraformerを使えるイメージを作成する
    - ベースイメージの選択
    - マルチステージビルドの実践
- Terraformerを使ってみる
    - AWS
    - Kubernetes（失敗）

# DockerにてTerraformerを使う

まず、DockerをEC2にインストールします。今までの記事とやり方は同じなので、詳細は割愛します。
1点変えたのは、`newgrp docker`で再ログインせずに一時的にグループを変更しました。ただし新しいshellになるので、shファイルの以降の処理が行われなくなることに注意しました。

https://qiita.com/kokohei39/items/89a975564a513a5025a4#２dockerを実行できる環境の作成

```bash:setup-docker.sh
#!/bin/bash -ex

cd $HOME

echo "install docker"
DOCKER_VERSION=20.10.13-2.amzn2 #yum list | grep docker
sudo yum install -y docker-$DOCKER_VERSION
sudo systemctl start docker
sudo systemctl enable docker
sudo gpasswd -a ec2-user docker
newgrp docker
```

# Terraformerを使えるイメージを作成する

## ベースとなるイメージの選択

Terraformerを使えるイメージを作成しますが、`FROM`で指定するベースのイメージを何にすればよいか考えます。Terraform、Terraformerを実行するためのソースファイルは必要ですが、それ以外は用途に応じて変更するとします。この記事ではイメージサイズに注目して、不要なファイルは除きできるだけ小さくできないかを考えます。以下のイメージを使うことを考えます。

|イメージ名|サイズ|備考|
|---|---|---|
|Terraform|110MB|Terraformが入ったイメージ<br>https://hub.docker.com/r/hashicorp/terraform/|
|Alpine|5.54MB|軽量なイメージとしてよく見る印象があります。|
|Distroless(static)|2.36MB|Alpineよりも軽いイメージです。<br>https://console.cloud.google.com/gcr/images/distroless/GLOBAL/static|
|CentOS7|204MB|参考までに色々入っているであろうCentOSのサイズを調べました。<br>https://hub.docker.com/_/centos|

サイズを見ると、Terraformのイメージは100MB以上あります。サイズを小さくするとしても100MBは超えるかもしれません。AlpineとDistrolessは２倍ほどサイズは違いますが、100MBのサイズ感で見れば、些細な差かもしれません。そのため、入っているソースファイルの差も考慮するとよいかもしれません。

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

上記で挙げたイメージをベースとして、Terraformerイメージを作成します。また、必要なファイルだけ入ったイメージを作成する方法としてマルチステージビルドを使います。
マルチステージビルドでは、最終的に出来上がるイメージとは別に、ソースファイルのビルドを行うために中間的なイメージを使うことができます。これにより、出来上がるイメージから、インストール時にのみ使うファイルや使わないパッケージを除きやすくなります。
注意点としては、中間的なイメージがいくつができるので、ディスク容量が必要になることがあります。

https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/

### Terraformベース（CentOS7、aws-cli経由）

それでは、TerraformイメージにTerraformerをインストールして、イメージを作成します。使うイメージの役割は以下の通りです。

|イメージ名|役割|備考|
|---|---|---|
|hashicorp/terraform:0.13.7|Terraform、Terraformerの実行|出来上がったイメージからTerraform、Terraformerを実行します。|
|centos:centos7|Terraformerのインストール|Terraformイメージでは、ごみが残ったり、インストール用のツールがないかもしれないので、CentOSイメージを使います。|
|amazon/aws-cli:2.7.21|AWSのconfig,credentialの設定|TerraformerからAWSにアクセスするために使います。AWSのアクセス用のファイルを作るだけですが、細かいミスをしないようAWS CLIを使ってみました。|

Try&Errorしつつ、最終的なDockerfileは以下の通りです。

```Dockerfile:Dockerfile
#AWSの認証情報。ビルド時に指定して、Dockerfileには書かないようにしました。
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ARG AWS_DEFAULT_REGION
ARG AWS_DEFAULT_OUTPUT=json
#Terraform、Terraformerについて
ARG TERRAFORMER_VERSION=0.8.21
ARG PROVIDER=all
ARG TMPDIR=/tmp/terraformer
ARG HOME_DIR=/root
ARG PROVIDER_FILE=provider.tf

FROM centos:centos7 as builder
#https://github.com/GoogleCloudPlatform/terraformer#installation
ARG TERRAFORMER_VERSION
ARG PROVIDER
ARG TMPDIR
WORKDIR ${TMPDIR}
RUN curl -LO https://github.com/GoogleCloudPlatform/terraformer/releases/download/${TERRAFORMER_VERSION}/terraformer-${PROVIDER}-linux-amd64
RUN chmod +x terraformer-${PROVIDER}-linux-amd64

FROM amazon/aws-cli:2.7.21 as aws-config
#https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ARG AWS_DEFAULT_REGION
ARG AWS_DEFAULT_OUTPUT
RUN aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
RUN aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
RUN aws configure set region $AWS_DEFAULT_REGION
RUN aws configure set output $AWS_DEFAULT_OUTPUT

FROM hashicorp/terraform:0.13.7
#Terraformerがサポートしているバージョンを指定します。（https://github.com/GoogleCloudPlatform/terraformer#capabilities）
ARG PROVIDER
ARG TMPDIR
ARG HOME_DIR
ARG PROVIDER_FILE
WORKDIR $TMPDIR
#Terraformのプラグインをインストールするために/tmp/terraformerディレクトリを作成します。こうしないとエラーになりました。
WORKDIR $HOME_DIR
COPY --from=builder ${TMPDIR}/terraformer-${PROVIDER}-linux-amd64 /usr/local/bin/terraformer
#必要な実行ファイルのみコピーして持ってきています。
COPY --from=builder /lib64/ld-linux-x86-64.so.2 /lib64/libpthread.so.0 /lib64/libc.so.6 /lib64/
#Terraformerの実行ファイルの依存ライブラリも持ってくる必要があります。
COPY --from=aws-config $HOME_DIR/.aws $HOME_DIR/.aws
#AWSの認証情報が入ったファイルをコピーして持ってきます。
COPY $PROVIDER_FILE $HOME_DIR/
RUN ["terraform","init"]
ENTRYPOINT [""]
#もともとterraformが実行されるようになっているので、上書きしました。
```

```tf:provider.tf
provider "aws" { version = "4.25.0" }
provider "kubernetes" { version = "2.12.1" }
```

Dockerfileで行っていることは上記に記載しました。他注意点としては以下の通りです。
・Terraformイメージでどこまで処理できるか不明なため、なるべく`RUN`は使わず、エラーとなることを防ぎました。
・Terraformerの依存ライブラリは以下のコマンドで確認できます。エラー文だけだとわかりづらいので注意が必要でした。

```bash
[root@4dcc09333e27 terraformer]# ldd terraformer-all-linux-amd64
        linux-vdso.so.1 =>  (0x00007ffee23fc000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fe0df421000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fe0df053000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fe0df63d000)
```

それでは、ビルドしてみます。

```bash
[ec2-user@ip-10-0-0-203 ~]$ docker build \
-f $HOME/Terraform/Dockerfile . \
-t my_tfr:Terraform \
--build-arg AWS_ACCESS_KEY_ID="..." \
--build-arg AWS_SECRET_ACCESS_KEY="..." \
--build-arg AWS_DEFAULT_REGION=...
Sending build context to Docker daemon  32.77kB
Step 1/38 : ARG AWS_ACCESS_KEY_ID
・・・
Successfully built aaa1f443c6c4
Successfully tagged my_tfr:Terraform
```

Terraform、Terraformerが実行できそうか確認します。

```bash
[ec2-user@ip-10-0-0-203 ~]$ docker run my_tfr:Terraform terraform -version
Terraform v0.13.7
+ provider registry.terraform.io/hashicorp/aws v4.25.0
+ provider registry.terraform.io/hashicorp/kubernetes v2.12.1

Your version of Terraform is out of date! The latest version
is 1.2.7. You can update by downloading from https://www.terraform.io/downloads.html
[ec2-user@ip-10-0-0-203 ~]$ docker run my_tfr:Terraform terraformer -v
version v0.8.21
```

### Alpineベース（CentOS7、aws-cli、Terraform経由）

次にAlpineをベースとしてイメージを作成してみます。
基本はTerraformベースと同じですが、最終的にTerraformとTerraformerを実行するのはAlpineイメージとなります。TerraformイメージはTerraformの実行ファイルを持ってくるために使います。

|イメージ名|役割|備考|
|---|---|---|
|hashicorp/terraform:0.13.7|Terraformのインストールを省くため。|中間イメージとして、Terraformを使い、最終的なイメージにTerraformだけ持ってきます。|
|docker.io/library/alpine:3.16.2|Terraform、Terraformerの実行||

```Dockerfile:Dockerfile
（変数の定義　省略）

FROM centos:centos7 as builder
（Terraformerのインストール　省略）

FROM amazon/aws-cli:2.7.21 as aws-config
（AWS認証情報の設定　省略）

FROM hashicorp/terraform:0.13.7 as terraform
#中間イメージとして、Terraformを使い、インストールを省く。

FROM docker.io/library/alpine:3.16.2
ARG PROVIDER
ARG TMPDIR
ARG HOME_DIR
ARG PROVIDER_FILE
WORKDIR $TMPDIR
WORKDIR $HOME_DIR
COPY --from=builder ${TMPDIR}/terraformer-${PROVIDER}-linux-amd64 /usr/local/bin/terraformer
COPY --from=builder /lib64/ld-linux-x86-64.so.2 /lib64/libpthread.so.0 /lib64/libc.so.6 /lib64/
COPY --from=aws-config $HOME_DIR/.aws $HOME_DIR/.aws
COPY --from=terraform /bin/terraform /bin/terraform
#↑ここだけ違う。Terraformの実行ファイルを持ってくる。
COPY $PROVIDER_FILE $HOME_DIR/
RUN ["terraform","init"]
ENTRYPOINT [""]
```

Terraformベースと同様にビルドすることができます。

### Distrolessベース（CentOS7、aws-cli、Terraform経由）

最後にDistrolessイメージを使います。Alpineベースと比べて、ベースとするイメージのみ違います。他コマンドは全く同じで大丈夫です。
具体的には、

```Dockerfile
FROM docker.io/library/alpine:3.16.2
```

から、

```Dockerfile
FROM gcr.io/distroless/static:latest
```

に書き換えるだけです。最低限のコマンドしか実行していないため、ベースとなるイメージを変えてもエラーとなりづらくなるのではないかと思います。
ただ、Distrolessでは実行できないコマンドがあります。具体例として、shがあります。

```bash
[ec2-user@ip-10-0-0-203 ~]$ docker run my_tfr:Alpine sh -c id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
[ec2-user@ip-10-0-0-203 ~]$ docker run my_tfr:Distroless sh -c id
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "sh": executable file not found in $PATH: unknown.
ERRO[0000] error waiting for container: context canceled
```

Distrolessイメージでshを使う方法として、debugタグのイメージを使う方法があります。しかし、`/bin/sh`ではなく、`/busybox/sh`が実行されることに注意が必要です。CentOSなどで動いていたshellが動かない可能性はあります。

```bash
[ec2-user@ip-10-0-0-203 ~]$ docker run -it gcr.io/distroless/static:debug
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /busybox/sh
    6 root      0:00 ps
```

## 出来上がったイメージのサイズを比較する

それでは色々な方法で作成したイメージのサイズを比較します。

```bash
[ec2-user@ip-10-0-0-203 ~]$ docker images | head -n 1; docker images | grep my_tfr
REPOSITORY                 TAG          IMAGE ID       CREATED          SIZE
my_tfr                     Distroless   0495e5802299   9 minutes ago    799MB
my_tfr                     Alpine       ca2316e1de8f   17 minutes ago   802MB
my_tfr                     Terraform    aaa1f443c6c4   33 minutes ago   822MB
```

TerraformベースがTerraformerをインストールしたことで、712MB大きくなっています。また、他ベースと比較すると、20MBほど大きくなっています。この結果が有益かは用途次第ですが、1つの基準になればと思います。

# 作成したTerraformerイメージを使ってみる

それでは作成したイメージを使って、AWSのtfファイルを作成してみます。

```bash
[ec2-user@ip-10-0-0-75 ~]$ docker run -v $HOME/tf-src:/root/generated/ my_tfr:Distroless terraformer import aws -r=ec2_instance,vpc
2022/08/13 04:45:13 aws importing default region
2022/08/13 04:45:13 aws importing... ec2_instance
2022/08/13 04:45:15 aws done importing ec2_instance
2022/08/13 04:45:15 aws importing... vpc
2022/08/13 04:45:15 aws done importing vpc
・・・
```

できたファイルを見てみます。

```bash
[ec2-user@ip-10-0-0-75 ~]$ cd tf-src/aws/ec2_instance/
[ec2-user@ip-10-0-0-75 ec2_instance]$ ls
instance.tf  outputs.tf  provider.tf  terraform.tfstate
[ec2-user@ip-10-0-0-75 ec2_instance]$ view instance.tf
・・・
resource "aws_instance" "<instance名>" {
  ami                         = "ami-02892a4ea9bfa2192"
  associate_public_ip_address = "true"
  availability_zone           = "ap-northeast-1a"

  capacity_reservation_specification {
    capacity_reservation_preference = "open"
  }

  cpu_core_count       = "2"
  cpu_threads_per_core = "1"

  credit_specification {
    cpu_credits = "standard"
  }

  disable_api_stop        = "false"
  disable_api_termination = "false"
  ebs_optimized           = "false"

  enclave_options {
    enabled = "false"
  }

  get_password_data                    = "false"
  hibernation                          = "false"
  instance_initiated_shutdown_behavior = "stop"
  instance_type                        = "t2.medium"
  ipv6_address_count                   = "0"
  key_name                             = "..."

  maintenance_options {
    auto_recovery = "default"
  }

  metadata_options {
    http_endpoint               = "enabled"
    http_put_response_hop_limit = "1"
    http_tokens                 = "optional"
    instance_metadata_tags      = "disabled"
  }

  monitoring = "false"

  private_dns_name_options {
    enable_resource_name_dns_a_record    = "false"
    enable_resource_name_dns_aaaa_record = "false"
    hostname_type                        = "ip-name"
  }

  private_ip = "10.0.0.75"

  root_block_device {
    delete_on_termination = "true"
    encrypted             = "false"
    volume_size           = "20"
    volume_type           = "gp2"
  }

  source_dest_check = "true"
  subnet_id         = "subnet-00b0aafb0a5599895"

  tags = {
    Author = "..."
    Name   = "..."
  }

  tags_all = {
    Author = "..."
    Name   = "..."
  }

  tenancy                = "default"
  vpc_security_group_ids = ["sg-0b7a74b0cbb945d6c"]
}
・・・
```

EC2の設定が一通り載っていました。このファイルを基にEC2を複製できそうかなと思います。
次にKubernetesで使ってみたかったのですが、うまくいきませんでした。

```bash
[ec2-user@ip-10-0-0-75 ~]$ docker run -v $HOME/tf-src:/root/generated/ my_tfr:Distroless terraformer import kubernetes list
[ec2-user@ip-10-0-0-75 ~]$ docker run -v $HOME/tf-src:/root/generated/ my_tfr:Distroless terraformer import kubernetes -r deployment
2022/08/13 05:03:42 kubernetes importing... deployment
2022/08/13 05:03:42 kubernetes error importing deployment, err: kubernetes: deployment not supported resource
2022/08/13 05:03:42 kubernetes Connecting....
```

原因を調べましたが、バージョン違いの問題かもしれません。ツールのそれぞれの対応バージョンは以下のページにあります。

https://github.com/GoogleCloudPlatform/terraformer#capabilities

https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs

https://github.com/hashicorp/terraform

Terraformerが対応しているTerraformバージョン(0.13)が2年近く前のものになります。それに合わせるのは難しいと感じました。以下のような別のツールを使う方がよいかもしれません。

https://velero.io/

# おわりに

Dockerのマルチステージビルドはイメージの違いに左右されにくく楽だなと感じました。
今まで手動実行していたものをDockerfileにまとめるだけでなく、ツールの良さを活かせると、今までできなかったことができるようになると思いました。
これからも勉強します。