Skip to content

Repository files navigation

🐙 GitHub Enterprise Terraform Module

Enterprise-account governance for GitHub Enterprise Cloud — one aggregation boundary that creates the organizations inside an enterprise, enforces the enterprise IP allow-list, sets the enterprise-wide security-analysis baseline, and (optionally) binds Enterprise Managed User IdP groups to teams. Secure-by-default, deeply typed, no this. Built for integrations/github v6.x. ⚠️ ENTERPRISE CLOUD ONLY.

TerraformGitHub providermoduletyperesources


🧩 Overview

This module governs the enterprise-account surface of GitHub Enterprise Cloud — the tier above an organization. It is an aggregation: there is no single dominant resource and no resource named this. Each concern is an independently-optional, role-named collection or singleton, so a caller manages any subset.

  • 🏢 Organizations — create and manage the organizations that live inside the enterprise (github_enterprise_organization, a for_each map), each with a billing email and at least one enterprise/org owner.
  • 🌐 IP allow-list — enforce the enterprise network perimeter with CIDR allow-list entries (github_enterprise_ip_allow_list_entry, a for_each map).
  • 🛡️ Security-analysis baseline — set the enterprise-wide defaults for new repositories: secret scanning + push protection on, GHAS opt-in (github_enterprise_security_analysis_settings, a singleton).
  • 🔗 EMU group mappings(optional, EMU-only) — bind external IdP groups to GitHub teams so membership is driven by your identity provider (github_emu_group_mapping, a for_each map).
  • 📤 Composition-ready outputs — emits organization_ids (numeric), organization_node_ids, and organization_names so downstream enterprise/org modules can target the orgs this module creates.

💡 Why it matters: the enterprise account is the apex of GitHub governance at a regulated institution. Creating orgs, locking the network perimeter, and setting the security baseline here — in code, under review — means every organization is born governed and every new repository inherits the institution's security posture from day zero.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

terraform-github-enterprise sits at the top of the dependency graph: it creates the organizations that every org-scope module then governs, and it is a sibling of terraform-github-enterprise-actions (which owns enterprise Actions specifically). The keystone terraform-github-repository lives further downstream — repositories are created inside the orgs this module produces.

flowchart TD
ENT["🏢 terraform-github-enterprise<br/>orgs · IP allow-list · security · EMU<br/>THIS MODULE"]
ENTACT["terraform-github-enterprise-actions<br/>sibling — enterprise Actions policy + runners"]
subgraph ORGSCOPE["Org-scope modules — run inside each org ENT creates"]
SET["terraform-github-organization-settings"]
RULE["terraform-github-organization-ruleset"]
MEM["terraform-github-membership"]
ROLES["terraform-github-organization-roles"]
end
REPO["🐙 terraform-github-repository<br/>keystone"]
ENT -->|organization_ids| ENTACT
ENT -->|organization_ids / names → provider owner| ORGSCOPE
ORGSCOPE --> REPO
style ENT fill:#8957E5,color:#fff
style REPO fill:#24292F,color:#fff
Loading

This module consumes the enterprise identifier (slug + node id, caller-supplied) and team slugs / IdP group ids; it emits organization ids and names. See the Cross-Module Contract.


🧬 What this module builds

An aggregation of four enterprise-account resources, each wired to the parent identifier the provider requires. The provider splits the enterprise identifier: organizations key on the enterprise node id (enterprise_id), while the IP allow-list and security-analysis settings key on the enterprise slug (enterprise_slug). EMU mappings key on team_slug + group_id and need neither.

