# Orchestrating GitHub with Terraform

Using Terraform to manage and automate GitHub repositories, teams, and permissions.

- **Author:** Patrick Heneise
- **Published:** 2025-09-09
- **Updated:** 2025-09-09
- **Tags:** github, terraform, automation, devops

---

import Image from '../../src/components/Image.astro'

## TLDR;

Treat your GitHub organization like infrastructure. Put repos, teams, and
permissions under version control, review changes with pull requests, and let
Terraform apply them safely and repeatably. Start with a small set of repos and
teams, lean on `locals` and `for_each` to stay DRY, and automate everything in
Terraform Cloud so org changes are auditable.

## Why Terraform for GitHub?

It's not news to store Infrastructure as Code (IaC) in your GitHub repo to
orchestrate Google Cloud Platform, AWS or Azure resources. But did you know that
you can also manage your GitHub organization with Terraform?

Your GitHub org is infrastructure. Managing it with Terraform brings version
control, code review, and repeatable automation to repositories, teams, and
permissions. Define sub-teams and access policies declaratively, derive
permissions from topics or a simple assignment file, and track every change in
git. It replaces brittle UI clicks with auditable, scalable workflows as your
organization grows.

## Getting Started

### Prerequisites

- Terraform or Terraform Cloud (free tier covers small orgs)
- Admin access on your GitHub organization
- A GitHub Personal Access Token (PAT) with org admin/repo scopes

### Initialize Terraform & GitHub

Create a new repository - `infrastructure`, for example - and add a `main.tf`
with the following content:

```hcl
provider "github" {
  token = var.GH_PAT
  owner = "boulderjs-playground"
}

provider "tfe" {}
```

Define the variable for the GitHub PAT in a `variables.tf` file:

```hcl
variable "GH_PAT" {
  description = "GitHub Personal Access Token"
  type        = string
  sensitive   = true
}
```

Create a `versions.tf` file to specify the required providers and Terraform
version:

```hcl
terraform {
  required_providers {
    github = {
      source  = "integrations/github"
      version = ">= 6.6.0"
    }
    tfe = {
      source  = "hashicorp/tfe"
      version = "0.68.2"
    }
  }
}
```

You'll also need to create the Terraform Cloud token (in the web UI, settings)
and store an Environment Variable named `TFE_TOKEN` in your Terraform Cloud
workspace (Variables section).

That’s the minimal setup.

### Create your repos

Now you can start creating your repository structure. You can define more
variables in the `locals` block, like topics, homepage URL, etc.

```hcl
locals {
  repos = {
    "website" = {
      description  = "BoulderJS website and blog"
      homepage_url = ""
      topics       = ["frontend", "developers"]
    },
    "events" = {
      description  = "BoulderJS Events"
      homepage_url = ""
      topics       = ["events", "admins"]
    },
    "talks" = {
      description  = "Talks & Proposals"
      homepage_url = ""
      topics       = ["events", "members"]
    }
  }
}

resource "github_repository" "repos" {
  for_each = local.repos

  name         = each.key
  description  = each.value.description
  homepage_url = each.value.homepage_url
  topics       = each.value.topics

  visibility             = "public"
  has_issues             = true
  has_downloads          = false
  has_discussions        = false
  has_projects           = false
  has_wiki               = false
  auto_init              = true
}
```

