Kubernetes を self-hosted Runner として利用してテスト効率を向上させた話

2024年12月19日掲載

キービジュアル

ソフトバンク アドベントカレンダー2024 19日目の記事です。
皆さん、こんにちは。共通プラットフォーム開発本部の日吉です。

相変わらず世間の AI ブームはすごいですね。汎用的な超知能と呼ばれる ASI が社会規範そのものを変えるのも近からず遠からずと思わせるほどです。

AIの進化による恩恵を大きく受けている人には私のようなエンジニアも含まれます。今や GitHub Copilot をはじめとしたAIコーディング支援ツールを一切活用していない開発者のほうが少ないのではないかと感じるほどに、AIによるコーディングが当たり前になりました。これによって、高度なプログラミング技術を持たない人でもアイデアを言語化する力があればアプリケーションを開発できる時代に突入し始めており、アプリケーションエンジニアにとってはコードが書けること以外の価値を訴求すべき時代とも言えるでしょう。

そんな時代だからこそ大切になってくることの一つがアプリケーションの品質だと私は考えています。品質に関する要件は機能要件に比べて言語化しづらいというのが一般的な見方です。だからこそ、ベストプラクティスに沿ってコードをリファクタリングしたり、網羅的なテスト仕様をテストコードに落とし込んだうえでCI/CDプロセスに組み込んだり、といった取り組みが、単にAIに書かせただけのコードとの違いを生み、お客様にとっての価値になるだろうと考えています。

今回の記事はそんなアプリケーションの品質を高める取り組みの一つである、CI/CDプロセスの実行基盤としてすっかりおなじみの GitHub Actions と、GitHub Actions  の self-hosted RunnerとしてAzure Kubernetes Services (以下、AKS)を活用したことについて書いていこうと思います。

目次

この記事では
  • GitHub ActionsとAKSを組み合わせてセルフホステッドランナーを並列利用する方法について説明します。
  • CI/CDプロセスを最適化し、E2Eテストの並列実行による実行時間の短縮を目指しました。
  • Playwrightを用いたE2Eテストの具体例も紹介し、実際の運用における課題と改善点についても触れています。

GitHub Actions とは

GitHub Actions に関する解説記事はWeb上にたくさん存在するので簡潔な説明に留めます。

GitHub Actions とは、GitHubが提供する CI/CD(継続的インテグレーション/継続的デリバリー)プラットフォームであり、開発プロセスの自動化を目的としたサービスです。リポジトリ内で発生した特定のイベント(例えば、コードのプッシュやプルリクエスト)をトリガーに、事前に定義したワークフローに従って、様々な処理 (例えばビルド、テスト、デプロイ) を自動で行わせることが可能です。

self-hosted Runner とは

GitHub Actions では通常、GitHub があらかじめ用意した GitHub-hosted Runner が実行環境として利用されます。

一方、self-hosted Runner とは、ユーザー自身が用意した環境をGitHub Actionsの実行環境として利用する機能です。self-hosted Runner を利用することで、独自のサーバーやインフラストラクチャを使用してCI/CDを実行することが可能になり、実行環境のカスタマイズ性が高まります。特にネットワークやOS、実行環境のスペックなど低レイヤーの制約に関してカスタマイズしたい場合には特に有用な手段です。

self-hosted Runner を利用して実現したいこと

私のチームでは Azure OpenAI Services を活用したWebアプリケーションを Microsoft Azure の App Service 上にデプロイしており、そのWebアプリケーションに対するE2Eテストを GitHub Actions のCIプロセスに組み込んでいました。

これまでも送信元IPアドレスを固定する目的で Azure 上にデプロイされた仮想マシンを self-hosted Runner として利用していたのですが、アプリケーションの成長とともにE2Eテストの項目が非常に多くなり、テストの実行時間が長くなりすぎているのが課題でした。

加えて、複数のメンバーが別々の Pull Request を同タイミングで起案した場合、一方のCIプロセスで仮想マシンが占有されてしまい、それが終わるまではもう一方のCIが始まらない、というような順序依存性の課題も抱えていました。

そこで、GitHub Actions の実行環境として仮想マシンに代わって Kubernetes を活用することで、実行ジョブの個数に応じて実行環境が自動的にスケールし、CIの実行時間を最適化させられると考えました。

Azure Kubernetes Services の採用理由

Kubernetes を利用する方法は多様ですが、今回 AKS を self-hosted Runner として利用しようと思い至ったのは、以下の3点が主な理由です。

  • Kubernetes のスケーラビリティを活用するため
  • Webアプリに対してアクセス可能なIPアドレスを制限するため
  • Webアプリが Azure に存在するため (敢えてオンプレ環境や他クラウドを利用する理由が無い)

スケーリング面のカスタマイズ

