Shogo's Blog

Dec 30, 2020 - 2 minute read - github

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

弊社では GitHub のレポジトリ管理に Terraform GitHub provider を使用しています。 いちいち手元で terraform planterraform 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 の話はどこ行ったかって?

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