🎃

git-logを上手に使って無駄なDockerビルドを回避する | Offers Tech Blog

2023/12/19に公開

こんにちは!
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 の開発メンバーを大募集中です。正社員はもちろん、副業でのジョインも歓迎です。とりあえず話を聞いてみたい!という方には カジュアル面談 がオススメです。

https://jobs.overflow.co.jp

Offers Tech Blog

Discussion