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.
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, afor_eachmap), 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, afor_eachmap). - π‘οΈ 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, afor_eachmap). - π€ Composition-ready outputs β emits
organization_ids(numeric),organization_node_ids, andorganization_namesso 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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
tf-mod-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 tf-mod-github-enterprise-actions (which owns enterprise Actions specifically). The keystone tf-mod-github-repository lives further downstream β repositories are created inside the orgs this module produces.
flowchart TD
ENT["π’ tf-mod-github-enterprise<br/>orgs Β· IP allow-list Β· security Β· EMU<br/>THIS MODULE"]
ENTACT["tf-mod-github-enterprise-actions<br/>sibling β enterprise Actions policy + runners"]
subgraph ORGSCOPE["Org-scope modules β run inside each org ENT creates"]
SET["tf-mod-github-organization-settings"]
RULE["tf-mod-github-organization-ruleset"]
MEM["tf-mod-github-membership"]
ROLES["tf-mod-github-organization-roles"]
end
REPO["π tf-mod-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
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.
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
Resource inventory
| Resource | Role |
|---|---|
github_enterprise_organization.organizations |
for_each map β organizations created/managed within the enterprise (keys on enterprise_id). |
github_enterprise_ip_allow_list_entry.ip_allow_list |
for_each map β enterprise IP allow-list CIDR entries (keys on enterprise_slug). |
github_enterprise_security_analysis_settings.security |
Singleton (count 0/1) β enterprise-wide security-analysis baseline (keys on enterprise_slug). |
github_emu_group_mapping.emu_groups |
for_each map (EMU-only) β binds external IdP groups to GitHub teams. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 (uses cross-variable validation, GA since 1.9) |
| Provider | integrations/github ~> 6.0 (validated against 6.12.1) |
| Source | integrations/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) andenterprise_slugβ see Architecture Notes. github_emu_group_mapping.group_idis anumber(the integer IdP group id), unlikegithub_team_sync_group_mappingwhere the group id is a string.
tf-mod-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
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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug # for IP allow-list + security
enterprise_id = data.github_enterprise.this.id # node id, for org creation
organizations = {
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 carryingadmin:enterprise). Auth and the target enterprise are provider concerns β never module variables.
Sourced from SCOPE.md.
| Input | Type | Source |
|---|---|---|
enterprise_slug |
string |
caller-supplied β the enterprise the provider identity owns (used by IP allow-list + security-analysis). Commonly data.github_enterprise.<x>.slug. |
enterprise_id |
string |
caller-supplied node id via data.github_enterprise.<x>.id (used by org creation). |
emu_group_mappings[].team_slug |
string |
tf-mod-github-team (slug output). |
emu_group_mappings[].group_id |
number |
external IdP via data.github_external_groups.<x>.external_groups[*].group_id. |
| Output | Description | Consumed by |
|---|---|---|
organization_ids |
Map: org key β numeric org id (database_id) |
tf-mod-github-enterprise-actions (allowed/selected orgs, set(number)); org-scope modules |
organization_node_ids |
Map: org key β node id (GraphQL global ID) | Org-scope modules' provider aliases / GraphQL targeting |
organization_names |
Map: org key β org login name | Composition / reporting; org-scope provider owner |
ip_allow_list_ids |
Map: entry key β allow-list entry id | Audit |
security_analysis |
Resolved security-analysis baseline (object; null when unmanaged) |
Compliance dashboards |
emu_group_mapping_ids |
Map: mapping key β id (EMU only) | Audit |
Every map collection defaults to
{}and the singleton tonull, 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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug
enterprise_id = data.github_enterprise.this.id
organizations = {
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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_id = data.github_enterprise.this.id
organizations = {
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 stablefor_eachhandle 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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_id = data.github_enterprise.this.id
organizations = {
sandbox = {
name = "casey-sandbox"
billing_email = "github-billing@financialpartners.com"
admin_logins = ["github-enterprise-admin"]
}
}
}βΉοΈ Cross-variable validation enforces this:
enterprise_idis required whenorganizationsis non-empty;enterprise_slugis required only whenip_allow_listorsecurity_analysisis set.
4 Β· IP allow-list perimeter β staged activation
module "enterprise" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug
ip_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 totrue.
5 Β· Security-analysis β full hardened baseline (GHAS licensed)
module "enterprise" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug
security_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 = truerequires a GitHub Advanced Security license, or the apply fails. It defaults tofalsefor exactly this reason.
6 Β· Security-analysis β no GHAS license (secret scanning + push protection only)
module "enterprise" {
source = "git::https://github.com/microsoftexpert/tf-mod-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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug
security_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 = { for g in data.github_external_groups.all.external_groups : g.group_name => g.group_id }
}
module "enterprise" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-enterprise?ref=v1.0.0"
# team_slug comes from tf-mod-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_mappingrequires an Enterprise Managed Users enterprise with SAML/OIDC SSO and SCIM configured. On a non-EMU enterprise every entry fails to apply β leaveemu_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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug # the slug you searched by
enterprise_id = data.github_enterprise.this.id # the GraphQL node id
#... organizations / ip_allow_list / security_analysis...
}βΉοΈ
data.github_enterprisealso exposesdatabase_id(numeric) andnameβ butenterprise_idhere 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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug
enterprise_id = data.github_enterprise.this.id
organizations = {
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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_id = data.github_enterprise.this.id
organizations = {
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/tf-mod-github-enterprise-actions?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug
permissions = {
enabled_organizations = "selected"
allowed_organization_ids = toset(values(module.enterprise.organization_ids)) # set(number)
}
}π‘
organization_idsare the numeric ids the enterprise-Actions policy expects asset(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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_id = data.github_enterprise.this.id
organizations = {
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/tf-mod-github-organization-settings?ref=v1.0.0"
providers = { github = github.platform }
billing_email = "github-billing@financialpartners.com"
}
β οΈ Providerownermust 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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug
security_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_analysisisnullwhen 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) tf-mod-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/tf-mod-github-enterprise?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug
enterprise_id = data.github_enterprise.this.id
organizations = {
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([for g in data.github_external_groups.all.external_groups : g.group_id if g.group_name == "platform-admins"])
}
}
}
###############################################################################
# 3) tf-mod-github-enterprise-actions (sibling) β consume the numeric org ids.
###############################################################################
module "enterprise_actions" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-enterprise-actions?ref=v1.0.0"
enterprise_slug = data.github_enterprise.this.slug
permissions = {
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/tf-mod-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.
Parent references (both default null; supply the one(s) your collections need)
enterprise_slugβ enterprise URL slug. Required whenip_allow_listis non-empty orsecurity_analysisis set.enterprise_idβ enterprise node id. Required whenorganizationsis 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;nullleaves 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 null
emu_group_mappings = map(object({
team_slug = string # GitHub team slug (required)
group_id = number # integer IdP group id (required)
}))| Output | Description | Sensitive |
|---|---|---|
organization_ids |
Map: org key β numeric database id. Empty {} when no orgs. |
no |
organization_node_ids |
Map: org key β node id (GraphQL global ID). Empty {} when no orgs. |
no |
organization_names |
Map: org key β org login name. Empty {} when no orgs. |
no |
ip_allow_list_ids |
Map: entry key β allow-list entry id. Empty {} when none. |
no |
security_analysis |
Resolved baseline object, or null when unmanaged. |
no |
emu_group_mapping_ids |
Map: 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.
- The enterprise identifier is split β the #1 gotcha.
github_enterprise_organizationkeys onenterprise_id(the GraphQL node id), whilegithub_enterprise_ip_allow_list_entryandgithub_enterprise_security_analysis_settingskey onenterprise_slug. This module accepts both and gates each with cross-variablevalidationso you only supply what your collections actually need. organization_idsβorganization_node_ids.organization_idsemits the numericdatabase_id(whattf-mod-github-enterprise-actionsconsumes asset(number));organization_node_idsemits the string nodeid(for GraphQL / provider targeting). Pick deliberately.- EMU
group_idis anumber. Distinct fromgithub_team_sync_group_mapping, where the IdP group id is a string. The value comes fromdata.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 withlifecycle { prevent_destroy = true }in the root β Terraform cannot setprevent_destroyfrom 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_mappingsdefaults{}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.
- π 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.
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.0β never a branch. Enterprise-account changes affect every organization and every member; a movingrefis unacceptable for governance.
β οΈ Review everyplanfor#... will be destroyedongithub_enterprise_organizationbefore applying β that is an organization deletion.
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
badfile withorganizationsset butenterprise_idnull β rejected with "enterprise_id β¦ is required when organizations is non-empty." - A
badfile withip_allow_listset butenterprise_slugnull β rejected with the matchingenterprise_slugmessage. - A full valid file plans cleanly (
organizations,ip_allow_list,security[0],emu_groupsall rendered) with zero "Invalid value for variable" errors.
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"
}
| Symptom | Cause | Resolution |
|---|---|---|
403 / 404 on every resource |
Identity is not an enterprise owner, or the target is Free/Team / a standalone org | Authenticate the provider with a PAT/App carrying admin:enterprise against a real Enterprise Cloud account. |
enterprise_id β¦ is required when organizations is non-empty |
organizations set but enterprise_id null |
Supply 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 null |
Supply enterprise_slug (e.g. data.github_enterprise.this.slug). |
| Apply fails enabling advanced security | GHAS not licensed | Set advanced_security_enabled_for_new_repositories = false (the default), or license GHAS. |
| EMU mapping apply fails on a non-EMU enterprise | github_emu_group_mapping requires EMU + SSO + SCIM |
Leave emu_group_mappings = {} unless the enterprise is EMU. |
| Locked out after activating the IP allow-list | Active list omits the runner/CI egress | Add 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 organization |
A key was removed from organizations, or a destroy was run |
This deletes the org. Confirm intent; for production set prevent_destroy in the root. |
| Secondary rate limit during a large apply | Many org/IP/EMU writes at once | Split bulk for_each applies; back off and re-run β each entry is one API write. |
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_enterpriseandgithub_external_groupsdata sources - module β tf-mod-github-enterprise-actions (sibling; enterprise Actions policy + runner groups)
- module β tf-mod-github-organization-settings / _ruleset / tf-mod-github-membership / tf-mod-github-organization-roles (org-scope governance, downstream)
- module β tf-mod-github-repository (keystone) and tf-mod-github-team (EMU mapping target)
- GitHub Docs β Enterprise accounts, IP allow lists, GitHub Advanced Security, and Enterprise Managed Users (SSO + SCIM)