やりたかったこと
リポジトリの外で管理しているデータを定期的にサイトへ反映するワークフローを運用していました。
構成はこんな感じです。
- データの実体はリポジトリの外(社内の別システムが配信するJSON)
- それを定期実行で取り込みデプロイ
- 具体的にはビルドしたイメージをECRにアップし、アップしたイメージを使うようなECSのtask definitionを作成してそちらに入れ替え
- 上記の処理をGitHub Actionsで実施
ここで困ったのが、定期実行のたびに毎回フルでビルドとデプロイが走ることです。
30分置きに定期実行した場合、48個のイメージがECRにアップされますし、ECSであれば48個のtask definitionが生成されます。
そこでやりたいことは、定期実行はしたい、ただしデータに差分がない時はデプロイをスキップしたい、ということになります。
この記事ではその実現方法を、実際のワークフローを題材に解説します。
問題は差分の検知をどうするか
問題はリポジトリ管理外のファイルをどう差分検知するか、という部分です。
Git管理されていないので git diff のような手は使えません。なんらかの形で前回のデプロイ時のファイルを保持しておく必要があります。
それ以外は簡単で、ECRへのアップやECSの更新を、条件にあった時だけ実行するように書けば済みます。
また、通常のデプロイの時はリポジトリ外のデータがどうあれデプロイしたいのですが、その条件分岐も用意する必要があります(後述)。
artifact を前回の状態の保管庫として使う
今回、前回のデプロイ時のファイルを保持するために使うのは GitHub Actions の artifact です。
artifact はビルド成果物を保存・共有するための機能ですが、これを実行をまたいだ状態の保管庫として転用します。流れはこうなります。
- 前回の成功した実行から、保存しておいたデータをダウンロードする
- 今回、配信元から最新データを取得する
- 前回ぶんと今回ぶんを比較して、差分の有無を判定する
- 今回ぶんを artifact として保存する(次回の比較材料になる)
- 差分があった時だけ、ビルドとデプロイを実行する
ポイントは、artifact を成果物ではなく状態として扱っているところです。前回の自分が残したメモを今回の自分が読み、今回の自分も次回のためにメモを残します。この受け渡しで、実行をまたいだ比較が成立します。
実装
題材にするワークフローの全体像から見ていきます。トリガーはこうなっています。
on:
schedule:
# 30分おきに実行
- cron: '0,30 * * * *'
push:
branches:
- main
定期実行(schedule)に加えて、main への push でも動くケースを例にします。
後で出てきますが、差分スキップを効かせるのは schedule のときだけで、push のときは常にデプロイさせたい、という方針です。
ステップ1: 前回の実行IDを取得する
artifact は実行(run)に紐づいて保存されます。前回ぶんをダウンロードするには、前回の成功した実行のIDが必要です。これを gh コマンドで引いてきます。
- id: run-id
run: |
echo 'run-id='$(gh run list -L 1 -R "${{ env.REPO }}" -b "${{ env.BRANCH }}" -w "${{ env.WORKFLOW }}" -s "${{ env.STATUS }}" --json databaseId -q '.[].databaseId') >> "$GITHUB_OUTPUT"
env:
BRANCH: main
STATUS: 'success'
REPO: 'your-org/your-repo'
WORKFLOW: 'Deploy (preview production)'
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
main ブランチで成功した、同じワークフローの最新の実行を1件だけ取り、その databaseId を後続ステップへ渡しています。
ステップ2: 前回のデータをダウンロードする
取得したIDを使って、前回の artifact をダウンロードします。
- name: Download products.json artifact
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: ${{ env.PRODUCTS_JSON_ARTIFACT_NAME }}
path: ${{ env.PRODUCTS_JSON_ARTIFACT_DESTINATION_PATH }}
run-id: ${{ steps.run-id.outputs.run-id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
このステップは continue-on-error を true にしています。初回実行時やartifactの期限切れなどで取得できない時は、エラーにはせずデプロイし、artifactを作ることで次回からスキップが効くようにしています。
ステップ3: 最新データを取得する
配信元のURLを叩いて、最新のデータを所定のパスへ書き出します。今回はcurlで取得します。
- name: Sync products.json
run: curl -sSf "$SOURCE_URL" -o "$DEST_PATH"
env:
SOURCE_URL: ${{ env.PRODUCTS_JSON_SOURCE_URL }}
DEST_PATH: ${{ env.PRODUCTS_JSON_DESTINATION_PATH }}
-sS は進捗表示を消しつつエラーだけは出す指定、-f は取得に失敗したらHTTPエラーでステップを落とすための指定です。取得できなかった壊れたファイルで差分判定に進まないよう、ここは確実に失敗させておきます。
これで新旧ファイルが揃いました。
ステップ4: 差分を判定する
2つのファイルを比較して、差分の有無を出力します。
- name: Check for products.json changes
id: diff_check
run: |
if [ ! -f ${{ env.PRODUCTS_JSON_ARTIFACT_PATH }} ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
elif cmp -s ${{ env.PRODUCTS_JSON_ARTIFACT_PATH }} ${{ env.PRODUCTS_JSON_DESTINATION_PATH }}; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
判定は3分岐になっています。
- 前回ぶんのファイルが無い → changed=true(初回や期限切れ。比較できないので安全側に倒してデプロイします)
- cmp で中身が一致 → changed=false(差分なし。スキップ対象です)
- 一致しない → changed=true(差分あり。デプロイします)
cmp -s は2つのファイルを比較し、中身が同じなら終了コード0、違えば0以外を返すコマンドです。-s は出力を抑えて終了コードだけ見るためのオプションで、シェルの条件分岐向けに使えます。
ステップ5: 今回のファイルを保存する
判定が終わったら、今回取得したデータを artifact として保存します。
- name: Upload products.json artifact
uses: actions/upload-artifact@v7
with:
name: ${{ env.PRODUCTS_JSON_ARTIFACT_NAME }}
path: ${{ env.PRODUCTS_JSON_DESTINATION_PATH }}
大事なのは、このアップロードは差分の有無に関係なく毎回実行する、という点です。
artifact は次回の比較のための状態なので、差分があった時だけ保存する、としてしまうと意図しないデプロイが発生します。ここは注意してください。
ステップ6: 差分がある時だけデプロイする
そしてデプロイ系のステップには、すべて同じ条件を付けます。
- name: Push application image to ECR
if: ${{ steps.diff_check.outputs.changed == 'true' || github.event_name != 'schedule' }}
uses: ./.github/actions/push-ecr
with:
...
ECRへのpush、デプロイ用ファイルの準備、ECSへのデプロイまで、コストのかかる処理すべてにこの if を付けて回ります。
条件は2つの OR で構成しています。
-
steps.diff_check.outputs.changed == 'true'— 差分があればデプロイ -
github.event_name != 'schedule'— トリガーが schedule 以外(push)ならデプロイ
差分がある時にだけデプロイする、としてしまうと、リリースブランチを更新した時に変更が反映されず困ります。
定期実行ではない場合は、差分がどうであれデプロイしたい意図的なアクションなので、無条件で通すのを忘れないようにしてください。
まとめ
定期デプロイで差分がない時にスキップする仕組みを整理すると、こうなります。
- Git管理外のファイルの差分検知をしたい場合は、GitHub Actions の artifact を状態の保管庫として利用すれば突き合わせができる
- artifact のアップロードは差分の有無に関わらず毎回行い、状態を更新し続ける
- 初回や期限切れは比較できないので、安全側に倒してデプロイする
- 差分スキップを効かせるのは定期実行のときだけにし、push は無条件で通す
参考になれば幸いです。