Self-hosted Runner in CodeBuildでレビュー機能を使ったときに承認待ちの間課金される問題とその対策
tl;dr
- GitHub Self-hosted Runner in AWS CodeBuildとレビュー機能(Reviewing deployments)を組み合わせると、ユーザーが承認するまでの間CodeBuildがずっと実行中になり、課金され続けます
- 複雑性は上がりますが、EnvironmentとJobをそれぞれ1つ余分に作って承認専用のダミージョブを置くことでこの課金を回避することができます
背景
2024年04月にSelf-hosted GitHub Actions runners in AWS CodeBuildという機能が公開されました。
これはGitHub ActionsのSelf-hosted RunnerとしてCodeBuildを選べるようになる機能です。これでGitHub Actionsの処理がVPCの壁を超えやすくなり、DBのマイグレーションなどをGitHub Actionsで行いやすくなりました。
また、Reviewing deploymentsという機能があります。
これは、GitHub Actionsのワークフロー実行の前に特定のユーザー(グループ)による承認ステップを置くものです。リリースマネージャーなどが最終確認するためなどに使用できます。
この2つはそれぞれ便利ですが、組み合わせると非常に都合の悪いことが発生します。
それは、承認待ちの間もCodeBuildで課金が発生してしまうことです。
実験
この承認待ちの間も課金されてしまうという現象はGitHub公式のランナーでは発生せず、GitHub Self-hosted Runner in AWS CodeBuildでは発生します[1]。
それを実験で確認します。
注: 以下、一部説明の名前と画像で表示される名前(Environment名、ユーザー名など)が食い違う箇所が出てきますが、説明文の方が正です。画像は参考程度にとらえてください。
実験環境
dev-with-reviewというEnvironmentがRequired Reviewers付きで作成されているものとします(画像参考)。
またGitHub Self-hosted Runner in AWS CodeBuildもセットアップされているものとし、 conditionの"repo:オーガニゼーションの名前/リポジトリ名:environment:dev-with-review"
の記述によって特定のEnvironmentと紐付いたジョブでのみ利用可能とします。
data "aws_iam_openid_connect_provider" "this" {
url = "https://token.actions.githubusercontent.com"
}
resource "aws_iam_role" "this" {
name = "self_hosted_runner"
assume_role_policy = data.aws_iam_policy_document.this.json
}
data "aws_iam_policy_document" "this" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
effect = "Allow"
condition {
test = "StringLike"
values = [
"repo:オーガニゼーションの名前/リポジトリ名:environment:dev-with-review"
]
variable = "token.actions.githubusercontent.com:sub"
}
principals {
identifiers = [data.aws_iam_openid_connect_provider.this.arn]
type = "Federated"
}
}
}
1. GitHub-hosted runners(デフォルトのランナー)の場合
以下のワークフローファイルをpushして実行します。
name: github-hosted-runner
on:
push: {}
jobs:
main:
environment: dev-with-review
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest # GitHub-hosted runners
steps:
- run: echo "hello, world"
すると次のようにワークフローが実行され、承認が待機されます | このまま待ちましたが、実行時間は増えていきません |
---|---|
数分後に承認すると | 実行はすぐ終わりましたが、 |
---|---|
やはり実行時間は実際の実行にかかった時間のみでした | |
以上の結果から GitHub-hosted runners を使った場合のコストはどれだけ承認が遅れても実際の実行時間のみとわかります。
2. Self-hosted Runner in CodeBuildの場合
以下のワークフローファイルをpushして実行します。
name: self-hosted-runner
on:
push: {}
jobs:
main:
environment: dev-with-review
permissions:
id-token: write
contents: read
runs-on: codebuild-project_a-${{ github.run_id }}-${{ github.run_attempt }} # GitHub Self-hosted Runner in AWS CodeBuild
steps:
- run: echo "hello, world"
すると次のようにワークフローが実行され、承認が待機されます | このまま待ちましたが、実行時間は増えていきません |
---|---|
ここまでは GitHub-hosted runners と同じです。
しかし、この時点でCodeBuild側ではビルドが開始されています | その後承認せず10分以上待ちましたが、ずっと実行されていました |
---|---|
以下はビルドログです。Listening for Jobs というメッセージでずっと待機しています |
GitHub Actions側のログはこのようになっています |
その後に承認すると実行はすぐ終わりましたが、 | GitHub Actions側の実行時間は実際の実行にかかった時間のみでした[2] |
---|---|
以上の結果から Self-hosted Runner in CodeBuild を使った場合、承認待ちの間もCodeBuildが実行されてしまい、コストがかかることがわかります。
実験結果のまとめ
今回使ったのはEC2を使ったCodeBuildでしたが、おそらくLambdaを使った場合でも同様だと思われます。また、基本的にはSelf-hosted Runnerが承認待ち状態になるまでに環境のセットアップを要求する、ということだと思われますので、CodeBuild以外のSelf-hosted Runnerでも事情は同様だと推測されます。
これによる問題はいくつかあります。
一つはCodeBuildのような実行時間でコストが増えていくサービスを使ったとき、不要なコストがかかることです。この承認待ちの間、確保されたコンピューティングコストは空転するだけで何の仕事もなさないため、全くの無駄ということになります。
もう一つはキューを占拠することです。Self-hosted Runnerでは同時実行数に上限があったり、同時には1つのジョブしか走らないようにしているケースがありますが、この承認待ちのジョブはあくまで実行中のステータスであるため、キューを占拠してしまいます。
承認待ちの間も課金される問題の対策
少し複雑になるものの、承認待ちの間も課金されなくする方法があります。
必要なものは以下です:
- Required Reviewersが設定されていない
dev-without-review
というEnvironment - 承認のためだけのダミーのジョブを作り、メインのジョブをダミージョブに依存させる
準備
まず、Reviewが設定されていない dev-without-review
というEnvironmentを作ります。
そして、CodeBuildの利用許可の紐付けをそちらへ切り替えます[3]。
data "aws_iam_policy_document" "this" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
effect = "Allow"
condition {
test = "StringLike"
values = [
- "repo:オーガニゼーションの名前/リポジトリ名:environment:dev-with-review"
+ "repo:オーガニゼーションの名前/リポジトリ名:environment:dev-without-review"
]
variable = "token.actions.githubusercontent.com:sub"
}
principals {
identifiers = [data.aws_iam_openid_connect_provider.this.arn]
type = "Federated"
}
}
}
最後に、ワークフロー定義を以下のように修正します。
name: self-hosted-runner-fix
on:
push: {}
jobs:
dummy-for-review:
environment: dev-with-review
runs-on: ubuntu-latest # GitHub-hosted runners
steps:
- run: echo "dummy"
main:
needs: dummy-for-review
environment: dev-without-review
permissions:
id-token: write
contents: read
runs-on: codebuild-project_a-${{ github.run_id }}-${{ github.run_attempt }} # GitHub Self-hosted Runner in AWS CodeBuild
steps:
- run: echo "hello, world"
以下ポイントです:
- 承認待ちのためだけのダミーのジョブ(dummy-for-review)を追加する
- ダミーのジョブはGitHub-hosted Runnerで実行させるため、
runs-on
をubuntu-latest
などにする - 本命のジョブ(main)はダミーのジョブに依存(needs)させる[4]
- 本命のジョブのEnvironmentは承認作業を必要とする
dev-with-review
から必要としないdev-without-review
へ変える
実際にやってみる
実行するとこのように最初のダミージョブで承認待ちされます | 承認待ちですが実行箇所がGitHub-hosted RunnerのためCodeBuildはまだ実行されません |
---|---|
その後、実行を承認すると | 以下のように進み始めました |
2つ目のメインのジョブに入った時点でCodeBuild側も実行されます | |
以下、完了時の状態です:
ジョブは通常通り終了 | 実行時間も普通でした |
---|---|
CodeBuildは承認待ちの間も待機しておらず、実行時間は最小です | |
まとめ
本記事ではSelf-hosted Runnerとレビュー機能を組み合わせたとき特有の問題とその回避方法について紹介しました。
この複雑性の増加と承認待ちのコスト増のどちらを取るかは、結構難しいところです。
なぜこういった複雑なワークフロー構造・Environment定義にしているのかが周りに伝わらないとこの設定の維持メンテナンスが難しく、把握していない人が悪意なくダミージョブを削除してしまう可能性があります。この記事を書いた背景はそこの説明をこの記事へのリンク一つで済ませられるようにする、という狙いがありました。
ただ、この記事があったとしてもEnvironmentが不要に増えてしまいdeploymentの画面が複雑になるなどのデメリットは残ります。その辺りを加味して、すぐに承認/否認できるようなチーム・組織だから多少のランニングコストの増加は許容すると決めてあえて回避策を取らない。そういった選択も全然ありだと思います。
-
ちなみにこの現象は承認待ちの間もランナーが待機状態で空転するという現象なので、おそらくCodeBuild以外のランナーでも同様のことは発生すると思います ↩︎
-
実際のところ、Self-hosted Runnerではどれだけ実行時間が伸びてもGitHubからの課金はないため、この実行時間は参考情報以上の意味はありません ↩︎
-
正確に言うと
dev-with-review
からの実行許可は取り消す必要はないのですが、このあともう使わないので削除します ↩︎ -
これによって承認後に実行されることを担保します ↩︎
Discussion