Terraform で始めるセルフホステッドランナー：terraform-aws-github-runner での構築手順

# はじめに

GitHub actions にはセルフホステッドランナー（Self-Hosted Runner）と呼ばれる機能があります。

https://docs.github.com/ja/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners

通常 GitHub actions のワークフローは GitHub 側が用意してくれたホストで実行されます。
一方セルフホステッドランナー（Self-Hosted Runner）は、GitHub Actions のワークフローをユーザーが用意したホストにできます。

セルフホステッドランナーを使うことにより、ユーザーが実行ホストを詳細にカスタマイズできたり、ワークフロー実行時にかかるランニングコストを抑えることができます。

調べてみると、この環境を Terraform で構築できるモジュールが見つかったので、実際に使ってみました。
構築中いくつかつまりポイントなどがあったので、それらも含めて紹介していきます。

# terraform-aws-github-runner とは

Terraform で GitHub actions の実行環境を手軽に構築することができるモジュールです。

https://github.com/github-aws-runners/terraform-aws-github-runner

もともとは PHILIPS 社(シェーバーや車のライトで有名なメーカー)が開発・管理されていました。

https://github.com/philips-labs/terraform-aws-github-runner

こちらのリポジトリは 2025 年 1 月 16 日にアーカイブされており、現在は上記のリポジトリに移ったみたいです。

## アーキテクチャ

こちらは公式のアーキテクチャ図です。

https://github.com/github-aws-runners/terraform-aws-github-runner/blob/main/docs/assets/aws-architecture.light.png

詳しいアーキテクチャが気になったので、少し調べてみました。

![terraform-aws-self-hosted-runner.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/b7767dbf-fe37-4b54-b478-8389f41c9ab0.png)

- GitHub actions のワークフロー開始をトリガーとして webhook を実行
- AWS はそのリクエストを受取、Spot Instance の作成
- 作成された Spot Instance は GitHub Actions にセルフホステッドランナーとして登録
- GitHub actions はセルフホステッドランナーが登録されたことを検知し、ワークフローを実行

アーキテクチャもわかったので、実際にこれを使って構築してみます。

# 構築手順

以下の流れで構築をしていきます。

- GitHub App を作成
- Terraform で AWS 環境を構築
- GitHub App に AWS のエンドポイントを登録
- 必要なリポジトリへインストール

## GitHub App の作成

ここでは大きく2つ作成します。

- GitHub App の作成
- 秘密鍵の作成

### GitHub App の作成

