Skip to content

officialdad/infra-components

Repository files navigation

infra-components

A reusable Terraform module library for the officialdad infrastructure. Each top-level directory is a component; the actual Terraform lives in that component's terraform/ subdirectory.

This repo is public so the environments repos can fetch modules over HTTPS with no auth. Modules are consumed via a pinned git source, e.g.:

terraform {
  source = "git::https://github.com/officialdad/infra-components.git//<component>/terraform?ref=<tag>"
}

The //<component>/terraform part selects the subdirectory inside this repo, and ?ref=<tag> pins a version.

This README is also the conventions contract for the officialdad IaC repos: the anatomy, the global object, naming, and release process below are shared by all three repos. See CHANGELOG.md for what changed in each tagged version, and CLAUDE.md for how agents work in this repo (the automated quality gate, design principles, and the component checklist).

Repos

Repo Role Module ref
infra-components Reusable Terraform modules tagged releases
infra-environments-dev Dev environment (ungated) tracks main
infra-environments-prod Prod environment (gated) pinned tags

Components

Clouds: both AWS (hashicorp/aws) and GCP (hashicorp/google) modules are kept here, so an environment can pick either stack. See CHANGELOG.md for the history.

Component Cloud Purpose Key outputs
vpc AWS Network foundation — wraps terraform-aws-modules/vpc (VPC + per-AZ subnets + NAT) vpc_id, private_subnet_ids, public_subnet_ids
ec2 AWS One or more EC2 instances (instances map, bootstrap-agnostic) via the ec2-instance + security-group modules; SSM access, no public IP, per-instance named ingress_rules instances (map keyed by instance key)
iam-policy AWS Generic IAM policy factory — wraps caller-composed JSON documents into named, tagged managed policies (feeds ec2 iam_role_policy_arns) policy_arns (map keyed by policy key)
ebs-volume AWS Standalone encrypted EBS data volumes (volumes map) in their own state — decoupled from the ec2 instance lifecycle so data survives a compute destroy/apply volumes (map keyed by volume key)
network GCP Network foundation — custom-mode VPC + regional subnet + Cloud NAT + IAP-SSH firewall (wraps Google Cloud Foundation Toolkit) network_name, subnetwork_self_link, ssh_tag
compute-engine GCP One or more Compute Engine VMs (instances map, bootstrap-agnostic); OS Login + IAP access, no external IP instances (map keyed by instance key)
github GitHub GitHub repositories as code (repo factory) repository_names, repository_urls
automation-roles AWS CI identity — GitHub Actions OIDC provider + the least-privilege IAM role the pipeline assumes (no static keys) role_arn, oidc_provider_arn

The components form two parallel dependency chains, one per cloud: vpcec2 (AWS) and networkcompute-engine (GCP) — in each, instances launch into the network the foundation component outputs. github and automation-roles are standalone (no network); automation-roles is a human-applied CI bootstrap, kept out of its own pipeline.

Anatomy of a component

Each component is a directory with a terraform/ subdir:

<component>/
├── README.md          # from .github/component-readme-template.md; Inputs/Outputs generated
└── terraform/
    ├── versions.tf    # required_version (min floor, >= 1.5.7) + required_providers (pinned ~> ranges)
    ├── variables.tf   # inputs; first variable is always `global` (except `github`, see below)
    ├── main.tf        # provider + resources
    └── outputs.tf     # values consumed by downstream components

Component READMEs follow one house style on a shared skeleton: prose sections (What it creates, Auth, Dependencies) plus an Inputs/Outputs table generated by terraform-docs between <!-- BEGIN_TF_DOCS --> markers — never hand-edited. Scaffold new ones from .github/component-readme-template.md; the style and table generation are enforced by pre-commit + CI (see CLAUDE.md → Component README style).

The global object

Every component takes a global object as its first variable, carrying environment-wide context (name, region, tags) so modules stay generic. The environments repo passes it once via a shared global.tfvars.

variable "global" {
  type = object({
    environment_name = string
    deploy_region    = string
    tags             = map(string)
  })
}

Use it for naming and tags: "${var.global.environment_name}-vpc", and merge(var.global.tags, { ... }) on every resource. On GCP, global.tags is sanitized into resource labels where the provider supports them (e.g. the compute-engine instance); GCP networking resources can't be labeled, so only naming carries through there.