Kubernetes は アプリケーションが動作する環境 (Pod) を様々な条件に基づいてスケールアウト/スケールインさせることが可能です。E2Eテストの並列数を GitHub Actions のワークフロー定義内で指定することで、ジョブ数と Kubernetes のスケーリング設計に応じて適切にオートスケールされるため、複数のジョブを並列実行することが可能になります。 

ネットワーク面のカスタマイズ

GitHub-hosted Runner を利用しながら IP アドレスを静的な値にする手段は今のところ Larger Runner を利用する他にありません (2024/12時点)。元から高性能な実行環境が必要な場合はこちらを採用するのも一つかもしれませんが、私の場合 E2E テストフレームワークが実行可能であれば特別スペックは求めていなかったので、Web アプリと同一テナント上に構築された AKS で静的な IP アドレスをもたせることで、Web アプリの送信元アクセス制御を行おうと思いました。

Actions Runner Controller とは

Kubernetes を self-hosted Runner として利用する場合、Actions Runner Controller (ARC) を活用します。ARC は GitHub Actions の self-hosted Runner を調整およびスケーリングするための Kubernetes オペレーターです。ARC は GitHub Actions ワークフローで定義されたジョブの数に基づいて、自動的にスケーリングされるランナースケールセットを作成し、ワークフローが複数のリポジトリで同時に実行される場合や、同一のワークフロー内で複数のジョブを同時実行したい場合でも、実行待ち時間を最小限にし、シームレスな CI/CD を実現します。

事前準備

今回の主題はARCのデプロイと、デプロイした環境を GitHub Actions で利用する設定手順とします。そのため、以下の準備が整っている前提で話を進めていきます。

  • AKS リソースがデプロイ済みであること
  • Podのスケーリングを考慮したネットワーク設計が済んでいること 
  • Helmが予めインストールされていること

self-hosted Runner と GitHub 間の認証トークン発行

self-hosted Runner が GitHub Actionsのランナーとして動作するためには何らかの認証の仕組みが必要です。 GitHubへの認証を行う手段としては大きく2つの方法が存在します。

  • Personal Access Token (PAT)
  • GitHub Apps

Personal Access Token の発行はユーザ個人で行えるため(管理者の承認は必要ですが)、認証トークンが個人のアカウントに紐づき、その管理がユーザに依存してしまいます。私の場合は self-hosted Runner を構築した後、他のチームにも活用してもらい中長期的に運用する予定だったので、定期的なトークンの再生成が不要な GitHub Apps を採用しました。

以下の手順で GitHub Apps を作成し、認証情報 (.pemファイル) はローカルに控えておきます。(参考: GitHub 公式より)

1. 組織の「設定」ページ GitHub Apps の新規作成を行う

2. Homepage URLを http://github.com/actions/actions-runner-controller に設定する

3. Repository permissions に以下の権限を追加して、アプリを作成する

  • Administration : read and write
  • Metadata : read-only

※画像はAdministration に read and write 権限を付与した状態

4. Organization permissions に以下の権限を追加

  • Self-hosted runners : Read and write

5. 作成したGitHub Appの設定画面から App ID の値をローカルに保存する

6. Private keys の項目からGenerate a private keyを実行し、.pemファイルをローカルに保存する

7. Install app をクリックして、作成した GitHub Apps を組織にインストールする

8. インストール完了後、installation_ID をローカルに保存する。insatallation_ID は以下のフォーマットでアプリのインストールページに保管されている。

https://github.com/organizations/ORGANIZATION/settings/installations/INSTALLATION_ID 

Actions Runner Controller (ARC) のデプロイ

先ほど発行した GitHub Apps の認証トークンを利用して、ARCのデプロイをしていきます。

構築対象のAKSクラスタへログイン

1. 以下のコマンドを実行して、ログインに必要なクレデンシャルを ~/.kube/config に保存します。

az login
az account set --subscription "TARGET_SUBSCRIPTION_ID"
az aks get-credentials --resource-group <TARGET_RESOURCE_GROUP> --name <TARGET_AKS_RESOURCE_NAME>

2. コマンドの実行結果を確認し、ターゲットのAKSリソースに接続していることを確認します。

Merged "<TARGET_AKS_RESOURCE_NAME>" as current context in /hoge/fuga/.kube/config

3. 以下のコマンドで接続対象のクラスタを確認する。今回の操作対象は{TARGET_CLUSTER_NAME} であると仮定して進めます。

kubectl config get-contexts

4. CURRENTに*がついているクラスタが現在接続しているクラスタです。

CURRENT   NAME                         CLUSTER                      AUTHINFO                   NAMESPACE
*         <TARGET_AKS_RESOURCE_NAME>   <TARGET_CLUSTER_NAME>   ************************ 

5. (Optional) 接続先のクラスタが異なる場合は、以下のコマンドで接続先のクラスタを切り換える。

kubectl config use-context {TARGET_CLUSTER_NAME}

