Governance Standards Iac Terraform - Azure/az-prototype GitHub Wiki
Standards for Terraform module layout, variable naming, and resource organization that all terraform-agent output must follow.
Domain: terraform
| Check | Description |
|---|---|
| STAN-TF-001 | Standard File Layout: Every Terraform module must use the standard file layout: main.tf (resources only), variables.tf (inputs), outputs.tf (outputs), locals.tf (computed values), providers.tf (terraform {}, required_providers, provider config, and backend). The terraform {} block — including required_providers — MUST appear in exactly ONE file (providers.tf). Do NOT create a separate versions.tf. main.tf must NOT contain terraform {} or provider {} blocks. Additional files are allowed for complex modules but must be logically named (e.g., networking.tf, iam.tf). |
| STAN-TF-002 | Variable Conventions: All variables must have a description and a type constraint. Use snake_case for variable names. Provide defaults for optional values. Use validation blocks for constrained inputs. |
| STAN-TF-003 | Resource Naming via Variables or Locals: Resource names should come from variables with defaults set to the deployment plan computed_name, or from locals that derive names from project variables. Variable defaults from the deployment plan (e.g., "pi-rg-worker-dev-wus3") are acceptable — the naming strategy already computed them. Do NOT force computed_name values into locals when they are already correct as variable defaults. |
| STAN-TF-004 | One Resource Type Per File for Complex Modules: When a module manages more than 5 resources, split logically related resources into separate files (e.g., networking.tf for subnets and NSGs, iam.tf for role assignments). |
| STAN-TF-005 | Use Data Sources for Existing Resources: Reference existing resources (resource groups, VNets, identities) via data sources, not by hardcoding IDs. Pass resource IDs as variables only when the resource is managed outside the module. |
| STAN-TF-006 | Cross-Stage Dependencies via Remote State: Multi-stage deployments MUST use terraform_remote_state data sources to read outputs from prior stages. NEVER hardcode resource names, IDs, or keys that belong to another stage. Each stage reads what it needs from prior stage state files and passes values via data.terraform_remote_state..outputs.. |
| STAN-TF-007 | Consistent Backend Configuration: Backend configuration must be consistent across all stages. For POC deployments, use local state (no backend block or backend "local" with a path). For production, use backend "azurerm" with ALL required fields populated with literal values (resource_group_name, storage_account_name, container_name, key). NEVER use variable references (var.*) in backend config — Terraform does not support them. NEVER leave required backend fields empty. If using remote backend, all stages must reference the same storage account and container, differing only in key. |
| STAN-TF-008 | Complete Stage Outputs: Every stage's outputs.tf MUST export all resource names, IDs, and endpoints that ANY downstream stage or application needs. At minimum: resource group name(s), managed identity client_id/principal_id, service endpoints, workspace IDs. NEVER output sensitive values (keys, connection strings) — if local auth is disabled, omit keys entirely. |
| STAN-TF-009 | Complete and Robust deploy.sh: Every stage MUST include a deploy.sh that is syntactically complete and runnable. It must use set -euo pipefail, include Azure login verification, run terraform init/plan/apply, export outputs to JSON, and include error handling via trap. NEVER truncate the script or leave strings unclosed. |
| STAN-TF-010 | Companion Resources for Disabled Auth: When disabling local/key-based authentication on any service (local_authentication_disabled = true, shared_access_key_enabled = false), the SAME stage MUST also create: (1) a user-assigned managed identity, (2) RBAC role assignments granting that identity access, (3) outputs for the identity's client_id and principal_id. Without these companion resources, applications cannot authenticate and the deployment is broken. |
Standard File Layout: Every Terraform module must use the standard file layout: main.tf (resources only), variables.tf (inputs), outputs.tf (outputs), locals.tf (computed values), providers.tf (terraform {}, required_providers, provider config, and backend). The terraform {} block — including required_providers — MUST appear in exactly ONE file (providers.tf). Do NOT create a separate versions.tf. main.tf must NOT contain terraform {} or provider {} blocks. Additional files are allowed for complex modules but must be logically named (e.g., networking.tf, iam.tf).
Rationale: Consistent file organization makes code reviewable and prevents merge conflicts across stages.
Agents: terraform-agent
- main.tf — resource definitions only (no terraform {} or provider {} blocks)
- variables.tf — all input variable declarations with descriptions and types
- outputs.tf — all output value declarations
- locals.tf — computed local values and naming conventions
- providers.tf — terraform {}, required_providers, backend, and provider configuration (ONE file, never duplicated)
Variable Conventions: All variables must have a description and a type constraint. Use snake_case for variable names. Provide defaults for optional values. Use validation blocks for constrained inputs.
Rationale: Parameterized configurations allow reuse across environments and prevent hardcoded values.
Agents: terraform-agent
- variable "location" { type = string; description = "Azure region" }
- Use validation blocks for SKU names, IP ranges, etc.
Resource Naming via Variables or Locals: Resource names should come from variables with defaults set to the deployment plan computed_name, or from locals that derive names from project variables. Variable defaults from the deployment plan (e.g., "pi-rg-worker-dev-wus3") are acceptable — the naming strategy already computed them. Do NOT force computed_name values into locals when they are already correct as variable defaults.
Rationale: The CLI naming module computes resource names per the project naming strategy. Variable defaults carry these computed names and are overridable at deploy time.
Agents: terraform-agent
- variable "resource_group_name" { default = "pi-rg-worker-dev-wus3" }
- locals { identity_name = "${var.zone_prefix}-id-worker-${var.environment}-${var.region_short}" }
- resource "azapi_resource" "resource_group" { name = var.resource_group_name }
One Resource Type Per File for Complex Modules: When a module manages more than 5 resources, split logically related resources into separate files (e.g., networking.tf for subnets and NSGs, iam.tf for role assignments).
Rationale: Modular Bicep templates promote reuse and simplify testing of individual components.
Agents: terraform-agent
- networking.tf — VNet, subnets, NSGs, route tables
- iam.tf — role assignments, managed identities
- monitoring.tf — diagnostic settings, alerts
Use Data Sources for Existing Resources: Reference existing resources (resource groups, VNets, identities) via data sources, not by hardcoding IDs. Pass resource IDs as variables only when the resource is managed outside the module.
Rationale: Parameterized configurations allow reuse across environments and prevent hardcoded values.
Agents: terraform-agent
- data "azapi_client_config" "current" {} for tenant/subscription IDs
- data "azapi_resource" "existing_rg" { type = "Microsoft.Resources/resourceGroups@2024-03-01"; name = var.rg_name }
Cross-Stage Dependencies via Remote State: Multi-stage deployments MUST use terraform_remote_state data sources to read outputs from prior stages. NEVER hardcode resource names, IDs, or keys that belong to another stage. Each stage reads what it needs from prior stage state files and passes values via data.terraform_remote_state..outputs..
Rationale: Proper state management prevents cross-stage conflicts and ensures reliable deployments.
Agents: terraform-agent, cloud-architect
- data "terraform_remote_state" "stage1" { backend = "azurerm"; config = { key = "stage1.tfstate" } }
- data "azapi_resource" "resource_group" { type = "Microsoft.Resources/resourceGroups@2024-03-01"; name = data.terraform_remote_state.stage1.outputs.resource_group_name }
Consistent Backend Configuration: Backend configuration must be consistent across all stages. For POC deployments, use local state (no backend block or backend "local" with a path). For production, use backend "azurerm" with ALL required fields populated with literal values (resource_group_name, storage_account_name, container_name, key). NEVER use variable references (var.*) in backend config — Terraform does not support them. NEVER leave required backend fields empty. If using remote backend, all stages must reference the same storage account and container, differing only in key.
Rationale: Proper state management prevents cross-stage conflicts and ensures reliable deployments.
Agents: terraform-agent
- POC: backend "local" {} — default terraform.tfstate in stage directory
- Cross-stage ref: ../stage-1-managed-identity/terraform.tfstate
- Production: backend "azurerm" { resource_group_name = "terraform-state-rg"; storage_account_name = "tfstate12345"; container_name = "tfstate"; key = "stage1.tfstate" }
Complete Stage Outputs: Every stage's outputs.tf MUST export all resource names, IDs, and endpoints that ANY downstream stage or application needs. At minimum: resource group name(s), managed identity client_id/principal_id, service endpoints, workspace IDs. NEVER output sensitive values (keys, connection strings) — if local auth is disabled, omit keys entirely.
Rationale: Complete outputs enable downstream stages to reference resources without hardcoding.
Agents: terraform-agent
- output "resource_group_name" { value = azapi_resource.resource_group.name }
- output "managed_identity_client_id" { value = azapi_resource.managed_identity.output.properties.clientId }
Complete and Robust deploy.sh: Every stage MUST include a deploy.sh that is syntactically complete and runnable. It must use set -euo pipefail, include Azure login verification, run terraform init/plan/apply, export outputs to JSON, and include error handling via trap. NEVER truncate the script or leave strings unclosed.
Rationale: Complete outputs enable downstream stages to reference resources without hardcoding.
Agents: terraform-agent
- #!/bin/bash\nset -euo pipefail\ntrap 'echo Deploy failed' ERR
- terraform output -json > stage-1-outputs.json
Companion Resources for Disabled Auth: When disabling local/key-based authentication on any service (local_authentication_disabled = true, shared_access_key_enabled = false), the SAME stage MUST also create: (1) a user-assigned managed identity, (2) RBAC role assignments granting that identity access, (3) outputs for the identity's client_id and principal_id. Without these companion resources, applications cannot authenticate and the deployment is broken.
Rationale: Complete outputs enable downstream stages to reference resources without hardcoding.
Agents: terraform-agent, cloud-architect
- resource "azapi_resource" "managed_identity" { type = "Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31"; ... }
- resource "azapi_resource" "cosmosdb_role_assignment" { type = "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-05-15"; body = { properties = { principalId = azapi_resource.managed_identity.output.properties.principalId } } }