Cloud Foundation Fabric is a comprehensive suite of Terraform modules and end-to-end blueprints designed for Google Cloud Platform (GCP). It serves two primary purposes:
- Modules: A library of composable, production-ready Terraform modules (e.g.,
project,net-vpc,gke-cluster). - FAST (Fabric FAST): An opinionated, stage-based landing zone toolkit for bootstrapping enterprise-grade GCP organizations.
- Philosophy: Lean, composable, and close to the underlying provider resources.
- Structure:
- Standardized interfaces: IAM, logging, organization policies, etc.
- Self-contained: Dependency injection via context variables is preferred over complex remote state lookups within modules.
- Flat: avoid using sub-modules to reduce complexity and minimize layer traversals.
- Naming: Avoid random suffixes; use explicit
prefixvariables.
- Factories: Many modules implement a data-driven "factory" pattern (often via a
factories_configvariable) to manage resources at scale using YAML data files. SeeFACTORIES.mdfor a comprehensive list.- Validation: Factory YAML files must conform to JSON schemas (typically stored in a
schemas/folder). Use a modeline (e.g.,# yaml-language-server: $schema=../schemas/project.schema.json) to enable IDE validation.
- Validation: Factory YAML files must conform to JSON schemas (typically stored in a
- Usage: Modules are designed to be forked/owned or referenced via Git tags (e.g.,
source = "github.com/...//modules/project?ref=v30.0.0").
- Purpose: Rapidly set up a secure, scalable GCP organization.
- Architecture: Divided into sequential "stages" (0-org-setup, 1-vpcsc, 2-security, 2-networking, etc.).
- Factories: Extensively uses YAML-based datasets and module factory patterns to drive configuration at scale, acting as a "translation machine" that expresses different architectural designs without changing the underlying stage code.
- Python-based utility scripts for documentation, linting, and CI/CD tasks.
- Key Scripts:
tfdoc.py: Auto-generates input/output tables inREADME.mdfiles.check_boilerplate.py: Enforces license headers.check_documentation.py: Verifies README consistency.changelog.py: Generates CHANGELOG.md sections based on version diffs.
- Terraform (or OpenTofu)
- Python 3.10+
- Dependencies:
pip install -r tests/requirements.txt pip install -r tools/requirements.txt
Always format code and update documentation before committing.
# Format Terraform code (check then fix)
terraform fmt -check -recursive modules/<module-name>
terraform fmt -recursive modules/<module-name>
# Check README consistency (variables table must match variables.tf)
python3 tools/check_documentation.py modules/<module-name>
# Regenerate README variables/outputs tables when check fails
# Note: tfdoc uses special HTML comments (<!-- BEGIN TFDOC -->) in READMEs. Do not manually edit these sections.
python3 tools/tfdoc.py --replace modules/<module-name>
# YAML linting
yamllint -c .yamllint --no-warnings <yaml-files>
# License/boilerplate check
python3 tools/check_boilerplate.py --scan-files <files>Common gotcha — unsorted variables ([SV] error): check_documentation.py requires variables in variables.tf to be in strict alphabetical order. When adding a new variable, insert it at the correct alphabetical position, not at the top of the file.
Our testing philosophy is simple: test to ensure the code works and does not break due to dependency changes. Example-based testing via README.md is the preferred approach.
Tests are triggered from HCL Markdown fenced code blocks using a special # tftest directive at the end of the block.
module "my-module" {
source = "./modules/my-module"
# ...
}
# tftest modules=1 resources=2 inventory=my-inventory.yaml- Inventory files (
YAML): Used to assert specific values, resource counts, or outputs from the terraform plan against an expected dataset. - Legacy Tests: Python-based tests using
pytestandtftestare supported but example-based tests should be used whenever possible.
# Run all tests
pytest tests
# Run specific module examples
pytest -k 'modules and <module-name>:' tests/examples
# Automatically generate an inventory file from a successful plan
pytest -s 'tests/examples/test_plan.py::test_example[terraform:modules/<module-name>:Heading Name:Index]'Note: TF_PLUGIN_CACHE_DIR is recommended to speed up tests.
- Branching: Use
username/feature-name. - Commits: Atomic commits with clear messages.
- Docs: Do not manually edit the variables/outputs tables in READMEs; use
tfdoc.py.
Several modules support symbolic variable interpolation via a context variable. This allows callers to pass symbolic references like "$project_ids:myprj" instead of raw values, which get resolved at plan time.
1. Add a context variable in variables.tf at its alphabetical position. Use keys relevant to the module — standard keys are locations, networks, project_ids, subnets; module-specific keys may be added (e.g., kms_keys, artifact_registries, secrets):
variable "context" {
description = "Context-specific interpolations."
type = object({
kms_keys = optional(map(string), {})
locations = optional(map(string), {})
networks = optional(map(string), {})
project_ids = optional(map(string), {})
})
default = {}
nullable = false
}2. Build ctx and ctx_p locals in main.tf. If the module has IAM condition support, exclude condition_vars from the flattening (it is passed directly to templatestring()):
locals {
ctx = {
for k, v in var.context : k => {
for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv
} # add: if k != "condition_vars" — only when condition_vars is a key
}
ctx_p = "$"
project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id)
region = lookup(local.ctx.locations, var.region, var.region)
}3. Apply lookups in resources. Three patterns:
# Simple field
project = local.project_id
# Nullable field (null must stay null, not looked up)
encryption_key_name = (
var.encryption_key_name == null
? null
: lookup(local.ctx.kms_keys, var.encryption_key_name, var.encryption_key_name)
)
# Deeply optional nested field
private_network = (
try(var.network_config.psa_config.private_network, null) == null
? null
: lookup(local.ctx.networks, var.network_config.psa_config.private_network,
var.network_config.psa_config.private_network)
)
# Per-element list
nat_subnets = [for s in var.nat_subnets : lookup(local.ctx.subnets, s, s)]4. Long ternaries are wrapped in parentheses with condition and branches on separate lines:
ip_address = (
var.address == null
? null
: lookup(local.ctx.addresses, var.address, var.address)
)Add a context test alongside existing module tests:
tests/modules/<module_name>/tftest.yaml— declare the module path and listcontext:undertests:tests/modules/<module_name>/context.tfvars— provide all required module variables using symbolic references; include acontextblock with maps that resolve themtests/modules/<module_name>/context.yaml— assert resolved (concrete) values in the plan output
Modify one existing README example (do not add a new one) to demonstrate context usage. The resolved values should match the existing inventory YAML so no inventory changes are needed.
- Variables & Interfaces:
- Prefer object variables (e.g.,
iam = { ... }) over many individual scalar variables. - Design compact variable spaces by leveraging Terraform's
optional()function with defaults extensively. - Use maps instead of lists for multiple items to ensure stable keys in state and avoid
for_eachdynamic value issues.
- Prefer object variables (e.g.,
- Naming: Never use random strings for resource naming. Rely on an optional
prefixvariable implemented consistently across modules. - IAM: Implemented within resources (authoritative
_bindingor additive_member) via standard interfaces. - Outputs: Explicitly depend on internal resources to ensure proper ordering (
depends_on). - File Structure:
- Move away from
main.tf,variables.tf,outputs.tf. - Use descriptive filenames:
iam.tf,gcs.tf,mounts.tf.
- Move away from
- Style & Formatting:
- Line Length: Enforce a 79-character line length limit for legibility (relaxed for long resource attributes and descriptions).
- Ternary Operators & Functions: Wrap complex ternary operators in parentheses and break lines to align
?and:. Split function calls with many arguments across multiple lines. - Locals Separation: Use module-level locals for values referenced directly by resources/outputs. Use block-level "private" locals prefixed with an underscore (
_) for intermediate transformations. - Complex Transformations: Move complex data transformations in
fororfor_eachloops tolocalsto keep resource blocks clean.
- Always break down complex requests into small, iterative tasks.
- For each task, propose the necessary edits and explicitly wait for user confirmation or discussion before proceeding.
- Always use the
replacetool to both perform and cleanly display the proposed text modifications. Do not silently overwrite files or use shell commands for text edits.