One exception: github takes no global. Its resources are org-scoped, not environment-scoped, and github_repository has nothing to tag — a global input would be a dead declaration (which tflint's recommended preset flags). Every other component takes global as its first variable.

Naming

Deterministic and readable — no random suffixes (we're single-region, two environments; we don't need global-uniqueness hashing).

  • Resources: <environment_name>-<component>[-<purpose>] e.g. dev-vpc, prod-app-sg, prod-app-alb-tg.
  • State buckets: tfstate-officialdad-<env>-<region-or-CHANGEME> (one bucket per environment, separate AWS accounts for dev vs prod).
  • State keys: one per component, set automatically by Terragrunt via ${path_relative_to_include()}/terraform.tfstate.
  • Tags: always include Environment, ManagedBy = "terraform", plus var.global.tags.

Versioning & releasing

Modules are versioned with git tags (vMAJOR.MINOR.PATCH), consumed via ?ref=<tag>. Dev can track a branch (?ref=main) while iterating; prod pins a tag.

  • MAJOR — breaking input/output change (callers must edit their config). Call it out loudly in CHANGELOG.
  • MINOR — new feature, backward compatible.
  • PATCH — bug fix, no interface change.

Release scope & pinned consumption

Tags are repo-wide, not per-component (vX.Y.Z, never vpc/vX.Y.Z). That's deliberate: each component is consumed by its own pinned source, so a single monorepo tag serves every component independently.

  • Each pin is fetched independently. A consumer's source = "…//vpc/terraform?ref=v0.7.0" pulls only that subtree at that tag. Components bump on their own schedule — leaving an unchanged component pinned at an older tag is valid, not drift.
  • The changelog says which components moved. Every entry is component-scoped (**vpc:** …, **ec2:** …) because it's generated from type(scope): subject commits with scope = the component. Diffing CHANGELOG.md between two tags (or the compare/vA...vB link) tells a consumer whether their component actually changed — and whether the pin is worth bumping.
  • Each tag ships a GitHub Release. The pushed tag publishes a GitHub Release (notes = the new CHANGELOG section), so "latest" and its notes are machine-discoverable — a bump bot (Renovate/Dependabot/custom) can open "bump to vX.Y.Z" PRs against it.

Releasing

Steps 3–4 (generate the CHANGELOG section, commit, tag) are automated — you pick the version and confirm the irreversible push; the tooling does the rest. One-time per clone: install git-cliff (see Toolchain) and run scripts/setup-hooks.sh to install the git hooks (Claude Code runs it on session start).

  1. Make the module change on a branch, open a PR, merge to main. The commit subjects are the changelog — write them type(scope): subject with scope = component, and flag input/output/ breaking changes there. CI previews the generated entries on your PR via cliff.toml.

  2. infra-environments-dev (tracks main) picks it up — apply and let it soak.

  3. Cut the release from main (after it has soaked):

    • In Claude Code: /release X.Y.Z — previews the generated section, then pushes on your confirm.
    • By hand: scripts/release.sh X.Y.Z.

    Either way scripts/release.sh generates the new ## [X.Y.Z] - <today> section from the unreleased Conventional Commits with git-cliff, splices it under ## [Unreleased], fixes the two compare links, and commits chore(release): vX.Y.Z + tags vX.Y.Zlocally, nothing pushed. No hand-curation: the commit subjects are the entries, and history ≤ v0.6.0 is frozen (only the new section is generated). Review with git show vX.Y.Z; undo with git tag -d vX.Y.Z && git reset --hard HEAD~1.

  4. Publish: git push origin main vX.Y.Z (branch + tag atomically — avoids the tag landing on a different commit if something races). A pre-push guard (scripts/check-release-tag.sh) refuses the push unless CHANGELOG.md has the matching ## [X.Y.Z] - <date> section. The pushed tag triggers changelog.yml, which publishes the GitHub Release from the same git-cliff config — so the Release matches the CHANGELOG section. No hand-written notes.

  5. Promote to prod: PR in infra-environments-prod bumping the component's versions.hcl ("vOLD""vX.Y.Z"), reviewed, then apply.

Commits

Conventional Commitstype(scope): subject, with an imperative subject. type is feat / fix / docs / refactor / chore / ci / revert; scope is the component, e.g. feat(ec2): add ami_ssm_parameter, fix(vpc): .... Mark a breaking change with feat(ec2)!: or a BREAKING CHANGE: footer. Reference an issue if there is one.

The format is enforced on the commit-msg stage by conventional-pre-commit — run scripts/setup-hooks.sh once per clone (it wires the pre-commit, commit-msg, and pre-push hooks). feat/fix/breaking line up with the PR template's Type field and the CHANGELOG sections.

Tagging stays manual and deliberate — we do not auto-release on merge. A human cuts the version tag after a change has soaked in dev (see Versioning & releasing).

Toolchain

  • Terraform 1.15.5, Terragrunt 1.0.7 (the environments repos pin these via .terraform-version / .terragrunt-version).
  • CI (.github/workflows/ci.yml) runs terraform fmt / validate / tflint per component — the matrix is derived from the filesystem (each <component>/terraform/ dir), so new components are validated automatically with no list to maintain.
  • git-cliff (changelog generation) — scripts/release.sh runs it locally to generate the CHANGELOG section from Conventional Commits; CI runs the same generator via orhun/git-cliff-action. Install it with cargo install git-cliff, brew install git-cliff, or a prebuilt binary from the releases (tested with 2.13.1).

Notes

  • All values are placeholders — no real AWS/GCP account IDs, credentials, or hostnames.
  • Modules are minimal but valid and applyable (real resource blocks), so you can grow them.
  • A real apply requires the relevant cloud credentials (AWS or GCP) and a state backend, both configured in the env repos.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors