運用負荷軽減!Google Groupを活かしたGitHub Teamメンバーの自動同期の仕組み

初めまして、プラットフォームエンジニアリング部門DevSecOpsグループ ソフトウェアデリバリーチーム(以降SDチーム)の舛田です。
今回は、GitHub Teamのユーザ管理を半自動化しようと奮闘中の取り組みについて書いていこうと思います。

GitHub導入とTeam管理

過去のTech Blog記事でも紹介されているのですが、弊社では2023年にBitbucketからGitHub Enterprise Cloudに移行しました。

そこで使えるようになった機能は様々、、、GitHub Copilotを始め、GitHub Actions(以降GHA) やGitHub Pagesなどなど、CI/CDは今後GHAの利用を推進していこうとしているSDチームの今日この頃です。

GitHubの機能は大変便利ですが、リポジトリの権限やCODEOWNERSを設定する際、組織情報に紐づいたGitHub Teamが自動ないし、半自動的に生成・メンテナンスされていると非常に便利だと思いませんか?

この1年間で弊社の組織体制は、テック系部門が2つ新設されたり、半年に一度の頻度で部門内グループの新設や名称変更が行われるなど、事業や組織の成長とともに頻繁に変動している状況です。このため、リポジトリの適切な管理・運用を行うにあたり、チーム管理の円滑化が課題となっていました。

ということで、ちょこっとどうにかしようと奮闘してみました。

これまでのTeam管理

前置きが少し長くなってしまったのですが、実はこれまでに既にteamを自動で生成、更新する仕組みは社内に整備されていました。

その仕組みでは、BigQueryに整備された従業員マスターの情報を元に組織構造を取得してteamを作成するものです。
しかし、この方法で生成されたteamには利用上ちょっと癖があり使いにくい以下のような問題点がありました。

  • 組織変更のコンテキスト情報がないため、業務は引き続き継続するが、組織名が変わるだけで新しいteamが作成されてしまうため、CODEOWNERSファイルのメンテナンスが大変。
  • チーム名がマルチバイトの文字列のみで作成された場合、CODEOWNERSなどで利用されるslug名がteamXXXのようになってしまい、レポジトリへの権限付与や検索が難しい。

例えば、私の所属するチームの場合、所属を「SD」と略称で表記することも多いのですが、従業員マスタには略称の情報は登録されておらず「ソフトウェアデリバリー」と登録されています。team slugはteam名から日本語を無視して作成されるため、team名をソフトウェアデリバリー(deptCode:XXXX)のように作成することでslugをdeptcode-XXXXの形で作成されるようになっています。

この場合レポジトリにteam単位で読み取り権限を付与しようとしても検索ができず不便でした。
(devsecops グループのteam slugはdevsecops-deptcode-xxxxxのように作成されます。)

また、teamの自動作成ツールで組織変更によりteam削除作業を実施した際に、新規チームが作成され直すことになり、リポジトリの権限がなくなる問題が出てきました。この影響で現在は、古いteamも残しつつ、新しいteamだけ生成する運用になっており、自動生成されたteamが500弱程度ある状況です。

結果的に、自動生成ツールは運用しつつも、各ユーザが手動でteamを作成・管理する運用が主流になっていました。(手動で生成されたteam数は200程度)

また、GitHub Enterprise Cloudのチーム同期機能も、IDプロバイダー自体の管轄が別部署なこと、そもそも弊社の組織構造がIDプロバイダーに連携されていなかったため、要求を満たせませんでした。(どうも、同じようなところがボトルネックになってそうな気がしていて、組織構造の扱いって難しいですね。。。)

Google Groupを元にしたTeam管理

上記問題点を解消して、社内のGitHub利用ユーザに利用してもらえるteamの管理運用をするために、社内で広く利用しているGoogle WorkspaceのGoogle Groupを元にGitHub Teamを作成することにしました。

社内ではドキュメントやスプレッドシートの権限管理に構造化されたGoogle Groupが広く利用されています。また、Google Workspaceへの依存度が高いため、Google Group自体のメンテナンスのモチベーションも高く、組織変更の際に必ず必要な申請の1つになっています。
そのため、GitHubにおけるリポジトリの権限管理や、CODEOWNERの設定についても直接組織情報を元にteamを作るのではなく、Google Workspaceにおける権限管理と同じようにGoogle Groupに依存させるのがいいのではないかと考えました。

