Terraform Modules
File Structure
Section titled “File Structure”infra/terraform/smb-ready-foundation/├── azure.yaml # azd manifest (provider: terraform)├── backend.tf # backend "azurerm" {} partial config├── versions.tf # terraform >= 1.9 + provider pins├── providers.tf # azurerm features block + data sources├── variables.tf # Input surface with validation├── locals.tf # Derived values (region short, suffix, tags)├── main.tf # 17 module calls + root import block├── outputs.tf # Wired to module outputs├── modules/│ ├── management-group/ # MG + subscription association│ ├── policy-assignments-mg/ # 33 MG-scoped policy assignments│ ├── resource-groups/ # 6 resource groups│ ├── budget/ # Consumption budget (sub scope)│ ├── defender/ # Defender for Cloud Free│ ├── network-hub/ # Hub VNet, NSG, 4 subnets, PDZ│ ├── network-spoke/ # Spoke VNet, NSG, optional NAT│ ├── firewall/ # Azure Firewall Basic (conditional)│ ├── route-tables/ # Spoke + gateway UDRs│ ├── vpn-gateway/ # VPN Gateway VpnGw1AZ (conditional)│ ├── peering/ # Hub↔spoke peering│ ├── monitoring/ # Log Analytics Workspace│ ├── backup/ # RSV + DefaultVMPolicy│ ├── policy-backup-auto/ # Sub-scope DINE + role assignments│ ├── migrate/ # Azure Migrate (azapi_resource)│ ├── keyvault/ # Key Vault + PE + diag settings│ └── automation/ # Automation Account + LAW link├── hooks/ # pre/post-provision (bash + PowerShell)├── scripts/ # bootstrap-tf-backend + remove└── tests/ └── scenarios.tftest.hclProvider Pins
Section titled “Provider Pins”| Component | Pin | Rationale |
|---|---|---|
| terraform | >= 1.9 | Required for import block + mock_provider |
| azurerm | ~> 4.0 | 4.x LTS-equivalent; ~> allows minor upgrades |
| azapi | ~> 2.0 | Needed for Azure Migrate (no azurerm support) |
| random | ~> 3.6 | Available for future SKU suffixing |
| null | ~> 3.2 | Required by terraform_data relay pattern |
Module Inventory (17 Modules)
Section titled “Module Inventory (17 Modules)”Unlike the Bicep track, the Terraform track uses raw azurerm_* / azapi_resource instead of AVM-TF registry modules. This is intentional — see ADR-0005 for rationale.
| Module | Resources | Conditional |
|---|---|---|
management-group | MG + subscription association | No |
policy-assignments-mg | 33 MG-scoped policy assignments | No |
resource-groups | 6 resource groups | No |
budget | Consumption budget | No |
defender | 4 pricing tiers + auto-provisioning | No |
network-hub | VNet, NSG, 4 subnets, PDZ | No |
network-spoke | VNet, NSG, 4 subnets, optional NAT | NAT conditional |
firewall | Firewall + policy + 2 rule groups | Yes |
route-tables | Spoke RT + optional gateway RT | Yes |
vpn-gateway | VPN Gateway + PIP | Yes |
peering | Hub↔spoke peering | Yes |
monitoring | Log Analytics Workspace | No |
backup | RSV + DefaultVMPolicy | No |
policy-backup-auto | DINE policy + 2 role assignments | No |
migrate | Azure Migrate (azapi) | No |
keyvault | Key Vault + PDZ + PE + diag | No |
automation | Automation + LAW link + diag | No |
Validation Status
Section titled “Validation Status”| Check | Status |
|---|---|
terraform fmt -check -recursive | ✅ Pass |
terraform init -backend=false | ✅ Pass |
terraform validate | ✅ Pass (3 cosmetic v5.0 deprecation warnings) |
terraform test (6 plan-mode runs) | ✅ Pass |
npm run validate:terraform | ✅ Pass |
npm run validate:iac-security-baseline | ✅ Pass |
Key Design Differences from Bicep
Section titled “Key Design Differences from Bicep”| Aspect | Bicep | Terraform |
|---|---|---|
| Scope split | Two templates (MG + subscription) | Single root composes both |
| AVM | AVM-first (13 modules) | Raw azurerm (1:1 parity simplifies review) |
| State | ARM deployments (stateless) | Remote state in Azure Storage |
| Unique suffix | uniqueString(subscription().subscriptionId) | substr(sha1(...), 0, 13) |
| MG import | N/A | Root import block for idempotency |
Terraform Track Guide Full deployment guide for the Terraform track
ADR-0006: Single-Root Composition Why Terraform uses one root instead of two-step deploy