Shogo's Blog

Dec 30, 2020 - 2 minute read - github

排他制御を行う GitHub Action を作った

弊社では GitHub のレポジトリ管理に Terraform GitHub provider を使用しています。 いちいち手元で terraform plan や terraform apply を叩くのは面倒なので、 GitHub Actions を利用することを考えました。 tf ファイルと現実のリソースとの不整合を避けるために、 これらのコマンドは排他的に実行する必要があります。 例えば terraform apply を実行している最中に terraform plan を実行することはできません。

ここで問題になってくるのが GitHub Actions のジョブ並列数です。 2020-12-30 現在、GitHub Actions は同時に 20 並列まで実行可能ですが、逆に並列数を制限できないという贅沢な悩みがあります。 一応 Matrix Build の並列数を制限するオプションはありますが、 ワークフローをまたいだ並列数の制限はできません。

これを解決するために作ったのが actions-mutex です。

使い方

ただワークフローから uses を使って呼び出すだけ。 面倒なアクセスキーの設定等は必要ありません。簡単ですね。

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: shogo82148/actions-mutex@v1
      - run: ": 排他的に実行する必要のあるタスク"

仕組み

actions-mutex と同様のことを実現する Action として GitHub Action Locks があります。 これの使用も考えたのですが、GitHub Action Locks はバックエンドに AWS DynamoDB を使用しています。 DynamoDB のテーブルを作成した上で AWS IAM を適切に設定する必要があり、セットアップが面倒です (まあ単に DynamoDB 食わず嫌いしているだけ、というのもあります)。

もうちょっとお手軽に実現できないか、と考えてできたのが actions-mutex です。

排他制御を実現するには「現在ジョブが実行されているか」を保存するために、何らかのデータストアが必要です。 しかも「ジョブの実行状況を取得する処理」と「ジョブが実行中であることを書き込む処理」をアトミックに実行できる必要があります。 これらの処理ができて、GitHub Actions からお手軽にアクセスできるデータストア・・・ そう、 Git ですね!

管理しているソースコードに不整合が起きないよう、Git にはロックを獲る仕組みが組み込まれています。 これを利用して actions-mutex では「現在ジョブが実行されているか」を「特定の名前のブランチが存在するか」で表すことにしました。

そうすると「ジョブが実行中であることを書き込む処理」は「テキトーなコミットを作成して、特定の名前のブランチに PUSH する処理」 に置き換えることができます。

$ echo "$RANDOM" > lock.txt
$ git add lock.txt
$ git commit -m 'テキトーなコミット'
$ git push origin HEAD:actions-mutex-lock/lock
To https://github.com/shogo82148/actions-mutex
 * [new branch]      HEAD -> actions-mutex-lock/lock

すでにブランチが存在していれば、 Git がこの処理を reject してくれます。 レポジトリの整合性を保つために、Git は「ブランチの存在確認」と「ブランチの作成」をアトミックに実行します。 この性質により「ジョブの実行状況を取得する処理」と「ジョブが実行中であることを書き込む処理」がアトミックに実行されると 保証できるわけです。

$ echo "$RANDOM" > lock.txt
$ git add lock.txt
$ git commit -m 'テキトーなコミット'
$ git push origin HEAD:actions-mutex-lock/lock
To https://github.com/shogo82148/actions-mutex
 ! [rejected]        HEAD -> actions-mutex-lock/lock (fetch first)
error: failed to push some refs to 'https://github.com/shogo82148/actions-mutex'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

一番面倒なところは Git がやってくれるので、あとは リモートレポジトリへの書き込み権限を設定するのと、 テキトーな間隔でポーリングする処理を追加するだけです。

制限

リモートレポジトリに新しいブランチを作成する都合上、リモートレポジトリへの書き込み権限が必要です。 他人からのプルリクエストをトリガーにした場合、 GitHub Actions で自動的に払い出される GitHub Token に書き込み権限が付与されないので、 実行に失敗します。

DynamoDB をバックエンドにしている GitHub Action Locks も AWS のアクセスキーを Secrets から読む必要があるので、 他人からプルリクエストをトリガーにした場合に動きません。

サードパーティの Actions ではここらへんが限界なので、 公式サポートを期待したいところです。

応用

ロックの獲得に使用する GitHub Token とレポジトリは、オプションで上書き可能です。 自分で発行した GitHub Token と、ロック専用のレポジトリを用意すれば、レポジトリをまたいだ排他制御も可能です。

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: shogo82148/actions-mutex@v1
        with:
          token: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
          repository: shogo82148/shogo82148
      - run: ": レポジトリ間で排他制御が必要なタスク"

まとめ

Git レポジトリをバックエンドに使用して、 ワークフローをまたいで並列数を制限する GitHub Action を作りました。

Terraform の話はどこ行ったかって?

・・・来年から本気出す。