git-logを上手に使って無駄なDockerビルドを回避する | Offers Tech Blog
こんにちは!
Offers, Offers MGR を運営している株式会社 overflow 所属、エンジニアの藤井です。
最近、既存機能のリプレースのため頻繁にデプロイ作業を繰り返していたのですが、Docker ビルドの時間が長くてイライラしていました。
Docker イメージのサイズは可能なかぎり最小を保つべきですが、それでも追加開発を繰り返せばギガバイト単位になってしまうことも珍しくはありません。
そうなると、Docker ビルドにかかる時間も長くなり、開発効率が落ちてしまいます。
そこで今回は、git-log を上手に使って無駄な Docker ビルドを回避する方法をご紹介します。
その前に、まずは Docker タグの戦略について考えていきます。
Dockerタグの戦略について
Microsoft のドキュメント "Recommendations for tagging and versioning container images"
を参考に、Docker タグの戦略を考えてみます。
この記事では、Stable tags と Unique tags の 2 つの戦略が紹介されています。
- Stable tags:
latest
,dev
,prod
など、固定的に割り当てられるタグ - Unique tags:
v1.0.0
など、一意に割り当てられるタグ
Stable tags はベースとなるイメージに対して使用することが推奨されており、デプロイ時に使用を避けるべきとされています。
一方で、Unique tags はデプロイ時に使用することが推奨されています。
これはアプリケーションのバージョニングを考えれば当然です。
たとえば商用環境でデプロイするイメージに対して盲目的に latest
タグのような Stable Tags を使用することは避けるべきでしょう。
※なお、latest
タグは悪なのか?という考察については、こちらの記事 が参考になります。
Unique Tags の生成方法として、ドキュメントでは以下の 4 つが提案されています。
- Date-time stamp
- Git commit
- Manifest digest
- Build ID
今回は、Unique Tags として Git commit を採用します。
そうすることで、git-log コマンドを組み合わせることで本当に必要な時にしかビルドしない、というテクニックが利用できます。
git logコマンドで必要なコミットを絞り込む
git log
はコミットの履歴を閲覧するためのコマンドですが、かなり高度なフィルター機能を備えています。
参考: https://git-scm.com/docs/git-log
たとえば Ruby on Rails アプリケーションの場合、spec ディレクトリ配下のテストコードが変更されたとしても、デプロイに利用する Docker のイメージを更新する必要性は薄いでしょう。
また、assets 系のファイルを S3 などにアップロードしている場合、app/assets ディレクトリも Docker イメージに含める必要性も薄いでしょう。
git log
では特定のパスだけのコミットを絞り込むことができます。
その際、path 指定の際には pathspec が利用できるため、除外したいパスも指定可能です。
たとえば、以下のようなコマンドを実行することで、app/assets 配下の変更を除外した最新のコミットを特定できます。
git log --format=format:%H -n 1 -- \
app \
config \
db \
lib
':(exclude)app/assets'
この結果で得られる Commit Hash を Docker イメージのタグとして利用することで、本当に Docker イメージの変更が必要なときだけビルドできます。
Github Actionsのカスタムアクションに組み込んでみる
以下、Github Actions のカスタムアクションとして実装したものを紹介します。
name: Docker Exist
description: Detect docker version by git log
inputs:
repository:
description: |
Specify the repository.
required: true
tag_prefix:
description: |
Specify the tag prefix.
required: false
target_dirs:
description: |
Specify the target directories to detect.
multiple directories can be specified by separating them with a space.
required: true
outputs:
exists:
description: |
If the docker version exists, it will be true.
value: ${{ steps.detect_docker.outputs.exists }}
sha:
description: |
The sha of the last commit.
value: ${{ steps.last_commit.outputs.sha }}
runs:
using: composite
steps:
- name: Detect last commit for ${{ inputs.repository }}
id: last_commit
shell: bash
run: |
# https://git-scm.com/docs/git-log
# ここで思ったように取得できない場合は、checkoutのfetch-depthがdefault(1)のままだと思われる
sha=$(git log --format=format:%H -n 1 -- ${{ inputs.target_dirs }})
echo "=== Effective commit ==="
git log ${sha} -n 1
echo "sha=${sha}" >> $GITHUB_OUTPUT
- name: Detect docker version for ${{ inputs.repository }} exists
id: detect_docker
shell: bash
run: |
sha=${{ steps.last_commit.outputs.sha }}
set +e
docker manifest inspect ${{ inputs.repository }}:${{ inputs.tag_prefix }}${sha} > /dev/null 2> /dev/null
exists=$?
set -e
if [ $exists -eq 0 ]; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
上記の通り git log
コマンドで特定のパスのコミットを絞り込み、その結果を Docker イメージのタグとして利用しています。
そして、docker manifest inspect
を利用することで対象のタグを持つ Docker イメージがリポジトリに存在するか確認しています。
この結果が true
であれば、Docker イメージが存在するので、Docker のビルドをスキップします。
注意する必要があるのは、actions/checkout でコードをチェックアウトする際、fetch-depth を指定する必要がある点です。
actions/checkout の fetch-depth はデフォルトで 1 になっているため、 git log
で取得できるコミットは常に最新の 1 つになってしまいます。
下記の例では fetch-depth を 100 に設定していますが、ここがあまりに大きすぎると Checkout 時の時間がかかってしまうので、バランスを考えて設定する必要があります。
name: CD - Build Docker Images
on:
workflow_call:
inputs:
deploy-env:
description: |
Deploy Environment
type: string
required: true
branch:
description: branch to deploy
type: string
required: true
git-log-depth:
description: checkout depth for git log
type: number
required: false
default: 100
jobs:
build:
runs-on: ubuntu-latest
concurrency:
group: build/${{ inputs.deploy-env }}
cancel-in-progress: true
steps:
- name: Check out specified branch code
id: checkout
uses: actions/checkout@v3
with:
ref: ${{ inputs.branch }}
fetch-depth: ${{ inputs.git-log-depth }} # for check git log
まとめ
今回は、git-log を上手に使って無駄な Docker ビルドを回避する方法をご紹介しました。
たとえばモジュラーモノリスに移行する場合などで、同じ Git リポジトリ内で複数のアプリケーションをビルドする必要が出てくるケースもあるでしょう。
そういった場合にもこのテクニックを活用することで、ビルド時間の短縮につながるでしょう。
ちょっとした修正しか行っていないのに、Docker のビルド時間が長くてイライラしている方は、ぜひ試してみてください。
エンジニア採用強化中
株式会社 overflow では Offers の開発メンバーを大募集中です。正社員はもちろん、副業でのジョインも歓迎です。とりあえず話を聞いてみたい!という方には カジュアル面談 がオススメです。
副業転職の Offers 開発チームがお送りするテックブログです。【エンジニア積極採用中】カジュアル面談、副業からのトライアル etc 承っております💪 jobs.overflow.co.jp
Discussion