Terraforming GitHub Teams

Terraforming teams

I have spent some time digging into how to setup a GitHub team that can be used fully as a team, while being managed entirely through our TerraForm setup.

module vs. resource

There are two ways to create a team, one is as a resource as a github_team and the other is as a module.

I want to use module because that supports the source_file directive that we can use to refer to a CSV-file that contains all the teams members, and avoid manually managing teams with the GitHub UI.

The steps required

Important New outputs must be added and applied before they are referenced in a file, so the minimum amount of PRs that are needed is two. Create the first after adding the output to ./outputs.tf.

  1. Add a CSV-file with the team members to ./teams/orgs/{team-name}.csv
  2. Add a team to ./teams.tf
  3. Add an output to ./outputs.tf
  4. Make sure there is a terraform_remote_state in the ./*/backend.tf file
  5. Add the team to each repo that the team owns in ./*/main.tf

Important information about parent team

It is possible to use github_team.developers.id as the parent_team_id, and that allows the team to be mentioned, assigned, and used across all repos, which is good for teams where everyone should have read and write access.

If it is true that all team members should have read/write, then the team does not need to be added to all the repos manually, and step 5 in the required steps can be skipped.

For teams where only some should have read/write, use github_team.parent-amut-teams.id and add the team only to repos where it is OK for everyone to have read/write access.

Setting it up

So first up, let's create a CSV-file with our team members:

full_name,username,team_role,github_role
Pat the Bunny,bunny,maintainer,member
Tom Waits,waits,member,member
Nick Cave,cave,maintainer,admin

Most of us are common folk, so the github_role should be member. Some people are admins, so makes sense to have them as admin. I am not sure what the consequences of messing this up are.

The team_role though, can be customized a bit, with either member or maintainer. In practice, a maintainer can do things like change team settings, such as pull request reminders, which is handy.

Next up, let's create the team in ./teams.tf:

module "{team-name}" {
  source = "./modules/org_team"
  gh_token = var.gh_token

  parent_team_id = github_team.parent-amut-teams.id

  team_name = "{display name for the team}"
  team_description = "{description of the team}"
  source_file = "${path.module}/teams/org/{team-name}.csv"
}

Replace the {placeholders} with real values.

The parent_team_id (github_team.parent-amut-teams.id) refers to the team used to represent the organisation structure, so that makes sense to use here.

We need to add an output to the ./outputs.tf file in order for us to actually refer to the team by value in other places.

In the ./outputs.tf file:

output "{team-name}" {
  value = module.{team-name}
}

It is important that the module.{team-name} placeholder is the same as the one used when the module was defined in ./teams.tf:

module "{team-name}" {}

So if the module was named foobar, the value for the output should be module.foobar.

Stage your changes to the files ./teams.tf, ./outputs.tf, and ./teams/org/{team-name}.csv, and commit them. Push to a new branch and create a pull request.[1]

It needs to be approved and applied before we continue with linking a repo to a team.

Once that is done, let's proceed.

Find the repo you want to link to the new team in ./*/main.tf, and modify it:

module "{repo-name}" {
  source            = "../modules/repos/"
  gh_token          = var.gh_token
  gh_webhook_secret = var.gh_webhook_secret

  name        = "{repo name}"
  description = "{repo description}"

  teams = {
    "developers"      = data.terraform_remote_state.tf_github_common.outputs.developers.id
    "{team-name}"     = data.terraform_remote_state.tf_github_common.outputs.{team-name}.team_id
  }

  whoami = {
    # lots of stuff
  }
}

Add the teams block, and add the developers line as-is. It is the default team that should always[2] have access to a repo.

The {team-name} line gives the team you created access to the repo, so it can be referenced as a member of the repo, in e.g. pull request reviewers.

Note the data.terraform_remote_state.tf_github_common.outputs.{team-name}.team_id part.

This should refer to the name you gave the output. The reason we use team_id is because we used a module, and that exposes the team_id variable.

developers on the other hand is defined as a resource, and that exposes the id variable.

The last check is to take a look at the ./*/backend.tf, located next to the ./*/main.tf file you found your repo in, and make sure it has the block:

data "terraform_remote_state" "tf_github_common" {
  backend = "gcs"
  config = {
    bucket = "amedia-tf-github-state-prod"
    prefix = "prod"
  }
}

So if your repo was in ./p/main.tf, the ./p/backend.tf file must contain the terraform_remote_state block. If it is not there, you need to add it, otherwise the output will not be available in main.tf.

Now, you can stage and commit your changes to the ./*/main.tf and ./*/backend.tf files. Push the changes to a new branch, and create a PR.[3]

Once that PR is approved and applied, you can now use your team for reviews, create reminders, and whatever else GitHub comes up with.

/v.


  1. This is the first PR you have to create. ↩︎

  2. Well, almost always, there are exceptions. ↩︎

  3. You can make all your changes to the repos your team should be a member of in this PR, or create multiple PRs. ↩︎