🔧

GitHub Actionsセルフホストランナーのhookスクリプトを試す

2022/08/10に公開

GitHub Actionsのセルフホストランナーにひっそりと追加されたジョブの前後にhookスクリプトを実行できる機能はご存知でしょうか?

https://github.blog/changelog/2022-04-04-github-actions-job-management-hooks-for-self-hosted-runners/

2022/04なので追加されたのもかなり最近ではあるのですが、リリースノートも目立たないのでほぼ誰も話題にしていないようです。個人的にはactions/runnerにpull-requestが出たときから気になっていた機能だったのですがすっかり忘れており、先日この記事を書いたときにやっと思い出したのでちょっと調べてみました。

https://zenn.dev/kesin11/articles/gha_releasenote_ghes

まずドキュメント

意外にちゃんとドキュメントが用意されていました。

https://docs.github.com/en/actions/hosting-your-own-runners/running-scripts-before-or-after-a-job

実装されたときのpull-request。

https://github.com/actions/runner/pull/1737

リポジトリに置かれているADR(おそらくArchitecture Decision Recordsのこと)。

https://github.com/actions/runner/blob/main/docs/adrs/1751-runner-job-hooks.md

順番としてはおそらく ADR -> pull-request -> ドキュメントの順に作られたと思われるので最終的なドキュメントを見ればおおよそのことが書いてあります。

使い方と何ができるのか

セルフホストランナーを起動するときの環境変数で ACTIONS_RUNNER_HOOK_JOB_STARTEDACTIONS_RUNNER_HOOK_JOB_COMPLETED のそれぞれにbashかpowershellのスクリプトのパスを書いておくと、そのセルフホストランナー上でジョブが実行される前と後にhookとしてスクリプトが実行されます。

実際にはジョブの最初と最後に Set up runnerComplete runner というステップが自動で差し込まれ、そこでスクリプトが実行されます。スクリプトの実行ログはそれぞれのステップから確認することが可能なのでセルフホストランナーの管理者ではないユーザーもスクリプト中に何が実行されているのかを見ることが可能です。

このスクリプトは同期的に実行され、タイムアウトの設定もないので長時間かかるような処理をこの中で行うことは望ましくありません。また、exit 0以外で終了してしまうと Set up runnerComplete runner のステップがFail扱いになってしまうので注意です。特にSet up runner の方でFailさせてしまった場合はユーザーがyamlに書いたジョブの中身が全く実行されないままにジョブ自体がFailで終了してしまうため、エラーハンドリングをしっかりと行って必ずexit 0で終了するようにしましょう。

実際のスクリプト

ACTIONS_RUNNER_HOOK_JOB_STARTED に設定するstart hookのスクリプトの例

#!/usr/bin/env bash
set -Eeo pipefail
set -x
trap catch ERR

# exit 0を返せないと後続のstepに進む前にFailedになってしまうので必ずexit 0で終了させる
catch() {
  echo "Trap ERR! But exit 0 for run job"
  exit 0
}

echo "Job start hook"

echo "Show webhook json"
cat $GITHUB_EVENT_PATH

echo "Show ENV"
env

# GITHUB_ENVに追加することで以降の各stepに以下の設定が自動的に追加される
# env:
#   FOOBAR: HOGE
echo "Set additional env"
echo "FOOBAR=HOGE" >> $GITHUB_ENV

# contextは参照できないっぽい
#  echo -e "${{ toJson(github) }}"

echo "Clean workspace before run job"
rm -rf "${GITHUB_WORKSPACE}"
mkdir -p "${GITHUB_WORKSPACE}"

echo "Add job start log"
DATE=$(date "+%Y/%m/%d %H:%M:%S")
LOG="$(dirname ${RUNNER_WORKSPACE})/job.log"
echo "${DATE} Job start: ${GITHUB_REPOSITORY} ${GITHUB_JOB}" >> "${LOG}"
echo "${DATE} WORKFLOW: ${GITHUB_WORKFLOW}" >> "${LOG}"
echo "${DATE} RUNNER_NAME: ${RUNNER_NAME}" >> "${LOG}"
echo "${DATE} RUN_ID": "${GITHUB_RUN_ID}" >> "${LOG}"

できること

できないこと

  • 各種contextは参照できない
    • {{ job.status }} のようにcontextに存在するが環境変数に存在しないような値を取得できない

ACTIONS_RUNNER_HOOK_JOB_COMPLETED に設定するcompleted hookのスクリプト例

!/usr/bin/env bash
set -Eeo pipefail
set -x
trap catch ERR

# exit 0を返せないと後続のstepに進む前にFailedになってしまうので必ずexit 0で終了させる
catch() {
  echo "Trap ERR! exit 0 for run job"
  exit 0
}

echo "Job completed hook"
# Completeのhookでも参照可能
cat $GITHUB_EVENT_PATH
env

# この3つは追記専用のファイルでおそらくステップごとに生成されている
# 従ってcompletedのhookの中で見てもstartのhookでセットしたenvは見えない
cat $GITHUB_PATH
cat $GITHUB_STEP_SUMMARY
cat $GITHUB_ENV

