GitHubの権限管理をTerraform化して幸せになろう

この記事はterraformアドベントカレンダー 2021 10日目の記事です。

https://qiita.com/advent-calendar/2021/terraform

会社等でgithubを使っている方の多くは [Organization](https://docs.github.com/organizations) を作って各リポジトリへのアクセス権を管理していると思います。

ところで以下のような出来事に心当たりはありませんか？

- 新しく組織に参加したメンバーが毎日のように「リポジトリにアクセスできないんですが・・・」と言ってきて **足りない権限を逐一手動追加させられる**
- 久々にリポジトリのアクセス権を確認したら **数ヶ月に退職したはずのアカウントが残ってる** 
- たまに志のある人がOrganization内にTeamを作成して権限を整理してくれても、 **Team自体がメンテされなくなって意味をなさなくなる** (そもそもチームの存在が気付かれなくて使われない等)

個人的には今まで関わってきた多くの組織でこういう経験をしてきました。

ということで、terraformでgithub権限管理をすることでこういう面倒くさいのを解消しましょう！

あまり知られていない気もしますが実はgithubは[公式provider](https://registry.terraform.io/providers/integrations/github/latest/docs)があります！

# チームを管理する
チーム関連の主要なリソースは

- [github_team](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/team) :チーム本体を作る
- [github_team_membership](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/team_membership): チームへのメンバーの参加を表現する
- [github_team_repository](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/team_repository):チームのリポジトリに対するアクセス権を表現する


があります。
複数リソースをまとめて定義したかったのでmoduleにしました。

```tf:module/team/main.tf
resource "github_team" "this" {
  name                      = var.name
  description               = var.description
  privacy                   = var.privacy
  create_default_maintainer = true # NOTE: 作成者を初期メンテナとしてチームに追加する。これが無いと空のチームが作成されてしまって操作不能になる。
}

resource "github_team_membership" "this" {
  for_each = var.members
  team_id  = github_team.this.id
  username = each.key
  role     = each.value
}

resource "github_team_repository" "this" {
  for_each   = var.permissions
  repository = each.key
  team_id    = github_team.this.id
  permission = each.value
}

output "team" {
  value = github_team.this
}

# (以下本当はちゃんと別ファイルに書いてますが紙面の都合でまとめてここに書いてます)
variable "name" {}
variable "description" {}
variable "privacy" {}
variable "members" {}
variable "permissions" {}
```
ただそれぞれのリソースを作ってるだけなので特に説明はありません。 `for_each` している点にだけ注意が必要かもしれません。
`create_default_maintainer` がミソで、これがないとチーム自体は作れても中身空っぽになってしまい作成者にチームのmaintain権限が付与されないので **Organizationの管理者しか操作できない(つまり消せない)** という状態になってしまいます。
applyするのがOrg管理者なら問題ないですが、そうじゃない人が作業するならtrueにしておいたほうがいいでしょう。

呼び出し側はこんな感じです

```tf
module "teams" {
  source      = "./modules/team"
  for_each    = local.teams
  name        = each.key
  description = each.value["description"]
  privacy     = each.value["privacy"]
  members     = each.value["members"]
  permissions = each.value["permissions"]
}
```
ローカル変数 `teams` をfor_eachしていますこの変数は以下のような構造です

```tf
locals {
  teams = {
    "hoge-project-admin" : {
      "description" : "hogeプロジェクトの管理者",
      "privacy" : "closed",
      "members" : {
        "moajo" : "maintainer",          # 責任者
        "nakamura-san" : "maintainer",   # 作業の主担当
        "suzuki-san" : "member",         # 作業のサブ担当
      },
      "permissions" : {
        "hoge-frontend" : "admin",
        "hoge-backend" : "admin",
      }
    },
    "fuga-project-developer" : {
      "description" : "fugaプロジェクトの開発者",
      "privacy" : "closed",
      "members" : {
        "moajo" : "maintainer",    # 責任者
        "watanabe-san" : "member", # 開発者
        "ito-san" : "member",      # 開発者
      },
      "permissions" : {
        "fuga-project" : "push"
      }
    },
  }
}
```
リソース定義が隠蔽されてほぼ設定ファイル化しているので、terraform詳しくない人でもmapを編集することができます。
HCLの構文は初見だと面食らいがちなので、あえてjson構文を使って誰でも読み書きできるように配慮しています。
ここではlocal変数として定義してますが、jsonやyamlの外部ファイルとして定義して [file()](https://www.terraform.io/docs/language/functions/file.html) 関数で読み取ってもいいでしょう。


# 外部コラボレータを管理する
Organizationを使っている場合でも、社外の人や業務委託、アルバイトの人などでOrganizationに加入してない人に権限を付与したい場合もあります

Organization外の人はTeamに参加させることができないので、リポジトリに直接コラボレータとして追加するしかありません。
terraformでは [repository_collaborator](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_collaborator)リソースを使って作成できます。

人数が多いとこちらも管理が大変なので、以下のようなmapとしてlocalを定義します

```tf
locals{
  # [githubID]:[リポジトリ名]:[権限]
  outsider_collaborators = {
    "yamada-san" : {            # バイトの山田さん(仮名)
      "hoge-frontend" : "push", # hoge案件の実装を手伝ってもらってます。契約終了時に削除。
    },
    "tanaka-san" : {           # HOGE社(社外)の田中さん(仮名)
      "fuga-project" : "pull", # 作業状況を見たいとのことで閲覧権限だけ渡しています。案件close時に削除。
    },
    "takahashi-san" : {           # 業務委託の高橋さん(仮名)
      "piyo-site" : "push",       # 開発にガッツリ入ってもらってます
      "awesome-app" : "maintain", # 開発にガッツリ入ってもらってます
    }
  }
}
```
そしてリソースを作ります

```tf
resource "github_repository_collaborator" "outsider_collaborators" {
  for_each = {
    for s in flatten([
      for username, value in local.outsider_collaborators : [
        for repo, permission in value : {
          "username" : username,
          "repository" : repo,
          "permission" : permission,
        }
      ]
    ]) : "${s["username"]}:${s["repository"]}" => s
  }

  repository = each.value["repository"]
  username   = each.value["username"]
  permission = each.value["permission"]
}
```
若干読みにくいですがmapをflattenして全部に対してfor_eachしています。
作られるリソースIDは `github_repository_collaborator.outsider_collaborators["takahashi-san:piyo-site"]` のようになるのでパット見でわかりやすいですね。

コツは権限を **付与した理由** や **いつまで付与するのか** をコメントで書いておくことです。
こうすることで後で棚卸しするのがめちゃくちゃ楽になります。
そしてこれらがコメントされていることはterraform定義変更PRのレビューで担保します。

こういうコメントを残せるようにしたり、gitで変更履歴を辿れるようになるのがIaCの良いところです。

# 運用のコツ
以下のような運用をしてますが、いまのところいい感じにワークしています。

- このterraform定義自体を専用のリポジトリで管理し、変更はPRで受け付けるようにします。
- slackに `#github_admin` みたいなチャンネルを作ってそこにこのリポジトリの通知が飛ぶように設定します。
- リポジトリのREADMEに編集方法について誰でもわかるような説明を書きます。
- その他質問は `#github_admin` チャンネルで受けるようにし、そのことを組織全体に宣伝します。
- ある程度の操作はorg管理者権限じゃなくてもできるのですが、権限的に詰む場合もあるのでorg管理者がいつでもapplyできる体勢にしておくと良いでしょう。
- PRレビュー、apply等の対応はそれなりの権限を持った担当の人を決めておくといいと思います。
- CIでapplyするのはちょっと怖いので手動でやってますが、fmtとかvalidationとかplanはCIでやるとベターだと思います。

# 効果
terraform管理下に集約するのは手段であって目的ではありません。
目的はあくまで場当たり的な権限付与や付与しっぱなしの放置を解消することです。

terraform化によって以下のようなメリットが得られます。

- 全ての権限が一箇所で確認できるようになり、一覧性が高まった
    - -> **github管理者のキャッチアップが楽になった**
    - -> **新規メンバが参加したとき、必要な権限設定に漏れが無くなった**
    - -> **退職者や異動者の対応が簡単確実になった**
        - 退職者はidをgrepして全部消すだけ、異動はそのタイミングで同じくgrepして棚卸しを行います。
    - -> 棚卸しが楽になった
- いつ誰が何故権限を変更したのかがgit/githubで追えるようになった
    - -> **「あれ？この権限なんで付いてるんだっけ？」が無くなった**

あ〜〜キレイに管理できるって幸せ〜〜〜