実現したいこと

  • Google Groupを指定して簡単にGitHub Teamを作成できること
  • Google Groupのメンバー更新に連動してGitHub Teamのメンバーも自動更新されること
  • また現状社内で一般的に利用されているteamを手動で作成するフローも保持すること

上記が満たすことができたらかなり利便性が高く、社内のGitHub利用者が新しく作ったGitHub Team管理・運用の仕組みを利用してくれると思ったため、以下2つについては完璧を求めずに諦める・妥協する方針にしました。

諦めたこと・妥協点

  • 完璧に組織情報・構造と同期したGitHub Teamを管理運用すること
  • 全てのGoogle GroupのGitHub Teamを自動で生成すること
  • (GitHub Team全てをterraform管理にすること。)

「全てのGoogle GroupのGitHub Teamを自動で生成すること」
本件は技術的には容易に実現可能ですが、SDチームの管理・運用負荷軽減のため、意図的にPR作成とTeam作成のフローを設けています。これにより、利用者にTeamのオーナーシップをもってもらって、Google Group削除時などのメンテナンスを各自で対応いただくことを目的としています。

具体的な作成時のフローは以下のように考えています。

  1. GitHub Teamを管理するリポジトリでブランチを切る
  2. 作成したいteamの定義をTerraformで追記してpush
  3. PR作成
  4. 上長のレビュー・承認をもらう
  5. PRマージ/Terraform applyでGitHub Team作成

具体的な取り組み

実現のために、以下のステップ/タスクが必要で一つ一つ取り組んでいきました。

  1. 社内Google Group情報の整備:
    • 既にGoogle Workspace関連のUser、Google Groupに所属するユーザの情報は社内のSaaS系情報が集約されるBQのdatasetに整備されていました。
  2. GitHubユーザIDと社内メールアドレスの変換情報整備:
    • Terraform GitHub Providerではteamにメンバーを追加するためにGitHub IDが必要となるため、弊社で管理しているOrgの全ユーザのメールアドレスからGitHub IDを取得できる情報を追加で上記BQのdatasetに整備しました。
    • (GitHub IDからメールアドレスを取得するのはGitHubのAPIを経由するだけで簡単なのですが、逆引きはそこそこ面倒でした。)
  3. Terraform Moduleの作成:
    • 指定のGoogle Groupに所属する全ユーザを取得するModule
    • ユーザのメールアドレスをGitHub IDに変換するModule
    • 上記情報を元にGitHub Teamを作成するModule
  4. Workflowの整備:
    • TerraformのCI/CD Workflowを整備
    • 日次でGoogle GroupとGitHub Teamを同期するWorkflowを整備
      • BQにあるGoogle Groupの情報が日次での更新のためteamのメンバー更新も日次で実施

完成した構成

運用上の懸念と対策

ということで一度Terraformに定義さえして今まで通りGoogle Groupのメンバーの管理をしていたら、メンバーの追加忘れで「リポジトリpushできません、権限ください」といったちょっと面倒くさい依頼がなくなるはずです!!

メールグループの削除・リネーム時の対応について

実際は安心するのはまだまだ、、組織構造が変更になった際メールグループが削除またはリネームされた際はどうしようかという問題があります。

この問題については、「妥協案で諦め」です。
リネームされたメールグループを自動で修正するGHAのWorkflowを作成するのは、年に1、2回程度しかないメールグループのリネーム処理のために作っても、ちゃんと動いてくれる保証もできないし、労力をかけて作りたくないのです。

ただし、定義したteamのメンバーが増えたり、減ったりしたタイミングで指定のSlackチャンネルに通知することができたら、メールグループが削除・リネームされた際も気付くことができるし、新規のメンバーが増えた時にteamに所属したことも確認ができて一石二鳥ではないでしょうか。

というわけでTerraformのレイヤーで完結する形でapply時に変更があればリソース単位で指定のSlackチャンネルに通知する機能を実現してみました。

実現したいこと

  • teamの定義ごとに通知チャンネルのattributeをもって、メンバーの増減があれば指定されたSlackチャンネルに通知する。

実現方法
Terraform applyを実行する環境はGHAのWorkflowで実行することを前提としてteamの定義を以下の形でPRを出してもらうようにしました。

module "test-team" {
  source     = "../modules/team-builder/v3"
  slack_notif_channel_id = "sandbox"

  team_name   = "test-team"
  description = "test用GitHub Teamです!" 
  user_mails  = [] #-> ["hoge@example.com"]
  group_mails = ["geho-group@example.com"]
}

