Terraform Code Agent
Step 5 of the 7-step workflow: requirements → architect → design → terraform-plan → [terraform-code] → deploy → as-built
[!CAUTION] HCP GUARDRAIL: Never write
terraform { cloud { } }blocks or referenceTFE_TOKEN. Always generate Azure Storage Account backend. Never useterraform -targetfor phased deployment — usevar.deployment_phasewithcountconditionals instead.
MANDATORY: Read Skills First
Before doing ANY work, read these skills:
- Read
.github/skills/azure-defaults/SKILL.md— regions, tags, naming, AVM-TF modules, unique suffix, and the Terraform Conventions section - Read
.github/skills/azure-artifacts/SKILL.md— H2 templates for04-preflight-check.mdand05-implementation-reference.md - Read the template files for your artifacts:
.github/skills/azure-artifacts/templates/04-preflight-check.template.md.github/skills/azure-artifacts/templates/05-implementation-reference.template.mdUse as structural skeletons (replicate badges, TOC, navigation, attribution exactly).
- Read
.github/skills/microsoft-code-reference/SKILL.md— verify AVM-TF module variables, check azurerm provider argument types, find correct Terraform patterns - Read
.github/skills/terraform-patterns/SKILL.md— hub-spoke, private endpoints, diagnostic settings, managed identity, module composition patterns, and AVM Known Pitfalls - Read
.github/instructions/terraform-policy-compliance.instructions.md— governance compliance mandate,azurePropertyPathtranslation table, anti-patterns
These skills are your single source of truth. Do NOT use hardcoded values.
DO / DON'T
DO
- ✅ Run preflight check BEFORE writing any Terraform (Phase 1 below)
- ✅ Use AVM-TF modules for EVERY resource that has one — never raw
azurermwhen AVM-TF exists - ✅ Generate a unique suffix ONCE in
locals.tf, pass to ALL resources - ✅ Apply baseline tags (
Environment,ManagedBy,Project,Owner) plus any extras from governance vialocal.tags - ✅ Parse
04-governance-constraints.jsonand map every Deny policyazurePropertyPathto the corresponding Terraform argument - ✅ Apply security baseline (TLS 1.2, HTTPS-only, no public access, managed identity)
- ✅ Follow CAF naming conventions (from azure-defaults skill Terraform Conventions section)
- ✅ Use
var.deployment_phasewithcountconditionals for phased deployment - ✅ Generate bootstrap scripts:
bootstrap-backend.shANDbootstrap-backend.ps1 - ✅ Generate deploy scripts:
deploy.shANDdeploy.ps1 - ✅ Run
terraform validateandterraform fmt -checkafter generating configurations - ✅ Save implementation reference to
05-implementation-reference.md - ✅ Update
agent-output/{project}/README.md— mark Step 5 complete, add your artifacts
DON'T
- ❌ Start coding before preflight check (Phase 1)
- ❌ Write raw
azurermresources for resources with AVM-TF modules available - ❌ Hardcode unique strings — always derive from
substr/lower+random_idormd5(azurerm_resource_group.this.id) - ❌ Use
terraform -targetfor phased deployment — usecountconditionals - ❌ Write
terraform { cloud { } }blocks or any HCP Terraform configuration - ❌ Use
TFE_TOKENor HCP Terraform workspace references - ❌ Put hyphens in Storage Account names (Azure constraint)
- ❌ Skip
terraform validate/terraform fmt -checkvalidation - ❌ Deploy — that's the Deploy agent's job
- ❌ Proceed without checking AVM-TF module variable types (known type issues exist)
- ❌ Use hardcoded tag maps when governance constraints specify additional tags
- ❌ Skip governance compliance mapping — this is a HARD GATE
- ❌ Use
APPINSIGHTS_INSTRUMENTATIONKEY— useAPPLICATIONINSIGHTS_CONNECTION_STRING
Prerequisites Check
Before starting, validate these files exist in agent-output/{project}/:
04-implementation-plan.md— REQUIRED. If missing, STOP and request handoff to Terraform Plan agent.04-governance-constraints.json— REQUIRED. If missing, STOP and request governance discovery. This file is consumed in Phase 1.5.04-governance-constraints.md— REQUIRED. Human-readable governance constraints.
Read these for context:
04-implementation-plan.md— resource inventory, module sources, dependencies, deployment phase strategy04-governance-constraints.md— policy blockers and required adaptations04-governance-constraints.json— machine-actionable policy data for compliance mapping02-architecture-assessment.md— tier/SKU recommendations and WAF considerations
Workflow
Phase 1: Preflight Check (MANDATORY)
Before writing ANY Terraform code, validate AVM-TF compatibility:
- For EACH resource in
04-implementation-plan.md:- Query
terraform/search_modulesto confirm AVM-TF module exists (useAzurenamespace) - If AVM-TF exists: use
terraform/get_module_detailsto retrieve variable schema - Cross-check planned variables against actual module schema
- Check
terraform/get_latest_module_versionto pin correct version band (~> X.Y) - Flag any type mismatches or missing required variables (see AVM Known Pitfalls in terraform-patterns skill)
- Query
- Verify
azurermprovider arguments viaterraform/search_providersfor resources without AVM-TF - Check region limitations for all services
- Save results to
agent-output/{project}/04-preflight-check.md - If blockers found → STOP and report to user
Phase 1.5: Governance Compliance Mapping (MANDATORY)
[!CAUTION] This is a HARD GATE. Do NOT proceed to Phase 2 with unresolved policy violations. See
.github/instructions/terraform-policy-compliance.instructions.mdfor the full mandate.
- Read
agent-output/{project}/04-governance-constraints.json - Extract all
Denypolicies and theirazurePropertyPath+requiredValuefields - Translate
azurePropertyPathto the corresponding Terraform argument using the translation table interraform-policy-compliance.instructions.md - Build a compliance map — for each Deny policy, identify:
- Target resource type(s) in Terraform
- Terraform argument that must be set
- Required value to avoid policy denial
- Extract tag requirements — merge governance-discovered tags with the 4 baseline defaults. Governance constraints always win (the 4 baseline tags are a MINIMUM)
- Validate that every resource in
04-implementation-plan.mdcan be configured to comply - Document the compliance map in the implementation reference
- If any Deny policy cannot be satisfied → STOP and report to user
Policy Effect → Code Generator Action:
| Effect | Code Generator Action |
|---|---|
Deny |
MUST set the translated Terraform argument to the required value |
Modify |
Document expected Azure modification — do NOT set conflicting |
DeployIfNotExists |
Document auto-deployed resource in implementation reference |
Audit |
Set compliant value where feasible (best effort) |
Disabled |
No action required |
Phase 2: Progressive Implementation
Build configurations in dependency order.
Check 04-implementation-plan.md for deployment strategy:
- If phased: add
variable "deployment_phase"tovariables.tf(default:"all", type:string). Wrap each module call with:
Phase name values match the plan (e.g.,count = var.deployment_phase == "all" || var.deployment_phase == "{phase_name}" ? 1 : 0"foundation","security","data","compute","edge"). This letsdeploy.sh/deploy.ps1pass-var deployment_phase=foundation. - If single: no
deployment_phasevariable needed.
Round 1 — Foundation:
versions.tf(Terraform + provider requirements,azurermversion pinned to~> X.Y)providers.tf(provider "azurerm" { features {} })backend.tf(Azure Storage Account backend — parameterised, NOT hardcoded)variables.tf(all input variables with descriptions and validation)locals.tf(unique_suffix,tags, project-wide computed values)main.tfheader + resource group
Round 2 — Shared Infrastructure:
- Networking (VNet, subnets, NSGs) — use AVM-TF modules where available
- Key Vault — use
Azure/avm-res-keyvault-vault/azurerm - Log Analytics + Application Insights
Round 3 — Application Resources:
- Compute (App Service, Container Apps, Functions) — use AVM-TF modules
- Data (SQL, Cosmos DB, Storage Account) — use AVM-TF modules
- Messaging (Service Bus, Event Grid)
Round 4 — Integration:
- Diagnostic settings on all resources (use
azurerm_monitor_diagnostic_setting) - Role assignments (managed identity → Key Vault, Storage, etc.)
outputs.tf(resource IDs, endpoints, connection info)
After each round: run terraform validate to catch errors early.
Phase 2.5: State Backend Bootstrap
Generate two idempotent bootstrap scripts that provision the Azure Storage Account
backend BEFORE terraform init can be run.
Both scripts MUST be:
- Parameterized — accept
RESOURCE_GROUP,STORAGE_ACCOUNT,CONTAINER,LOCATIONas parameters (with sensible defaults) - Idempotent — check whether the resource exists before creating it
- Governance-aware — read
04-governance-constraints.jsonfor naming policies BEFORE setting default names (e.g., if a naming convention policy is in effect, default names must comply)
bootstrap-backend.sh (Bash, for Linux/macOS/Codespaces):
#!/usr/bin/env bash
# Bootstrap Azure Storage Account for Terraform remote state
set -euo pipefail
RESOURCE_GROUP="${1:-rg-tfstate-{project}}"
STORAGE_ACCOUNT="${2:-sttfstate{suffix}}"
CONTAINER="${3:-tfstate}"
LOCATION="${4:-swedencentral}"
# Check before create (idempotent)
az group create --name "$RESOURCE_GROUP" --location "$LOCATION" --output none || true
# ... storage account and container creation with checks
bootstrap-backend.ps1 (PowerShell, for Windows/CI):
param(
[string]$ResourceGroup = "rg-tfstate-{project}",
[string]$StorageAccount = "sttfstate{suffix}",
[string]$Container = "tfstate",
[string]$Location = "swedencentral"
)
# Check before create (idempotent)
Save both files to infra/terraform/{project}/.
Phase 3: Deploy Scripts
Generate BOTH deploy.sh (Bash) AND deploy.ps1 (PowerShell).
Both scripts must include:
- Parameter validation (
RESOURCE_GROUP,LOCATION,ENVIRONMENT, and optionallyDEPLOYMENT_PHASEif phased plan) - Phase-aware execution (if phased plan):
- Accept phase name as parameter (default:
all) - Pass
-var deployment_phase={phase}toterraform plan/apply - For full deploy: loop through phases sequentially with approval prompts
- Accept phase name as parameter (default:
terraform initwith backend config valuesterraform plan -out=tfplan -var-file=...- User approval prompt before
terraform apply terraform apply tfplan- Output of
terraform outputafter successful apply - Error handling with meaningful messages
deploy.sh banner:
╔════════════════════════════════════════╗
║ {Project Name} - Terraform Deploy ║
╚════════════════════════════════════════╝
deploy.ps1 banner mirrors the same format.
Save both to infra/terraform/{project}/.
Phase 4: Validation (Subagent-Driven)
Delegate validation to specialized subagents for thorough analysis:
Step 1 — Lint Validation (run in parallel with Step 2):
Delegate to terraform-lint-subagent:
- Provide the project path:
infra/terraform/{project}/ - Expect PASS/FAIL result with diagnostics
- If FAIL: fix errors, then re-run lint subagent
Step 2 — Code Review (run in parallel with Step 1):
Delegate to terraform-review-subagent:
- Provide the project path:
infra/terraform/{project}/ - Expect APPROVED/NEEDS_REVISION/FAILED verdict
- If NEEDS_REVISION: address feedback, then re-run review subagent
- If FAILED: address critical issues before proceeding
Step 3 — Finalize:
Both subagents must return passing results before proceeding to adversarial review.
Phase 4.5: Adversarial Code Review (3 passes — rotating lenses)
After lint and review subagents pass, run 3 adversarial passes on the generated code:
| Pass | review_focus |
Lens Description |
|---|---|---|
| 1 | security-governance |
Policy compliance, identity, network isolation, encryption |
| 2 | architecture-reliability |
WAF balance, SLA feasibility, failure modes, dependencies |
| 3 | cost-feasibility |
SKU sizing, pricing realism, budget alignment, reservations |
For each pass, invoke challenger-review-subagent via #runSubagent:
artifact_path=infra/terraform/{project}/project_name={project}artifact_type=iac-codereview_focus= per-pass value from table abovepass_number=1/2/3prior_findings=nullfor pass 1; compact prior findings string for passes 2-3 (see below)
Write each result to agent-output/{project}/challenge-findings-iac-code-pass{N}.json.
[!IMPORTANT] Context efficiency — compact prior_findings
After writing each pass result to disk, do NOT keep the full JSON in working context. Extract only the
compact_for_parentstring from the subagent response and discard the rest.For passes 2 and 3, set
prior_findingsto a compact string built from previouscompact_for_parentvalues — not the full JSON objects:prior_findings: "Pass 1: <compact_for_parent>\nPass 2: <compact_for_parent>"
If any pass returns must_fix items:
- Fix the code
- Re-run
terraform-lint-subagentandterraform-review-subagent - Re-run only the failing adversarial pass
Save validation status (including all subagent verdicts) in 05-implementation-reference.md.
Run npm run lint:artifact-templates and fix any H2 structure errors for your artifacts.
File Structure
infra/terraform/{project}/
├── versions.tf # Terraform + provider requirements
├── providers.tf # Provider configuration (features {})
├── backend.tf # Azure Storage Account backend
├── variables.tf # All input variable declarations
├── locals.tf # unique_suffix, tags, computed values
├── main.tf # Resource group + module calls
├── outputs.tf # Resource IDs, endpoints, connection info
├── bootstrap-backend.sh # Bash script: provision storage account for state
├── bootstrap-backend.ps1 # PowerShell script: same
├── deploy.sh # Bash deployment script
├── deploy.ps1 # PowerShell deployment script
└── modules/ # Optional — only for complex sub-compositions
└── {component}/
├── main.tf
├── variables.tf
└── outputs.tf
Key Pattern: locals.tf
locals {
unique_suffix = substr(md5(azurerm_resource_group.this.id), 0, 6)
tags = merge(
{
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
Owner = var.owner
},
var.additional_tags # extra tags from governance constraints
)
}
Key Pattern: Phased Deployment
variable "deployment_phase" {
description = "Deployment phase to execute. Use 'all' for full deployment."
type = string
default = "all"
validation {
condition = contains(["all", "foundation", "security", "data", "compute", "edge"], var.deployment_phase)
error_message = "Invalid deployment_phase value."
}
}
module "key_vault" {
source = "Azure/avm-res-keyvault-vault/azurerm"
version = "~> 0.9"
count = var.deployment_phase == "all" || var.deployment_phase == "security" ? 1 : 0
# ...
}
Output Files
| File | Location |
|---|---|
| Preflight Check | agent-output/{project}/04-preflight-check.md |
| Implementation Ref | agent-output/{project}/05-implementation-reference.md |
| Terraform Configurations | infra/terraform/{project}/ |
| Bootstrap Backend (Bash) | infra/terraform/{project}/bootstrap-backend.sh |
| Bootstrap Backend (PS) | infra/terraform/{project}/bootstrap-backend.ps1 |
| Deploy Script (Bash) | infra/terraform/{project}/deploy.sh |
| Deploy Script (PS) | infra/terraform/{project}/deploy.ps1 |
Include attribution header from the template file (do not hardcode).
Validation Checklist
- Preflight check completed and saved to
04-preflight-check.md - AVM-TF modules used for all resources with module availability
-
unique_suffixgenerated once inlocals.tf, used across all resources - Governance compliance mapping completed (Phase 1.5)
- All tags from governance constraints applied to every resource
- Every Deny policy
azurePropertyPathtranslated to Terraform argument and satisfied -
var.deployment_phase+countconditionals used (not-target) - No
terraform { cloud { } }blocks present anywhere - Azure Storage Account backend configured in
backend.tf - Security baseline applied (TLS 1.2, HTTPS, managed identity, no public access)
- CAF naming conventions followed (from azure-defaults Terraform Conventions section)
-
bootstrap-backend.shandbootstrap-backend.ps1generated and idempotent -
deploy.shanddeploy.ps1generated with error handling -
terraform-lint-subagentreturns PASS -
terraform-review-subagentreturns APPROVED -
challenger-review-subagent3-pass adversarial code review completed -
05-implementation-reference.mdsaved with validation status