There are several more options you can set. Check the
[Terraform GitHub Provider documentation](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository)
for more details. If you already have an org with several repos, you can
[create `import` blocks](https://developer.hashicorp.com/terraform/language/import).

<Image
  src="/articles/2025/tf-screenshot.png"
  width={1819}
  height={1180}
  alt="terraform plan output"
  className="my-8 rounded-lg shadow-lg"
/>

### Create teams

Same as with the repos, you can define your teams in a `locals` block and create
them with a `for_each` loop.

```hcl
locals {
  teams = {
    "admins" = {
      description = "BoulderJS Admins"
      privacy     = "closed"
    },
    "advocates" = {
      description = "BoulderJS Advocates"
      privacy     = "closed"
    },
    "members" = {
      description = "BoulderJS Members"
      privacy     = "closed"
    },
    "developers" = {
      description = "Developers"
      privacy     = "closed"
    }
  }
}

resource "github_team" "parents" {
  for_each    = local.teams
  name        = each.key
  description = each.value.description
  privacy     = each.value.privacy
}
```

You can even create & manage more complex "sub-team" structures by adding a
`parent_team_id` attribute to the `github_team` resource. GitHub Teams are
awesome for managing permissions and access to repositories, mentioning an
entire team in issues or pull requests or even auto-assigning reviewers. Via the
CODEOWNERS file and
[GitHub Team Settings](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/team_settings)
you can define the review request delegation algorithm (e.g., Round Robin) for
pull request reviews.

### Assign repositories to teams

Last but not least, you can assign your repositories to the teams you've
created.

```hcl
locals {
  # Create team-repo permissions based on topic matching
  team_repo_permissions = flatten([
    for repo_name, repo_config in local.repos : [
      for team_name, team_config in local.teams : {
        team_name = team_name
        repo_name = repo_name
        permission = contains(repo_config.topics, team_name) ? "maintain" : "pull"
      }
    ]
  ])
}

resource "github_team_repository" "permissions" {
  for_each = {
    for perm in local.team_repo_permissions :
    "${perm.team_name}-${perm.repo_name}" => perm
  }

  team_id    = github_team.parents[each.value.team_name].id
  repository = github_repository.repos[each.value.repo_name].name
  permission = each.value.permission
}
```

This uses the `topics` attribute from the `local.repos` definition to match
teams to repos. If a topic matches a team name, that team gets `maintain`
permissions, otherwise just `pull` access.

You can use other strategies, like a permissions file, or a more complex
matching algorithm.

### Apply your configuration

Just add & commit all files to your git repo and push it to GitHub. You can set
up Terraform Cloud to (automatically) plan & apply your configuration on every
push to the main branch or run plans on feature branches. Make sure to add the
`GH_PAT` variable in your Terraform Cloud workspace.

### More possibilities

#### GitHub Issue Templates

You can use the `github_repository_file` resource to create issue templates in
your repositories. Just create a local folder with your templates and use the
`for_each` loop to create them in each repository.

```hcl
resource "github_repository_file" "talks_issue_template_config" {
  repository          = "talks"
  branch              = "main"
  file                = ".github/ISSUE_TEMPLATE/config.yml"
  content             = file("${path.module}/files/ISSUE_TEMPLATE/config.yml")
  commit_message      = "Add issue template config"
  commit_author       = "Terraform"
  commit_email        = "terraform@boulderjs.org"
  overwrite_on_create = true
  depends_on          = [github_repository.repos]
}
```

#### Branch Protection Rules

You can enforce branch protection rules on your repositories to ensure code
quality and consistency. Here's an example of a strict ruleset that requires
status checks and pull request reviews before merging.

```hcl
resource "github_repository_ruleset" "strict" {
  name        = "strict"
  repository  = github_repository.repos["website"].name
  target      = "branch"
  enforcement = "active"

  conditions {
    ref_name {
      include = ["~DEFAULT_BRANCH"]
      exclude = []
    }
  }

  bypass_actors {
    actor_id    = 0
    actor_type  = "OrganizationAdmin"
    bypass_mode = "always"
  }

  rules {
    deletion         = false
    non_fast_forward = true

    required_status_checks {
      required_check {
        context = "lint-pr"
      }
    }

    pull_request {
      required_approving_review_count   = 2
      required_review_thread_resolution = true
      dismiss_stale_reviews_on_push     = true
    }
  }
}
```

### Tips & lessons learned

Keep your GitHub configuration separate from your application environments; a
dedicated repository or workspace keeps boundaries and audits clear. Terraform
Cloud is an excellent home for state and for running plans and applies, which
beats juggling local state and ad‑hoc workflows. Lean on `for_each` and `locals`
to keep configurations DRY and scalable, and package common patterns as modules
so you can reuse them confidently across teams and repos.

If you have questions or want a second set of eyes on your setup,
[reach out on Bluesky](https://bsky.app/profile/zentered.co) or
[start a discussion on GitHub](https://github.com/zentered/zentered/discussions/new?category=ideas-feedback).