flowchart TD
EID["var.enterprise_id<br/>(enterprise node ID)"]
ESLUG["var.enterprise_slug"]
TEAMS["var.emu_group_mappings<br/>team_slug + group_id"]
ORG["github_enterprise_organization · organizations<br/>for_each map(object)"]
IP["github_enterprise_ip_allow_list_entry · ip_allow_list<br/>for_each map(object)"]
SEC["github_enterprise_security_analysis_settings · security<br/>singleton (count 0/1)"]
EMU["github_emu_group_mapping · emu_groups<br/>for_each map(object) · EMU-only"]
EID --> ORG
ESLUG --> IP
ESLUG --> SEC
TEAMS --> EMU
ORG --> O1["organization_ids · organization_node_ids · organization_names"]
IP --> O2["ip_allow_list_ids"]
SEC --> O3["security_analysis"]
EMU --> O4["emu_group_mapping_ids"]
style ORG fill:#8957E5,color:#fff
style IP fill:#8957E5,color:#fff
style SEC fill:#8957E5,color:#fff
style EMU fill:#8957E5,color:#fff
Loading

Resource inventory

ResourceRole
github_enterprise_organization.organizationsfor_each map — organizations created/managed within the enterprise (keys on enterprise_id).
github_enterprise_ip_allow_list_entry.ip_allow_listfor_each map — enterprise IP allow-list CIDR entries (keys on enterprise_slug).
github_enterprise_security_analysis_settings.securitySingleton (count 0/1) — enterprise-wide security-analysis baseline (keys on enterprise_slug).
github_emu_group_mapping.emu_groupsfor_each map (EMU-only) — binds external IdP groups to GitHub teams.

✅ Provider / Versions

RequirementValue
Terraform>= 1.12.0 (uses cross-variable validation, GA since 1.9)
Providerintegrations/github~> 6.0 (validated against 6.12.1)
Sourceintegrations/github only — never the deprecated hashicorp/github

Schema notes that bite:

  • Enterprise resources are v6-era and Enterprise-Cloud-only — they 403/404 on Free/Team plans and standalone orgs.
  • The enterprise identifier is split across enterprise_id (node id) and enterprise_slug — see Architecture Notes.
  • github_emu_group_mapping.group_id is a number (the integer IdP group id), unlike github_team_sync_group_mapping where the group id is a string.

📁 Module Structure

terraform-github-enterprise/
├── providers.tf # github provider requirement (~> 6.0); NO provider {} block
├── variables.tf # enterprise_slug + enterprise_id; organizations / ip_allow_list /
│ # emu_group_mappings collections; security_analysis singleton
├── main.tf # 4 role-named resources (no `this`); for_each + singleton count
├── outputs.tf # organization_ids/node_ids/names, ip_allow_list_ids,
│ # security_analysis, emu_group_mapping_ids
├── README.md # this file
└── SCOPE.md # in/out-of-scope, consumes/emits, token scopes, prerequisites

⚙️ Quick Start

The smallest useful call: create one organization and apply the secure security-analysis baseline. Look the enterprise up by slug with the github_enterprise data source so you supply both the slug and the node id.

data"github_enterprise""this" {
slug="financial-partners"
}
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slug# for IP allow-list + securityenterprise_id=data.github_enterprise.this.id# node id, for org creationorganizations={
platform = {
name ="casey-platform"
billing_email ="github-billing@financialpartners.com"
admin_logins = ["github-enterprise-admin"]
}
}
# Empty object = singleton ON with secure defaults:# secret scanning + push protection enabled for new repos; GHAS opt-in.security_analysis={}
}

⚠️ The provider must authenticate as an enterprise owner (a PAT or GitHub App carrying admin:enterprise). Auth and the target enterprise are provider concerns — never module variables.


🔌 Cross-Module Contract

Sourced from SCOPE.md.

Consumes

InputTypeSource
enterprise_slugstringcaller-supplied — the enterprise the provider identity owns (used by IP allow-list + security-analysis). Commonly data.github_enterprise.<x>.slug.
enterprise_idstringcaller-supplied node id via data.github_enterprise.<x>.id (used by org creation).
emu_group_mappings[].team_slugstringterraform-github-team (slug output).
emu_group_mappings[].group_idnumberexternal IdP via data.github_external_groups.<x>.external_groups[*].group_id.

Emits