ARC のインストール

1. 以下のコマンドを実行して、AKSクラスタ上にARCをインストールします。

NAMESPACE="arc-systems"
helm install arc \
    --namespace "${NAMESPACE}" \
    --create-namespace \
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

2. コマンドが成功すると以下のような出力が得られます。NAME_SPACEで指定した名前の通りであることを確認してください。

Pulled: ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller:0.8.1
Digest: sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxx
NAME: arc
LAST DEPLOYED: Fri Jan 19 08:00:17 2024
NAMESPACE: arc-systems
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing gha-runner-scale-set-controller.

Your release is named arc.

Kubernetes の Secret セットアップ

1. Runner scale set をデプロイするための名前空間を事前に作成します。

RUNNER_NAMESPACE="arc-runners"
echo ${RUNNER_NAMESPACE}    
kubectl create namespace ${RUNNER_NAMESPACE}

2. 予め取得しておいた App ID、installation_ID、.pem ファイルで以下のコマンドのパラメータを書き換えて実行します。

# シェル変数の定義
GITHUB_ACTIONS_RUNNER_SECRET="github-actions-runner-secret"
RUNNER_NAMESPACE="arc-runners"
GITHUB_APP_ID="xxxxxx"
GITHUB_APP_INSTALLATION_ID="yyyyyyyy"
PRIVATE_KEY_PATH="/PATH/TO/THE/FILE.pem"

# secret登録
kubectl create secret generic "${GITHUB_ACTIONS_RUNNER_SECRET}" \
  --namespace="${RUNNER_NAMESPACE}" \
  --from-literal=github_app_id="${GITHUB_APP_ID}" \
  --from-literal=github_app_installation_id="${GITHUB_APP_INSTALLATION_ID}" \
  --from-file=github_app_private_key="${PRIVATE_KEY_PATH}"

3. 以下のコマンドで認証トークンが Secret に登録されたことを確認します。

kubectl describe secrets/"${GITHUB_ACTIONS_RUNNER_SECRET}" -n "${RUNNER_NAMESPACE}"

4. 以下のような出力が得られれば Secret の登録が正常に行えています。この Secret は Runner scale set (GitHub Actions のジョブ実行リクエストに応じて処理を行うPodの実体) をデプロイするときに使われます。

Name:         github-actions-runner-secret
Namespace:    arc-runners
Labels:       <none>
Annotations:  <none>
Type:  Opaque
Data
====
github_app_private_key:      1679 bytes
github_app_id:               6 bytes
github_app_installation_id:  8 bytes

Runner scale set のデプロイ

GitHub Actions からのジョブ実行リクエストに応じて、自動的にPodがスケールするときに利用される self-hosted-runner の実体を担います。 以下の表を参考にパラメータを設定した後、コマンドを実行してください。

Runner scale set デプロイ時に設定可能なオプション変数
Parameter説明

INSTALLATION_NAME

GitHub Actions ワークフロー定義内で呼び出す時の runs-on で指定する名前を入力する。

NAMESPACE

self-hosted-runnerのpodがデプロイされる名前空間。Runner scale setの名前空間はARCとは異なる名前空間を利用することが推奨されている。

GITHUB_CONFIG_URL

今回デプロイする self-hosted-runner を利用するリポジトリ、Organization または Enterprise の URL を設定する。

GITHUB_ACTIONS_RUNNER_SECRET

予め登録しておいた Secret の名前。

その他

多数のオプションが存在するため、GitHub 公式ドキュメントを参照して下さい。

1. 以下のコマンドを実行して、Runner scale setをデプロイします。

# シェル変数の定義
INSTALLATION_NAME="self-hosted-runner-cluster"
GITHUB_CONFIG_URL="https://github.com/{ORGANIZATION_ID or ENTERPRISE_ID}"

# インストールコマンドの実行
helm install "${INSTALLATION_NAME}" \
    --namespace "${RUNNER_NAMESPACE}" \
    --set githubConfigUrl="${GITHUB_CONFIG_URL}" \
    --set githubConfigSecret="${GITHUB_ACTIONS_RUNNER_SECRET}" \
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

2. デプロイが正常に完了すると、以下のように返ってきます。

Pulled: ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set:0.8.1
Digest: sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxx
NAME: autoscale-runner-set
LAST DEPLOYED: Fri Jan 19 09:23:43 2024
NAMESPACE: arc-runners
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing gha-runner-scale-set.

Your release is named autoscale-runner-set.

self-hosted Runner の動作確認

ここまでの手順で Kubernetes 版 self-hosted Runner の準備が整いました。

GitHub の組織設定から Actions -> Runners を確認すると、Runner scale setのINSTALLATION_NAME に登録した名前で self-hosted-runnerが登録されていることが確認できます。

