Skip to content

Commit ce586d8

Browse files
committed
chore: commit all outstanding changes
1 parent f679cb2 commit ce586d8

13 files changed

+107
-125
lines changed

AzureArchitecture/deploymentStampLayer.bicep

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
@description('Azure AD administrator login for SQL Server (user or group UPN)')
2-
param sqlAadAdminLogin string = ''
3-
4-
@description('Azure AD administrator objectId for SQL Server (user or group objectId)')
5-
param sqlAadAdminObjectId string = ''
6-
7-
@description('Azure AD tenantId for SQL Server')
8-
param sqlAadAdminTenantId string = ''
91
// --------------------------------------------------------------------------------------
102
// CELL Layer Module
113
// - Deploys isolated application/data resources for a single CELL
@@ -37,6 +29,9 @@ param storageAccountName string
3729
@description('Name for the Key Vault for this CELL/Stamp')
3830
param keyVaultName string
3931

32+
@description('Optional salt to ensure unique resource names for repeated deployments (e.g., date, initials, or random chars)')
33+
param salt string = ''
34+
4035
@description('Name for the Cosmos DB account for this CELL/Stamp')
4136
param cosmosDbStampName string
4237

@@ -343,7 +338,7 @@ resource storageLifecyclePolicy 'Microsoft.Storage/storageAccounts/managementPol
343338