OutputDescriptionConsumed by
organization_idsMap: org key → numeric org id (database_id)terraform-github-enterprise-actions (allowed/selected orgs, set(number)); org-scope modules
organization_node_idsMap: org key → node id (GraphQL global ID)Org-scope modules' provider aliases / GraphQL targeting
organization_namesMap: org key → org login nameComposition / reporting; org-scope provider owner
ip_allow_list_idsMap: entry key → allow-list entry idAudit
security_analysisResolved security-analysis baseline (object; null when unmanaged)Compliance dashboards
emu_group_mapping_idsMap: mapping key → id (EMU only)Audit

📚 Example Library

Every map collection defaults to {} and the singleton to null, so each block below adds exactly one concern. Empty maps are never an error — index any output safely.

1 · Minimal — one organization + secure security baseline
data"github_enterprise""this" { slug="financial-partners" }
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slugenterprise_id=data.github_enterprise.this.idorganizations={
platform = {
name ="casey-platform"
billing_email ="github-billing@financialpartners.com"
admin_logins = ["github-enterprise-admin"]
}
}
security_analysis={} # secret scanning + push protection ON (secure defaults)
}
2 · Many organizations at scale from a map(object)
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_id=data.github_enterprise.this.idorganizations={
platform = {
name ="casey-platform"
billing_email ="github-billing@financialpartners.com"
admin_logins = ["github-enterprise-admin", "platform-lead"]
display_name ="Platform"
description ="Platform engineering"
}
data = {
name ="casey-data"
billing_email ="github-billing@financialpartners.com"
admin_logins = ["github-enterprise-admin"]
}
apps = {
name ="casey-apps"
billing_email ="github-billing@financialpartners.com"
admin_logins = ["github-enterprise-admin"]
}
}
}

💡 The map key (platform, data, apps) is the stable for_each handle and the key of every output map — choose it once and never change it.

3 · Organizations only — enterprise_slug not required
# When you ONLY create organizations, you need just enterprise_id.# enterprise_slug stays null because no ip_allow_list / security_analysis is set.module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_id=data.github_enterprise.this.idorganizations={
sandbox = {
name ="casey-sandbox"
billing_email ="github-billing@financialpartners.com"
admin_logins = ["github-enterprise-admin"]
}
}
}

ℹ️ Cross-variable validation enforces this: enterprise_id is required when organizations is non-empty; enterprise_slug is required only when ip_allow_list or security_analysis is set.

4 · IP allow-list perimeter — staged activation
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slugip_allow_list={
hq_egress = {
ip ="203.0.113.0/24"
name ="HQ egress range"
is_active =true
}
vpn = {
ip ="198.51.100.10/32"
name ="Corporate VPN"
is_active =true
}
ci_runners = {
ip ="192.0.2.0/27"
name ="Self-hosted CI egress"
is_active =false# 🔒 stage inactive, verify coverage, THEN activate
}
}
}

🔒 The allow-list only tightens access. Once any entry is active, only listed ranges reach the enterprise. An incomplete active list can lock out CI and the Terraform runner itself — stage with is_active = false, verify, then flip to true.

5 · Security-analysis — full hardened baseline (GHAS licensed)
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slugsecurity_analysis={
advanced_security_enabled_for_new_repositories =true# needs GHAS license
secret_scanning_enabled_for_new_repositories =true
secret_scanning_push_protection_enabled_for_new_repositories =true
secret_scanning_validity_checks_enabled =true
}
}

⚠️advanced_security_enabled_for_new_repositories = true requires a GitHub Advanced Security license, or the apply fails. It defaults to false for exactly this reason.

6 · Security-analysis — no GHAS license (secret scanning + push protection only)
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slug# Leave advanced_security false (the default). Secret scanning + push protection# default to true — this is the secure baseline that applies without GHAS.security_analysis={}
}
7 · Security-analysis — custom push-protection bypass link
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slugsecurity_analysis={
secret_scanning_push_protection_custom_link ="https://intranet.financialpartners.com/security/secret-push-bypass"
}
}

💡 The custom link is shown to developers when a push is blocked — point it at your internal bypass-request runbook.

