🐧

lock 機構のための GitHub Action を作った

2024/10/29に公開

lock 機構を実現するための GitHub Action を作ったので紹介します。

https://github.com/suzuki-shunsuke/lock-action

背景

lock 機構によって複数のプロセスが同時に実行されるのを防ぐことが出来ます。

例えば GitHub Flow で Pull Request (以下 PR) をマージしたらデプロイを実行する場合に、デプロイ直前に lock を取り完了後に unlock することで、デプロイ中に他の PR がマージされてデプロイが複数同時に実行されるようなことを防ぐことが出来ます。

また、 Terraform で State の分割作業を行う際に、作業前に lock を取り完了後に unlock することで作業中に terraform plan, apply が実行されるのを防ぐことが出来ます。

今回は GitHub Actions で簡単に lock 機構を実現するための Action を開発しました。

特徴

今回の action の特徴です。

  • AWS や GCP といった外部サービスには依存しません
  • JavaScript Action として実装され shell や Git のような外部コマンドにも依存しません
  • lock の状態管理のために GitHub Repository の branch を利用
  • branch の操作は全て GitHub API で実行されるため、事前に actions/checkout でリポジトリをチェックアウトする必要もありません
  • lock / unlock の履歴を残せます

使い方

基本は次のように action を実行するだけです。

deploy 実行中に lock を取る
jobs:
  deploy-foo:
    runs-on: ubuntu-24.04
    permissions:
      contents: write # lock / unlock するのに必要
    steps:
      - uses: suzuki-shunsuke/lock-action@v0.1.1
        id: lock
        with:
          mode: lock # 必須 lock / unlock / check
          key: foo # 必須
          post_unlock: "true" # 任意。 post step で unlock する
      - run: bash deploy.sh

keymode が必須です。

key は必須で、 lock を一意に識別する文字列です。
サービスや環境を含めることで、サービスや環境毎に lock を管理できます。
単一の key しかないのであれば default などてきとうな文字列を指定してください。

mode には lock, unlock, check のいずれかを指定ください。

  • lock: Lock します
  • unlock: Unlock します
  • check: Lock 状態をチェックします

デフォルトでは lock を取るだけで unlock しないので job が終わったあとも lock は残ります。
post_unlock: "true" を指定することで post step で unlock をすることが出来ます。
lock を取ったつもりが unlock されていたというのが怖いので、デフォルトでは unlock しないようにしています。

また lock action 実行時に既に lock が取られていた場合、 step は失敗します。
既に lock が取られていた場合でも処理を続行したい場合、 ignore_already_locked_error で処理を続行することが出来ます。

- uses: suzuki-shunsuke/lock-action@v0.1.1
  id: lock
  with:
    key: foo
    mode: lock
    ignore_already_locked_error: "true"
- run: echo "already locked"
  if: steps.lock.outputs.already_locked == 'true' # output で結果を参照できる

lock を取れてないのに気づかずそのまま処理が続行されるのは危険なため、デフォルトでは lock が既に取られていたら失敗するようにしています。

post_unlock を使わずに unlock することも出来ます。

- uses: suzuki-shunsuke/lock-action@v0.1.1
  with:
    key: foo
    mode: unlock # unlock する

lock が取られているかどうか check することも出来ます。

- uses: suzuki-shunsuke/lock-action@v0.1.1
  id: check
  with:
    key: foo
    mode: check # check する
- run: echo "already locked"
  if: steps.check.outputs.already_locked == 'true' # output で結果を参照できる

仕組み

lock 機構を実現するには lock の状態をどこかしらに永続化する必要があります。
色々やり方はありますが、今回は GitHub Repository の branch を使っています。

action は lock を取る際に ${key_prefix}${key} という branch を作成します。
key_prefix はデフォルトで lock__ ですが、 input で変更できます。

branch の commit message によって lock 状態を管理します。

commit message の例
unlock by suzuki-shunsuke: test
{
  "message": "test",
  "state": "unlock",
  "actor": "suzuki-shunsuke",
  "github_actions_workflow_run_url": "https://github.com/suzuki-shunsuke/test-github-action/actions/runs/11545637203?pr=237",
  "pull_request_number": 237
}

このように commit message を見ればどの workflow run で誰がいつ lock / unlock をしたか分かります。
branch の history を見ればいつ誰が lock / unlock したのか分かります。

余談ですが、 commit message に PR へのリンクを載せると以下のように PR のタイムライン上に表示されて鬱陶しいのでリンクを含めないようにしています。

Example

workflow_dispatch を用いた簡単なサンプル workflow を提供しています。
この workflow を適当なリポジトリにコピーして workflow_dispatch を実行することで lock 機構を試すことが出来ます。
この workflow では 選択する mode によって動作が変わります。
このサンプルでは terraform plan, apply を題材にしていますがあくまでサンプルなので実際に terraform を実行したりはしません。

  1. lock: lock を取ります。 workflow 完了後も lock されたままになります
  2. unlock: lock を解除します
  3. check: lock が取られているか確認し、結果を出力します
  4. terraform_plan: lock が取られているか確認し terraform plan を実行したふりをします。 lock が既に取られていたら失敗します
  5. terraform_apply: lock を取り terraform apply を実行したふりをします。 120 秒ほど lock を取ります。 lock が既に取られていたら失敗します。終わったら lock を解放します

まず lock が取られていない状態で、 terraform_plan を選択して workflow を実行すると普通に成功するはずです。
terraform_apply を選択して実行すると 2 分ほど lock が取られ、最後に lock が解放されます。
その状態で terraform_planterraform_apply を選択して実行すると失敗するはずです。
terraform_plan は lock を取らないので terraform_plan を並列実行しても問題ありませんし、 terraform_plan 実行中に terraform_apply を実行することも出来ます。
terraform state の分割などで一定期間 lock を取ったままにしておきたい場合、まず lock を選択して実行して lock を取ります。
そうすると workflow が終わったあとも lock されたままになります。
作業完了後 unlock を選択して実行すると lock が解放されます。

key_prefix, key の制約

この action は ${key_prefix}${key} という branch を作成するため、 ${key_prefix}${key} は GitHub 及び Git のブランチ名の制約を受けます。

https://docs.github.com/ja/get-started/using-git/dealing-with-special-characters-in-branch-and-tag-names

https://git-scm.com/docs/git-check-ref-format

Alternatives

この action 以外にも lock 機構を実現する action は幾つかあります。

https://github.com/github/lock

公式の action ですが、単純に lock を管理するだけではなく色々複雑なことをやっています。
自分の求めているものとは違うと感じたので試してもいません。
GitHub API で branch を操作している点は今回の action と同様です。

https://github.com/ben-z/gh-action-mutex

  • Docker action なためオーバーヘッドが大きい
  • shell script で実装されていて Git に依存している
  • 恐らく必ず post step で unlock するようになっているため、 Terraform State の分割作業中は lock を取るといった使い方には向かない (workflow を実行しっぱなしにすれば可能ではあるが)

Discussion