-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDeploy-Azure.ps1
More file actions
341 lines (288 loc) · 14.2 KB
/
Copy pathDeploy-Azure.ps1
File metadata and controls
341 lines (288 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
<#
.SYNOPSIS
Deploys the Azure-native BitLockerKeyMonitor stack from infra/main.bicep,
using pure az CLI (no dependency on azd).
.DESCRIPTION
This script is the Azure counterpart of Deploy-Remote.ps1 (which targets
the on-prem Windows Server topology).
It performs:
1. Prerequisite check - az CLI, bicep, dotnet SDK
2. Authentication check - reuses existing az login
3. Parameter resolution - signed-in user principalId, etc.
4. Bicep build - lints + compiles infra/main.bicep
5. What-if preview - shows planned changes (skippable)
6. Subscription-scope deployment - az deployment sub create
7. Post-provision - prints outputs, hints for the SQL secret
8. (optional) App publish - publishes Functions + Web.Azure zip and
pushes them via az functionapp/webapp deploy
Idempotent: running it again on the same EnvironmentName updates the
existing resources rather than recreating them.
.PARAMETER SubscriptionId
Azure subscription. Defaults to the currently selected one.
.PARAMETER Location
Azure region (default: italynorth).
.PARAMETER EnvironmentName
Short name used as the azd-env-name tag and in resource names (default: dev).
.PARAMETER SqlAdminPrincipalId
Object ID of the Entra ID principal (user or group) to set as SQL AAD admin.
Defaults to the signed-in user.
.PARAMETER SqlAdminLoginName
Display name for the SQL AAD admin (default: signed-in user UPN).
.PARAMETER SkipPreview
Skip the what-if preview step (useful in CI).
.PARAMETER SkipDeploy
Only run validation + preview, do not actually deploy.
.PARAMETER PublishApps
After provisioning, publish + deploy the Functions and Web.Azure apps.
.PARAMETER PopulateSqlSecret
After provisioning, prompt for the SQL connection string and write it
to the Key Vault secret 'sql-connection-string'.
.EXAMPLE
# Validate only (no changes to Azure)
.\Deploy-Azure.ps1 -SkipDeploy
.EXAMPLE
# Full deploy of infra + apps to dev environment
.\Deploy-Azure.ps1 -EnvironmentName dev -PublishApps -PopulateSqlSecret
.EXAMPLE
# Production deploy to West Europe with a security group as SQL admin
.\Deploy-Azure.ps1 -EnvironmentName prod -Location westeurope `
-SqlAdminPrincipalId 11111111-2222-3333-4444-555555555555 `
-SqlAdminLoginName "BitLockerMonitor-SqlAdmins" `
-PublishApps
#>
[CmdletBinding()]
param(
[string]$SubscriptionId,
[string]$Location = 'italynorth',
[ValidatePattern('^[a-z0-9-]{1,20}$')]
[string]$EnvironmentName = 'dev',
[string]$SqlAdminPrincipalId,
[string]$SqlAdminLoginName,
[string]$HybridAgentPrincipalId,
[switch]$SkipPreview,
[switch]$SkipDeploy,
[switch]$PublishApps,
[switch]$PopulateSqlSecret
)
$ErrorActionPreference = 'Stop'
$script:RepoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$script:BicepEntry = Join-Path $RepoRoot 'infra\main.bicep'
$script:DeploymentName = "blkmon-$EnvironmentName-$(Get-Date -Format 'yyyyMMddHHmmss')"
function Write-Step($msg) {
Write-Host ""
Write-Host "==> $msg" -ForegroundColor Cyan
}
function Assert-Command($name, $hint) {
if (-not (Get-Command $name -ErrorAction SilentlyContinue)) {
throw "Missing prerequisite: $name. $hint"
}
}
# --- 1. Prerequisite check ----------------------------------------------------
Write-Step "1/8 Checking prerequisites"
Assert-Command 'az' "Install Azure CLI: https://aka.ms/installazurecli"
Assert-Command 'dotnet' "Install .NET 10 SDK: https://dot.net"
$azVer = (az version --output json | ConvertFrom-Json).'azure-cli'
Write-Host " az CLI : $azVer"
Write-Host " .NET SDK : $(dotnet --version)"
# Ensure bicep is available (az auto-installs on first use, but pre-flight here)
az bicep version --only-show-errors 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host " Installing Bicep..."
az bicep install --only-show-errors | Out-Null
}
$bicepVer = az bicep version --only-show-errors 2>&1 | Select-Object -First 1
Write-Host " Bicep : $bicepVer"
if (-not (Test-Path $BicepEntry)) {
throw "Bicep entry point not found: $BicepEntry"
}
# --- 2. Authentication --------------------------------------------------------
Write-Step "2/8 Verifying Azure authentication"
$account = az account show --output json 2>$null | ConvertFrom-Json
if (-not $account) {
throw "Not logged in to az CLI. Run: az login"
}
if ($SubscriptionId) {
az account set --subscription $SubscriptionId | Out-Null
$account = az account show --output json | ConvertFrom-Json
}
$SubscriptionId = $account.id
Write-Host " Tenant : $($account.tenantId)"
Write-Host " Subscription : $($account.name) ($SubscriptionId)"
Write-Host " Signed in as : $($account.user.name)"
# --- 3. Parameter resolution --------------------------------------------------
Write-Step "3/8 Resolving deployment parameters"
$principalId = az ad signed-in-user show --query id -o tsv
$principalUpn = $account.user.name
if (-not $SqlAdminPrincipalId) { $SqlAdminPrincipalId = $principalId }
if (-not $SqlAdminLoginName) { $SqlAdminLoginName = $principalUpn }
$paramObj = @{
'$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#'
contentVersion = '1.0.0.0'
parameters = @{
environmentName = @{ value = $EnvironmentName }
location = @{ value = $Location }
principalId = @{ value = $principalId }
tenantId = @{ value = $account.tenantId }
sqlAdminPrincipalId = @{ value = $SqlAdminPrincipalId }
sqlAdminLoginName = @{ value = $SqlAdminLoginName }
}
}
if ($HybridAgentPrincipalId) {
$paramObj.parameters.hybridAgentPrincipalId = @{ value = $HybridAgentPrincipalId }
}
$paramFile = Join-Path $env:TEMP "blkmon-params-$EnvironmentName.json"
$paramObj | ConvertTo-Json -Depth 10 | Set-Content -Path $paramFile -Encoding utf8
Write-Host " environmentName = $EnvironmentName"
Write-Host " location = $Location"
Write-Host " principalId = $principalId"
Write-Host " sqlAdminPrincipalId = $SqlAdminPrincipalId"
Write-Host " sqlAdminLoginName = $SqlAdminLoginName"
if ($HybridAgentPrincipalId) {
Write-Host " hybridAgentPrincipalId = $HybridAgentPrincipalId"
}
Write-Host " parameters file = $paramFile"
# --- 4. Bicep build (lint + compile) -----------------------------------------
Write-Step "4/8 Building Bicep (lint + compile)"
$bicepOut = Join-Path $env:TEMP "blkmon-bicep-$EnvironmentName"
if (-not (Test-Path $bicepOut)) { New-Item -ItemType Directory -Path $bicepOut | Out-Null }
az bicep build --file $BicepEntry --outdir $bicepOut
if ($LASTEXITCODE -ne 0) { throw "Bicep build failed." }
Write-Host " OK -> $bicepOut\main.json"
# --- 5. What-if preview ------------------------------------------------------
if (-not $SkipPreview) {
Write-Step "5/8 What-if preview (no changes applied yet)"
az deployment sub what-if `
--name $DeploymentName `
--location $Location `
--template-file $BicepEntry `
--parameters "@$paramFile"
if ($LASTEXITCODE -ne 0) { throw "What-if preview failed." }
}
else {
Write-Step "5/8 What-if preview SKIPPED (-SkipPreview)"
}
if ($SkipDeploy) {
Write-Host ""
Write-Host "SkipDeploy specified. Stopping after validation + preview." -ForegroundColor Yellow
return
}
# --- 6. Subscription-scope deployment ----------------------------------------
Write-Step "6/8 Deploying (az deployment sub create)"
Write-Host " Deployment name: $DeploymentName"
az deployment sub create `
--name $DeploymentName `
--location $Location `
--template-file $BicepEntry `
--parameters "@$paramFile" `
--output none
if ($LASTEXITCODE -ne 0) { throw "az deployment sub create exited with $LASTEXITCODE" }
# Fetch deployment status (just to confirm it succeeded; outputs are read individually below).
$provState = az deployment sub show --name $DeploymentName --query "properties.provisioningState" -o tsv 2>$null
if ($LASTEXITCODE -ne 0) { throw "az deployment sub show failed for $DeploymentName" }
if ($provState -ne 'Succeeded') { throw "Deployment failed (state: $provState)." }
Write-Host " Deployment state: $provState"
# Read each declared output via az --query to bypass JSON enumeration entirely.
# (Earlier attempts that parsed properties.outputs in this script context
# consistently saw zero keys despite the file on disk being valid — likely a
# PowerShell pipeline-context interaction. The direct --query path is robust.)
$outputNames = @(
'AZURE_LOCATION', 'AZURE_TENANT_ID', 'AZURE_RESOURCE_GROUP',
'AZURE_APP_CONFIG_ENDPOINT', 'AZURE_KEY_VAULT_NAME', 'AZURE_KEY_VAULT_URI',
'AZURE_SERVICE_BUS_FQDN', 'AZURE_SQL_SERVER_FQDN', 'AZURE_SQL_DATABASE_NAME',
'WEB_URI', 'FUNCTIONS_URI', 'WEB_IDENTITY_CLIENT_ID', 'FUNC_IDENTITY_CLIENT_ID'
)
$script:Outputs = @{}
foreach ($name in $outputNames) {
# ARM normalizes output names by lower-casing every leading uppercase letter
# EXCEPT the last one before a lowercase/underscore boundary. So:
# AZURE_LOCATION -> azurE_LOCATION
# FUNCTIONS_URI -> functionS_URI
# WEB_URI -> weB_URI
# Build the ARM-side name by finding the run of leading uppercase letters,
# lower-casing all but the LAST one, then concatenating the remainder.
$i = 0
while ($i -lt $name.Length -and [char]::IsUpper($name[$i])) { $i++ }
if ($i -gt 1) {
# Lower-case chars 0..($i-2), keep char ($i-1) and onwards.
$armName = $name.Substring(0, $i - 1).ToLower() + $name.Substring($i - 1)
}
else {
$armName = $name.Substring(0, 1).ToLower() + $name.Substring(1)
}
$val = az deployment sub show --name $DeploymentName --query "properties.outputs.$armName.value" -o tsv 2>$null
if (-not [string]::IsNullOrWhiteSpace($val)) { $script:Outputs[$name] = $val }
}
Write-Host ""
Write-Host "Deployment outputs ($($script:Outputs.Count)):" -ForegroundColor Green
foreach ($key in ($script:Outputs.Keys | Sort-Object)) {
Write-Host (" {0,-32} = {1}" -f $key, $script:Outputs[$key])
}
# --- 7. Optional: populate SQL connection string in Key Vault ----------------
Write-Step "7/8 SQL connection string secret"
if ($PopulateSqlSecret) {
$kvName = $script:Outputs['AZURE_KEY_VAULT_NAME']
$sqlFqdn = $script:Outputs['AZURE_SQL_SERVER_FQDN']
$sqlDb = $script:Outputs['AZURE_SQL_DATABASE_NAME']
$defaultCs = "Server=tcp:$sqlFqdn,1433;Database=$sqlDb;Authentication=Active Directory Default;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
Write-Host " Suggested connection string (using Active Directory Default auth, picks up the managed identity automatically):"
Write-Host " $defaultCs" -ForegroundColor DarkGray
$cs = Read-Host " Press Enter to accept, or paste an override"
if ([string]::IsNullOrWhiteSpace($cs)) { $cs = $defaultCs }
az keyvault secret set --vault-name $kvName --name 'sql-connection-string' --value $cs --output none
if ($LASTEXITCODE -ne 0) { throw "Failed to write sql-connection-string to Key Vault." }
Write-Host " OK -> kv://$kvName/secrets/sql-connection-string"
}
else {
$kvName = $script:Outputs['AZURE_KEY_VAULT_NAME']
Write-Host " SKIPPED. To populate later run:" -ForegroundColor Yellow
Write-Host " az keyvault secret set --vault-name $kvName --name sql-connection-string --value '<connection-string>'"
}
# --- 8. Optional: publish + deploy the apps ---------------------------------
Write-Step "8/8 Application code deploy"
if ($PublishApps) {
$funcAppHost = $script:Outputs['FUNCTIONS_URI']
$webAppHost = $script:Outputs['WEB_URI'] -replace '^https://', ''
$funcAppName = ($funcAppHost -split '\.')[0]
$webAppName = ($webAppHost -split '\.')[0]
$stagingRoot = Join-Path $env:TEMP "blkmon-publish-$EnvironmentName"
if (Test-Path $stagingRoot) { Remove-Item $stagingRoot -Recurse -Force }
New-Item -ItemType Directory -Path $stagingRoot | Out-Null
# --- Functions ---
$funcOut = Join-Path $stagingRoot 'func'
Write-Host " Publishing Functions -> $funcOut"
dotnet publish (Join-Path $RepoRoot 'src\BitLockerKeyMonitor.Functions\BitLockerKeyMonitor.Functions.csproj') `
-c Release -o $funcOut --nologo -v minimal
if ($LASTEXITCODE -ne 0) { throw "dotnet publish (Functions) failed." }
$funcZip = Join-Path $stagingRoot 'func.zip'
Compress-Archive -Path "$funcOut\*" -DestinationPath $funcZip -Force
Write-Host " Deploying Functions -> $funcAppName"
az functionapp deployment source config-zip --resource-group $script:Outputs['AZURE_RESOURCE_GROUP'] `
--name $funcAppName --src $funcZip --output none
if ($LASTEXITCODE -ne 0) { throw "Function App zip deploy failed." }
# --- Web.Azure ---
$webOut = Join-Path $stagingRoot 'web'
Write-Host " Publishing Web.Azure -> $webOut"
dotnet publish (Join-Path $RepoRoot 'src\BitLockerKeyMonitor.Web.Azure\BitLockerKeyMonitor.Web.Azure.csproj') `
-c Release -o $webOut --nologo -v minimal
if ($LASTEXITCODE -ne 0) { throw "dotnet publish (Web.Azure) failed." }
$webZip = Join-Path $stagingRoot 'web.zip'
Compress-Archive -Path "$webOut\*" -DestinationPath $webZip -Force
Write-Host " Deploying Web.Azure -> $webAppName"
az webapp deploy --resource-group $script:Outputs['AZURE_RESOURCE_GROUP'] `
--name $webAppName --src-path $webZip --type zip --output none
if ($LASTEXITCODE -ne 0) { throw "Web App zip deploy failed." }
Write-Host ""
Write-Host " Apps deployed."
Write-Host " Web : $($script:Outputs['WEB_URI'])"
Write-Host " Functions: https://$funcAppHost"
}
else {
Write-Host " SKIPPED. To publish app code later run this script with -PublishApps" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Done. Subscription: $($account.name)" -ForegroundColor Green
Write-Host "Resource group : $($script:Outputs['AZURE_RESOURCE_GROUP'])"
Write-Host "Key Vault : $($script:Outputs['AZURE_KEY_VAULT_NAME'])"
Write-Host "App Configuration: $($script:Outputs['AZURE_APP_CONFIG_ENDPOINT'])"
Write-Host "Web URL : $($script:Outputs['WEB_URI'])"
Write-Host "Functions URL : $($script:Outputs['FUNCTIONS_URI'])"