Skip to content

microsoftexpert/tf-mod-github-enterprise

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

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.

Terraform GitHub provider module type resources


🧩 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

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
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

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.

βœ… Provider / Versions

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) 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

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

βš™οΈ 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/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 carrying admin:enterprise). Auth and the target enterprise are provider concerns β€” never module variables.


πŸ”Œ Cross-Module Contract

Sourced from SCOPE.md.

Consumes

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.

Emits

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

πŸ“š 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/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 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/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_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/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 to true.

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 = 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/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_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/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_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/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_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/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"
}

⚠️ 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/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_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) 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.


πŸ“₯ Inputs

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

  • enterprise_slug β€” enterprise URL slug. Required when ip_allow_list is non-empty or security_analysis is set.
  • enterprise_id β€” enterprise node id. Required when organizations 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 null

emu_group_mappings = map(object({
  team_slug = string # GitHub team slug (required)
  group_id  = number # integer IdP group id (required)
}))

🧾 Outputs

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.


🧠 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_ids β‰  organization_node_ids. organization_ids emits the numeric database_id (what tf-mod-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.0 β€” never 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

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.

πŸ”— 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 β€” 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)

About

No description or website provided.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages