Actions Runner Controller (ARC) による Kubernetes 上での GitHub Actions ランナーのスケーリング

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

導入の背景と課題

木曜日の午前2時15分、キューで滞留していた本番環境向けHotfixのアラートによりPagerDutyで叩き起こされました。30件の統合テストスイートがGitHub Actionsパイプラインを完全に詰まらせていたのです。

GitHubホストランナーの同時実行上限である20並列に達し、4台の静的 c5.2xlarge EC2ランナーのCPU使用率は100%に張り付いていました。その一方で、すぐ隣にあるステージング用EKSクラスターでは、64コアものCPUがアイドル状態で眠っていました。問題は明白でした。静的なランナー群は誰もコードをコミットしない週末に毎月数千ドルものコストを浪費する一方で、リリースラッシュ時にはリソース不足でパンクしてしまうのです。

標準のGitHubホストランナーは手軽ですが、チーム規模が大きくなると利用料金が急増します。標準のLinuxコンピュートで1分あたり0.008ドルの場合、月間50,000分のビルドを実行するだけで瞬く間に予算が吹き飛びます。

さらに深刻なのは、ホストランナーからはVPC内の内部データベースやプライベートレジストリ、事前ウォーム済みのコンテナキャッシュに直接アクセスできない点です。静的VMを用意すればネットワークの壁は回避できますが、24時間365日稼働させ続けるのは費用の無駄遣いです。より優れたアプローチは、ビルドワークロードを固定のVMから切り離し、ライフサイクルの管理を直接Kubernetesオペレーターに委ねることです。

Actions Runner Controller (ARC) は、GitHub公式がサポートするKubernetesオペレーターです。HTTPSロングポーリング接続を介してワークフローキューを監視し、オンデマンドでクラスター内にエフェメラル(使い捨て)なランナーPodをプロビジョニングします。ジョブが完了した瞬間にPodは破棄されるため、不要な状態の残存やジョブ間でのディスク汚染を防ぎ、開発者が業務を終えた夜間にはコンピュートコストを完全にゼロへ抑えられます。

インストール手順

ARCのオートスケーリングアーキテクチャを展開するには、Kubernetesクラスター(v1.25以上)とHelm 3.8以上が必要です。ARCは役割を2つのHelmチャートに分割しています。カスタムリソースを管理するコントローラーマネージャーと、個々のワーカープールを表すスケールセットです。

1. GitHub認証の設定

ARCはGitHub Appを使用してOrganizationまたはリポジトリに対して認証を行います。これにより、Personal Access Tokenに課される1時間あたり5,000リクエストの厳しい制限を回避できます。以下の最小限の権限を持つOrganization用GitHub Appを作成します:

  • Organization Permissions > Self-hosted runners: Read & write
  • Repository Permissions > Actions: Read-only
  • Repository Permissions > Metadata: Read-only
  • Repository Permissions > Checks: Read-only

App IDとInstallation IDをメモし、秘密鍵(.pem)をダウンロードします。これらの認証情報をコントローラーのネームスペース内にKubernetes Secretとして保存します:

# ARC用のネームスペースを作成
kubectl create namespace arc-systems

# 認証情報をSecretに保存
kubectl create secret generic arc-github-app \
  --namespace=arc-systems \
  --from-literal=github_app_id="348210" \
  --from-literal=github_app_installation_id="42981045" \
  --from-file=github_app_private_key=/path/to/arc-app.private-key.pem

2. ARCオペレーターのデプロイ

GitHubのOCIレジストリからコントローラーマネージャーをインストールします。このチャートにはCustom Resource Definitions(CRD)とコントローラーのロジックが含まれています:

helm install arc \
  --namespace arc-systems \
  --create-namespace \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

次の手順へ進む前に、コントローラーPodが正常に稼働していることを確認します:

kubectl get pods -n arc-systems -l app.kubernetes.io/name=gha-runner-scale-set-controller