動作確認のために、以下のようなGitHub Actions のワークフロー定義ファイルを用意しました。

name: Build and deploy Node.js app to Azure Web App
on:
  workflow_dispatch:
jobs:
  example_matrix:
    runs-on: autoscale-runner-set
    strategy:
      matrix:
        version: [10, 12]
    steps:
      - name: prepare-various-ver-containers
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.version }}

このワークフロー定義は、異なるバージョンの Node.js をインストールしたRunnerをビルドするだけの処理を記述したものです。実際の利用シーンであればセットアップ完了後に、異なるバージョンのNode.js でアプリが正常に動作するのかテストしたり、動作の違いを比較したりするでしょう。

ここでは同時に2つの runner (pod) が立ち上がり、それぞれが独立した異なる環境として動作することを確認したいと思います。

workflow 実行中に以下のコマンドを実行すると、arc-runners という名前空間上に2つのPodが立ち上がっていることが分かります。

kubectl get pods -n arc-runners -o wide
NAME                                      READY   STATUS    RESTARTS   AGE     IP           NODE                              NOMINATED NODE   READINESS GATES
autoscale-runner-set-jkl44-runner-2dbrd   1/1     Running   0          4m17s   10.0.1.125   aks-default-35787661-vmss00000o   <none>           <none>
autoscale-runner-set-jkl44-runner-p8mp7   1/1     Running   0          4m17s   10.0.1.109   aks-default-35787661-vmss00000o   <none>           <none>

Playwright 実行環境への応用

私達の Web アプリケーションでは、テストフレームワーク Playwright を利用した E2E テストを CI プロセスに組み込んでいます。AKS の self-hosted Runner 環境が整ったことで、これらの CI プロセスも並列実行できるようになりました。

以下は私達の環境で利用しているワークフロー定義から一部を抽出したものです。

e2e_test:
    timeout-minutes: 30
    runs-on: self-hosted-runner-cluster
    needs: prepare-containers
    strategy:
      matrix: ${{ fromJSON(needs.prepare-containers.outputs.matrixE2E) }}
      fail-fast: false

    steps:
      - name: Update git
        run: |
          sudo add-apt-repository ppa:git-core/ppa
          sudo apt-get update
          sudo apt-get install -y git
      - uses: actions/create-github-app-token@v1
        id: app-token
        with:
          app-id: ${{ secrets.GH_APP_READ_ALL_ID }}
          private-key: ${{ secrets.GH_APP_READ_ALL_PRIVATE_KEY }}
          owner: sbopsv
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          token: ${{ steps.app-token.outputs.token }}
          persist-credentials: false
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: "npm"
      - name: Install dependencies
        run: |
          npm ci
      - name: Install Playwright Browsers
        run: |
          echo "インストールするブラウザ: ${INSTALL_PLAYWRIGHT_BROWSERS}"
          npx playwright install --with-deps $INSTALL_PLAYWRIGHT_BROWSERS
      - name: SB Run Playwright tests
        run: |
          npx playwright test --shard=$((${{ strategy.job-index }} + 1))/${{ strategy.job-total }}
        env:
          LOGIN_URL: ${{ vars.LOGIN_URL }}
          USER_ID: ${{ vars.USER_ID }}
          MGMT_USER_MAIL: ${{ vars.MGMT_USER_MAIL }}
          USER_PASS: ${{ secrets.USER_PASS }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: blob-report-${{ strategy.job-index }}
          path: blob-report
          retention-days: 3

matrix の組み方によって、様々な応用が考えられますが、例えば spec ファイルごとに分割して同時実行する、ブラウザ種別ごとに分割して複数のブラウザ環境での動作試験をする、などの活用法が考えられます。

Runner の環境は実行のたびに別のPodが立ち上がるので、そのたびに依存系のインストールのステップが残ってしまっているのが今の課題だと感じています。Playwright 公式が公開しているビルド済みイメージを利用することで、よりCIの時間短縮につながるので、ぜひ今後改善したいと思います。

まとめ

この記事では、Azure Kubernetes Services(AKS)を活用したGitHub Actionsのself-hosted Runner の構築方法と、それを用いた Playwright による E2E テストの応用について解説しました。

AI 技術の進化に伴い、アプリケーション開発の現場でも一層の品質向上が求められる中、AKS と GitHub Actions の組み合わせがもたらす効率化が、皆さんの開発者体験の向上に少しでも寄与すれば幸いです。

最後までお読みいただきありがとうございました。

ソフトバンクアドベントカレンダー 20日目もおたのしみに!

関連サービス

セキュアなAzure OpenAI Service環境をパッケージとして提供するサービスです。よりスムーズに生成AIの導入を実現することができます。

MSP(Managed Service Provider)サービスは、お客さまのパブリッククラウドの導入から運用までをトータルでご提供するマネージドサービスです。

おすすめの記事

条件に該当するページがございません