-
Notifications
You must be signed in to change notification settings - Fork 95
Terraform Plan Review Guide
Chris Panagiotidis edited this page Jun 13, 2026
·
1 revision
This page explains how to review a Terraform plan for the Azure Landing Zone Lab before apply. The goal is to catch security, reliability, cost, and drift problems before Azure resources change.
Use this page with:
Collect these before approving an apply:
| Input | Source |
|---|---|
| Environment profile | environments/<profile>.tfvars |
| Terraform plan summary | GitHub Actions plan job |
| Plan artifact | GitHub Actions artifact |
| Security scan output | tfsec, Checkov, Gitleaks |
| OPA output | policy-check job |
| Cost estimate | Infracost job or manual Azure calculator review |
| Changed files | Pull request diff |
For syntax and basic review without remote backend:
terraform init -backend=false
terraform plan -var-file=environments/cheap-lab.tfvars -out=tfplan
terraform show -no-color tfplan
terraform show -json tfplan > tfplan.jsonDo not commit tfplan, tfplan.json, .terraform, or backend config files.
| Question | Expected answer |
|---|---|
| Is the selected profile correct? | Yes, usually cheap-lab first |
| Are there unexpected destroys? | No |
| Are high-cost resources enabled? | Only if intentionally selected |
| Are public IPs expected? | Only for approved public entry points |
| Is public RDP blocked? | Yes |
| Are private endpoints and private DNS aligned? | Yes |
| Are diagnostics wired to real resources? | Yes for deployed supported resources |
| Are tags applied consistently? | Yes |
| Control | Pass condition | Fail example |
|---|---|---|
| Public RDP | No broad 0.0.0.0/0 RDP source |
Jumpbox source allows all internet |
| LB RDP NAT |
enable_lb_rdp_nat_rules = false by default |
NAT rules expose workload VMs on 3389/3390 |
| SQL public access | Disabled unless explicitly justified | SQL public network access enabled in lab |
| Key Vault network ACL | Deny-by-default when private endpoint path is intended | Public network default allow without reason |
| Storage public access | Public blob access disabled | Anonymous blob access enabled |
| Private DNS | Linked to VNets that need resolution | Private endpoint exists but name does not resolve |
| State | Remote backend used with protected storage | Local state committed or copied into docs |
| CI auth | OIDC only |
AZURE_CREDENTIALS client secret JSON required |
Expensive services should be intentional:
| Resource type | Common reason to enable | Review action |
|---|---|---|
| Azure Firewall | Central inspection scenario | Confirm short-lived lab need |
| VPN Gateway | Site-to-site or on-prem simulation | Confirm SKU and destroy plan |
| Application Gateway WAF | Public web ingress scenario | Confirm WAF mode and backend health |
| NAT Gateway | Stable outbound SNAT | Confirm required subnets only |
| AKS | Kubernetes workload scenario | Confirm node count and SKU |
| SQL | Data tier scenario | Confirm public access disabled and SKU |
| Windows VMs | AD DS and IIS lab | Confirm shutdown schedule or destroy date |
Pass condition: cost estimate is reviewed and high-cost toggles are either off or documented.
| Area | What to inspect |
|---|---|
| CIDR overlap | No VNet address ranges overlap |
| Peering | Hub and enabled spokes have bidirectional peering |
| Routes | Firewall route next hop exists only when firewall is enabled |
| NSGs | Inbound rules are scoped and tier-specific |
| Public ingress | Application Gateway, Load Balancer, and VPN are intentional |
| Private endpoints | Private endpoint subnet and DNS zones are present together |
| Flow logs | VNet flow logs are enabled only when planned |
Use JSON to confirm critical controls:
terraform show -json tfplan > tfplan.jsonReview targets:
| Control | Search intent |
|---|---|
| Public IPs | Count azurerm_public_ip resources |
| NSG rules | Look for destination port 3389 and source 0.0.0.0/0
|
| SQL | Confirm public_network_access_enabled is false when SQL is enabled |
| Key Vault | Confirm network ACL intent |
| Storage | Confirm HTTPS-only and public blob access disabled |
| Diagnostics | Confirm azurerm_monitor_diagnostic_setting resources for deployed services |
Expected OPA behavior:
- Deny unsafe defaults such as missing tags, invalid regions, or public SQL access.
- Warn on public IPs, broad inbound NSGs, large VM SKUs, or missing hardening options.
Use this style in a PR or run review:
Plan reviewed for cheap-lab.
Adds: expected hub, identity, management, shared, and workload resources.
Changes: none unexpected.
Destroys: 0.
Public exposure: no public RDP and no LB RDP NAT.
Cost: Firewall, VPN, App Gateway, NAT Gateway, AKS disabled.
Security gates: fmt, validate, tfsec, Checkov, Gitleaks, OPA passed.
Approved for apply.
Do not approve apply when:
- Destroy count is non-zero and not explained.
- Public RDP appears in plan.
- A public data plane is enabled without a written reason.
- Backend state settings are unknown.
- Cost estimate includes high-cost services that were not requested.
- Security gates are skipped, cancelled, or soft-failed unexpectedly.
- The plan was generated from a different commit than the apply.
Next page: Azure Portal Validation Evidence
- Architecture Overview
- Full Environment Inventory
- Network Topology
- Shared Services
- Workloads
- Module Reference
- Security Model
- Security Operations
- Identity and Access
- Governance and Policy
- Production Readiness Review
- Monitoring and Diagnostics
- Operations Runbooks
- Azure Portal Validation Evidence
- Cost Management
- Disaster Recovery and Resilience