ビルド時間短縮のために途中ステージをpushする
始めに
弊社のシステムでは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が遅くて悩んでいる方はぜひ参考にしてみてください。
Discussion