GitHub Organizationのユーザ管理をTerraformとGitHub Actionsで自動化
この記事は「さくらインターネット Advent Calendar 2024」の4日目の記事です。
さくらインターネットにおけるGitHubのユーザ管理と課題
さくらインターネット社内ではソフトウェア開発のためにGitHub Enterprise Serverを利用しており、様々なOrganizationが存在しています。各Organizationのリソース(メンバー/チームなど)管理についてはOrganizationのオーナー権限を持つメンバーの裁量に委ねられていますが、手動で行っているケースが多く、メンバーの追加/削除(のためのワークフローの整備)や棚卸し等の作業が煩雑で特に規模の大きいOrganizationでは担当者の負担になっていました。
TerraformとGitHub Actionsによる自動化の仕組みを導入
そこでTerraformとGitHub Actionsを利用してこれらに関連するプロセスの自動化を行うことにしました。具体的には下記のようにGitHub上で申請者(あるいはその代理人)がプルリクエストを作成してCODEOWNERSで設定された承認者が対象のプルリクエストを承認した後、マージされると同時にGitHub Actionsでterraform applyを実行することでGitHub Organizaitonのメンバーやチームを自動的に更新、といった流れになります。
プルリクエストを作成/マージ後の動作イメージ
プルリクエストを作成すると、GitHub Actionsによって terraform plan が実行されて、その実行結果がGitHubのコメントで通知されるので、レビュワーは変更内容とこのコメント内容を確認した上でプルリクエストを承認します。(「Details (Click me)」をクリックするとterraformリソースの変更内容の詳細が表示される)
プルリクエストをマージすると、GitHub Actionsによって terraform apply が実行されて、その実行結果がGitHubのコメントで通知されます。(「Details (Click me)」をクリックするとterraformリソースの実際の変更内容の詳細が表示される)
これらの動作の実現にはtfcmtを利用しています。(なお、この例ではFine-grained PATを利用しているので、私のアカウント(ta-kubo)がコメントしたように表示されていますが、現在の運用ではGitHub Appのインストールアクセストークンを利用しています。こちらについては後述します)
メンバー/チームのリスト管理
GitHub OrganizaitonのメンバーやチームはTerraformのリソースとして管理することになるので、これらのリソースもすべてTerraformのリソースとして扱います。しかし、メンバーやチーム等のデータソースはTerraformに詳しくない人でも更新できるようにしたいということあり、メンバーであれば以下のような形でYAMLで管理するようにしています。
# メンバーのリストはYAMLで別ファイルに切り出す
locals {
members = toset(yamldecode(file("path/to/members.yml")))
}
resource "github_membership" "members" {
for_each = { for i in local.members : i.username => i }
username = each.value.username
role = each.value.role
}
チーム(と各チームのメンバー)の場合も同様です。
locals {
teams = toset(yamldecode(file("path/to/teams.yml")))
team_members = {
some-team-1 = yamldecode(file("path/to/some-team-1/members.yml")),
some-team-2 = yamldecode(file("path/to/some-team-2/members.yml")),
...
}
}
resource "github_team" "teams" {
for_each = { for i in local.teams : i.name => i }
name = each.value.name
description = each.value.description
privacy = each.value.privacy
}
resource "github_team_membership" "some-team-1" {
for_each = { for i in local.team_members["some-team-1"] : i.username => i }
team_id = github_team.teams["some-team-1"].id
username = each.value.username
role = each.value.role
}
resource "github_team_membership" "some-team-2" {
for_each = { for i in local.team_members["some-team-2"] : i.username => i }
team_id = github_team.teams["some-team-2"].id
username = each.value.username
role = each.value.role
}
...
後述しますが、Organizationのメンバーや各チームのメンバーのリストはCODEOWNERSによる権限管理を適切に行うために個別のディレクトリに格納しています。
メンバーやチームの追加/削除の承認のための権限管理
メンバーやチームの追加/削除のプルリクエストはリポジトリに登録されているメンバー(かチームのメンバー)であれば誰でも作成可能ですが、プルリクエストの承認は.github/CODEOWNERSで定義したメンバーやチーム(のメンバー)でしかできないようにすることで信頼性を担保しています。実際のCODEOWNERSはもっと行数がありますが、イメージとしては以下のような感じです。
yaml/teams/some-team-1 @some-org/some-team-1
yaml/teams/some-team-2 @some-org/some-team-2
なお、プルリクエストをマージする際にコードオーナーのレビューを必須にする際はブランチ保護の設定で「Require a pull request before merging -> Require review from Code Owners」にチェックを入れるのを忘れないようにしましょう。
また、誰でも手動でチーム作成ができる状態だとTerraform管理下にあるチームと実際のOrganizationのチームの状態に乖離が生じやすくなるので、Organizationの設定から行ける以下の項目にチェックを入れないようにしています。
GitHub Actionsで利用するアクセストークン
GitHub Actionsで何かしらのジョブを実行する際、通常であればGITHUB_TOKENシークレットを利用するのが適切ですが、今回のようにOrganizaitonレベルの操作を行う際はGIHTUB_TOKENシークレットでは権限が足りないので利用できません。
今回の仕組みを構築した直後はPAT(Fine-grained)を利用していたのですが、属人化が避けられないという課題もあり、早々にGitHub Appのインストールアクセストークンを利用する形に切り替えました。(なお、実際に切り替えるにあたっては「GitHub Appsトークン解体新書:GitHub ActionsからPATを駆逐する技術」を大いに参考にさせていただきました 🙇)
Discussion