GitHub Self-hosted Runner in AWS CodeBuildを試す


https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner.html
に記載のある、AWS CodeBuildをGitHub ActionsのSelf-hosted Runnerとして使用可能な機能を試します。
確認時点では英語のドキュメントのみあり、日本語版ではページごとありませんでした。

2024-05-14時点での内容です。最新情報は上記ドキュメントを参照ください。また実際に動かしてみるのが一番わかると思います。
普段はGitHub ActionsをGitHub-hosted Runnerで動かしているので、CodeBuildにはあまり慣れていません。CodeBuild使っていれば常識的な部分もあるかもしれません。

:::note
2024-12-03時点の内容にアップデートした記事を投稿しました。
https://qiita.com/k-kojima-yumemi/items/573bda88d0fb607b3224
この記事の内容も有効ですが、書き方が新しくなっている箇所などがあります。
:::

## 基本的な使い方

AWS CodeBuild上でGitHubに接続設定したプロジェクトを用意し、その名前をGitHub Actionsのyamlのruns-onに記載することで動作します。
Terraformで表すと以下のような設定です。

```terraform
resource "aws_codebuild_project" "main" {
  name               = "github_runner"
  service_role       = aws_iam_role.cb.arn
  project_visibility = "PRIVATE"
  badge_enabled      = false

  artifacts {
    type = "NO_ARTIFACTS"
  }
  source {
    buildspec           = <<-EOF
version: 0.2
phases:
  build:
    commands:
      - echo "Hello"
EOF
    git_clone_depth     = 1
    insecure_ssl        = false
    location            = "https://github.com/OWNER/REPOSITORY.git"
    report_build_status = false
    type                = "GITHUB"
    git_submodules_config {
      fetch_submodules = false
    }
  }
  environment {
    compute_type                = "BUILD_LAMBDA_2GB"
    image                       = "aws/codebuild/amazonlinux-x86_64-lambda-standard:nodejs20"
    image_pull_credentials_type = "CODEBUILD"
    type                        = "LINUX_LAMBDA_CONTAINER"
  }
  logs_config {
    cloudwatch_logs {
      status     = "ENABLED"
      group_name = aws_cloudwatch_log_group.main.id
    }
  }
}

resource "aws_codebuild_webhook" "main" {
  project_name = aws_codebuild_project.main.name
  build_type   = "BUILD"
  filter_group {
    filter {
      pattern = "WORKFLOW_JOB_QUEUED"
      type    = "EVENT"
    }
  }
}
```

Self-hosted Runnerとして動かすためには、CodeBuildのWebhookで `WORKFLOW_JOB_QUEUED` のイベントを受け取るように設定します。
この設定があるプロジェクトがSelf-hosted Runnerとして認識されるようです。
コンソールでは以下のような設定になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/ee1bd3c0-d1bc-e13b-8e98-f44758aef292.png)


buildspecについては、Self-hosted Runnerとして稼働させる場合無視されます。そのため適当なbuildspecを入れるようにしています。リポジトリ内のbuildspec.yamlを使用する設定になっていても問題ありません。
コンソールから作成した場合にはリポジトリ内のbuildspec.yamlを使用する設定になっているようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/3c55912a-ef9d-ffd8-3b64-a71fab93177b.png)


上記で作成した、 `github_runner` の名前のプロジェクトを使用するために、 GitHub Actionsのyamlに以下のように記述します。

```yaml
jobs:
  build:
    runs-on: codebuild-github_runner-${{ github.run_id }}-${{ github.run_attempt }}
```

詳しくはドキュメントに記載がありますが、 `codebuild-<プロジェクト名>-${{ github.run_id }}-${{ github.run_attempt }}` と記載すると動きます。
この記載ではCodeBuildに設定されているランタイムで動作します。上の例ではx86_64のnodejs20がLambda上で動作します。

