メインコンテンツまでスキップ

ACA JobとKEDAによるセルフホストランナー

Azure Container Apps (ACA) JobとKEDA (Kubernetes-based Event Driven Autoscaler) の github-runner スケーラーを組み合わせることで、GitHub Actionsのワークフューキューに基づいてオンデマンドにスケールするエフェメラルなセルフホストランナーを構築できます。

セルフホストランナーとは

GitHub Actionsでは、ジョブを実行する「ランナー」として2種類のオプションがあります。

比較項目GitHub-hosted RunnerSelf-hosted Runner
インフラ管理GitHub 側で管理自分で用意・管理
OSUbuntu / 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の種類

種類説明ユースケース
ManualAPIやCLIからの手動トリガーバッチ処理、移行タスク
Scheduledcron式によるスケジューリング定期レポート、クリーンアップ
Event-drivenKEDAスケーラーに基づく自動実行セルフホストランナー ← ここ

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) トークンを生成でき、よりセキュアです。

  1. GitHubの組織設定 → Developer settingsGitHub AppsNew GitHub App
  2. 以下の権限を設定:
    • Repository permissions
      • Actions: Read-only
    • Organization permissions
      • Self-hosted runners: Read and write
  3. App IDとPrivate Keyを保存

2. Azure リソースの準備

Container Apps Environment(VNet統合)
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 をベースイメージとして使用できますが、カスタムツールを追加することが多いです。

Dockerfile
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定義

terraform/modules/aca-runner/main.tf
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. ワークフロー側の設定

.github/workflows/ci.yml
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 TokenApp専用、最小権限、自動失効✅ 推奨

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を作成し、特定のリポジトリのみがセルフホストランナーを使用できるよう制限します。

組織設定での Runner Group 設定
# 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. 依存関係のキャッシュ戦略

コンテナが毎回新規起動するエフェメラル環境では、ビルド時間短縮のためにキャッシュ戦略が重要です。

.github/workflows/ci.yml
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. コンテナイメージのライフサイクル管理

.github/workflows/update-runner-image.yml
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 に近い実行時間のジョブへのアラート
  • 失敗率: ジョブ失敗の増加を早期検知
Azure Monitor アラートの例
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]

トラブルシューティング

ランナーが起動しない

  1. KEDAスケーラーのログを確認: ACA Environmentのシステムログを確認
  2. GitHub APIのレートリミット: Personal Access Tokenは50req/h、GitHub Appは15000req/hまで
  3. JITトークンの有効期限: Just-in-Timeトークンは短時間で失効するため、登録処理を素早く行う

ジョブが割り当てられない

  1. ランナーラベルの一致: ワークフローの runs-on とランナーに設定したラベルが完全一致しているか確認
  2. ランナーグループのリポジトリ設定: ランナーグループが対象リポジトリへのアクセスを許可しているか確認
  3. KEDAポーリング遅延: デフォルトのポーリング間隔(30秒)により、ジョブが見えるまで遅延が生じる

ネットワーク接続エラー

  1. サブネットのNSGルール: アウトバウンドで *.github.com (443) 、*.githubusercontent.com (443) を許可
  2. VNet統合の確認: ACA EnvironmentがVNetに正しく統合されているか確認

まとめ

ACA Job + KEDA github-runner スケーラーの組み合わせは、以下の点で優れたセルフホストランナー基盤となります。

  • ゼロコールドスタート対応: ジョブが来たときだけ起動し、アイドル時はコストゼロ
  • 完全なエフェメラル実行: 各ジョブがクリーンな環境で実行されセキュリティリスクを最小化
  • VNet統合: プライベートAzureリソースへのシームレスなアクセス
  • Managed Identity: パスワードレスでAzureリソースを操作
  • Terraformによる管理: インフラをコードとして宣言的に管理

これらの特徴により、「GitHub-hosted Runnerでは実現できないが、常時起動のセルフホストランナーは管理コストが高い」という課題を解決できます。