344339
// Key Vault for CELL with security hardening
345340
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
346-
name: keyVaultName
341+
name: empty(salt) ? keyVaultName : '${keyVaultName}${salt}'
347342
location: location
348343
properties: {
349344
sku: {
@@ -422,7 +417,7 @@ resource sqlEncryptionKey 'Microsoft.KeyVault/vaults/keys@2023-02-01' = {
422417

423418
// Diagnostic settings for Key Vault
424419
resource keyVaultDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
425-
name: '${keyVaultName}-diagnostics'
420+
name: empty(salt) ? '${keyVaultName}-diagnostics' : '${keyVaultName}${salt}-diagnostics'
426421
scope: keyVault
427422
properties: {
428423
workspaceId: globalLogAnalyticsWorkspaceId
@@ -487,15 +482,15 @@ resource sqlPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = if
487482

488483
// Private endpoint for Key Vault
489484
resource keyVaultPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = if (enablePrivateEndpoints && !empty(privateEndpointSubnetId)) {
490-
name: '${keyVaultName}-pe'
485+
name: empty(salt) ? '${keyVaultName}-pe' : '${keyVaultName}${salt}-pe'
491486
location: location
492487
properties: {
493488
subnet: {
494489
id: privateEndpointSubnetId
495490
}
496491
privateLinkServiceConnections: [
497492
{
498-
name: '${keyVaultName}-psc'
493+
name: empty(salt) ? '${keyVaultName}-psc' : '${keyVaultName}${salt}-psc'
499494
properties: {
500495
privateLinkServiceId: keyVault.id
501496
groupIds: ['vault']
@@ -749,27 +744,17 @@ resource sqlServer 'Microsoft.Sql/servers@2022-11-01-preview' = {
749744
identity: {
750745
type: 'SystemAssigned'
751746
}
752-
// Note: Only AAD admin is configured to comply with AAD-only authentication policy.
747+
// Note: Only SQL admin login is configured. AAD-only authentication is NOT enabled by default.
753748
properties: {
749+
administratorLogin: sqlAdminUsername
750+
administratorLoginPassword: sqlAdminPassword
754751
version: '12.0'
755752
// Security hardening
756753
minimalTlsVersion: '1.2'
757754
publicNetworkAccess: 'Disabled'
758755
}
759756
}
760757

761-
// Azure AD administrator for SQL Server (required for AAD-only authentication)
762-
resource sqlServerAadAdmin 'Microsoft.Sql/servers/administrators@2022-11-01-preview' = if (!empty(sqlAadAdminObjectId) && !empty(sqlAadAdminLogin) && !empty(sqlAadAdminTenantId)) {
763-
name: 'activeDirectory'
764-
parent: sqlServer
765-
properties: {
766-
administratorType: 'ActiveDirectory'
767-
login: sqlAadAdminLogin
768-
sid: sqlAadAdminObjectId
769-
tenantId: sqlAadAdminTenantId
770-
}
771-
}
772-
773758
// SQL Server firewall rule is not created because public network access is disabled.
774759
// If you enable public access, add a conditional firewall rule accordingly.
775760

AzureArchitecture/globalLayer.bicep

Lines changed: 3 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Endpoints are now filtered in the deployment script. Use as-is.
12
// --------------------------------------------------------------------------------------
23
// Module: globalLayer
34
// Purpose: Provisions global/shared resources such as DNS Zone, Traffic Manager, and Front Door.
@@ -88,19 +89,7 @@ resource trafficManager 'Microsoft.Network/trafficManagerProfiles@2022-04-01' =
8889
port: 443
8990
path: '/health'
9091
}
91-
endpoints: [
92-
for (endpoint, i) in regionalEndpoints: if (!empty(endpoint.fqdn)) {
93-
name: 'regional-endpoint-${i + 1}'
94-
type: 'Microsoft.Network/trafficManagerProfiles/externalEndpoints'
95-
properties: {
96-
target: endpoint.fqdn
97-
endpointStatus: 'Enabled'
98-
endpointLocation: endpoint.location
99-
weight: 1
100-
priority: i + 1
101-
}
102-
}
103-
]
92+
endpoints: regionalEndpoints
10493
}
10594
}
10695

@@ -184,62 +173,8 @@ resource apimRoute 'Microsoft.Cdn/profiles/afdEndpoints/routes@2023-05-01' = if
184173
]
185174
}
186175

187-
// Secondary Origin Group for regional Application Gateways (fallback/direct routing)
188-
resource regionalOriginGroup 'Microsoft.Cdn/profiles/originGroups@2023-05-01' = if (length([for endpoint in regionalEndpoints: if (!empty(endpoint.fqdn)) endpoint]) > 0) {
189-
name: 'regional-agw-origins'
190-
parent: frontDoor
191-
properties: {
192-
loadBalancingSettings: {
193-
sampleSize: 4
194-
successfulSamplesRequired: 3
195-
additionalLatencyInMilliseconds: 50
196-
}
197-
healthProbeSettings: {
198-
probePath: '/health'
199-
probeRequestType: 'GET'
200-
probeProtocol: 'Https'
201-
probeIntervalInSeconds: 100
202-
}
203-
}
204-
}
205176

206-
// Origins for each regional Application Gateway (fallback routing)
207-
resource regionalOrigins 'Microsoft.Cdn/profiles/originGroups/origins@2023-05-01' = [for (endpoint, i) in regionalEndpoints: if (!empty(endpoint.fqdn)) {
208-
name: 'agw-${endpoint.location}-origin'
209-
parent: regionalOriginGroup
210-
properties: {
211-
hostName: endpoint.fqdn
212-
httpPort: 80
213-
httpsPort: 443
214-
originHostHeader: endpoint.fqdn
215-
priority: (i % 5) + 1 // Ensure priority is between 1-5
216-
weight: 1000
217-
enabledState: 'Enabled'
218-
enforceCertificateNameCheck: true
219-
}
220-
}]
221-
222-
// Fallback Route for direct regional access (bypassing APIM)
223-
resource regionalRoute 'Microsoft.Cdn/profiles/afdEndpoints/routes@2023-05-01' = if (length([for endpoint in regionalEndpoints: if (!empty(endpoint.fqdn)) endpoint]) > 0) {
224-
name: 'regional-fallback-route'
225-
parent: frontDoorEndpoint
226-
properties: {
227-
customDomains: []
228-
originGroup: {
229-
id: regionalOriginGroup.id
230-
}
231-
originPath: null
232-
ruleSets: []
233-
supportedProtocols: ['Http', 'Https']
234-
patternsToMatch: ['/direct/*', '/regional/*'] // Specific patterns for direct regional access
235-
forwardingProtocol: 'HttpsOnly'
236-
linkToDefaultDomain: 'Enabled'
237-
httpsRedirect: 'Enabled'
238-
}
239-
dependsOn: [
240-
regionalOrigins
241-
]
242-
}
177+
// Fallback routing for regional Application Gateways must be implemented manually if needed.
243178

244179
@description('Enable diagnostic settings for Front Door (some categories may be restricted by SKU/region).')
245180
param enableFrontDoorDiagnostics bool = false

AzureArchitecture/host-main.bicep

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@description('Optional salt to ensure unique resource names for repeated deployments (e.g., date, initials, or random chars)')
2+
param salt string = ''
13

24
// Orchestrator for regional networking and CELL deployments in the Host subscription
35
// Consumes a central Log Analytics workspace resource ID from the Hub deployment
@@ -193,7 +195,8 @@ module deploymentStampLayers './deploymentStampLayer.bicep' = [for (cell, idx) i
193195
sqlAdminPassword: sqlAdminPassword
194196
sqlDbName: toLower('sqldb-cell${substring(cell.cellName, length(cell.cellName) - 2, 2)}-z${string(length(cell.availabilityZones))}-${(regionShortNames[?cell.regionName] ?? substring(cell.regionName, 0, 3))}-${envShort}')
195197
storageAccountName: toLower('st${(geoShortNames[?cell.geoName] ?? substring(cell.geoName, 0, 2))}${(regionShortNames[?cell.regionName] ?? substring(cell.regionName, 0, 3))}cell${substring(cell.cellName, length(cell.cellName) - 2, 2)}z${string(length(cell.availabilityZones))}${envShort}${substring(uniqueString(subscription().id, cell.regionName, cell.cellName, deployment().name), 0, 2)}')
196-
keyVaultName: toLower('kv-${(geoShortNames[?cell.geoName] ?? substring(cell.geoName, 0, 2))}-${(regionShortNames[?cell.regionName] ?? substring(cell.regionName, 0, 3))}-cell${substring(cell.cellName, length(cell.cellName) - 2, 2)}-${envShort}-${substring(uniqueString(subscription().id, cell.regionName, cell.cellName), 0, 2)}')
198+
keyVaultName: toLower('kv-${(geoShortNames[?cell.geoName] ?? substring(cell.geoName, 0, 2))}-${(regionShortNames[?cell.regionName] ?? substring(cell.regionName, 0, 3))}-cell${substring(cell.cellName, length(cell.cellName) - 2, 2)}-${envShort}-${substring(uniqueString(subscription().id, cell.regionName, cell.cellName), 0, 2)}${empty(salt) ? '' : salt}')
199+
salt: salt
197200
cosmosDbStampName: toLower('cosmos${(geoShortNames[?cell.geoName] ?? substring(cell.geoName, 0, 2))}${(regionShortNames[?cell.regionName] ?? substring(cell.regionName, 0, 3))}cell${substring(cell.cellName, length(cell.cellName) - 2, 2)}z${string(length(cell.availabilityZones))}${envShort}${substring(uniqueString(subscription().id, cell.regionName, cell.cellName, deployment().name), 0, 6)}')
198201
tags: union(tags, {
199202
geo: cell.geoName

AzureArchitecture/main.bicep

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@description('Optional salt to ensure unique resource names for repeated deployments (e.g., date, initials, or random chars)')
2+
param salt string = ''
13
// Requires Bicep CLI v0.20.0 or later for array filtering with ternary operator
24
@description('Enable deployment of global Function Apps and their plans/storage (disable in smoke/lab to avoid quota)')
35
param enableGlobalFunctions bool = true
@@ -433,7 +435,8 @@ module deploymentStampLayers './deploymentStampLayer.bicep' = [
433435
storageAccountName: toLower('st${uniqueString(subscription().id, cell.regionName, 'CELL-${padLeft(string(index + 1), 2, '0')}')}z${string(length(cell.availabilityZones))}')
434436
// Key Vault name: max 24 chars, alphanumeric only, must start with letter, end with letter/digit
435437
// Example: kvs-wus2-p-abc123de
436-
keyVaultName: toLower('kvs${take(cell.regionName, 3)}${take(environment, 1)}${substring(uniqueString(subscription().id, 'kv', cell.regionName, environment, 'CELL-${padLeft(string(index + 1), 2, '0')}'), 0, 8)}')
438+
keyVaultName: toLower('kvs${take(cell.regionName, 3)}${take(environment, 1)}${substring(uniqueString(subscription().id, 'kv', cell.regionName, environment, 'CELL-${padLeft(string(index + 1), 2, '0')}'), 0, 8)}${empty(salt) ? '' : salt}')
439+
salt: salt
437440
// Cosmos DB name: 3-44 chars, lowercase, letters, numbers, hyphens only, must start with a letter
438441
cosmosDbStampName: toLower('cosmos${take(cell.geoName, 3)}${take(cell.regionName, 3)}${padLeft(string(index + 1), 2, '0')}z${string(length(cell.availabilityZones))}${substring(uniqueString(subscription().id, cell.geoName, cell.regionName, string(index)), 0, 6)}')
439442
tags: union(baseTags, {

AzureArchitecture/main.parameters.json

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,6 @@
4747
"sqlAdminPassword": {
4848
"value": "G7!rT9p#xQ2z@LmS"
4949
},
50-
"sqlAadAdminLogin": {
51-
"value": "srnichols_live.com#EXT#@fdpo.onmicrosoft.com"
52-
},
53-
"sqlAadAdminObjectId": {
54-
"value": "90379532-1038-4db5-8709-b0dc8fec9cad"
55-
},
56-
"sqlAadAdminTenantId": {
57-
"value": "16b3c013-d300-468d-ac64-7eda0820b6d3"
58-
},
5950
"enableGlobalFunctions": {
6051
"value": true
6152
},

AzureArchitecture/single-sub-deployment-step1.ps1

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# Helper: Filter out endpoints with empty/null FQDNs
2+
function Filter-ValidEndpoints {
3+
param(
4+
[Parameter(Mandatory=$true)]
5+
[array]$Endpoints
6+
)
7+
return $Endpoints | Where-Object { $_.fqdn -and $_.fqdn.Trim() -ne '' }
8+
}
9+
110
# PowerShell script to deploy main.bicep, extract management portal outputs, and write to management-portal.parameters.json
211

312
# Set variables
@@ -6,6 +15,13 @@ $ParametersFile = "./AzureArchitecture/host-main.parameters.json"
615
$DeploymentName = "stamps-host-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
716
$OutputFile = "./AzureArchitecture/management-portal.parameters.json"
817

18+
19+
# (Optional) If you have a regionalEndpoints array to pass, filter it here before deployment
20+
# Example usage:
21+
# $regionalEndpoints = ... # Load or construct your endpoints array
22+
# $filteredEndpoints = Filter-ValidEndpoints -Endpoints $regionalEndpoints
23+
# Then pass $filteredEndpoints to Bicep as a parameter
24+
925
# Deploy host-main.bicep at subscription scope and capture outputs
1026
$deploymentResult = az deployment sub create `
1127
--name $DeploymentName `

AzureArchitecture/single-sub-deployment-step2.ps1

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# Helper: Filter out endpoints with empty/null FQDNs
2+
function Filter-ValidEndpoints {
3+
param(
4+
[Parameter(Mandatory=$true)]
5+
[array]$Endpoints
6+
)
7+
return $Endpoints | Where-Object { $_.fqdn -and $_.fqdn.Trim() -ne '' }
8+
}
9+
110
# PowerShell script to configure post-deployment traffic routing between Front Door, APIM, and Application Gateways
211
# This script should be run after single-sub-deployment-step1.ps1 completes successfully
312

@@ -59,34 +68,36 @@ if (-not $trafficManager) {
5968

6069
Write-Host "Found Traffic Manager: $($trafficManager.name) - FQDN: $($trafficManager.fqdn)" -ForegroundColor Green
6170

71+
6272
# Get regional Application Gateways
6373
Write-Host "Step 2: Discovering regional Application Gateways..." -ForegroundColor Cyan
6474

6575
$resourceGroups = az group list --query "[?starts_with(name, '$regionalRgPrefix')].name" --output tsv
6676
$appGateways = @()
6777

6878
foreach ($rgName in $resourceGroups) {
69-
$agw = az network application-gateway list --resource-group $rgName --query "[0].{name:name, fqdn:frontendIPConfigurations[0].publicIPAddress.fqdn, location:location}" | ConvertFrom-Json
70-
if ($agw) {
71-
# Get the actual public IP FQDN
72-
$pipName = az network application-gateway show --name $agw.name --resource-group $rgName --query "frontendIPConfigurations[0].publicIPAddress.id" --output tsv | Split-Path -Leaf
73-
$pip = az network public-ip show --name $pipName --resource-group $rgName --query "{fqdn:dnsSettings.fqdn, ipAddress:ipAddress}" | ConvertFrom-Json
74-
75-
$appGateways += @{
76-
name = $agw.name
77-
resourceGroup = $rgName
78-
location = $agw.location
79-
fqdn = $pip.fqdn
80-
ipAddress = $pip.ipAddress
81-
}
82-
83-
Write-Host "Found App Gateway: $($agw.name) in $($agw.location) - FQDN: $($pip.fqdn)" -ForegroundColor Green
79+
$agw = az network application-gateway list --resource-group $rgName --query "[0].{name:name, fqdn:frontendIPConfigurations[0].publicIPAddress.fqdn, location:location}" | ConvertFrom-Json
80+
if ($agw) {
81+
# Get the actual public IP FQDN
82+
$pipName = az network application-gateway show --name $agw.name --resource-group $rgName --query "frontendIPConfigurations[0].publicIPAddress.id" --output tsv | Split-Path -Leaf
83+
$pip = az network public-ip show --name $pipName --resource-group $rgName --query "{fqdn:dnsSettings.fqdn, ipAddress:ipAddress}" | ConvertFrom-Json
84+
$appGateways += @{
85+
name = $agw.name
86+
resourceGroup = $rgName
87+
location = $agw.location
88+
fqdn = $pip.fqdn
89+
ipAddress = $pip.ipAddress
8490
}
91+
Write-Host "Found App Gateway: $($agw.name) in $($agw.location) - FQDN: $($pip.fqdn)" -ForegroundColor Green
92+
}
8593
}
8694

95+
# Filter out any app gateways with empty/null FQDNs before using them
96+
$appGateways = Filter-ValidEndpoints -Endpoints $appGateways
97+
8798
if ($appGateways.Count -eq 0) {
88-
Write-Error "No Application Gateways found in regional resource groups"
89-
exit 1
99+
Write-Error "No Application Gateways with valid FQDNs found in regional resource groups"
100+
exit 1
90101
}
91102

92103
Write-Host "Step 3: Configuring APIM backends for regional Application Gateways..." -ForegroundColor Cyan

VERSION

Whitespace-only changes.

docs/THREE_STEP_DEPLOYMENT_GUIDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
---
2+
13
# Three-Step Deployment Process for Management Portal and Routing
24

35
This guide outlines the recommended three-step process for deploying the Stamps Pattern infrastructure, global routing, and the Management Portal application. This approach separates infrastructure, routing, and application deployment for maximum reliability and clarity.
@@ -85,5 +87,19 @@ This guide outlines the recommended three-step process for deploying the Stamps
8587
- Adjust parameter values and secrets as needed for your environment.
8688
8789
---
90+
## Special Parameters for Repeated Deployments
91+
92+
**salt**: To avoid Azure soft-delete conflicts (e.g., with Key Vaults), a `salt` parameter is now supported. This can be any short string (date, initials, random chars) and will be appended to resource names like Key Vault. Update the `salt` value in your deployment parameters or script for each new deployment attempt on the same day.
8893
94+
**How to use:**
95+
- Add or update the `salt` parameter in your deployment command or parameters file (e.g., `-salt 20250824a`).
96+
- This ensures unique resource names and prevents soft-delete name collisions.
97+
98+
**Example:**
99+
```pwsh
100+
pwsh ./scripts/single-sub-deployment-step1.ps1 -SubscriptionId <subscription-id> -Location <primary-location> -Environment <env> -Salt 20250824a
101+
```
102+
103+
If you encounter a Key Vault name conflict, simply change the `salt` value and redeploy.
104+
---
89105
For more details, see the project documentation or ask in the project discussions.

management-portal/dab/schema.graphql

Whitespace-only changes.

0 commit comments

Comments
 (0)