distrolessイメージを使ってイメージサイズを改善する

## はじめに

この記事では既存のコンテナイメージをdistrolessイメージを使って改善していく記事です。
主な内容としては実践したときのメモを中心に書きます。
（忘れやすいことなど）誤りなどがあれば書き直していく予定です。

今回利用したソースコードは以下のリポジトリにあります。

[distroless_docker_image GitHub](https://github.com/ymd65536/distroless_docker_image)

※のちほど、finchを使った記事も挙げます。

## 環境

今回は以下の環境で動作確認を行っています。そのほかの環境で動かす場合の参考にしてください。

- MacBook
  - Apple Sillicon M1
  - Sonoma 14.0
- Rancher Desktop
  - v1.7.0
- Python
  - Python 3.9

※まずは、[Rancher Desktop](https://rancherdesktop.io/)をインストールしてください。
※Python3.9と記載していますが、dockerイメージにおけるPythonです。

## 既存のコンテナイメージ

今回改善するイメージは以下です。

```dockerfile
FROM python:3.9-alpine as builder

RUN apk add --no-cache musl-dev mysql-dev gcc
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt

CMD ["python", "main.py"]
```

imageを構築する過程でpipを実行しています。pipでインストールするパッケージは以下のとおりです。

```txt:requirements.txt
alembic==1.13.1
annotated-types==0.6.0
boto3==1.4.8
botocore==1.8.11
certifi==2023.11.17
charset-normalizer==3.3.2
docutils==0.14
exceptiongroup==1.2.0
functions==0.7.0
greenlet==3.0.3
idna==3.6
iniconfig==2.0.0
jmespath==0.9.3
Mako==1.3.0
MarkupSafe==2.1.3
mysqlclient==2.2.1
packaging==23.2
pluggy==1.3.0
pydantic==2.5.3
pydantic_core==2.14.6
pytest==7.4.4
python-dateutil==2.6.1
python-multipart==0.0.6
pytz==2023.3.post1
requests==2.31.0
s3transfer==0.1.12
schemas==0.7.1
six==1.11.0
SQLAlchemy==2.0.25
SQLAlchemy-Utils==0.41.1
tomli==2.0.1
typing_extensions==4.9.0
urllib3==2.1.0
xmltodict==0.13.0
```

なお、mysqlclientをインストールしているため、`apk`でmysql-devをインストールしています。
イメージをビルドします。

```bash
docker build -t test_alpine:1 .
```

サイズを確認するため、`docker images`を実行します。

```bash
docker images
```

実行結果は以下のとおりです。

```text
REPOSITORY                           TAG          IMAGE ID       CREATED         SIZE
test_alpine                          1            374875ee50bb   8 seconds ago   333MB
```

サイズは333MBです。このコンテナイメージをdistrolessイメージに置き換えていきます。

## distrolessイメージを使って改善していく

まずはどんな感じのdockerfileになるかを確認します。

```dockerfile
FROM python:3.9-alpine as builder

WORKDIR /app
COPY . .
RUN pip --no-cache-dir install --upgrade pip && pip install --no-cache-dir --target site-packages -r requirements.txt

FROM gcr.io/distroless/python3-debian11:nonroot

WORKDIR /app
COPY --from=builder /app .
ENV PYTHONPATH=/app/site-packages
CMD ["./main.py"]
```

なお、requirements.txtは以下のとおりです。変更点としてはmysqlclientを削除して代わりに`mysql-connector-python==8.2.0`をインストールしています。

```txt:requirements.txt
alembic==1.13.1
annotated-types==0.6.0
boto3==1.4.8
botocore==1.8.11
certifi==2023.11.17
charset-normalizer==3.3.2
docutils==0.14
exceptiongroup==1.2.0
functions==0.7.0
greenlet==3.0.3
idna==3.6
iniconfig==2.0.0
jmespath==0.9.3
Mako==1.3.0
MarkupSafe==2.1.3
# mysqlclient==2.2.1
packaging==23.2
pluggy==1.3.0
pydantic==2.5.3
pydantic_core==2.14.6
pytest==7.4.4
python-dateutil==2.6.1
python-multipart==0.0.6
pytz==2023.3.post1
requests==2.31.0
s3transfer==0.1.12
schemas==0.7.1
six==1.11.0
SQLAlchemy==2.0.25
SQLAlchemy-Utils==0.41.1
tomli==2.0.1
typing_extensions==4.9.0
urllib3==2.1.0
xmltodict==0.13.0
mysql-connector-python==8.2.0
```

イメージをビルドします。

```bash
docker build -t test_distroless:1 .
```

サイズを確認するため、`docker images`を実行します。

```bash
docker images
```

実行結果は以下のとおりです。

```text
REPOSITORY                           TAG          IMAGE ID       CREATED         SIZE
test_alpine                          1            374875ee50bb   8 seconds ago   333MB
test_distroless                      1            e6ee09fc6a8e   2 minutes ago   132MB
```

サイズは132MBです。distrolessイメージを使うことでほぼ同じPythonのパッケージを使いつつ、イメージサイズを大幅に削減できました。

### distrolessイメージを作るときのポイント

イメージサイズを削減できたところでdistrolessイメージを作るときのポイントを確認します。
主に以下のポイントです。

- マルチステージビルドを使う
- その他のパッケージに依存したパッケージをインストールしないもしくは代わりを用意する

それぞれ見ていきましょう。

## マルチステージビルドを使う

dockerにはマルチステージビルドという機能があります。マルチステージビルドとは複数の`FROM`を使ってイメージを構築する機能です。

以下のようなメリットがあります。

- 他のイメージから取得したものをコピー
- 保存されているビルドイメージ（ビルドキャッシュ）から必要なものをコピーすることでビルドの高速化
  - パッケージインストール時に発生するダウンロード時間をより短くできる

ただし、以下のようなデメリットもあります。

- 特定のビルドイメージに依存する形になってしまう
- ビルドイメージを保存するため、保存データ容量が大きくなる

また、昨今においてはさまざまなアーキテクチャで開発を行うことが多くなってきているため
ビルドイメージを取得する際にアーキテクチャによってビルドイメージが異なることがあります。

ビルドイメージの構築はCI/CDなどを構築してクラウド上でやるのが良いでしょう。

## その他のパッケージに依存したパッケージをインストールしない

distrolessイメージを使うときには、その他のパッケージに依存したパッケージをインストールしないようにする必要があります。

今回の場合はmysqlclientをインストールしているためにmysql-devもインストールしていますが。
distrolessイメージを使うときにはmysqlclientをインストールしないようにする必要があります。
そのため、mysqlclientを削除してmysql-connector-pythonをインストールしています。

mysqlclientをインストールする場合は依存しているapkのパッケージをインストールする必要がありますが
distrolessイメージにはシェルとパッケージシステムが存在しないため、パッケージをインストールできません。

つまり、distrolessイメージを使うときにはその他のパッケージに依存したパッケージをインストールしないようにするかもしくは代わりとなるパッケージを用意する必要があります。

ちなみにmysqlclientをインストールしようとすると`pkg-config`が正常に動作せず、インストールに失敗します。

## まとめ

今回は既存のコンテナイメージをdistrolessイメージを使って改善しました。
distrolessイメージを使うことでイメージサイズを大幅に削減できるため、使えるところでは使っていきたいです。

ただし、マルチステージビルドを使う必要があり、ビルドイメージをいくつか保存する必要があるためビルドイメージの運用には注意が必要です。

イメージをビルドする際はクラウドベースのビルド環境を構築しておきましょう。