GitHub ActionsからAWSにアクセスする際にはOIDCを使用されるかと思いますが、OIDCによる接続は問題なく動作します。
例えば以下のjobであれば1回目の `aws sts get-caller-identity` ではCodeBuild自身の、Assume Roleした後の2回目の呼び出しではAssume Roleした先のロールが表示されることを確認しました。
CodeBuild実行ロールとAssume Roleするロールに特別な設定は不要で、GitHub-hosted Runnerで実行する時と同じように使えます。
```yaml
oidc:
  timeout-minutes: 5
  runs-on: codebuild-RUNNER-${{ github.run_id }}-${{ github.run_attempt }}
  permissions:
    id-token: write
    contents: read
  steps:
    - run: aws sts get-caller-identity # CodeBuildの実行ロールが表示される
    - uses: aws-actions/configure-aws-credentials@v4
      with:
        role-session-name: 'Session'
        aws-region: 'ap-northeast-1'
        role-to-assume: ${{ secrets.AWS_ARN }}
    - run: aws sts get-caller-identity # Assume Roleしたロールが表示される
```

### ランタイムの変更

`runs-on` に特殊な記述をすることで実行するランタイムを変更できます。

EC2インスタンスを使用する場合は以下のように設定できます。image等のパラメータはドキュメントを参照してください。
```yaml
runs-on: codebuild-<project-name>-${{ github.run_id }}-${{ github.run_attempt }}-<image>-<image-version>-<instance-size>
```

Lambdaを使用する場合は以下の設定になります。こちらもパラメータはドキュメントを参照してください。
```yaml
runs-on: codebuild-<project-name>-${{ github.run_id }}-${{ github.run_attempt }}-<environment-type>-<runtime-version>-<instance-size>
```

このランタイムの設定はCodeBuildのプロジェクトの設定を上書きします。
もともとEC2を使用するように設定されていても、runs-onがLambdaの記法になっていればLambdaが使用されます。
このことから、AWSで提供されているイメージを使用する場合CodeBuildのプロジェクトは1つでよく、runs-onでランタイムやスペックを調整する運用がいいと思われます。

Lambdaを使用する際には使い物にならないイメージもあります。注意点の欄を参照ください。

### カスタムイメージの使用

CodeBuildではEC2、Lambdaともにカスタムイメージを設定できます。
このカスタムイメージでもGitHub Self-hosted Runnerを動かすことが可能です。