8 · EMU group mappings — ⚠️ EMU enterprises only
# Discover the integer IdP group ids exposed to the enterprise.data"github_external_groups""all" {}
locals {
ext={ forgindata.github_external_groups.all.external_groups:g.group_name=>g.group_id }
}
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"# team_slug comes from terraform-github-team; group_id is the NUMBER from the data source.emu_group_mappings={
platform_admins = {
team_slug ="platform-admins"
group_id = local.ext["platform-admins"]
}
auditors = {
team_slug ="auditors"
group_id = local.ext["security-auditors"]
}
}
}

⚠️EMU-ONLY.github_emu_group_mapping requires an Enterprise Managed Users enterprise with SAML/OIDC SSO and SCIM configured. On a non-EMU enterprise every entry fails to apply — leave emu_group_mappings = {}. Each mapping is authoritative for its group; removing a key removes the binding.

9 · Discover the enterprise identifiers via the data source
data"github_enterprise""this" {
slug="financial-partners"
}
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slug# the slug you searched byenterprise_id=data.github_enterprise.this.id# the GraphQL node id#... organizations / ip_allow_list / security_analysis...
}

ℹ️ data.github_enterprise also exposes database_id (numeric) and name — but enterprise_id here wants the node id (.id).

10 · Secure / hardened enterprise baseline — everything together
data"github_enterprise""this" { slug="financial-partners" }
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slugenterprise_id=data.github_enterprise.this.idorganizations={
platform = {
name ="casey-platform"
billing_email ="github-billing@financialpartners.com"
admin_logins = ["github-enterprise-admin"]
}
}
ip_allow_list={
hq = { ip ="203.0.113.0/24", name ="HQ", is_active =true }
vpn = { ip ="198.51.100.10/32", name ="VPN", is_active =true }
}
security_analysis={
advanced_security_enabled_for_new_repositories =true
secret_scanning_enabled_for_new_repositories =true
secret_scanning_push_protection_enabled_for_new_repositories =true
secret_scanning_validity_checks_enabled =true
}
}
11 · Wire organization_ids → enterprise Actions (sibling)
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_id=data.github_enterprise.this.idorganizations={
platform = { name ="casey-platform", billing_email ="github-billing@financialpartners.com", admin_logins = ["github-enterprise-admin"] }
data = { name ="casey-data", billing_email ="github-billing@financialpartners.com", admin_logins = ["github-enterprise-admin"] }
}
}
module"enterprise_actions" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise-actions?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slugpermissions={
enabled_organizations ="selected"
allowed_organization_ids =toset(values(module.enterprise.organization_ids)) # set(number)
}
}

💡 organization_ids are the numeric ids the enterprise-Actions policy expects as set(number)toset(values(...)) collapses the handle-keyed map into the set.

12 · Wire organization_names → org-scope provider aliases
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_id=data.github_enterprise.this.idorganizations={
platform = { name ="casey-platform", billing_email ="github-billing@financialpartners.com", admin_logins = ["github-enterprise-admin"] }
}
}
# One provider alias per org governs INSIDE that org. The owner must be a# statically-known login, so apply the enterprise module first to create the org.provider"github" {
alias="platform"owner="casey-platform"# == module.enterprise.organization_names["platform"]
}
module"platform_settings" {
source="git::https://github.com/microsoftexpert/terraform-github-organization-settings?ref=v1.0.0"providers={ github = github.platform }
billing_email="github-billing@financialpartners.com"
}

⚠️ Provider owner must be known at plan time — you cannot derive it dynamically from a not-yet-created org in the same apply. Create the orgs first (this module), then add the org-scope modules with their aliases.

13 · Assert the security baseline in a check block
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slugsecurity_analysis={}
}
check"secret_scanning_enforced" {
assert {
condition=(module.enterprise.security_analysis!=null&&
module.enterprise.security_analysis.secret_scanning_push_protection_enabled_for_new_repositories)
error_message="Enterprise secret-scanning push protection must be enabled for new repositories."
}
}

ℹ️ security_analysis is null when the singleton is unmanaged — guard for null before indexing it.