このように hoge@example.com がuser_mailsに追加された場合、以下のようにslackの#sandboxチャンネルに投稿されます。

Slack通知のProviderがあれば簡単に実現できそうなのですが、サクッと調べた感じではなさそうだったのでModuleとして作成しました。

variable 
...
}

resource "terraform_data" "plan_changes_notif" {
  triggers_replace = [var.trigger]

  provisioner "local-exec" {
    command = <<-EOT
      if [ -e ${var.tfplan_file} ]; then
        echo "Terraform plan file exists: ${var.tfplan_file}"
      else
        echo "Terraform plan file does not exist: ${var.tfplan_file}"
        exit 0
      fi

      if [ -n $SLACK_TOKEN ]; then
        echo "Slack token is set"
        echo "Slack token: $SLACK_TOKEN"
      else
        echo "Slack token is not set"
        SLACK_TOKEN=${var.slack_token}
      fi

      RESOURCE_ADDRESS=$(terraform show -json ${var.tfplan_file} | jq -r '.resource_changes[] | select(.change.actions[] == "update" and .change.before.id == "${var.trigger_id}") | .address') 
      
      CHANGES_BEFORE=$(terraform show -json ${var.tfplan_file} | jq -c '.resource_changes[] | select(.change.actions[] == "update" and .change.before.id == "${var.trigger_id}")' | jq -rS '.change.before.members | map(del(.role)) | sort_by(.username)')
      
      CHANGES_AFTER=$(terraform show -json ${var.tfplan_file} | jq -c '.resource_changes[] | select(.change.actions[] == "update" and .change.before.id == "${var.trigger_id}")' | jq -rS '.change.after.members | map(del(.role)) | sort_by(.username)')

      if [ -n "$CHANGES_AFTER" ]; then
        # 差分を取得
        echo "$CHANGES_BEFORE" > /tmp/before.json
        echo "$CHANGES_AFTER" > /tmp/after.json
        DIFF_OUTPUT=$(diff -u -U 0 /tmp/before.json /tmp/after.json || true)
        echo "DIFF: $DIFF_OUTPUT" 

        # Slackメッセージの作成(JSONエスケープに注意)
        TITLE="*Terraform リソース更新通知* \n\n \`$RESOURCE_ADDRESS\`"

        # 差分を簡略化して表示
        SIMPLE_DIFF=$(echo "$DIFF_OUTPUT" | sed 's/"/\\"/g'| grep 'username' | grep '^[+-]')
        
        # Slackに通知送信
        echo "Sending notification to Slack channel ${var.slack_channel_id}"
        curl -s -X POST \
          -H "Authorization: Bearer $SLACK_TOKEN" \
          -H "Content-type: application/json; charset=utf-8" \
          --data "{\"channel\":\"${var.slack_channel_id}\",\"text\":\"$TITLE \\n\\n*変更が検出されました。* \\n $SIMPLE_DIFF\"}" \
          https://slack.com/api/chat.postMessage
      else
        echo "No resources being updated, skipping notification"
      fi
    EOT
  }
}

GHAでCDを前提としている理由としては、普通にTerraformを利用するだけではリソースのlocal exec でstateの更新内容を参照できないため、apply実行時にplan -output=plan.tfplanで出力した後、apply実行時にlocal-exec内でplan.tfplanファイルを読み出しstateの更新差分を取得する方式にしたこと、SLACK_TOKENを環境変数に設定しないといけない構成にしたことが背景にあります。

まとめ

今回の取り組みで、ある程度組織変更に柔軟に対応できるGitHub Team管理の仕組みを構築できたと思います。

今回の実装をするために、

  • google groupsのメンバー、github userの情報がBQに整備されている。
  • GHAからGoogle Cloudのリソースを簡単に触るためSA発行の仕組みが整備されている。
  • 社内でslack appを利用するためのドキュメントが整備されている。
  • terraform をGHAで実行するためのcomposite actionsが整備されている。

などなど、社内でこれまでに整備された様々な土台・基礎のおかげで「github teamsの作成半自動化」はサクッと整えることができました。
「github teamsの作成半自動化」も次の何かの新しいアイデアの土台・基礎になってくれると嬉しいなと思います。

社内のこういった取り組みは作った後の周知が大事になってくるため、今後徐々に社内で認知されるように広報活動を継続しつつ、また運用の中で出てきた要望を拾いながら、より使いやすいものになるように改善を重ねていきます。

ありがとうございました。