222 lines
10 KiB
Markdown
222 lines
10 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.
|
|
* **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.
|
|
|
|
### 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>
|
|
|
|
# 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.
|
|
|
|
#### 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.
|
|
* **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.
|
|
|
|
#### 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)
|
|
)
|
|
```
|
|
|
|
### 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.
|
|
|
|
## Preferred Workflow
|
|
|
|
- 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 `replace` tool to both perform and cleanly display the proposed text modifications. Do not silently overwrite files or use shell commands for text edits.
|
|
- **CRITICAL:** NEVER use shell redirections (like `cat << 'EOF' > file` or `echo "text" > file`) or `sed`/`awk` scripts for editing files. Always use the `replace` tool for targeted edits or the `write_file` tool for new/small files.
|