Files
hunfabric/GEMINI.md
Julio Castillo 2eaa0d5e27 Add support for dynamic tags (#3897)
* Allow creation of dynamic tags

* Extend project factory and related modules to support dynamic values

* Extend folder and organization modules

* project and organization readme

* Simplify dynamic tag support and remove unnecessary restrictions

  • Schemas & Validations: Removed the restriction that forbade combining IAM fields with  allowed_values_regex  on tags. Updated validations in  project  and  organization  modules, and
  simplified all relevant JSON schemas.
  • Module Tag Bindings: Simplified the  tag_value  assignment in  folder ,  project ,  gcs ,  bigquery-dataset , and  kms  modules by removing the defensive  can(regex(...))  check and
  calling  templatestring  directly.
  • Outputs: Removed the  tags_dynamic  output from  project  and  organization  modules, as the same information is now available in  tag_keys .
  • Project Factory: Updated  tag_vars_projects  in  projects.tf  to use the native  namespaced_name  attribute and filtered manually for dynamic tags.

* fix(organization, project): fix linting and tests for dynamic tag support

- Align allowed_values_regex and description extraction in _tags_merged
  locals to use lookup() for consistency with other fields.
- Fix spacing in project context variable (alphabetical ordering).
- Update organization tags test to include the new cost_center tag key
  with allowed_values_regex.
- Update project tags test to include the new cost_center tag key and
  reflect the resolved allowed_values_regex on environment.

* refactor(gcs): refine tag bindings and fix context test

- Add _tag_bindings local to pre-resolve context references, enabling
  templatestring to receive a direct map reference (required by Terraform).
- Use var.context.tag_vars instead of the non-existent local.ctx.tag_vars.
- Fix HCL syntax in context.tfvars (escaped inner quotes).
- Update context test inventory to reflect 3 tag bindings including a
  dynamic value resolved via templatestring.

* refactor: align modules with tag binding context pattern

- Add _tag_bindings local + templatestring dance to cloud-run-v2,
  compute-vm, folder, kms modules (bigquery-dataset already had it)
- Exclude tag_vars from local.ctx in cloud-run-v2, compute-vm, folder,
  kms, project modules (bigquery-dataset already had it)
- Add tag_vars to context variable in cloud-run-v2, compute-vm modules
  (others already had it)
- Update all context tests with dynamic tag binding values using
  var.context.tag_vars

* docs: add module-level tftest.yaml test instructions to GEMINI.md

* docs: regenerate READMEs after tag-regex alignment

- Regenerate variable tables in 7 module READMEs to reflect
  line number shifts from prior tag-regex changes
- Add tag_vars exclusion to gcs ctx local
- Fix whitespace alignment in iam-service-account and
  project-factory tag_vars blocks
- Update tftest resource counts for organization and project
- Remove tags_dynamic from organization/project output tables

* fix(project-factory): update test inventory for tag_bindings module split

- Move tag binding address from folder-2 to folder-2-iam in test
  inventory (tag_bindings moved from creation to IAM modules)
- Update module instance count from 34 to 35
- Regenerate README tables after terraform fmt line shifts
- Apply terraform fmt to variables.tf

* refactor(project-factory): remove unnecessary depends_on from folder-iam modules

Folder IAM modules depend on their own folder creation modules, not
on module.projects. The explicit depends_on was leftover from an
earlier design.

* FAST stages

* Address review comments.

- FAST Stages:
  - Added tag_keys to output-files.tf in 0-org-setup to pass org tags via tfvars.
  - Sorted tag_keys and tag_values in output-files.tf.
  - Updated project-factory, networking, and security stages to use tag_keys.
  - Filtered tag_keys for dynamic tags only.
- Modules:
  - Excluded tag_vars from local.ctx in iam-service-account and organization.
  - Simplified tag_value in iam-service-account.
- Tests:
  - Updated test inventories for 0-org-setup and project-factory.

* Fix tf format

* Fix tfdoc

* docs: add ADR for templatestring vars convention and update status of base path ADR

* More tfdoc

* Update schemas

* Use endswith in context loop

* Address review

* Update FAST readmes

* Update last modules

* Terraform fmt

* Revert alloydb

* Fix whitespace

---------

Co-authored-by: Ludovico Magnocavallo <ludo@qix.it>
2026-04-24 20:45:45 +00:00

275 lines
14 KiB
Markdown

# Cloud Foundation Fabric (CFF)
## Project Overview
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:
1. **Modules:** A library of composable, production-ready Terraform modules (e.g., `project`, `net-vpc`, `gke-cluster`).
2. **FAST (Fabric FAST):** An opinionated, stage-based landing zone toolkit for bootstrapping enterprise-grade GCP organizations.
## Key Components
### 1. Modules (`/modules`)
* **Philosophy:** Lean, composable, and close to the underlying provider resources. Modules are designed to be containers for all aspects related to usage of a resource type (e.g., folder, project, vpc, etc.). This includes IAM, sub-resources (e.g. subnets and routes for a network), and org policies where applicable.
* **Boundary:** Unrelated resources (like a dataset for a project) should never be part of the same module, except in the two "aggregation modules" (`project-factory` and `net-vpc-factory`). Never break this boundary as a first approach.
* **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 `prefix` variables.
* **Factories:** Many modules implement a data-driven "factory" pattern (often via a `factories_config` variable) to manage resources at scale using YAML data files. See `FACTORIES.md` for 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.
* **Usage:** Modules are designed to be forked/owned or referenced via Git tags (e.g., `source = "github.com/...//modules/project?ref=v30.0.0"`).
### 2. FAST (`/fast`)
* **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. Factories are generally implemented in the underlying modules, not in FAST stages, *unless* the stage needs to iterate over standard modules or resources (e.g., `dns` zones, `net-firewall-policy`, `ncc_hubs`).
### 3. Tools (`/tools`)
* Python-based utility scripts for documentation, linting, and CI/CD tasks.
* **Key Scripts:**
* `tfdoc.py`: Auto-generates input/output tables in `README.md` files.
* `check_boilerplate.py`: Enforces license headers.
* `check_documentation.py`: Verifies README consistency.
* `changelog.py`: Generates CHANGELOG.md sections based on version diffs.
## Development Workflow
### Prerequisites
* **Terraform** (or OpenTofu)
* **Python 3.10+**
* **Dependencies:**
```bash
pip install -r tests/requirements.txt
pip install -r tools/requirements.txt
```
### Common Tasks
#### 1. Formatting & Linting
Always format code and update documentation before committing.
```bash
# Format Terraform code (check then fix)
terraform fmt -check -recursive modules/<module-name>
terraform fmt -recursive modules/<module-name>
# Format Python code
# ALWAYS run yapf with the repository's .style.yapf configuration after editing any Python file.
# You can use the local virtual environment or run it directly:
~/venv/bin/yapf -i <python-files>
# 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.
# You can configure tfdoc via HTML comments in the README (e.g., <!-- TFDOC OPTS files:1 show_extra:1 -->).
# To add a file description to the generated table, use a comment in the .tf file: # tfdoc:file:description My description.
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>
# Schema changes
# A schema change should be reflected in all the other places that use the same schema.
# These are documented in and can be checked via tools/duplicate-diff.py.
```
**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.
#### 2. Testing
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.
```hcl
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. **DO NOT hand-code inventory files from scratch.** Extract only the necessary bits relevant to the test scenario from the generated output.
* **External Files:** If a README test requires external files (e.g., for factories), mock them using the `# tftest-file id=myid path=path/to/file.yaml` directive in a separate YAML block, and add `files=myid` to the `tftest` directive.
* **FAST Stages & `tftest.yaml`:** FAST stages often lack README examples. For these, use `tftest`-based tests by creating `tfvars` and `yaml` inventory pairs in `tests/fast/...` and tying them together with a `tftest.yaml` file.
* **Legacy Tests:** Python-based tests using `pytest` and `tftest` are supported but example-based tests should be used whenever possible.
```bash
# 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.
#### 4. Module-level `tftest.yaml` Tests
Modules with their own `tests/modules/<module_name>/tftest.yaml` define test scenarios (e.g., context resolution, IAM variants) using `tfvars` + YAML inventory pairs. Run them individually:
```bash
# Run a specific test from a module's tftest.yaml
pytest 'tests/modules/<module_name>/tftest.yaml::<test_name>' --tb=short -s
```
For example:
```bash
pytest 'tests/modules/organization/tftest.yaml::context' --tb=short -s
pytest 'tests/modules/project/tftest.yaml::context' --tb=short -s
```
#### 3. Contributing
* **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`.
## Adding Context Support to a Module
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.
### Pattern
**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`):
```hcl
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()`):
```hcl
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:
```hcl
# 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:
```hcl
ip_address = (
var.address == null
? null
: lookup(local.ctx.addresses, var.address, var.address)
)
```
**5. YAML Interpolation:** In factory YAML files, use the `$` prefix convention to reference the lookup map keys (e.g., `parent: $folder_ids:teams/team-a`).
### Tests
Add a `context` test alongside existing module tests:
- `tests/modules/<module_name>/tftest.yaml` — declare the module path and list `context:` under `tests:`
- `tests/modules/<module_name>/context.tfvars` — provide all required module variables using symbolic references; include a `context` block with maps that resolve them
- `tests/modules/<module_name>/context.yaml` — assert resolved (concrete) values in the plan output
### README example
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.
## Architecture & Conventions
* **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_each` dynamic value issues.
* **Naming:** Never use random strings for resource naming. Rely on an optional `prefix` variable implemented consistently across modules.
* **IAM:** Implemented within resources (authoritative `_binding` or 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`.
* **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 `for` or `for_each` loops to `locals` to keep resource blocks clean.
## Debugging Terraform Context & Locals
When troubleshooting how variables, context, or locals are being evaluated during a `plan` (especially within factories or FAST stages), do not rely solely on `pytest` failure outputs or `grep`.
**ALWAYS** use a fast-failing `terraform_data` precondition to dump the exact runtime state of the data structure. Inject this snippet temporarily into the module being debugged:
```hcl
resource "terraform_data" "debug_dump" {
lifecycle {
precondition {
# The condition is intentionally designed to fail to trigger the error_message
condition = local.target_variable == null
error_message = yamlencode(local.target_variable)
}
}
}
```
Run the specific `pytest` plan test. The test will fail, and the captured output will contain the fully evaluated YAML representation of your target variable, making context resolution issues immediately obvious.
## File Modification Rules
- **CRITICAL:** NEVER use shell redirection (`cat << EOF`, `echo "..." >`, `>>`, `tee`) to create, overwrite, or append to files.
- For creating files, ALWAYS use the native `write_file` tool.
- For targeted edits or appending to a single file, ALWAYS use the native `replace` tool. (To append, match the last few lines of the file and replace them with the same lines plus your new content).
- **EXCEPTION (Pattern/Bulk Edits):** You MAY use shell commands (like `sed -i`, `perl -pi`, or `find ... xargs sed`) ONLY for regex-based or pattern-based replacements, particularly across multiple files, where the exact-match `replace` tool is not feasible.
- **Ambiguity & Paths:** When encountering unfamiliar or unexpected repository structures, paths, or tool executions, always pause and offer the user the choice to either explain or authorize further independent investigation, rather than making assumptions or guessing paths.
To run specific FAST stage tests, use the syntax `pytest tests/fast/stages/s<stage_num>_<stage_name>/tftest.yaml::<test_name>`. For example: `pytest tests/fast/stages/s0_org_setup/tftest.yaml::starter-gcd`.