[https://github.com/settings/apps/new](https://github.com/settings/apps/new) から作成できます。

各設定値は以下の通りです。

- GitHub App name：任意の名前
- Homepage URL：[https://github-aws-runners.github.io/terraform-aws-github-runner/getting-started/#setup-guide](https://github-aws-runners.github.io/terraform-aws-github-runner/getting-started/#setup-guide)（必須なので書いていますが、おそらくなんでも大丈夫）
- Webhook：Active のチェックを外す(あとで有効化するので一旦外しておきます)
- Permissions（すべて Repository permissions）
  - Actions：Read-only
  - Administration：Read and write（個人のリポジトリの場合）
  - Checks：Read-only
  - Metadata：Read-only

上記の設定が終わったら「Create GitHub App」を選択します。

「App ID」は Terraform で使うので控えておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/c54875fe-8037-4f0b-8ed7-74c3014511a1.png)

### 秘密鍵の作成

「Private keys」で秘密鍵を発行します。

ボタンは App の General カテゴリにあります。

![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/33e1dd61-3c77-49a2-b404-90e302eecb75.png)

## Terraform で AWS 環境を構築

ディレクトリ構成は以下の通りです。

```bash
.
├── lambdas-download
│   ├── runner-binaries-syncer.zip
│   ├── runners.zip
│   └── webhook.zip
├── main.tf
└── 秘密鍵.pem
```

lambdas-download の下の zip ファイルは公式 Releases からダウンロードします。

https://github.com/github-aws-runners/terraform-aws-github-runner/releases

秘密鍵.pem はさきほど GitHub App で作成した秘密鍵を配置する。

```main.tf
locals {
  key_base64 = base64encode(file("./秘密鍵.pem"))
}

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.27"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
    local = {
      source  = "hashicorp/local"
      version = "~> 2.0"
    }
  }

  required_version = ">= 1.3.0"
}
provider "aws" {
  region = "ap-northeast-1"
}

resource "random_id" "random" {
  byte_length = 20
}

# Create a VPC
resource "aws_vpc" "test" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "test-vpc"
  }
}

# Create an Internet Gateway
resource "aws_internet_gateway" "test_igw" {
  vpc_id = aws_vpc.test.id
  tags = {
    Name = "test-igw"
  }
}

# Create a Public Route Table
resource "aws_route_table" "test_public_rt" {
  vpc_id = aws_vpc.test.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.test_igw.id
  }
  tags = {
    Name = "test-public-rt"
  }
}

# Associate Public Route Table with Subnet 1
resource "aws_route_table_association" "test_subnet_1_association" {
  subnet_id      = aws_subnet.test_subnet_1.id
  route_table_id = aws_route_table.test_public_rt.id
}

# Associate Public Route Table with Subnet 2
resource "aws_route_table_association" "test_subnet_2_association" {
  subnet_id      = aws_subnet.test_subnet_2.id
  route_table_id = aws_route_table.test_public_rt.id
}

# Create Subnet 1
resource "aws_subnet" "test_subnet_1" {
  vpc_id                  = aws_vpc.test.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true
  tags = {
    Name = "test-subnet-1"
  }
}

# Create Subnet 2
resource "aws_subnet" "test_subnet_2" {
  vpc_id                  = aws_vpc.test.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = "ap-northeast-1c"
  map_public_ip_on_launch = true
  tags = {
    Name = "test-subnet-2"
  }
}

module "github-runner" {
  source  = "github-aws-runners/github-runner/aws"
  version = "~> 6.0.0"

  aws_region = "ap-northeast-1"
  vpc_id     = aws_vpc.test.id
  subnet_ids = [aws_subnet.test_subnet_1.id, aws_subnet.test_subnet_2.id]

  prefix = "gh-ci"

  github_app = {
    key_base64     = local.key_base64
    id             = "{さきほど取得したAppを入れる}"
    webhook_secret = "webhook_secret"
  }

  webhook_lambda_zip                = "lambdas-download/webhook.zip"
  runner_binaries_syncer_lambda_zip = "lambdas-download/runner-binaries-syncer.zip"
  runners_lambda_zip                = "lambdas-download/runners.zip"
  enable_organization_runners       = false
}

output "runners" {
  value = {
    lambda_syncer_name = module.github-runner.binaries_syncer.lambda.function_name
  }
}

output "webhook_endpoint" {
  value = module.github-runner.webhook.endpoint
}

output "webhook_endpoint" {
  value = module.github-runner.webhook.endpoint
}

output "webhook_secret" {
  value     = random_id.random.hex
}

```

AWS へのデプロイ権限があるターミナルでデプロイします。

```bash
terraform init
terraform plan
terraform apply
```

デプロイが完了すると、webhook_url と webhook_secret がコンソールに出力されるので控えておきます。

```bash
Apply complete! Resources: 101 added, 0 changed, 0 destroyed.

Outputs:

runners = {
  "lambda_syncer_name" = "gh-ci-syncer"
}
webhook_endpoint = "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/webhook"
webhook_secret = xxxxxxxx
```

## GitHub App に AWS のエンドポイントを登録

やることは2つです。

- Webhook URL の登録
- Event の設定

### Webhook URL の登録

GitHub App の General で Webhook URL を以下のように設定します。

- Active：チェック
- Webhook URL：先ほど Terraform で発行した内容
- WebhookSecret：先ほど Terraform で発行した内容

「Save Changes」をクリックする。

Advanced で ping が通ったログを確認できればOKです。

![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/ce998ccc-21b6-4b18-a1c8-ea66283696ce.png)

### Event の設定

ping が通ると Permissioin & events に Subscribe to events が追加されるので以下の項目にチェックを入れて保存します。

- Workflow job

※公式ドキュメントだと「Workflow job か Check run のどちらか片方」と書いてあったが、自分の場合 Workflow job でないと動きませんでした。

## 必要なリポジトリへインストール

Install App で自分のアカウントを選択します。

Only select repositories でセルフホステッドランナーを使いたいリポジトリを選択します。

![image (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/4a3bc897-0c37-4ff6-ac99-f07811c772d5.png)

選択後、Install をクリックします。

ここまでで構築は完了です。

# 動かしてみる

最初はランナーが登録されていない状態です。

![image (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/9f573565-c26e-4527-80e9-35fbdc6a4e0a.png)

以下のワークフローを実行してみます。

```yaml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

  workflow_dispatch:

jobs:
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v3

      - name: Run a one-line script
        run: echo Hello, world!
```

実行してしばらくすると、セルフホステッドランナーが追加されています。

![image (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/dc0396f9-68ed-49d0-9dc6-adf811b61fe0.png)

ワークフローが動いていることも確認できました。

![image (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/6b863cce-19ee-4e0d-ae5f-090243145b49.png)

# まとめ

今回は GitHub Actions のセルフホステッドランナーを terraform-aws-github-runner モジュールを使って AWS 上に構築する方法を解説しました。
実際に構築完了後、ワークフローを実行することで、自動的に AWS 上に Spot Instance が作成され、GitHub Actions のセルフホステッドランナーとして登録・動作することを確認しました。
セルフホステッドランナーを使うことで、実行環境を詳細にカスタマイズできるだけでなく、ランニングコストの削減も可能になります。
また、ワークフロー実行時だけインスタンスが立ち上がるため、コスト効率の良い CI/CD 環境を実現できます。
みなさんもぜひ terraform-aws-github-runner を使ってみてください。

# 参考

https://zenn.dev/odan/scraps/49824db8591494

https://zenn.dev/counterworks/articles/9423abf0c35309

https://www.docswell.com/s/miyajan/ZW1XJX-large-scale-github-actions-self-hosted-runner-by-philips-terraform-module