Pythonのdistrolessコンテナを使って脆弱性を撲滅した話

## はじめに
- AWS ECS+ECRで動いているpythonのサービスがあった
    - ただし、ECRのスキャン機能でものすごい数の脆弱性が出ていた...
- 1つずつ解消していくのは現実的ではないため、distrolessコンテナを導入することになった

## distrolessとは
- Googleが提供しているコンテナイメージ
- アプリケーションとそのランタイム依存関係のみ含まれる(=必要最小限)
    - パッケージマネージャー、シェルなどは含まれない
- 導入メリットとして、①軽量であること、②セキュアであることが挙げられる

## やったことざっくりと
- 以前は「python:3.9.13-slim-buster」のイメージを使用してビルドしていた
    - これを1段階目「python:3.9.13-slim-buster」→2段階目「gcr.io/distroless/python3」のマルチステージビルドにする
- 1段階目
    - パッケージをインストールし、コピー用ディレクトリに置く
    - コードをコピー用ディレクトリに展開する
- 2段階目
    - PYTHONPATHにパッケージのパスを追加する
    - 1段階目のコピー用ディレクトリからコピーする

## 辿り着いたDockerfile(一部省略)
- ARGの内容は、実行時に `--build-arg` で引数として指定する運用
- `RUNNER_IMAGE_NAME` にdistrolessコンテナイメージを設定している
    - dev環境の場合は `gcr.io/distroless/python3:debug`
        - `:debug` のイメージにはshellが含まれているため
    - stg, prod環境の場合は `gcr.io/distroless/python3` 
- `APP_NAME` = コードが格納されたディレクトリ
```
ARG RUNNER_IMAGE_NAME

// ここから1段階目
FROM python:3.9.13-slim-buster AS builder

ARG APP_NAME
ARG DC_TZ
ARG ...

COPY requirements.txt /tmp/

RUN set -x \
 && \
 : "timezones" \
 && cp /usr/share/zoneinfo/${DC_TZ} /etc/localtime \
 && cp -a --parents /etc/localtime /opt \
 && \
 : "lib install from pip" \
 && mv /tmp/requirements.txt ./ \
 && pip3 install --upgrade pip \
 && pip3 install --no-cache-dir -r requirements.txt \
 && cp -a --parents /usr/local/lib/python3.9/site-packages /opt

COPY ${APP_NAME} /opt/opt/${APP_NAME}

// ここから2段階目
FROM ${RUNNER_IMAGE_NAME}

ARG APP_NAME
ARG DC_LANG
ARG ...

ENV LANG ${DC_LANG}
ENV ...
ENV PYTHONPATH=/usr/local/lib/python3.9/site-packages:/opt/${APP_NAME}/libs:/opt/${APP_NAME}

COPY --from=builder /opt /

WORKDIR /opt/${APP_NAME}

CMD ["***.py"]

```

## ポイント
### パッケージのインストール場所とコピー場所
```
 && pip3 install --upgrade pip \
 && pip3 install --no-cache-dir -r requirements.txt \
 && cp -a --parents /usr/local/lib/python3.9/site-packages /opt
```
- (1段階目)pipインストールしたパッケージが入っているディレクトリをコピー用ディレクトリ(/opt)にコピー
- 私はこれを忘れていてしばらく詰まりました
```
COPY ${APP_NAME} /opt/opt/${APP_NAME}
```
- (1段階目)コードをコピー用ディレクトリにコピー
- 運用上、コードを `opt/${APP_NAME}` というディレクトリ構造で格納したいため、`/opt(コピー用ディレクトリ)/opt(格納用ディレクトリ)`という指定に...(コピー用ディレクトリの名前をtempとかに変えればよかった)
```
ENV PYTHONPATH=/usr/local/lib/python3.9/site-packages:/opt/${APP_NAME}/libs:/opt/${APP_NAME}
```
- (2段階目)パッケージが入っているディレクトリをPYTHONPATHに追加
```
COPY --from=builder /opt /
```
- (2段階目)1段階目のコピー用ディレクトリ(/opt)の中身をコピー
- これでインストールしたパッケージもコードも使えるようになる

### CMDの変更
- distroless `gcr.io/distroless/python3`の entrypoint は `python` に設定されていることに注意
    - https://github.com/GoogleContainerTools/distroless/blob/main/experimental/python3/README.md
- 以前はCMDに `python3` を入れていたが削除する必要があった

```
// 変更前
CMD ["python3", "***.py"]

// 変更後
CMD ["***.py"]
```

## 成果
![スクリーンショット 2022-12-06 17.54.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390780/63e2b7cb-ccfc-4f17-273c-ed4688289ff5.png)

-  あんなにあった脆弱性がなくなった！
-  サイズも削減された！

## 今後に向けての課題
- python3のdistrolessは本番運用向きではないらしい...
    - https://github.com/GoogleContainerTools/distroless
    - `The following images are also published on gcr.io, but are considered experimental and not recommended for production usage: ...`
    - 今回は内部向けのサービスだったため踏み切ったが、外部向けに展開するサービスには向いていないかも
- 今回のサービスはパッケージも少なめだったが、複雑なサービスになったときに動作がどこまで保証されるか未検証

## 参考記事
実装時、大変お世話になりました！
- [仕事でPythonコンテナをデプロイする人向けのDockerfile (2): distroless編 ](https://future-architect.github.io/articles/20200514/)
- [セキュアで軽量なdistrolessコンテナを作成する](https://qiita.com/t_katsumura/items/462e2ae6321a9b5e473e)