Setting the file. One moment.
Validate Terraform · Azure Validate · microsoft/azure-skills · Skills Docs
ContentsBack to the top of the page 28.8
Errors · azd
references/recipes/terraform/scripts/validate-terraform.ps1
references/recipes/terraform/scripts/ validate-terraform.ps1
PowerShell · 180 lines · 7 KB
14 . PARAMETER SubscriptionId
15 Optional subscription to select before checks.
16 . EXAMPLE
17 .\validate-terraform.ps1
18 # Validate ./infra
19 . EXAMPLE
20 .\validate-terraform.ps1 -InfraDir ./infra -SubscriptionId 00000000-0000-0000-0000-000000000000
21 # Validate an explicit directory against a specific subscription
22 . NOTES
23 Exit code: 0 when every non-skipped step passes, 1 when any step fails.
24 #>
25 param (
26 [ string ]$InfraDir = "./infra" ,
27 [ string ]$SubscriptionId
28 )
29
30 $ErrorActionPreference = "Continue"
31
32 $steps = [ System.Collections.Generic.List [ object ]]::new()
33
34 function Add-Result {
35 param ([ string ]$Name , [ string ]$Status , [ string ]$ErrorText = "" )
36 # Results are collected here and rendered once in the summary at the end.
37 $steps.Add([ pscustomobject ] @ { Name = $Name; Status = $Status; Error = $ErrorText })
38 }
39
40 function Test-Command {
41 param ([ string ]$Name)
42 return [ bool ]( Get-Command $Name - ErrorAction SilentlyContinue)
43 }
44
45 Write-Host "Terraform validation preflight - infra dir: $InfraDir "
46 Write-Host ""
47
48 # --- 1. Terraform installed --------------------------------------------------
49 if ( Test-Command "terraform" ) {
50 Add-Result "Terraform installed" "PASS"
51 } else {
52 Add-Result "Terraform installed" "FAIL" "terraform not found on PATH. Install: https://developer.hashicorp.com/terraform/install"
53 }
54
55 # --- 2. Azure CLI installed --------------------------------------------------
56 $hasAz = Test-Command "az"
57 if ($hasAz) {
58 Add-Result "Azure CLI installed" "PASS"
59 } else {
60 Add-Result "Azure CLI installed" "FAIL" "az not found on PATH. Install the Azure CLI: mcp_azure_mcp_extension_cli_install(cli-type: `" az `" )"
61 }
62
63 # --- 3. Authentication -------------------------------------------------------
64 if ($hasAz) {
65 if ($SubscriptionId) {
66 $subOut = az account set -- subscription $SubscriptionId 2>&1
67 if ( $LASTEXITCODE -eq 0 ) {
68 Add-Result "Select subscription" "PASS"
69 } else {
70 Add-Result "Select subscription" "FAIL" ($subOut | Out-String ).Trim()
71 }
72 }
73 $accountOut = az account show - o none 2>&1
74 if ( $LASTEXITCODE -eq 0 ) {
75 Add-Result "Authenticated (az account show)" "PASS"
76 } else {
77 Add-Result "Authenticated (az account show)" "FAIL" ($accountOut | Out-String ).Trim()
78 }
79 } else {
80 Add-Result "Authenticated (az account show)" "SKIP" "Azure CLI not installed"
81 }
82
83 # --- infra dir presence gate -------------------------------------------------
84 $haveTf = ( Test-Command "terraform" ) -and ( Test-Path - Path $InfraDir - PathType Container)
85
86 function Invoke-Tf {
87 param ([ string ]$Name , [ string []] $Args )
88 if ( -not $haveTf) {
89 Add-Result $Name "SKIP" "terraform unavailable or infra dir ' $InfraDir ' not found"
90 return
91 }
92 Push-Location $InfraDir
93 try {
94 # Stream output to a temp file so large output (e.g. terraform plan) is
95 # not held in memory; only read it back when the command fails.
96 $tmp = New-TemporaryFile
97 & terraform @Args *> $tmp.FullName
98 if ( $LASTEXITCODE -eq 0 ) {
99 Add-Result $Name "PASS"
100 } else {
101 $content = Get-Content - Raw $tmp.FullName
102 if ( $null -eq $content) { $content = "" }
103 Add-Result $Name "FAIL" $content.Trim()
104 }
105 Remove-Item $tmp.FullName - ErrorAction SilentlyContinue
106 } finally {
107 Pop-Location
108 }
109 }
110
111 # --- 4. Initialize -----------------------------------------------------------
112 Invoke-Tf "terraform init" @ ( "init" , "-input=false" )
113
114 # --- 5. Format check ---------------------------------------------------------
115 Invoke-Tf "terraform fmt -check" @ ( "fmt" , "-check" , "-recursive" )
116
117 # --- 6. Validate syntax ------------------------------------------------------
118 Invoke-Tf "terraform validate" @ ( "validate" )
119
120 # --- 7. Plan preview ---------------------------------------------------------
121 Invoke-Tf "terraform plan" @ ( "plan" , "-input=false" , "-out=tfplan" )
122
123 # --- 8. State backend --------------------------------------------------------
124 Invoke-Tf "terraform state list" @ ( "state" , "list" )
125
126 # --- 9. Go-style template-variable scan --------------------------------------
127 if ( Test-Path - Path $InfraDir - PathType Container) {
128 $hits = Get-ChildItem - Path $InfraDir - Recurse - Include "*.tf" , "*.tfvars.json" - ErrorAction SilentlyContinue |
129 Select-String - Pattern '{{ *\.Env\.' |
130 ForEach-Object { "{0}:{1}:{2}" -f $_ .Path , $_ .LineNumber , $_ .Line.Trim() }
131 if ($hits) {
132 $detail = "Found unresolved Go-style template variables - replace {{ .Env.VAR }} with `$ {VAR} (azd envsubst format): `n " + ($hits -join " `n " )
133 Add-Result "Template-variable scan ({{ .Env.* }})" "FAIL" $detail
134 } else {
135 Add-Result "Template-variable scan ({{ .Env.* }})" "PASS"
136 }
137 } else {
138 Add-Result "Template-variable scan ({{ .Env.* }})" "SKIP" "infra dir ' $InfraDir ' not found"
139 }
140
141 # --- 10. main.tfvars.json JSON syntax ----------------------------------------
142 $tfvars = Join-Path $InfraDir "main.tfvars.json"
143 if ( Test-Path - Path $tfvars - PathType Leaf) {
144 try {
145 Get-Content - Raw $tfvars | ConvertFrom-Json - ErrorAction Stop | Out-Null
146 Add-Result "main.tfvars.json is valid JSON" "PASS"
147 } catch {
148 Add-Result "main.tfvars.json is valid JSON" "FAIL" $_ .Exception.Message
149 }
150 } else {
151 Add-Result "main.tfvars.json is valid JSON" "SKIP" " $tfvars not found"
152 }
153
154 # --- summary -----------------------------------------------------------------
155 Write-Host ""
156 Write-Host "==================== SUMMARY ===================="
157 "{0,-40} {1}" -f "STEP" , "RESULT" | Write-Host
158 "{0,-40} {1}" -f "----" , "------" | Write-Host
159 foreach ($s in $steps) {
160 "{0,-40} {1}" -f $s.Name , $s.Status | Write-Host
161 }
162 Write-Host "================================================="
163
164 $failed = @ ($steps | Where-Object { $_ .Status -eq "FAIL" })
165 if ($failed.Count -gt 0 ) {
166 Write-Host ""
167 Write-Host "----- FAILURE DETAILS -----"
168 foreach ($s in $failed) {
169 Write-Host ""
170 Write-Host "### $( $s.Name ) "
171 Write-Host $s.Error
172 }
173 Write-Host ""
174 Write-Host "RESULT: $( $failed.Count ) step(s) failed. See remediation guidance in README.md."
175 exit 1
176 }
177
178 Write-Host ""
179 Write-Host "RESULT: All checks passed. Ready for azure-deploy."
180 exit 0