TerraformでGitHubのteamやmemberを管理する

こんにちは、株式会社HRBrainのプラットフォームチームに所属している永山([@ikorihn](https://github.com/ikorihn))です。

GitHub Organization配下のチームや所属メンバーの管理をTerraformに移行しましたので、この記事ではその時の対応内容をお伝えします。

本記事はHRBrain Advent Calendar 2024の13日目の記事です。

https://qiita.com/advent-calendar/2024/hrbrain

## はじめに

所属メンバーやチームの数が増えてくるに従い、以下のような課題が発生していました。

- だれがどのチームに所属しているか把握しづらい
- 異動の際に管理者が手動でチームメンバーを変更していて手間な上、履歴が残らない
- 本名とGitHubアカウントとの突合がしづらい

これらの問題を解決するため、メンバーの一覧や各チームの定義をTerraformで管理することにしました。

## GitHub Provider

公式に提供されている[GitHub Provider](https://registry.terraform.io/providers/integrations/github/latest/docs)を使用します。

### providerの設定

GitHubの操作に必要なtokenは、GitHub Actions内で生成したアクセストークンを渡すことにします(後述)

```tf:provider.tf
terraform {
  required_providers {
    github = {
      source  = "integrations/github"
      version = "~> 6.0"
    }
  }
}

provider "github" {
  owner = "<organization name>"
  token = var.github_app_token
}

variable "github_app_token" {
  type = string
}
```

### 所属メンバー

メンバーの管理には[github_membership](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/membership)リソースを使用します。
`for_each` で定義しました。

```tf:members.tf
locals {
  hrbrain_members = {
    full_name1 = { username = "<github user 1>", role = "admin" },

    full_name2 = { username = "<github user 2>", role = "member" },
    full_name3 = { username = "<github user 3>", role = "member" },
  }
}

resource "github_membership" "members" {
  for_each = local.hrbrain_members
  username = each.value.username
  role     = each.value.role
}
```

メンバーの一覧は、組織ownerにCSVでダウンロードしてもらったものを加工して作成しました。

```csv
login	name	saml_name_id
user1	User Name1	full_name1@ドメイン
user2	User Name2	full_name2@ドメイン
```

### チーム

チーム自体の定義には[github_team](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/team)リソース、チームメンバーの管理には[github_team_members](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/team_members)リソースを使用します。

```tf:team_team1.tf
resource "github_team" "team1" {
  name           = "team1"
  parent_team_id = github_team.developer.id # 子チームとして定義する場合はこれを指定する
  privacy        = "closed"
}

resource "github_team_members" "team1" {
  team_id = github_team.team1.id
  members {
    username = github_membership.members["full_name2"].username
    role     = "maintainer"
  }
  members {
    username = github_membership.members["full_name3"].username
    # roleを指定しない場合デフォルトでmemberとなる
  }
}
```

このファイルをチームごとに作成しています。
ただ、既存のチームの設定すべてを手で書いていくのが大変でしたので、後述のimport blockと `terraform plan -generate-config-out` を用いて作成しました。

## 既存の設定をTerraform管理下にimportする

すでに作成済みのチームやメンバーをTerraformの管理化に追加するためにimportしていきます。
Terraform v1.5.0で追加された[Import block](https://developer.hashicorp.com/terraform/language/import)を使用して移行しました。

importブロックのidにはインポート元、toにはインポート先リソースの識別子を指定します。
idのフォーマットは、resourceドキュメントのImportを見るとわかります。
たとえば[github_membership](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/membership#import)であれば `<organization>:<username>` を指定します。

### メンバーのimportブロックを作る

`id` に `<organization>:<username>`、`to` に `github_membership` のリソース識別子を指定します。
こちらも `for_each` で作成できます。

```tf:members.tf
import {
  for_each = local.hrbrain_members
  id       = "<organization>:${each.value.username}"
  to       = github_membership.members[each.key]
}
```

### チームのimportブロックを作る

GitHubのAPIでチーム一覧をJSONで取得したあと、[github_team](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/team) および[github_team_members](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/team_members) のimportブロックに加工します。

```shell
$ gh api --paginate /orgs/<organization>/teams > teams.json

$ cat teams.json | jq -cr '.[] | "import {|  id = " + (.id | tostring) + "|  to = github_team." + .slug + "|}"' | sed 's/|/\n/g' > import_teams.tf
$ cat teams.json | jq -cr '.[] | "import {|  id = " + (.id | tostring) + "|  to = github_team_members." + .slug + "|}"' | sed 's/|/\n/g' | >> import_team_members.tf
```

```tf:import_teams.tf
import {
  id = 1234567
  to = github_team.team1
}
import {
  id = 1234568
  to = github_team.team2
}

import {
  id = 1234567
  to = github_team_members.team1
}
import {
  id = 1234568
  to = github_team_members.team2
}
```

[generate-config-out](https://developer.hashicorp.com/terraform/language/import/generating-configuration) オプションによって、importブロックからHCLを作成できるようになっています。

```shell
$ terraform plan -generate-config-out=generated.tf
```

実行すると次のようなファイルが生成されるので、これを編集して各チームのtfファイルを作成しました。

```tf:generated.tf
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform from "1234567"
resource "github_team" "team1" {
  name           = "team1"
  privacy        = "closed"
}

# __generated__ by Terraform from "1234568"
resource "github_team" "team2" {
  name           = "team2"
  privacy        = "closed"
}

# __generated__ by Terraform from "1234567"
resource "github_team_members" "team1" {
  team_id = jsonencode("1234567")
  members {
    username = "user1"
    role     = "maintainer"
  }
}

# __generated__ by Terraform from "1234568"
resource "github_team_members" "team2" {
  team_id = jsonencode("1234568")
  members {
    username = "user1"
    role     = "maintainer"
  }
  members {
    username = "user2"
    role     = "member"
  }
}
```

あとは `terraform apply` を実行すると、リソースがimportされます。

```shell
$ terraform apply
Terraform will perform the following actions:

  # github_team.team1 will be imported
    resource "github_team" "team1" {
....

Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
```

ここまで来たらimportブロックは削除してOKです。

## GitHub ActionsでTerraformを実行する

プルリクエストベースで `terraform apply` を実行するために、GitHub Actionsを設定します。
GitHubのアクセストークンは、job内で[Create GitHub App Token](https://github.com/actions/create-github-app-token)を使って生成します。

まずは[GitHub Appを作成](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)します。
権限は[Organization permissions for "Members"](https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28#organization-permissions-for-members)をつけます。
作成したGitHub AppのIDをリポジトリのvariableに、private keyをsecretに設定します。

```yaml:.github/workflows/update_github.yaml
name: Update GitHub member/team
on:
  push:
    branches:
      - main
jobs:
  apply:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9
      - run: terraform init -input=false
      - name: Generate a GitHub token
        id: generate-github-token
        uses: actions/create-github-app-token@v1
        with:
          # GitHub Appsの情報を設定する
          app-id: ${{ vars.APP_ID }}
          private-key: ${{ secrets.APP_PRIVATE_KEY }}
      - run: terraform apply -input=false -auto-approve -lock=false -no-color
        env:
          TF_VAR_github_app_token: ${{ steps.generate-github-token.outputs.token }}
```

## まとめ

これまでGitHubのチームやメンバーをWeb画面上から手動で設定していたのを、Terraform管理下に移行する手順を紹介しました。
Terraform化したことにより、次のようなメリットがありました。

- 本名とGitHubアカウントとの突き合わせが容易になった
- だれがどのチームに所属しているか一覧で確認しやすくなった
- 退職時や異動時に本名でgrepできるようになったので、漏れが出にくくなった
- プルリクベースで設定変更が行えるようになった

ここまで読んでいただきありがとうございました！参考になれば幸いです。

## 参考リンク

- [Terraform で実現する効率的な GitHub 権限管理 - カミナシ エンジニアブログ](https://kaminashi-developer.hatenablog.jp/entry/2023/08/29/github-access-management)
- [GitHubの権限管理をTerraform化して幸せになろう #GitHub - Qiita](https://qiita.com/moajo/items/a840ecf88607ff583c02)