Github ActionsのセルフホステッドランナーにAzure Container Apps(ACA) jobsを活用する

## はじめに

　Azure Container Apps jobs(以降、ACA jobsとする)が[8月末にGA](https://azure.microsoft.com/ja-jp/updates/generally-available-azure-container-apps-jobs/)されました。
個人的に使ってみて感じた強みとして、Azure PipelinesやGithub Actionsのセルフホステッドランナーとして活用できる点です。
　本記事では、Github ActionsのセルフホステッドランナーにACA jobsを活用する方法を書いていきます。

## ACA jobsについて

### Azure Container Apps
　ACA jobsはAzure Container Apps(ACA)の新しい機能です。[ACA](https://learn.microsoft.com/ja-jp/azure/container-apps/start)は公式ドキュメントの説明では以下のように謳っています。
```
サーバーレス プラットフォームでコンテナー化されたアプリケーションとマイクロサービスを実行できるフル マネージド環境です。
```
　文字だけ読むとなんか凄そうなサービスですね！実はこのサービスの下側はk8sが動いています。一般的にk8sをシステムに採用する場合、高い学習コストや半年に1度の重たいメンテを課せられます。そういった煩わしい運用面のコストや責任をユーザから取り除いて使いやすくしたというのがACAとうサービスです。今回紹介するのはその新機能となるACA jobsです。

### ACA jobsの概要

　ACA jobsは手動、スケジュール、イベントドリブンの3つタイプのトリガーをサポートしており、そのトリガーによって処理を実行することができます。k8sを使ったことある方ならピンときた方もおられると思いますが、[k8sのjobs](https://kubernetes.io/ja/docs/concepts/workloads/controllers/job/)の機能をACAで使えるようにした感じだと思われます。

### ACA jobsによるセルフホステッドランナー

　ACA jobsのもう1つの機能として、Azure PipelinesやGithub Actionsのセルフホステッドランナーとして活用できます。セルフホステッドランナーの注意点として、VMをランナーとする場合、使っていないときでも維持費が掛かってしまいます。

#### Azure Pipelines
　Azure PipelinesのセルフホステッドランナーはVMSSと呼ばれるVMをスケールさせるAzureサービスを使用することでゼロスケールが可能になり、その費用は掛からなくなります。
VMSSをACA jobsに置き換えることでVMからコンテナにすることができるので、起動時間が圧倒的に早くなります(体感)。

#### Github Actions

　Github Actionsの場合は公式ではk8s等で構築する[actions-runner-controller](https://docs.github.com/ja/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller)で自動スケーリングする機構を作る方法が紹介されています。ただ、前述の方法は学習コストが重いk8sを扱えることを前提としたり、半年に1度アプデ更新等の重たいメンテナンス作業が必須になったりします。しかしながら、ACAはk8sをラップしたサービスのため、その問題は解消されます。

## Github ActionsのセルフホステッドランナーにACA jobsを活用する

### 利点

　個人的にGithub Actionsのセルフホステッドとして、ACA jobsを利用する利点は以下です。

- Github Actionsより価格が安い
  - 最低ラインぐらいスペックを価格比較
    - [Github Actionsの価格](https://docs.github.com/ja/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates)
      - Linux 2vCPUの場合、$0.008/分
    - [ACA jobsの価格](https://azure.microsoft.com/ja-jp/pricing/details/container-apps/) 東日本リージョン
      - 2vCPU * ($0.000024 * 60) + 2メモリ * ($0.000003 * 60) = $0.0032400000000000003/分
- Azure Subscriptionにお財布を統合できる
- ゼロスケール可能 (使っていなくても課金されない)
- k8sを意識しなくて良い

### 構築

　基本的には[チュートリアル:Azure Container Apps ジョブを使用してセルフホスト型 CI/CD ランナーとエージェントをデプロイする](https://learn.microsoft.com/ja-jp/azure/container-apps/tutorial-ci-cd-runners-jobs?pivots=container-apps-jobs-self-hosted-ci-cd-github-actions&tabs=bash)を実施することでほぼ構築することができます。

### スケールについて

　ACA jobsを使用する以上、どこまでスケールするのかは気になるところです。[公式ドキュメント](https://learn.microsoft.com/ja-jp/azure/container-apps/scale-app?pivots=azure-cli#scale-definition)では最大300と記載されていますが、実際触るとAzure Portalから最大100まで設定可能であることは確認できています。ただ、1コンテナに割り当てるCPU/メモリによってクォータ制限に引っかかる可能性があるため、ちゃんと最大数が知りたい場合は負荷検証等が必要になるかと思います。

### セルフホステッドランナーのイメージについて

　前述のチュートリアルを実施しているでは[こちら](https://github.com/Azure-Samples/container-apps-ci-cd-runner-tutorial)のイメージを使用しており、[actions-runner](https://github.com/actions/runner/pkgs/container/actions-runner)のベースイメージに縛られるので、おそらく自由度はそれほど高くないと思います。

　今回はこのイメージを改造して、Azure CLIやPowershellを実行可能なセルフホステッドランナーを紹介します

```Dockerfile
# https://github.com/Azure-Samples/container-apps-ci-cd-runner-tutorial/blob/main/Dockerfile.github
# 上記のGithubのファイルをカスタマイズしています

FROM ghcr.io/actions/actions-runner:latest

USER root

# install curl and jq
RUN apt update && apt-get install -y curl jq wget apt-transport-https software-properties-common && \
    apt clean && \
    rm -rf /var/lib/apt/lists/*

# install Azure CLI
# https://learn.microsoft.com/ja-jp/cli/azure/install-azure-cli-linux?pivots=apt#option-1-install-with-one-command
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash

# install PowerShell
# https://learn.microsoft.com/ja-jp/powershell/scripting/install/install-ubuntu?view=powershell-7.3#installation-via-package-repository-the-package-repository
RUN wget -q "https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb" \
    && dpkg -i packages-microsoft-prod.deb \
    && rm packages-microsoft-prod.deb \
    && apt update \
    && apt install -y powershell \
    && apt clean \
    && rm -rf /var/lib/apt/lists/*

# install Bicep
# https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/bicep/install#linux
RUN curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64 \
    && chmod +x ./bicep \
    && mv ./bicep /usr/local/bin/bicep

COPY ./entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh

USER runner

ENTRYPOINT ["./entrypoint.sh"]
```

entrypoint.shは[こちら](https://github.com/Azure-Samples/container-apps-ci-cd-runner-tutorial/blob/main/github-actions-runner/entrypoint.sh)から変更はないです。

　チュートリアルのコンテナイメージを使用する場合、Github Actionsをキックして、10秒ぐらいでコンテナが立ち上がります。なかなか早いです。


### Github Appを使用する

　紹介されているチュートリアルではGithub PATを使用しており、ニーズとしてGithub Appも使用したいという方がおられると思うので、その方法をこちらで紹介します。

 #### コンテナイメージを改修
 
 　PATを使用している箇所は[entrypoint.sh](https://github.com/Azure-Samples/container-apps-ci-cd-runner-tutorial/blob/main/github-actions-runner/entrypoint.sh)のため、このスクリプトを以下のように改造することで対応できます。

```shell
#!/bin/sh -l

# download Github App private key
az config set auto-upgrade.enable=no
private_key="github_app_private_key.pem"
az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID
az storage blob download --account-name $AZURE_STORAGE -c $AZURE_STORAGE_CONTAINER -n $AZURE_STORAGE_BLOB -f $private_key
az logout

# get token from Github App
# https://zenn.dev/tmknom/articles/github-apps-token#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB
# 上記のスクリプトを参考にしました
base64url() {
  openssl enc -base64 -A | tr '+/' '-_' | tr -d '='
}

sign() {
  openssl dgst -binary -sha256 -sign "./$private_key"
}

header="$(printf '{"alg":"RS256","typ":"JWT"}' | base64url)"
now="$(date '+%s')"
iat="$((now - 60))"
exp="$((now + (3 * 60)))"
template='{"iss":"%s","iat":%s,"exp":%s}'
payload="$(printf "$template" "$GITHUB_APP_ID" "$iat" "$exp" | base64url)"
signature="$(printf '%s' "$header.$payload" | sign | base64url)"
jwt="$header.$payload.$signature"
rm ./$private_key

installation_id="$(curl --location --silent --request GET \
  --url "https://api.github.com/repos/$GITHUB_OWNER/$GITHUB_REPO/installation" \
  --header "Accept: application/vnd.github+json" \
  --header "X-GitHub-Api-Version: 2022-11-28" \
  --header "Authorization: Bearer $jwt" \
  | jq -r '.id'
)"

token="$(curl --location --silent --request POST \
  --url "https://api.github.com/app/installations/$installation_id/access_tokens" \
  --header "Accept: application/vnd.github+json" \
  --header "X-GitHub-Api-Version: 2022-11-28" \
  --header "Authorization: Bearer $jwt" \
  | jq -r '.token'
)"

# https://github.com/Azure-Samples/container-apps-ci-cd-runner-tutorial/blob/main/github-actions-runner/entrypoint.sh
# 上記のGithubのファイルを流用しています
registration_token="$(curl -X POST -fsSL \
  -H 'Accept: application/vnd.github.v3+json' \
  -H "Authorization: Bearer $token" \
  -H 'X-GitHub-Api-Version: 2022-11-28' \
  "https://api.github.com/repos/$GITHUB_OWNER/$GITHUB_REPO/actions/runners/registration-token" \
  | jq -r '.token')"

./config.sh --url https://github.com/$GITHUB_OWNER/$GITHUB_REPO --token $registration_token --unattended --ephemeral && ./run.sh
```

この構成では、以下の環境変数を使っています。
|環境変数|説明|
|-|-|
|AZURE_CLIENT_ID|サービスプリンシパルのClinet ID|
|AZURE_CLIENT_SECRET|サービスプリンシパルのClient Secret|
|AZURE_TENANT_ID|サービスプリンシパルのテナント ID|
|AZURE_STORAGE|Blob Storage名|
|AZURE_STORAGE_CONTAINER|Blob StorageのContainer名|
|AZURE_STORAGE_BLOB|Blob名|
|GITHUB_APP_ID|Github AppのID|
|GITHUB_OWNER|Github Owner名|
|GITHUB_REPO|Github リポジトリ名|

　Github Appを使ったtoken取得には[こちらのドキュメント](https://zenn.dev/tmknom/articles/github-apps-token#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB)から流用しています。
また、この例ではGithub Appの秘密鍵をAzure Blob Storageから取得するようにしていますが、取れれば何でも良いです。

#### Azure Container Apps Jobsの設定変更

　2023年12月頃に[こちらのissue](https://github.com/microsoft/azure-container-apps/issues/921)がCloseされ、Azure Container Apps JobsにGithub Appの設定を追加することができるようになりました。

```bash
$ az containerapp job update -n MyContainerAppJob -g MyResourceGroup --scale-rule-auth "appKey=app-key"

$ az containerapp job secret set -n MyContainerappjob -g MyResourceGroup --secrets "app-key=keyvaultref:$KEY_VAULT_PEM_URL,identityref:$USER_ASSIGNED_IDENTITIE"
```

　上記のように、--scale-rule-authの設定と--secretsの設定を変更することでGithub Appを使用することができます。設定するにはGithub Appの秘密鍵をAzure Key Vaultに登録とAzure KeyVaultにアクセスするためのマネージドIDを発行が必要になります。

- [KEDAのGithub Runner Scalerのリファレンス](https://keda.sh/docs/2.12/scalers/github-runner/)
  - [KEDAのコード](https://github.com/kedacore/keda/blob/release/v2.12/pkg/scalers/github_runner_scaler.go#L423-L429)

## まとめ

　個人的にGithub Actionsでのゼロスケールを簡単に実現できるため、非常に素晴らしいサービスだと感じました。コンテナイメージの制約はありますが、要件に合致する場合は是非使用すると良いと感じました。
 　弊社の場合は、Visual Studioのライセンスを保有している方は月6000クレジット分のAzureリソースを使用できるため、個人での検証/開発等でGithub Actionsを使用できるのは良い点だと考えています。一方で、Azure Pipelins経由でゲームのビルドとかで利用している方はGPU等を積んだVMでビルドしたほうが早いのでACA jobsを無理に使用しない方が良いです。