ACA JobとKEDAによるセルフホストランナー
Azure Container Apps (ACA) JobとKEDA (Kubernetes-based Event Driven Autoscaler) の github-runner スケーラーを組み合わせることで、GitHub Actionsのワークフューキューに基づいてオンデマンドにスケールするエフェメラルなセルフホストランナーを構築できます。
セルフホストランナーとは
GitHub Actionsでは、ジョブを実行する「ランナー」として2種類のオプションがあります。
| 比較項目 | GitHub-hosted Runner | Self-hosted Runner |
|---|---|---|
| インフラ管理 | GitHub 側で管理 | 自分で用意・管理 |
| OS | Ubuntu / Windows / macOS | 任意(Linux, Windows, macOS, コンテナ等) |
| ネットワーク | パブリックインターネット | 任意(プライベートネット可) |
| カスタムソフトウェア | 制限あり | 自由にインストール可能 |
| コスト | 使用時間に応じた課金 | インフラコストのみ |
| 起動時間 | 通常 1〜3 分 | カスタマイズ可能 |
セルフホストランナーは、自分が管理するインフラ上でランナーエージェントを実行します。
セルフホストランナーを使うユースケース
1. プライベートネットワークへのアクセスが必要な場合
GitHub-hosted Runnerはパブリックインターネットからアクセスします。そのためAzure Virtual Network (VNet) 上にあるリソース(プライベートエンドポイントで保護されたAzure SQL Database、ACR、Key Vaultなど)には直接アクセスできません。セルフホストランナーをVNet内に配置することで、これらのリソースへのプライベートアクセスが可能になります。
2. 特定のハードウェア・スペックが必要な場合
- GPUが必要なMLワークフロー: モデルのトレーニングや推論テストでGPUインスタンスを使用したい
- 大容量メモリが必要なビルド: Javaの大規模プロジェクトや.NETソリューションで32GB以上のメモリが必要
- 高速ストレージ: キャッシュの読み書きが多いビルドでNVMe SSDを使用したい
3. セキュリティ・コンプライアンス要件がある場合
- データ主権: GDPR等の規制により、コードや成果物をGitHubのインフラ上に置けない
- 機密コードの保護: ソースコードやビルド成果物を外部クラウドの共有インフラに置けない
- セキュリティ監査: ランナーの実行環境を完全に制御・監査する必要がある
- シークレット管理: Azure Key Vault等のプライベートなシークレット管理ツールと統合したい
4. カスタム環境が必要な場合
- 社内ツールの事前インストール: 特定のSDK、ライセンス済みソフトウェア、内部ツールなど
- 固定IPアドレス: 外部サービスのIPホワイトリスト登録が必要な場合
- ステートフルなキャッシュ: Dockerレイヤーキャッシュや依存関係キャッシュを永続化したい場合
5. コスト最適化
- 大規模なCI/CDパイプライン: GitHub-hosted Runnerの利用時間が多い組織では、自前インフラの方がコスト効率が良い場合がある
- スポットインスタンスの活用: AzureのSpot VMやACA Jobs のスポット機能でコストを削減できる
エフェメラルランナーとは
従来のセルフホストランナーは「常時起動型」でした。しかしこの方式では以下の問題が生じます。
- セキュリティリスク: 実行環境が複数のジョブ間で共有され、シークレットや成果物が残留する可能性
- リソースの無駄: ジョブがない間もランナーが起動し続ける
- スケール対応困難: ジョブが大量に来ても処理できない
エフェメラルランナー(--ephemeral フラグ付き)は、1つのジョブを実行したら自動的に登録解除される使い捨てのランナーです。セキュリティと効率の両面で優れており、現在のベストプラクティスとして推奨されています。
Azure Container Apps (ACA) Jobとは
Azure Container Apps Jobは、コンテナベースのタスクを実行するための仕組みです。
Container Appsの主要概念
Jobの種類
| 種類 | 説明 | ユースケース |
|---|---|---|
| Manual | APIやCLIからの手動トリガー | バッチ処理、移行タスク |
| Scheduled | cron式によるスケジューリング | 定期レポート、クリーンアップ |
| Event-driven | KEDAスケーラーに基づく自動実行 | セルフホストランナー ← ここ |
Event-drivenジョブはKEDAのスケーリングルールに基づき、イベント数に応じてジョブインスタンスを自動的に作成・削除します。
KEDAとgithub-runnerスケーラー
KEDAとは
KEDA (Kubernetes-based Event Driven Autoscaler) は、外部のイベントソース(キュー、トピック、メトリクスなど)に基づいてコンテナをスケールするオープンソースのコンポーネントです。CNCFのプロジェクトとして広く採用されており、Azure Container AppsはKEDAを内部で使用しています。
github-runnerスケーラーの仕組み
KEDAの github-runner スケーラーは、GitHubの特定リポジトリまたはオーガニゼーションのActionsワークフューキューを監視し、キュー内の待機中のジョブ数に基づいてACA Jobのインスタンス数をスケールします。
スケーリングロジック
KEDA は targetWorkflowQueueLength の値を基準に、以下の計算でインスタンス数を決定します:
desired replicas = ⌈ pending jobs / targetWorkflowQueueLength ⌉
例えば、キューに5つのジョブがあり targetWorkflowQueueLength が1の場合、5つのランナーインスタンスが起動します。
アーキテクチャ全体像
セットアップ手順
1. GitHub App の作成(推奨)
Personal Access Token (PAT) ではなく、GitHub App を使用することを強く推奨します。GitHub AppはJust-in-Time (JIT) トークンを生成でき、よりセキュアです。
- GitHubの組織設定 → Developer settings → GitHub Apps → New GitHub App
- 以下の権限を設定:
- Repository permissions
- Actions: Read-only
- Organization permissions
- Self-hosted runners: Read and write
- Repository permissions
- App IDとPrivate Keyを保存
2. Azure リソースの準備
resource "azurerm_container_app_environment" "runner_env" {
name = "cae-github-runners"
location = var.location
resource_group_name = var.resource_group_name
infrastructure_subnet_id = azurerm_subnet.aca.id
internal_load_balancer_enabled = true
tags = var.tags
}
3. ランナーコンテナイメージの作成
GitHub公式の actions/runner をベースイメージとして使用できますが、カスタムツールを追加することが多いです。
FROM ghcr.io/actions/actions-runner:latest
# カスタムツールのインストール(例:Azure CLI)
USER root
RUN apt-get update && apt-get install -y \
azure-cli \
&& rm -rf /var/lib/apt/lists/*
USER runner
ghcr.io/actions/actions-runner は GitHub が提供する公式のランナーイメージです。定期的に更新されるため、タグを固定しすぎず、CI/CDで定期的にリビルドすることを推奨します。
4. ACA JobのTerraform定義
resource "azurerm_container_app_job" "github_runner" {
name = "caj-github-runner"
location = var.location
resource_group_name = var.resource_group_name
container_apps_environment_id = var.aca_environment_id
# エフェメラルランナー: 1ジョブで終了
replica_timeout_in_seconds = 1800 # 30分タイムアウト
replica_retry_limit = 0 # リトライなし(失敗はジョブ側で管理)
# KEDA github-runner スケーラー
event_trigger_config {
parallelism = 1
replica_completion_count = 1
scale {
min_executions = 0 # アイドル時はゼロスケール
max_executions = 10 # 最大同時実行数
polling_interval_in_seconds = 30 # ポーリング間隔
rules {
name = "github-runner-scaler"
type = "github-runner"
metadata = {
owner = var.github_org
runnerScope = "org" # "repo" or "org" or "enterprise"
targetWorkflowQueueLength = "1" # 1ジョブにつき1ランナー
labels = "self-hosted,linux,azure"
}
authentication {
secret_name = "github-app-auth"
trigger_parameter = "personalAccessToken"
}
}
}
}
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.runner.id]
}
template {
container {
name = "runner"
image = "${var.acr_login_server}/github-runner:latest"
cpu = 2.0
memory = "4Gi"
env {
name = "GITHUB_APP_ID"
value = var.github_app_id
}
env {
name = "GITHUB_APP_PRIVATE_KEY"
secret_name = "github-app-private-key"
}
env {
name = "GITHUB_ORGANIZATION"
value = var.github_org
}
env {
name = "RUNNER_LABELS"
value = "self-hosted,linux,azure"
}
env {
name = "EPHEMERAL"
value = "true" # エフェメラルモード有効化
}
}
}
secret {
name = "github-app-private-key"
identity = azurerm_user_assigned_identity.runner.id
key_vault_secret_id = azurerm_key_vault_secret.github_app_private_key.id
}
registry {
server = var.acr_login_server
identity = azurerm_user_assigned_identity.runner.id
}
}
5. ワークフロー側の設定
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
# セルフホストランナーを指定
runs-on: [self-hosted, linux, azure]
steps:
- uses: actions/checkout@v4
- name: プライベートリソースへのアクセス
run: |
# VNet内のプライベートエンドポイント経由でアクセス
az login --identity
az acr login --name myprivateregistry
ベストプラクティス
セキュリティ
1. エフェメラルランナーを必ず使用する
# ランナー起動時に --ephemeral フラグを指定
./config.sh --url ... --token ... --ephemeral
エフェメラルランナーは1ジョブ実行後に自動的に登録解除されます。これにより:
- ジョブ間でのシークレット漏洩なし
- 環境の汚染なし
- 各ジョブがクリーンな環境で実行される
2. Personal Access Tokenの代わりにGitHub Appを使用する
| 方式 | セキュリティ | 推奨度 |
|---|---|---|
| Personal Access Token (PAT) | ユーザーに紐づく、有効期限の管理が難しい | ❌ 非推奨 |
| Fine-grained PAT | 権限を絞れるが依然ユーザーに紐づく | △ 許容 |
| GitHub App + JIT Token | App専用、最小権限、自動失効 | ✅ 推奨 |
3. Managed Identity(Workload Identity)を活用する
Azure Key Vaultのシークレット取得や、ACRへのログインはManaged Identityで認証し、長期的なシークレットをコンテナに埋め込まない。
# Key VaultへのManaged Identityアクセス権付与
resource "azurerm_role_assignment" "runner_kv_secrets" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_user_assigned_identity.runner.principal_id
}
# ACRへのManaged Identityアクセス権付与
resource "azurerm_role_assignment" "runner_acr_pull" {
scope = azurerm_container_registry.main.id
role_definition_name = "AcrPull"
principal_id = azurerm_user_assigned_identity.runner.principal_id
}
4. ランナーグループで実行を制限する
組織レベルでRunner Groupsを作成し、特定のリポジトリのみがセルフホストランナーを使用できるよう制限します。
# Organization Settings > Actions > Runner Groups
# - Group name: azure-private-runners
# - Repository access: Selected repositories のみ許可
# - Allow public repositories: オフ(重要)
パブリックリポジトリでセルフホストランナーを使用すると、悪意のあるPRがランナー環境でコードを実行できる危険があります。パブリックリポジトリでは必ず pull_request_target の使用を制限するか、GitHub-hosted Runnerを使用してください。
パフォーマンス・スケーラビリティ
5. スケーリングパラメータを適切に設定する
scale {
# アイドル時の完全ゼロスケール(コスト削減)
min_executions = 0
# 組織のCI/CDピーク時のジョブ数に合わせて設定
max_executions = 20
# KEDAのポーリング間隔(短すぎるとAPIレートリミットに注意)
polling_interval_in_seconds = 30
}
6. CPU・メモリサイズをワークロードに合わせて設定する
container {
# .NET / Java 等の重いビルドの場合
cpu = 4.0
memory = "8Gi"
# シンプルなスクリプト実行の場合
# cpu = 0.5
# memory = "1Gi"
}
7. ランナーのタイムアウト設定
resource "azurerm_container_app_job" "github_runner" {
# ジョブのタイムアウト(ワークフローの最大実行時間より長く設定)
replica_timeout_in_seconds = 3600 # 1時間
}
コスト最適化
8. ゼロスケールを活用する
min_executions = 0 を設定することで、ジョブがない時はランナーが一切起動しません。これにより非稼働時のコストをゼロにできます。
9. 依存関係のキャッシュ戦略
コンテナが毎回新規起動するエフェメラル環境では、ビルド時間短縮のためにキャッシュ戦略が重要です。
steps:
# Azure Blob StorageをキャッシュストレージとしてActions Cache APIで活用
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
運用・監視
10. コンテナイメージのライフサイクル管理
name: Update Runner Image
on:
schedule:
# 毎週月曜日にランナーイメージを更新
- cron: '0 1 * * 1'
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push runner image
uses: docker/build-push-action@v5
with:
context: ./runner
push: true
tags: |
${{ secrets.ACR_LOGIN_SERVER }}/github-runner:latest
${{ secrets.ACR_LOGIN_SERVER }}/github-runner:${{ github.sha }}
11. Azure Monitor でランナーを監視する
以下のメトリクスとログをAzure Monitorで収集・アラートを設定します。
- ジョブ実行数: 1時間あたりのジョブ数の急増・減少
- 起動時間:
replica_timeout_in_secondsに近い実行時間のジョブへのアラート - 失敗率: ジョブ失敗の増加を早期検知
resource "azurerm_monitor_metric_alert" "runner_job_timeout" {
name = "github-runner-job-timeout"
resource_group_name = var.resource_group_name
scopes = [azurerm_container_app_job.github_runner.id]
description = "GitHub Runnerジョブのタイムアウト検知"
criteria {
metric_namespace = "Microsoft.App/jobs"
metric_name = "FailedCount"
aggregation = "Total"
operator = "GreaterThan"
threshold = 3
}
action {
action_group_id = var.alert_action_group_id
}
}
12. ランナーのラベルを用途別に分ける
複数の用途でランナーを使い分ける場合、ラベルで分離します。
# 標準ランナー
env {
name = "RUNNER_LABELS"
value = "self-hosted,linux,azure"
}
# 高スペックランナー(ML/大規模ビルド用)
env {
name = "RUNNER_LABELS"
value = "self-hosted,linux,azure,high-memory"
}
jobs:
ml-training:
runs-on: [self-hosted, linux, azure, high-memory]
トラブルシューティング
ランナーが起動しない
- KEDAスケーラーのログを確認: ACA Environmentのシステムログを確認
- GitHub APIのレートリミット: Personal Access Tokenは50req/h、GitHub Appは15000req/hまで
- JITトークンの有効期限: Just-in-Timeトークンは短時間で失効するため、登録処理を素早く行う
ジョブが割り当てられない
- ランナーラベルの一致: ワークフローの
runs-onとランナーに設定したラベルが完全一致しているか確認 - ランナーグループのリポジトリ設定: ランナーグループが対象リポジトリへのアクセスを許可しているか確認
- KEDAポーリング遅延: デフォルトのポーリング間隔(30秒)により、ジョブが見えるまで遅延が生じる
ネットワーク接続エラー
- サブネットのNSGルール: アウトバウンドで
*.github.com(443) 、*.githubusercontent.com(443) を許可 - VNet統合の確認: ACA EnvironmentがVNetに正しく統合されているか確認
まとめ
ACA Job + KEDA github-runner スケーラーの組み合わせは、以下の点で優れたセルフホストランナー基盤となります。
- ゼロコールドスタート対応: ジョブが来たときだけ起動し、アイドル時はコストゼロ
- 完全なエフェメラル実行: 各ジョブがクリーンな環境で実行されセキュリティリスクを最小化
- VNet統合: プライベートAzureリソースへのシームレスなアクセス
- Managed Identity: パスワードレスでAzureリソースを操作
- Terraformによる管理: インフラをコードとして宣言的に管理
これらの特徴により、「GitHub-hosted Runnerでは実現できないが、常時起動のセルフホストランナーは管理コストが高い」という課題を解決できます。