制約として、curlかwgetがイメージ内に必要になります。 `ubuntu:jammy` のようなイメージを使用してしまうとRunnerのインストールができずに実行が失敗します。
`node:20` のようなイメージであれば問題ありません。
Lambdaで使用する際にはECRにあるイメージに限定されますが、[Amazon ECR Public Gallery](https://gallery.ecr.aws/)のdocker/library配下に基本的なイメージがあるのでそこまで困ることはないと思います。

この場合上記のランタイムの変更はできません。Webhookで以下のエラーが出ます。
```json
{"message":"Invalid input: cannot use a CodeBuild curated image with imagePullCredentialsType SERVICE_ROLE"}
```
CIが起動せずずっとwaitingの状態になります。

## 注意点

### 起動が始まらない場合

実行が始まらない場合はCodeBuildのログか、ドキュメントにある通りGitHubのWebhookの記録を見ると解決できることがあります。

例えばランタイムを変更して使用している場合、存在しないランタイムを指定するとCodeBuild側がWebhookの呼び出しに対して400を返します。
そうするとActionsの実行画面ではずっと起動待ちの状態になります。

```
Waiting for a runner to pick up this job... 
```

Self-hosted runnerなので特に課金が発生したりはしませんが、実行中だと再実行もできないのでjob単位でtimeoutを設定しておくのがいいと思います。
標準では6時間と設定されているので適当な長さを指定してください。

### Lambdaのイメージ

AWSではdotnet6やGo、Java、nodeなどのLambdaのイメージがCodeBuild用に用意されていますが、CIでよく使われるであろう `actions/checkout` が動かないイメージがありました。
EC2での実行ではどのランタイムでも動作を確認しました。

`actions/checkout` が動作するイメージ
* corretto21
* nodejs20
* python3.12
(各言語のバージョンについては調査していません)

`actions/checkout` が動作しなかったイメージ
* dotnet6
* go1.21
* ruby3.2
これらのイメージではx86_64(linux-lambda)でもaarch64(arm-lambda)でも失敗します。

実行したところ下記メッセージが表示され失敗しました。
```
/tmp/codebuild/output/src652/src/7a41e58b_4d64_4177_8f5d_5e169f2e7544/actions-runner/externals/node20/bin/node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by /tmp/codebuild/output/src652/src/7a41e58b_4d64_4177_8f5d_5e169f2e7544/actions-runner/externals/node20/bin/node)
/tmp/codebuild/output/src652/src/7a41e58b_4d64_4177_8f5d_5e169f2e7544/actions-runner/externals/node20/bin/node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by /tmp/codebuild/output/src652/src/7a41e58b_4d64_4177_8f5d_5e169f2e7544/actions-runner/externals/node20/bin/node)
```
そのうち動作するようになるかもしれませんが、2024-05-14時点では動作しませんでした。
これらの言語を使用する際にはEC2を使用するか、setup-**のactionsを使用する必要があります。
setup-**のactionsが動作するかは言語によるので、調査の必要があります。
AWSで提供されているLambdaはAmazonLinuxなので、その辺りも考慮の必要があります。

Lambdaには[こちら](https://docs.aws.amazon.com/codebuild/latest/userguide/lambda.html#lambda.limitations)で記述のある制約が存在します。
特にパッケージマネージャーは特権を必要とする場合使用できないので注意が必要です。
あとdockerもLambdaでは使用できないのでその場合はEC2を使うことになります。

### Lambdaのカスタムイメージ

CodeBuildの環境タイプをARM Lambdaにしている場合、node:20のイメージの起動ができませんでした。
Linux Lambdaに変更したら起動するようになったので、x86_64のイメージを使用しているような挙動でした。
そのためmanifestを使用して同じタグで複数のアーキテクチャに対応しているようなイメージはLambdaのARMでは使用できない可能性があります。AWS側の変更でそのうち修正されるのかもしれません。

この場合CodeBuild側のフェーズ詳細の欄に以下のログがありました。
```
CLIENT_ERROR: Image public.ecr.aws/docker/library/node:20 architecture mismatch: exec format error
```

### Organizationで使用する場合

この機能ではリポジトリごとのWebhookを利用してCodeBuildを起動させています。そのためリポジトリごとにSelf-hosted Runnerを使用可能になっている必要があります。
Organizationによっては、OrgレベルでのSelf-hosted Runnerは許可されていてもリポジトリレベルでは許可されていないことがあります。
その場合CodeBuildのログに400や404でGitHubと通信できなかったと表示されます。

リポジトリレベルでは許可されていなかった際には以下のログが表示されました。
```
[Container] 2024/05/14 06:09:18.935404 Running on CodeBuild On-demand
[Container] 2024/05/14 06:09:18.935416 Waiting for agent ping
[Container] 2024/05/14 06:09:19.136591 Waiting for DOWNLOAD_SOURCE
[Container] 2024/05/14 06:09:19.566743 Phase is DOWNLOAD_SOURCE
[Container] 2024/05/14 06:09:19.567558 CODEBUILD_SRC_DIR=/codebuild/output/src3911739186/src
[Container] 2024/05/14 06:09:19.567607 YAML location is /codebuild/readonly/buildspec.yml
[Container] 2024/05/14 06:09:19.571112 Processing environment variables
[Container] 2024/05/14 06:09:19.796892 No runtime version selected in buildspec.
[Container] 2024/05/14 06:09:19.896397 Error while fetching Github runner token: error code 400: Runner token unavailable: POST https://api.github.com/repos/OWNER/REPOSITORY/actions/runners/registration-token: 404 Not Found []
[Container] 2024/05/14 06:09:19.896412 Phase complete: DOWNLOAD_SOURCE State: FAILED
[Container] 2024/05/14 06:09:19.896423 Phase context status code: CLIENT_ERROR Message: Error while fetching Github runner token: error code 400: Runner token unavailable: POST https://api.github.com/repos/OWNER/REPOSITORY/actions/runners/registration-token: 404 Not Found []
```

ちなみにGitHubに接続する設定が未設定だと以下のログになります。
```
[Container] 2024/04/26 10:35:19.366788 YAML location is /tmp/codebuild/readonly/buildspec.yml
[Container] 2024/04/26 10:35:19.367037 Processing environment variables
[Container] 2024/04/26 10:35:19.368519 Error while fetching Github runner token: 400 error calling github/runnerToken: 400 Bad Request
[Container] 2024/04/26 10:35:19.429987 Runtime error (*clienterr.PhaseContextError: Error while fetching Github runner token: 400 error calling github/runnerToken: 400 Bad Request)
```

### `actions/cache` の使用

EC2のCodeBuildで、Dockerでビルドしたイメージをcacheに保存していたのですが、よく `actions/cache/restore@v4` や `actions/cache/save@v4` の実行中にRunnerが終了してしまうことがありました。
CodeBuildのログにはなにもなく、フェーズ詳細の欄に `Internal Service Error: CodeBuild is experiencing issues` と記載がありました。
うまくいくときも稀にあるのですが、失敗パターンの方が多くcacheを使わない方法を検討しています。
ライタイムをUbuntuからAmazonLinux2023に変更して試してみましたが同じ箇所で失敗していました。

Javaのコンパイルをする際にGradleのキャッシュを保存するようにしていたのですが、そちらでは時間はかかるものの特に問題なく動作していたので環境依存なのかもしれません。

### ghの使用

ARMのLambdaを使用してさくっとPRやIssueの管理をしたい場合があります。
Lambdaは1秒単位の課金で、ARMであればGitHub-HostedのRunnerよりもかなりコストが低いので小さいスクリプトを実行する際には重宝します。
その時非常に便利なのがghなのですが、Lambdaでは標準で入っていないので動作しません。
dnfコマンドも動作しないので、実行可能なバイナリをインストールする必要があります。

例えば以下のコマンドです。
```bash
gh_url=$(curl https://api.github.com/repos/cli/cli/releases/latest | jq -r '.assets | .[] | select(.name | endswith("arm64.tar.gz")) | .browser_download_url')
curl -L -o /tmp/gh.tar.gz "${gh_url}"
mkdir /tmp/gh
tar -xvzf /tmp/gh.tar.gz -C /tmp/gh
echo "$(dirname $(ls /tmp/gh/**/bin/gh))" >> $GITHUB_PATH
```

### dockerコンテナが残り続ける?

EC2のCodeBuildでコンテナを起動するCIを実行した後、同じCIを再実行すると同じ名前のコンテナがあるという理由でコンテナ立ち上げに失敗することがありました。
その後再実行しても同じ現象には当たらなかったので稀にしか発生しないのかもしれません。
composeを使用していたので最後に `docker compose down` を入れるようにしました。

## 終わりに

AWS CodeBuildでSelf-hosted Runnerを使用できる機能を試しました。

やや動作が不安定だったりする箇所もありましたが、GitHub Actionsでやっていたちょっとしたことを済ませるには経済的かなと思いました。
LambdaのARMであれば一番低いスペックで \\$0.00001/s (\\$0.0006/min)となります。課金は秒単位です。
GitHub-hostedでは\\$0.008/minになり、分単位の課金なので軽いタスクをやらせるだけであればLambdaのほうが経済的と判断できます。