🏗️ 14 · End-to-end composition — the full enterprise, wired outputs → inputs
################################################################################ 1) Look up the enterprise.###############################################################################data"github_enterprise""this" {
slug="financial-partners"
}
################################################################################ 2) terraform-github-enterprise — create the orgs, lock the perimeter, set the# security baseline, bind EMU groups (EMU enterprise).###############################################################################data"github_external_groups""all" {}
module"enterprise" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slugenterprise_id=data.github_enterprise.this.idorganizations={
platform = { name ="casey-platform", billing_email ="github-billing@financialpartners.com", admin_logins = ["github-enterprise-admin"] }
data = { name ="casey-data", billing_email ="github-billing@financialpartners.com", admin_logins = ["github-enterprise-admin"] }
}
ip_allow_list={
hq = { ip ="203.0.113.0/24", name ="HQ", is_active =true }
vpn = { ip ="198.51.100.10/32", name ="VPN", is_active =true }
}
security_analysis={
advanced_security_enabled_for_new_repositories =true
secret_scanning_enabled_for_new_repositories =true
secret_scanning_push_protection_enabled_for_new_repositories =true
}
emu_group_mappings={
platform_admins = {
team_slug ="platform-admins"
group_id =one([forgindata.github_external_groups.all.external_groups:g.group_idifg.group_name=="platform-admins"])
}
}
}
################################################################################ 3) terraform-github-enterprise-actions (sibling) — consume the numeric org ids.###############################################################################module"enterprise_actions" {
source="git::https://github.com/microsoftexpert/terraform-github-enterprise-actions?ref=v1.0.0"enterprise_slug=data.github_enterprise.this.slugpermissions={
enabled_organizations ="selected"
allowed_organization_ids =toset(values(module.enterprise.organization_ids))
}
}
################################################################################ 4) Org-scope governance — one provider alias per org ENT created.###############################################################################provider"github" {
alias="platform"owner="casey-platform"# == module.enterprise.organization_names["platform"]
}
module"platform_settings" {
source="git::https://github.com/microsoftexpert/terraform-github-organization-settings?ref=v1.0.0"providers={ github = github.platform }
billing_email="github-billing@financialpartners.com"
}
output"enterprise_org_ids" { value=module.enterprise.organization_ids }
output"enterprise_org_names" { value=module.enterprise.organization_names }

💡 The data flow is one-directional: enterprise creates orgs → emits ids/names → enterprise-Actions and org-scope modules consume them. Apply the enterprise module first so the orgs exist before their governors reference them.


📥 Inputs

Parent references (both default null; supply the one(s) your collections need)

  • enterprise_slug — enterprise URL slug. Required whenip_allow_list is non-empty or security_analysis is set.
  • enterprise_id — enterprise node id. Required whenorganizations is non-empty.

Collections (each a map(object) defaulting to {})

  • organizations — orgs to create within the enterprise.
  • ip_allow_list — enterprise IP allow-list CIDR entries.
  • emu_group_mappings — EMU IdP-group → team bindings (EMU-only).

Singleton

  • security_analysis — nullable object; null leaves the baseline unmanaged.
Full object schemas
organizations=map(object({
name =string# org login/slug (required)
billing_email =string# required, validated as email
admin_logins =set(string) # required, ≥ 1 owner login
display_name =optional(string)
description =optional(string)
}))
ip_allow_list=map(object({
ip =string# single IP or CIDR (required)
name =optional(string)
is_active =optional(bool, true)
}))
security_analysis=object({
advanced_security_enabled_for_new_repositories =optional(bool, false) # needs GHAS
secret_scanning_enabled_for_new_repositories =optional(bool, true)
secret_scanning_push_protection_enabled_for_new_repositories =optional(bool, true)
secret_scanning_validity_checks_enabled =optional(bool, false)
secret_scanning_push_protection_custom_link =optional(string)
}) # default nullemu_group_mappings=map(object({
team_slug =string# GitHub team slug (required)
group_id =number# integer IdP group id (required)
}))

