Skip to content

refactor: extract project into a reusable Terraform module #20

@gjed

Description

@gjed

Summary

Refactor the current project into a standalone, reusable Terraform module that any consuming repo can call with a single module block. The consuming repo should only need to maintain YAML config files and a minimal main.tf.

Motivation

Currently, using this project for a new GitHub org requires forking or copying the entire repo. Extracting it into a reusable module lets multiple orgs adopt the YAML-driven config pattern without duplicating Terraform logic.

Design

Module interface

The module accepts a config_path variable pointing to the consumer's YAML config directory and handles all parsing, merging, and resource creation internally.

variable "config_path" {
  description = "Path to the root config directory containing config.yml and subdirectories"
  type        = string
}

variable "webhook_secrets" {
  type      = map(string)
  default   = {}
  sensitive = true
}

variable "github_read_delay_ms" {
  type    = number
  default = 0
}

variable "github_write_delay_ms" {
  type    = number
  default = 100
}

Module structure (published module)

terraform-github-org/
├── main.tf                  # Org-level resources + module.repositories call
├── yaml-config.tf           # All YAML parsing/merging logic
├── variables.tf             # Module inputs (config_path, webhook_secrets, etc.)
├── outputs.tf               # Module outputs
├── modules/
│   └── repository/          # Existing submodule (unchanged)
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── README.md

Consumer repo structure

my-org-github/
├── config/
│   ├── config.yml
│   ├── group/
│   │   └── *.yml
│   ├── repository/
│   │   └── *.yml
│   ├── ruleset/
│   │   └── *.yml
│   └── webhook/
│       └── *.yml
├── main.tf                  # ~15 lines: provider + module call
├── outputs.tf
├── backend.tf
└── Makefile                 # Optional

Consumer main.tf example

terraform {
  required_version = ">= 1.6"
  required_providers {
    github = {
      source  = "integrations/github"
      version = "~> 6.0"
    }
  }
}

provider "github" {
  owner = "my-org"
}

module "github_org" {
  source      = "git::https://github.com/gjed/terraform-github-org.git?ref=v1.0.0"
  config_path = "${path.root}/config"
}

Implementation tasks

  • Remove the provider "github" block from the module — consumers must configure their own provider
  • Add config_path variable and replace all hardcoded config/ paths in yaml-config.tf with var.config_path-relative paths
  • Verify file() / fileset() calls work correctly with consumer-relative paths (must use path.root-based resolution)
  • Update module outputs to expose organization so consumers can reference it
  • Update onboard-repos.sh and offboard-repos.sh to support nested module state paths (e.g. module.github_org.module.repositories["repo"])
  • Ship validate-config.py and .pre-commit-config.yaml as a consumer template/scaffold (not inside the TF module)
  • Create a consumer template repo or example directory showing the minimal setup
  • Add documentation (README) for the published module
  • Tag initial release

Notes

  • The owner field can no longer be read from YAML by the module to configure the provider (providers are configured before modules run). Consumers set owner directly in their provider block.
  • Terraform's file() requires paths known at plan time — config_path must be a static string (e.g. "${path.root}/config"), not a computed value.
  • The YAML-config-inside-module approach is intentional. Forcing consumers to pre-process YAML would just push complexity around for no benefit.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions