始めに
弊社のシステムではECSを使用しているのですが、ここ最近Dockerイメージのビルド時間が大幅に延長されてしまっていました。そのうち、大幅な時間を占めているのがライブラリのインストール時間で、CPUの使用率が高くなって応答が非常に遅くなっていました。
uv.lock
等のロックファイルが取り扱われている環境であればインストールでは常に同じライブラリが使用されるものですし、ライブラリインストールが完了した状態のイメージをRepositoryにアップロードすることで短縮することを目指しました。
今回の記事では、Dockerのマルチステージビルドを扱って処理時間を短縮することを目指します。
環境
- Docker
- 21以降
- GitHub Actions
実装
Repositoryの作成
ライブラリをインストールしたイメージをアップロードするためのRepositoryを使用します。
dev-dependencies
ありのイメージと、なしのイメージをアップロードしたかったので、2つ用意しました。
- https://hub.docker.com/r/kirimaru/fastapi-practice_dev-runtime
- https://hub.docker.com/r/kirimaru/fastapi-practice_prod-runtime
マルチステージビルドができるDockerファイルの作成
マルチステージビルドができるDockerファイルを用意します。全体の内容については今回の解説には不要なので省きます。
今回の場合、次のステージを用意しました。
- base
- 一番基準になるステージです。本番とベースイメージを分けたい場合はdev_base, prod_baseが生まれます
- dev_runtime
- 開発用のイメージを作るためのステージ。4. testで流用する。
- dev
- ローカル用ステージ
- test
- 2.のdev_runtimeを使用してCIを高速で処理させる
- prod_runtime
- 本番用イメージ。
dev-dependencies
を抜いているだけ
- 本番用イメージ。
- prod
- 本番用ステージ
ARG RUNTIME_TAG=latest # ベースイメージ FROM python:3.12-slim AS base ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 ENV UV_PROJECT_ENVIRONMENT="/usr/local/" WORKDIR /app COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv # 1. 開発用ランタイムのビルド FROM base AS dev_runtime COPY src /app/src COPY README.md pyproject.toml .python-version uv.lock ./ COPY tests /app/tests COPY alembic /app/alembic COPY alembic.ini .env ./ RUN uv sync --frozen --no-cache --dev ## ローカルはあんまりここを分けるメリットがない ## 2. 開発用ランタイムを使用して起動 FROM dev_runtime AS dev COPY src /app/src CMD ["uv", "run", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--reload"] ## 3. test 用 FROM kirimaru/fastapi-practice_dev-runtime:${RUNTIME_TAG} AS test ARG RUNTIME_TAG # NOTE: compose ファイルでマウントするなら不要 COPY src /app/src # 4. 本番用ランタイムのビルド FROM base AS prod_runtime COPY README.md pyproject.toml .python-version uv.lock ./ RUN uv sync --frozen --no-cache # 5. 本番用ランタイムを使用して起動 FROM kirimaru/fastapi-practice_prod-runtime:${RUNTIME_TAG} AS prod ARG RUNTIME_TAG COPY src /app/src COPY .env ./ CMD ["uv", "run", "uvicorn", "src.main:app", "--host", "0.0.0.0"]
ロックファイルのハッシュ取得
ロックファイルのハッシュを取得し、それをDockerのタグにします。そうすることで、ライブラリの更新がされたかどうかをチェックできます。また、念のためDockerのイメージ名と混ざらないようにpython-
等のprefixも付与しています。
export LOCK_HASH=python-$(sha1sum < uv.lock | cut -d' ' -f1) # Docker側でわかりやすい変数に変更 export RUNTIME_TAG=$LOCK_HASH
Dockerイメージのpull
過去に作成したイメージがあればそれを取得します。
docker pull kirimaru/fastapi-practice_dev-runtime:$RUNTIME_TAG
Dockerイメージのbuild and push
pullしたイメージが存在しない場合、イメージをビルドします。作成したイメージをRepositoryにpushします。
if [ $? -ne 0 ]; then docker buildx build --target dev_runtime -t kirimaru/fastapi-practice_prod-runtime:$RUNTIME_TAG . docker push kirimaru/fastapi-practice_dev-runtime:$RUNTIME_TAG fi
pullしたイメージを使用する
開発用ビルドイメージをもとにテストしたいので、compose.yml
のtargetに定義したtest
を指定します。また、タグをロックファイルのハッシュにしているので、パラメータとして渡してあげます。
services: api: build: context: . target: ${BUILD_TARGET:-dev} args: - RUNTIME_TAG=${RUNTIME_TAG}
export BUILD_TARGET=test docker compose up -d
ソースコード
- https://github.com/hirotoKirimaru/fastapi-practice/blob/cd935568cf52293c9cc2ce883694b607e402918b/Dockerfile
- https://github.com/hirotoKirimaru/fastapi-practice/blob/cd935568cf52293c9cc2ce883694b607e402918b/compose.yml
- https://github.com/hirotoKirimaru/fastapi-practice/blob/c095102aea311efb9b9e3e1ce6f4d6e6b8b519e7/.github/workflows/build.yml
- https://github.com/hirotoKirimaru/fastapi-practice/blob/c095102aea311efb9b9e3e1ce6f4d6e6b8b519e7/.github/workflows/test.yml
終わりに
1回あたり全体で20分くらいかかっていたのを10分弱まで省略できました。特にlint
するだけのCIでは、イメージのpullがなくなったことで10分弱かかっていたのを2分程度で完了するほど高速処理になっています。
CIが遅くて悩んでいる方はぜひ参考にしてみてください。