echo "Show workspace temp directory"
ls -a "${RUNNER_TEMP}"/*
# add_path_xxxx
# set_env_yyyy
# step_summary_zzzz
# このようなファイルがステップごとに生成されているので中身を見れば各ステップでセットした値を見ることができるかもしれない

echo "Add job completed log"
DATE=$(date "+%Y/%m/%d %H:%M:%S")
LOG="$(dirname ${RUNNER_WORKSPACE})/job.log"
echo "${DATE} Job completed: ${GITHUB_REPOSITORY} ${GITHUB_JOB}" >> "${LOG}"
echo "${DATE} WORKFLOW: ${GITHUB_WORKFLOW}" >> "${LOG}"
echo "${DATE} RUNNER_NAME: ${RUNNER_NAME}" >> "${LOG}"
echo "${DATE} RUN_ID": "${GITHUB_RUN_ID}" >> "${LOG}"

できること、できないことはstartのhookと変わりません。

使い道の考察

1. 独自ログ出力

各種GITHUB環境変数とwebhookのjsonにアクセスできるのである程度の情報までは独自のログとして出せそうです。

ただしcontextにはアクセスできないので、現状ではjob contextを見ないと分からないジョブのステータスなどにアクセスできないです。completed hookの時点でジョブが終了したタイミングは分かるものの、ジョブが成功か失敗だったのかが分からないと片手落ちなので何か方法がないかdiscussionで質問してみました。

セルフホストランナーをクラウド上でまとめて動かしている場合であればログをS3やGCSに保存したり、Cloudwatchなどに送ることでセルフホストランナーの管理者側でどのリポジトリのジョブがどれだけ実行されているのかなどの情報を収集することが可能になりそうです。

2. セットアップ処理

例としてはADRに載っていたリンクから抜粋になりますが actions/runner#1543, actions/runner#1469 あたり。dockerのキャッシュを保存しておいたEBSをジョブの実行前にアタッチすることでジョブの中のdockerでキャッシュを効かせるといったユースケースが紹介されてました。

ジョブの実行前のタイミングからワークスペースディレクトリのパスをGITHUB_WORKSPACEから得ることができるため、例えばジョブの実行前にディレクトリのパスにEBSをマウントするみたいな使い方も面白いかもしれない。

他には具体的な活用方法は思いつきませんでしたが、実行されるジョブのリポジトリなどの情報に応じて環境変数にデフォルトの値を動的にセットするという使い方も面白そうです。

3. クリーンアップ処理

例えば docker logingcloud auth など普通に実行すると認証情報がホストマシンに残り続けてしまうタイプのツールがワークフローの中で実行された場合、次に実行されるジョブでその認証情報を利用できてしまいます。GitHubがホストするランナーであれば毎回VMが作り直されているので問題ないですが、基本的には1台のマシンで動かし続けるセルフホストランナーではこの挙動が望まれない場合[1]もあります。

completed hookに各種ツールの認証情報をクリーンする処理を仕込んでおくことで次回ジョブに認証情報が引き継がれないようにできそうです。可能かどうかは試してはいませんが、いっそ$HOMEの中身を毎回クリーンにしてもいいかもしれません。

4. ランナーの状態を監視

startとcompletedのタイミングでhookできるということは、モニタリング用のエージェントに情報を送ることでどのランナーがIdle状態もしくはジョブを実行中かどうかの状態を監視が可能になりそうです。

実際、k8s上にランナーを構築するactions-runner-controllerではhooksを利用して機能を追加したようです actions-runner-controller/actions-runner-controller#1268

セルフホストランナーをAWS上に構築している場合、例えばCloudwatchにジョブ実行中のステータスのランナーの数を送ることで 常にIdleとして起動しておきたいランナー数 >= 起動済みのランナー数 - ジョブ実行中のランナー数 といった条件で監視してランナーを動かすEC2やECSのオートスケールもできそうかなと思いました。

GitHubからのwebhookを受け取ってCloudwatchに送信するだけのサーバーを置くことでも実現できそうですが、ランナー側から直接情報を送ることが可能になるとセルフホストランナーを構築するアーキテクチャに幅ができそうです。

GHESで利用できるのか?

このhookスクリプト機能は公式にはGHES v3.6のリリースノートに記載されているのでv3.6からが公式サポートということになります。ちなみにv3.6は執筆時点の8/10ではまだRCですので使えるところは相当に珍しいと思います。

ですが、自分が実験した限りではGHES v3.3であってもセルフホストランナーを本来GHESでサポートされている以上のバージョンのものを使用することで動くことを確認しました。実際にはmyoung34/docker-github-actions-runnerの2.294.0で確認しています。

セルフホストランナーの本体であるactions/runnerは後方互換性がよく保たれているため、自分の経験ではGHESがサポートしているバージョン以上のランナーを動かしていることで問題が起きた記憶はありません。ですがあくまで非公式な使い方になるはずなのでそこはご注意ください。

脚注
  1. 本来はジョブごとに認証情報を切り替えたい場合にセット処理を忘れて意図しない認証情報でアクセスしてしまうといった事故が起きてしまう可能性など。 ↩︎

DeNA Engineers

Discussion