設定方法

オペレーターが起動したら、オートスケーリング対応のランナースケールセットを設定します。スケールセットのチャートは、GitHubのジョブキューを監視してエフェメラルなランナーを立ち上げる軽量なリスナーPodを管理します。

オートスケーリングランナーセットのデプロイ

まず、オートスケーリングの境界値とコンテナのリソース目標を明記した runner-values.yaml ファイルを作成します:

githubConfigUrl: "https://github.com/your-org"
githubConfigSecret: "arc-github-app"

## オートスケーリングの制約設定
minRunners: 0
maxRunners: 30

runnerGroup: "k8s-infra-runners"

template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        command: ["/home/runner/run.sh"]
        resources:
          requests:
            cpu: "1000m"
            memory: "2Gi"
          limits:
            cpu: "2000m"
            memory: "4Gi"

作成したカスタム設定値を使用してランナースケールセットのチャートをインストールします:

helm install k8s-runner-set \
  --namespace arc-runners \
  --create-namespace \
  -f runner-values.yaml \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

minRunners: 0 に設定することで、キューが空のときにPod数をゼロまでスケールダウンできます。ジョブが runs-on: k8s-runner-set を指定すると、リスナーがキューイングイベントを検知し、KubernetesスケジューラーにPodの作成を要求します。

ワークフローでランナーを指定する

.github/workflows/ci.yml 内で、パイプラインジョブのターゲットとしてスケールセット名を指定します:

name: Integration Tests
on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  test:
    runs-on: k8s-runner-set
    steps:
      - name: ソースコードのチェックアウト
        uses: actions/checkout@v4

      - name: ランナー環境の確認
        run: |
          echo "実行中ホスト: $(hostname)"
          free -m
          nproc

動作確認とモニタリング

ワークフローの手動実行(workflow_dispatch)をトリガーするか、テストブランチにプッシュして、ARCがどのように応答するか確認します。

1. Podのスケーリング動作の確認

ランナー用のネームスペースでPodの生成状況をリアルタイムで追跡します:

kubectl get pods -n arc-runners -w

リスナーは常駐して稼働します。ジョブがトリガーされると、ランナーPodは ContainerCreating から Running へ遷移し、タスクを完了したのち自動的に終了します:

NAME                                           READY   STATUS        RESTARTS   AGE
k8s-runner-set-754b578d-listener               1/1     Running       0          12m
k8s-runner-set-runner-8f2jc                    1/1     Running       0          18s
k8s-runner-set-runner-8f2jc                    0/1     Completed     0          52s
k8s-runner-set-runner-8f2jc                    0/1     Terminating   0          54s

2. トラブルシューティング時のコントローラーログ確認

ワークフローがキューに入ったままランナーがスケジューリングされない場合は、リスナーおよびコントローラーのログを確認して権限やネットワークの不具合を調査します:

# オペレーターマネージャーのログを確認
kubectl logs -n arc-systems -l app.kubernetes.io/name=gha-runner-scale-set-controller --tail=100

# スケールセットリスナーのログを確認
kubectl logs -n arc-runners -l app.kubernetes.io/component=runner-scale-set-listener

接続エラーの多くは、Secret内のRSA秘密鍵の形式不正や、GitHub Appで Self-hosted runners: Read & write 権限の付与を忘れていることが原因です。

3. Karpenterによるノードプレッシャーへの対応

ARCをKarpenterやCluster Autoscalerなどのノードオートスケーラーと組み合わせます。一度に20台のランナーが起動された場合、Pending状態のPodを検知したKarpenterが45秒未満でスポットEC2インスタンスをプロビジョニングします。ジョブが完了すると、Karpenterはそれらのインスタンスを安全にドレインして終了させます。これにより、業務のピーク時には高いCIスループットを維持しつつ、夜間に休眠状態のノードへ無駄な費用を払う心配がなくなります。

Share: