GitHub を Terraformで管理する

# はじめに

GitHubのレポもterraformで管理しておくと便利なことがいくつかあります。

- 権限変更履歴が把握しやすくなる
- Admin権限でないとみれない設定をコード上で誰でも確認できるようになる
- レポジトリの設定の統一 (Branch のRuleset (branch protection) 、マージ方法などの設定)
    - GitHub Template Repositoryを使ってレポジトリを作成してもレポジトリの設定まではコピーされない

3年前に似たような記事を書いていましたが、2025年の時点でやっている管理を書いておきます。

https://qiita.com/nakamasato/items/d12a3d5362dbd8a70299

# 構成

- backend: GCS bucket (S3などでも可)
- ci/cd: GitHub Actions (Terraform Cloudでもよいが最近ほとんどGitHub Actionsに統一してるのでGitHub Actionsにしました)

# 設定手順

## 1. GitHub App の作成

1. **GitHub設定にアクセス**
   ```
   https://github.com/settings/apps (個人アカウント)
   または
   https://github.com/organizations/YOUR_ORG/settings/apps (組織)
   ```

2. **新しいGitHub Appを作成**
   - [New GitHub App](https://github.com/settings/apps/new) をクリック
   - 基本情報を入力:
     - **App name**: `GH Resource Manager`
     - **Description**: `Manage GitHub resources using Terraform`
     - **Homepage URL**: あなたのリポジトリURL

3. **権限設定**
   Repository permissions で以下を設定:
   ```
   Administration: Read & write    # リポジトリ設定管理（vulnerability alerts含む）
   Contents: Read & write          # ファイル操作
   Metadata: Read                  # 基本情報読み取り
   Pull requests: Read & write     # PR管理
   Actions: Read                   # GitHub Actionsアクセス（必要に応じて）
   ```

4. **Where can this GitHub App be installed?**
   - "Only on this account" を選択（個人の場合）
   - または適切な組織を選択

5. **Create GitHub App** をクリック

## 2. GitHub App のインストール

1. **App設定ページ**で "Install App" タブをクリック
2. 対象のアカウント/組織で "Install" をクリック
3. **Repository access** で以下のいずれかを選択:
   - "All repositories" (管理するRepositoryの権限を取得する必要がある)

## 3. 認証情報の取得

GitHub App設定ページで以下を取得:

1. **App ID**: General タブの "App ID" をコピー
2. **Private Key**:
   - General タブの "Private keys" セクション
   - "Generate a private key" をクリック
   - ダウンロードされた `.pem` ファイルの内容をコピー

## 4. GitHub Secrets の設定

リポジトリの Settings > Secrets and variables > Actions で以下を追加:

```
GH_APP_APP_ID=12345
GH_APP_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
```

![Screenshot 2025-06-19 at 22.12.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/d33ca857-03d0-41a8-8e5b-929f1d171f01.png)


**注意**: 現在の実装では `installation-id` は不要です（自動検出されます）。

## 5. Terraform設定

現在の `provider.tf` は既に最適化されています：
```hcl:provider.tf
terraform {
  required_providers {
    github = {
      source  = "integrations/github"
      version = "~> 6.0"
    }
  }
}
# Configure the GitHub Provider
# Local: gh auth login
# CI: GITHUB_TOKEN env var
provider "github" {
  owner = "nakamasato"
}
```

この設定により：
- **ローカル**: GitHub CLIの認証情報を自動使用
- **CI**: `GITHUB_TOKEN` 環境変数から自動読み取り

`terraform.tf`にバックエンドの設定を記載します。

```hcl:terraform.tf
terraform {

  backend "gcs" {
    bucket = "yourproject-bucket-terraform"
    prefix = "github"
  }
}
```

あとは、Terraformで管理したいチーム、メンバー、レポジトリ、GitHub Actionsシークレット Branch Protectionなどを設定します。

- github_team
- github_membership
- github_team_members
- github_team_membership
- github_repository_collaborators
- github_repository_collaborator
- github_repository
- github_actions_secret
- github_branch_protection_v3
- ...

例. https://github.com/nakamasato/mysql-operator repoの管理

```hcl:github_repository.tf
resource "github_repository" "mysql-operator" {
  name                        = "mysql-operator"
  description                 = "Manage MySQL users, databases, schemas, etc."
  allow_merge_commit          = false
  allow_rebase_merge          = false
  allow_update_branch         = true
  has_downloads               = true
  has_issues                  = true
  has_projects                = false
  has_wiki                    = false
  vulnerability_alerts        = true
  delete_branch_on_merge      = true
  squash_merge_commit_title   = "PR_TITLE"
  squash_merge_commit_message = "BLANK"
  homepage_url                = "https://nakamasato.github.io/mysql-operator/"
  topics = [
    "kubernetes",
    "kubernetes-operator",
    "managed-by-terraform",
  ]
  pages {
    cname = ""
    source {
      branch = "gh-pages"
      path   = "/"
    }
  }

  visibility = "public"
}
```

## 6. ローカル環境の設定

GitHub CLIでログイン：
```bash
gh auth login
```

これで Terraform が自動的に GitHub CLI の認証情報を使用します。

## 7. GitHub Actionsワークフローの作成

以下の例は`github`以下にterraformコードがあるケースを想定しています。

1. PRでplan + tfcmtを使ってコメント
2. mainにpushでapply

```yaml:.github/workflows/github.yml
name: github

on:
  push:
    branches: [main]
    paths:
      - .terraform-version
      - '.github/workflows/github.yml'
      - 'github/**'
      - '!github/**md'
  pull_request:
    branches: [main]
    paths:
      - .terraform-version
      - '.github/workflows/github.yml'
      - 'github/**'
      - '!github/**md'

jobs:
  terraform:
    permissions:
      contents: read
      pull-requests: write  # PR にコメントを投稿するのに必要
      issues: write         # tfcmt が一番最初にRepository labelを追加するのに必要
      id-token: write       # authenticate gcpでworkload identityを使用するときに必要
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Generate GitHub App Token
        id: generate_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.GH_APP_APP_ID }} # Repository SecretにGitHub Appの App IDを設定しておきます
          private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} # Repository SecretにGitHub AppのPrivate keyを設定しておきます
          owner: ${{ github.repository_owner }}

      # backendにgcsを使ってるために必要 (s3を使っている場合はawsのauthに置き換え)
      - name: Authenticate to GCP
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/<project_number>/locations/global/workloadIdentityPools/github/providers/github-provider # 自分のWorkload Identity Providerを書いてください
          service_account: github-actions@<project>.iam.gserviceaccount.com # 自分のService Accountを書いてください (backendのGCS bucket への書き込み権限を付与しておく必要があります)
          create_credentials_file: 'true'

      - name: set-tf-version
        id: set-tf-version
        run: |
          echo "terraform_version=$(cat .terraform-version)" >> $GITHUB_OUTPUT

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ steps.set-tf-version.outputs.terraform_version }}
      - name: Terraform Init
        working-directory: github
        run: terraform init

      - name: Terraform Plan
        if: github.event_name != 'push' || github.ref_name != 'main'
        env:
          # https://github.com/suzuki-shunsuke/tfcmt/blob/6cb66ca833829e713563b928f0b99f1401dd6677/pkg/notifier/github/client.go#L71-L79
          TFCMT_GITHUB_TOKEN: ${{ secrets.github_token }} # tfcmt用のgithub token (権限は上のpermissionsで設定されている)
          GITHUB_TOKEN: ${{ steps.github-app-token.outputs.token }} # Terraform github provider 用のGitHub token (GitHub appで権限は設定済み)
        run: tfcmt -var "target:${{ inputs.project }}" plan -patch -- terraform plan -no-color

      - name: Terraform Apply
        if: github.event_name == 'push' && github.ref_name == 'main'
        env:
          # https://github.com/suzuki-shunsuke/tfcmt/blob/6cb66ca833829e713563b928f0b99f1401dd6677/pkg/notifier/github/client.go#L71-L79
          TFCMT_GITHUB_TOKEN: ${{ secrets.github_token }} # tfcmt用のgithub token (権限は上のpermissionsで設定されている)
          GITHUB_TOKEN: ${{ steps.github-app-token.outputs.token }} # Terraform github provider 用のGitHub token (GitHub appで権限は設定済み)
        run: tfcmt -var "target:${{ inputs.project }}" apply -- terraform apply -no-color -auto-approve -input=false
```

:::note info
GitHub Tokenは二種類あるのに注意
- Terraform のGitHub providerの認証に使うトークン 
    - `GITHUB_TOKEN`環境変数を使って GitHub Appから取得したトークンを渡す
- tfcmtがterraform plan/applyの結果を PRにコメントとして残す/PRに変更に対応するlabelをつける 
    - `TFCMT_GITHUB_TOKEN`環境変数を使って、workflowで生成される `secrets.github_token`で取得できるトークンを渡す

githubのprovider設定でimplicitに認証して、ローカルからplanを実行するときもgh コマンドさえ設定してあれば簡単に実行ができるので便利にしているので、CIからは`GITHUB_TOKEN`環境変数を通して認証することでCI/Localでのバランスを取れた設定しにしています。
```tf
provider "github" {
  owner = "nakamasato"
}
```

単純に`GITHUB_TOKEN`にGitHub appから取得したトークンを渡すと、Repositoryのlabelの作成権限がないので、tfcmt のlabel作成に失敗してしまいます。
:::


# テスト

## ローカルテスト
```bash
cd github
gh auth login  # 必要に応じて
terraform init
terraform plan
```

## GitHub Actionsテスト
1. 変更をコミット・プッシュ
2. Actions タブでワークフロー実行を確認
3. ログでエラーがないか確認

![Screenshot 2025-06-19 at 22.03.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/1eae8590-9017-46cf-b389-dd818af28ccd.png)

こんな感じのコメントが来ていればOk :tada: 

# トラブルシューティング

1. **GitHub App権限エラー**
   ```
   Error: GET https://api.github.com/repos/owner/repo: 404 Not Found
   ```
   - App の Repository permissions を確認
   - Installation が正しいリポジトリに設定されているか確認

2. **Vulnerability alerts権限エラー**
   ```
   Error: 403 Resource not accessible by integration []
   ```
   - GitHub App の Administration 権限が Read & write に設定されているか確認
   - Installation を更新して新しい権限を承認
   - すべてのリポジトリに対してインストールされていることを確認

2. **Installation ID不明**
   ```bash
   # 全てのinstallationを確認
   gh api /user/installations
   ```

3. **Private Key形式エラー**
   - PEMファイルの内容をそのままコピー
   - 改行文字も含めて正確にコピー

# Tips: Reusable GitHub ActionsWorkflow

backend をGCS bucketで管理する場合は、Reusable workflowを作って簡単に設定できるようにしています。

https://github.com/nakamasato/github-actions/blob/main/.github/workflows/reusable-terraform-github.yml

```yaml:reusable-terraform-github.yml
name: terraform-github
on:
  workflow_call:
    inputs:
      working_directory:
        type: string
        required: false
        default: "."
      project:
        type: string
        required: false
        default: "github"
      # GCP config: currently only support GCS backend
      workload_identity_provider:
        type: string
        required: false
      service_account:
        type: string
        required: false
    secrets:
      # GitHub App
      gh_app_id:
        required: true
      gh_private_key:
        required: true

concurrency:
  group: terraform-github-${{ inputs.project }}
  cancel-in-progress: false

jobs:
  terraform:
    permissions:
      issues: write # allow to create repo label
      contents: read
      id-token: write
      pull-requests: write
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ${{ inputs.working_directory }}
    steps:
      - uses: actions/checkout@v4

      - name: Generate GitHub App Token
        id: github-app-token
        uses: actions/create-github-app-token@v2
        with:
          app-id: ${{ secrets.gh_app_id }}
          private-key: ${{ secrets.gh_private_key }}
          owner: ${{ github.repository_owner }}

      - name: install aqua
        uses: aquaproj/aqua-installer@v4.0.0
        with:
          aqua_version: v2.51.2

      - name: install tfcmt via aqua
        run: |
          if [ -f aqua.yaml ];then
            aqua i
          else
            aqua g -i suzuki-shunsuke/tfcmt
          fi

      # For GCS bucket backend
      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: google-github-actions/auth@v2
        with:
          create_credentials_file: 'true'
          workload_identity_provider: ${{ inputs.workload_identity_provider }}
          service_account: ${{ inputs.service_account }}

      - name: set-tf-version
        id: set-tf-version
        working-directory: .
        run: |
          echo "terraform_version=$(cat .terraform-version)" >> $GITHUB_OUTPUT

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ steps.set-tf-version.outputs.terraform_version }}

      - name: Terraform fmt
        id: fmt
        run: terraform fmt -check -recursive

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        if: github.event_name != 'push' || github.ref_name != 'main'
        env:
          # https://github.com/suzuki-shunsuke/tfcmt/blob/6cb66ca833829e713563b928f0b99f1401dd6677/pkg/notifier/github/client.go#L71-L79
          TFCMT_GITHUB_TOKEN: ${{ secrets.github_token }} # For tfcmt (permissions are set above)
          GITHUB_TOKEN: ${{ steps.github-app-token.outputs.token }} # For github provider (permissions are set for the GitHub app)
        run: tfcmt -var "target:${{ inputs.project }}" plan -patch -- terraform plan -no-color

      - name: Terraform Apply
        if: github.event_name == 'push' && github.ref_name == 'main'
        env:
          # https://github.com/suzuki-shunsuke/tfcmt/blob/6cb66ca833829e713563b928f0b99f1401dd6677/pkg/notifier/github/client.go#L71-L79
          TFCMT_GITHUB_TOKEN: ${{ secrets.github_token }} # For tfcmt (permissions are set above)
          GITHUB_TOKEN: ${{ steps.github-app-token.outputs.token }} # For github provider (permissions are set for the GitHub app)
        run: tfcmt -var "target:${{ inputs.project }}" apply -- terraform apply -no-color -auto-approve -input=false
```

これを使うことで、設定がとても簡単になります。

実際に使う場合は以下のように設定するだけになります。

```yaml
name: github

on:
  push:
    branches: [main]
    paths:
      - .terraform-version
      - '.github/workflows/github.yml'
      - 'github/**'
      - '!github/**md'
  pull_request:
    branches: [main]
    paths:
      - .terraform-version
      - '.github/workflows/github.yml'
      - 'github/**'
      - '!github/**md'

jobs:
  terraform:
    uses: nakamasato/github-actions/.github/workflows/reusable-terraform-github.yml@1.11.2
    with:
      working_directory: github
      workload_identity_provider: projects/<your project number>/locations/global/workloadIdentityPools/github/providers/github-provider
      service_account: github-actions@<your project>.iam.gserviceaccount.com
    secrets:
      gh_app_id: ${{ secrets.GH_APP_APP_ID }}
      gh_private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
```

# 参考

- https://registry.terraform.io/providers/integrations/github/latest/docs