🧾 Outputs

OutputDescriptionSensitive
organization_idsMap: org key → numeric database id. Empty {} when no orgs.no
organization_node_idsMap: org key → node id (GraphQL global ID). Empty {} when no orgs.no
organization_namesMap: org key → org login name. Empty {} when no orgs.no
ip_allow_list_idsMap: entry key → allow-list entry id. Empty {} when none.no
security_analysisResolved baseline object, or null when unmanaged.no
emu_group_mapping_idsMap: mapping key → id (EMU only). Empty {} when none.no

ℹ️ No output is sensitive — org ids/names, allow-list ids, and the security baseline are governance metadata, not secrets. No member PII and no credentials are emitted. There are no ARNs on GitHub.


🧠 Architecture Notes

  • The enterprise identifier is split — the #1 gotcha.github_enterprise_organization keys on enterprise_id (the GraphQL node id), while github_enterprise_ip_allow_list_entry and github_enterprise_security_analysis_settings key on enterprise_slug. This module accepts both and gates each with cross-variable validation so you only supply what your collections actually need.
  • organization_idsorganization_node_ids.organization_ids emits the numericdatabase_id (what terraform-github-enterprise-actions consumes as set(number)); organization_node_ids emits the string node id (for GraphQL / provider targeting). Pick deliberately.
  • EMU group_id is a number. Distinct from github_team_sync_group_mapping, where the IdP group id is a string. The value comes from data.github_external_groups.<x>.external_groups[*].group_id.
  • Organization creation is destructive on delete. Removing a key from organizations (or destroying the module) deletes the org and may take everything in it. Production enterprises should wrap this module with lifecycle { prevent_destroy = true } in the root — Terraform cannot set prevent_destroy from a variable, so opting in is intentionally left to the root per environment.
  • Security-analysis applies going forward. The baseline affects new repos/orgs only; existing ones are unaffected. The provider defaults every toggle to false; this module overrides secret scanning + push protection to ON as the secure baseline, leaving GHAS opt-in (it is license-gated).
  • The IP allow-list tightens, never loosens. Once any entry is active, only listed ranges reach the enterprise. Stage entries inactive, verify, then activate — an incomplete active list can lock out CI and the runner.
  • EMU is optional and EMU-only.emu_group_mappings defaults {} so non-EMU enterprises never instantiate it. Each mapping is authoritative for its group.
  • Aggregation, not composite. No resource named this; each concern is role-named and independently optional. Every output map is empty ({}) — never an error — so callers can index any output regardless of which subset was managed.
  • Enterprise vs org scope. This module owns the enterprise account surface. Per-org settings/rulesets/membership/roles deliberately live in the org-scope modules, which run inside each org via a provider alias. The boundary keeps enterprise-owner-only operations isolated from day-to-day org governance.
  • No tags, no timeouts. GitHub has neither; there is no universal tail to copy from the azurerm library.

🧱 Design Principles

  • 🔒 Secure by default — the security-analysis baseline turns secret scanning + push protection ON; GHAS is opt-in only (license-gated, fails closed rather than breaking applies).
  • 🌐 Perimeter tightens, never loosens — the IP allow-list is a deny-by-default control once active; staged activation is documented to prevent lockout.
  • 🪪 Least privilege & explicit ownership — every organization must declare at least one admin_login; auth is an enterprise-owner provider concern, never a module variable.
  • 🧩 Independently optional — manage any subset of the enterprise surface; each collection defaults empty and each parent reference is required only when used.
  • 🛟 Safe by construction — high-impact org deletion is flagged for prevent_destroy; EMU is gated to EMU enterprises; cross-variable validation rejects malformed calls at plan time.
  • 🧾 Auditable, no secrets — emits exactly the governance metadata compliance needs, with zero sensitive outputs.

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan # review carefully — org creation/deletion is high-impact
terraform apply # a human applies after review
terraform output

⚠️ Always pin the module to a tag — ?ref=v1.0.0never a branch. Enterprise-account changes affect every organization and every member; a moving ref is unacceptable for governance.

⚠️ Review every plan for #... will be destroyed on github_enterprise_organization before applying — that is an organization deletion.


🧪 Testing

The offline proof gate (no cloud, no credentials needed):

terraform fmt -check # zero formatting diffs
terraform validate # configuration is valid
tflint # core rules (no dedicated GitHub ruleset exists)

Variable validation runs early in plan, before provider auth — so you can prove the contract with throwaway -var-files:

  • A bad file with organizations set but enterprise_id null → rejected with "enterprise_id … is required when organizations is non-empty."
  • A bad file with ip_allow_list set but enterprise_slug null → rejected with the matching enterprise_slug message.
  • A full valid file plans cleanly (organizations, ip_allow_list, security[0], emu_groups all rendered) with zero "Invalid value for variable" errors.

💬 Example Output

organization_ids = {
"data" = 123456790
"platform" = 123456789
}
organization_node_ids = {
"data" = "O_kgDOABCD5679"
"platform" = "O_kgDOABCD5678"
}
organization_names = {
"data" = "casey-data"
"platform" = "casey-platform"
}
ip_allow_list_ids = {
"hq" = "LE_kwHOAA..."
"vpn" = "LE_kwHOAB..."
}
security_analysis = {
"advanced_security_enabled_for_new_repositories" = true
"secret_scanning_enabled_for_new_repositories" = true
"secret_scanning_push_protection_enabled_for_new_repositories" = true
"secret_scanning_push_protection_custom_link" = null
"secret_scanning_validity_checks_enabled" = false
"enterprise_slug" = "financial-partners"
}
emu_group_mapping_ids = {
"platform_admins" = "12345"
}

🔍 Troubleshooting

SymptomCauseResolution
403 / 404 on every resourceIdentity is not an enterprise owner, or the target is Free/Team / a standalone orgAuthenticate the provider with a PAT/App carrying admin:enterprise against a real Enterprise Cloud account.
enterprise_id … is required when organizations is non-emptyorganizations set but enterprise_id nullSupply enterprise_id (the node id) — e.g. data.github_enterprise.this.id.
enterprise_slug is required when ip_allow_list … or security_analysis …Those collections set but enterprise_slug nullSupply enterprise_slug (e.g. data.github_enterprise.this.slug).
Apply fails enabling advanced securityGHAS not licensedSet advanced_security_enabled_for_new_repositories = false (the default), or license GHAS.
EMU mapping apply fails on a non-EMU enterprisegithub_emu_group_mapping requires EMU + SSO + SCIMLeave emu_group_mappings = {} unless the enterprise is EMU.
Locked out after activating the IP allow-listActive list omits the runner/CI egressAdd the missing range (or temporarily deactivate via the UI/API), re-apply with full coverage; stage is_active = false next time.
Plan: … to destroy on an organizationA key was removed from organizations, or a destroy was runThis deletes the org. Confirm intent; for production set prevent_destroy in the root.
Secondary rate limit during a large applyMany org/IP/EMU writes at onceSplit bulk for_each applies; back off and re-run — each entry is one API write.

🔗 Related Docs

  • SCOPE.md — in/out-of-scope resources, consumes/emits, required token scopes, GitHub prerequisites, provider gotchas
  • integrations/github provider — Enterprise Organization, Enterprise IP Allow List Entry, Enterprise Security Analysis Settings, and EMU Group Mapping resource references
  • integrations/github provider — github_enterprise and github_external_groups data sources
  • module — terraform-github-enterprise-actions (sibling; enterprise Actions policy + runner groups)
  • module — terraform-github-organization-settings / _ruleset / terraform-github-membership / terraform-github-organization-roles (org-scope governance, downstream)
  • module — terraform-github-repository (keystone) and terraform-github-team (EMU mapping target)
  • GitHub Docs — Enterprise accounts, IP allow lists, GitHub Advanced Security, and Enterprise Managed Users (SSO + SCIM)

About

Terraform module: terraform-github-enterprise

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages