Skip to content

Commit d167a97

Browse files
author
Mike Swantek
committed
Add postgresql provisioning and fabric automation for mirror
1 parent 8c76feb commit d167a97

10 files changed

Lines changed: 886 additions & 28 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ This accelerator extends the [AI Landing Zone](https://github.com/Azure/ai-landi
2222

2323
### Solution Architecture
2424

25-
| ![Architecture](./img/Architecture/AI-Landing-Zone-without-platform.png) |
25+
| ![Architecture](./img/Architecture/Deploy-AI-App-in-Prod-Architecture_final.png) |
2626
|---|
2727

2828
### Key Components

azure.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ hooks:
7171
interactive: false
7272
shell: pwsh
7373
continueOnError: false
74+
75+
# Stage 7.4: Prepare PostgreSQL for Fabric mirroring (server params + role)
76+
- run: ./scripts/automationScripts/FabricWorkspace/Mirror/run_postgresql_mirroring_prep_with_public_access.ps1
77+
interactive: false
78+
shell: pwsh
79+
continueOnError: false
80+
81+
# Stage 7.5: Create PostgreSQL Mirrored Database (if PostgreSQL is provisioned)
82+
- run: ./scripts/automationScripts/FabricWorkspace/Mirror/create_postgresql_mirror.ps1
83+
interactive: false
84+
shell: pwsh
85+
continueOnError: false
7486

7587
# Stage 8: Setup Fabric Workspace Private Link (for VNet integration)
7688
- run: ./scripts/automationScripts/FabricWorkspace/SecureWorkspace/setup_fabric_private_link.ps1

docs/DeploymentGuide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ Edit `infra/main.bicepparam` or set environment variables:
179179
| Parameter | Description | Default |
180180
|-----------|-------------|---------|
181181
| `networkIsolation` | Enable network isolation | `false` |
182+
| `postgreSqlNetworkIsolation` | PostgreSQL private networking toggle (defaults to `networkIsolation`) | `networkIsolation` |
182183
| `useExistingVNet` | Reuse an existing VNet | `false` |
183184
| `existingVnetResourceId` | Existing VNet resource ID (when `useExistingVNet=true`) | `` |
184185
| `vmUserName` | Jump box VM admin username | `` |

docs/PARAMETER_GUIDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,17 @@ az cognitiveservices account list-usage \
437437

438438
## Individual Service Configuration
439439

440+
### PostgreSQL Flexible Server (Repo Wrapper)
441+
442+
Use these in `infra/main.bicepparam` when deploying via this repo. `postgreSqlNetworkIsolation` defaults to `networkIsolation`.
443+
444+
```bicep-params
445+
param deployPostgreSql = true
446+
param postgreSqlNetworkIsolation = networkIsolation
447+
```
448+
449+
When `postgreSqlNetworkIsolation` is `false`, PostgreSQL uses public access and does not create private endpoints or private DNS resources.
450+
440451
### Storage Account
441452

442453
```json

infra/main.bicep

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -205,24 +205,197 @@ param purviewAccountResourceId string = ''
205205
@description('Optional. Existing Purview collection name')
206206
param purviewCollectionName string = ''
207207

208+
// ========================================
209+
// PARAMETERS - POSTGRESQL FLEXIBLE SERVER
210+
// ========================================
211+
212+
@description('Deploy PostgreSQL Flexible Server.')
213+
param deployPostgreSql bool = false
214+
215+
@description('PostgreSQL Flexible Server name.')
216+
param postgreSqlServerName string = 'pg${resourceToken}'
217+
218+
@description('Enable network isolation for PostgreSQL (private DNS + private endpoint).')
219+
param postgreSqlNetworkIsolation bool = networkIsolation
220+
221+
@description('Create and link the PostgreSQL private DNS zone to the VNet.')
222+
param deployPostgreSqlPrivateDnsLink bool = true
223+
224+
@description('Optional override for the PostgreSQL private DNS VNet link name.')
225+
param postgreSqlPrivateDnsLinkNameOverride string = ''
226+
227+
@description('PostgreSQL admin username.')
228+
param postgreSqlAdminLogin string = 'pgadmin'
229+
230+
@description('PostgreSQL admin password.')
231+
@secure()
232+
param postgreSqlAdminPassword string
233+
234+
@description('Store PostgreSQL admin password in Key Vault.')
235+
param enablePostgreSqlKeyVaultSecret bool = true
236+
237+
@description('Key Vault secret name for PostgreSQL admin password.')
238+
param postgreSqlAdminSecretName string = 'postgres-admin-password'
239+
240+
@description('PostgreSQL role name for Fabric mirroring.')
241+
param postgreSqlFabricUserName string = 'fabric_user'
242+
243+
@description('Key Vault secret name for the Fabric mirroring PostgreSQL role password.')
244+
param postgreSqlFabricUserSecretName string = 'postgres-fabric-user-password'
245+
246+
@description('PostgreSQL SKU name (tier + family + cores).')
247+
param postgreSqlSkuName string = 'Standard_D2s_v3'
248+
249+
@description('PostgreSQL tier aligned with SKU.')
250+
@allowed([
251+
'Burstable'
252+
'GeneralPurpose'
253+
'MemoryOptimized'
254+
])
255+
param postgreSqlTier string = 'GeneralPurpose'
256+
257+
@description('PostgreSQL availability zone. -1 means no zone preference.')
258+
@allowed([
259+
-1
260+
1
261+
2
262+
3
263+
])
264+
param postgreSqlAvailabilityZone int = -1
265+
266+
@description('PostgreSQL high availability mode.')
267+
@allowed([
268+
'Disabled'
269+
'SameZone'
270+
'ZoneRedundant'
271+
])
272+
param postgreSqlHighAvailability string = 'Disabled'
273+
274+
@description('PostgreSQL high availability standby zone. -1 means no zone preference.')
275+
@allowed([
276+
-1
277+
1
278+
2
279+
3
280+
])
281+
param postgreSqlHighAvailabilityZone int = -1
282+
283+
@description('PostgreSQL version.')
284+
@allowed([
285+
'11'
286+
'12'
287+
'13'
288+
'14'
289+
'15'
290+
'16'
291+
'17'
292+
'18'
293+
])
294+
param postgreSqlVersion string = '16'
295+
296+
@description('PostgreSQL storage size in GB.')
297+
param postgreSqlStorageSizeGB int = 32
298+
208299
// ========================================
209300
// FABRIC CAPACITY DEPLOYMENT
210301
// ========================================
211302

212303
var effectiveFabricCapacityMode = fabricCapacityMode
213304
var effectiveFabricWorkspaceMode = fabricWorkspaceMode
305+
var effectiveLocation = !empty(location) ? location : resourceGroup().location
214306

215307
var envSlugSanitized = replace(replace(replace(replace(replace(replace(replace(replace(toLower(environmentName), ' ', ''), '-', ''), '_', ''), '.', ''), '/', ''), '\\', ''), ':', ''), ',', '')
216308

217309
var envSlugTrimmed = substring(envSlugSanitized, 0, min(40, length(envSlugSanitized)))
218310
var capacityNameBase = !empty(envSlugTrimmed) ? 'fabric${envSlugTrimmed}' : 'fabric${baseName}'
219311
var capacityName = substring(capacityNameBase, 0, min(50, length(capacityNameBase)))
220312

313+
var effectiveVnetResourceId = useExistingVNet && !empty(existingVnetResourceId)
314+
? existingVnetResourceId
315+
: resourceId('Microsoft.Network/virtualNetworks', vnetName)
316+
317+
var postgreSqlPrivateDnsZoneName = 'privatelink.postgres.database.azure.com'
318+
var postgreSqlPrivateDnsLinkNameRaw = '${postgreSqlServerName}-vnetlink'
319+
var postgreSqlPrivateEndpointNameRaw = '${postgreSqlServerName}-pe'
320+
var postgreSqlPrivateDnsLinkName = substring(postgreSqlPrivateDnsLinkNameRaw, 0, min(80, length(postgreSqlPrivateDnsLinkNameRaw)))
321+
var effectivePostgreSqlPrivateDnsLinkName = !empty(postgreSqlPrivateDnsLinkNameOverride)
322+
? postgreSqlPrivateDnsLinkNameOverride
323+
: postgreSqlPrivateDnsLinkName
324+
var postgreSqlPrivateEndpointName = substring(postgreSqlPrivateEndpointNameRaw, 0, min(80, length(postgreSqlPrivateEndpointNameRaw)))
325+
326+
var effectiveKeyVaultResourceId = !empty(keyVaultResourceId)
327+
? keyVaultResourceId
328+
: resourceId('Microsoft.KeyVault/vaults', keyVaultName)
329+
330+
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
331+
name: last(split(effectiveKeyVaultResourceId, '/'))
332+
}
333+
334+
resource postgreSqlPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (deployPostgreSql && postgreSqlNetworkIsolation) {
335+
name: postgreSqlPrivateDnsZoneName
336+
location: 'global'
337+
tags: deploymentTags
338+
}
339+
340+
resource postgreSqlPrivateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (deployPostgreSql && postgreSqlNetworkIsolation && deployPostgreSqlPrivateDnsLink) {
341+
name: '${postgreSqlPrivateDnsZone.name}/${effectivePostgreSqlPrivateDnsLinkName}'
342+
location: 'global'
343+
properties: {
344+
virtualNetwork: {
345+
id: effectiveVnetResourceId
346+
}
347+
registrationEnabled: false
348+
}
349+
}
350+
351+
var postgreSqlPrivateEndpoints = postgreSqlNetworkIsolation ? [
352+
{
353+
name: postgreSqlPrivateEndpointName
354+
subnetResourceId: '${effectiveVnetResourceId}/subnets/${peSubnetName}'
355+
privateDnsZoneGroup: {
356+
privateDnsZoneGroupConfigs: [
357+
{
358+
privateDnsZoneResourceId: postgreSqlPrivateDnsZone.id
359+
}
360+
]
361+
}
362+
}
363+
] : []
364+
365+
module postgreSqlFlexibleServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.15.2' = if (deployPostgreSql) {
366+
name: 'postgresql-flexible'
367+
params: {
368+
availabilityZone: postgreSqlAvailabilityZone
369+
highAvailability: postgreSqlHighAvailability
370+
highAvailabilityZone: postgreSqlHighAvailabilityZone
371+
name: postgreSqlServerName
372+
skuName: postgreSqlSkuName
373+
tier: postgreSqlTier
374+
administratorLogin: postgreSqlAdminLogin
375+
administratorLoginPassword: postgreSqlAdminPassword
376+
managedIdentities: {
377+
systemAssigned: true
378+
}
379+
publicNetworkAccess: postgreSqlNetworkIsolation ? 'Disabled' : 'Enabled'
380+
version: postgreSqlVersion
381+
storageSizeGB: postgreSqlStorageSizeGB
382+
privateEndpoints: postgreSqlPrivateEndpoints
383+
tags: deploymentTags
384+
}
385+
}
386+
387+
resource postgreSqlAdminSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = if (deployPostgreSql && enablePostgreSqlKeyVaultSecret) {
388+
name: '${keyVault.name}/${postgreSqlAdminSecretName}'
389+
properties: {
390+
value: postgreSqlAdminPassword
391+
}
392+
}
393+
221394
module fabricCapacity 'modules/fabric-capacity.bicep' = if (effectiveFabricCapacityMode == 'create') {
222395
name: 'fabric-capacity'
223396
params: {
224397
capacityName: capacityName
225-
location: location
398+
location: effectiveLocation
226399
sku: fabricCapacitySku
227400
adminMembers: fabricCapacityAdmins
228401
tags: deploymentTags
@@ -233,14 +406,6 @@ module fabricCapacity 'modules/fabric-capacity.bicep' = if (effectiveFabricCapac
233406
// OUTPUTS - Pass through from AI Landing Zone
234407
// ========================================
235408

236-
var effectiveVnetResourceId = useExistingVNet && !empty(existingVnetResourceId)
237-
? existingVnetResourceId
238-
: resourceId('Microsoft.Network/virtualNetworks', vnetName)
239-
240-
var effectiveKeyVaultResourceId = !empty(keyVaultResourceId)
241-
? keyVaultResourceId
242-
: resourceId('Microsoft.KeyVault/vaults', keyVaultName)
243-
244409
var effectiveAiSearchResourceId = !empty(aiSearchResourceId)
245410
? aiSearchResourceId
246411
: resourceId('Microsoft.Search/searchServices', searchServiceName)
@@ -276,6 +441,16 @@ output fabricCapacityResourceIdOut string = effectiveFabricCapacityResourceId
276441
output fabricCapacityName string = effectiveFabricCapacityName
277442
output fabricCapacityId string = effectiveFabricCapacityResourceId
278443

444+
// PostgreSQL outputs
445+
output postgreSqlServerNameOut string = deployPostgreSql ? postgreSqlFlexibleServer.outputs.name : ''
446+
output postgreSqlServerResourceId string = deployPostgreSql ? postgreSqlFlexibleServer.outputs.resourceId : ''
447+
output postgreSqlServerFqdn string = deployPostgreSql ? postgreSqlFlexibleServer.outputs.fqdn : ''
448+
output postgreSqlSystemAssignedPrincipalId string = deployPostgreSql ? postgreSqlFlexibleServer.outputs.systemAssignedMIPrincipalId : ''
449+
output postgreSqlAdminSecretName string = deployPostgreSql && enablePostgreSqlKeyVaultSecret ? postgreSqlAdminSecretName : ''
450+
output postgreSqlAdminLoginOut string = deployPostgreSql ? postgreSqlAdminLogin : ''
451+
output postgreSqlFabricUserNameOut string = deployPostgreSql ? postgreSqlFabricUserName : ''
452+
output postgreSqlFabricUserSecretNameOut string = deployPostgreSql && enablePostgreSqlKeyVaultSecret ? postgreSqlFabricUserSecretName : ''
453+
279454
var effectiveFabricWorkspaceName = effectiveFabricWorkspaceMode == 'byo'
280455
? (!empty(fabricWorkspaceName) ? fabricWorkspaceName : (!empty(environmentName) ? 'workspace-${environmentName}' : 'workspace-${baseName}'))
281456
: (!empty(environmentName) ? 'workspace-${environmentName}' : 'workspace-${baseName}')

infra/main.bicepparam

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,40 @@ param deploymentTags = {}
3434
param appConfigLabel = 'ai-lz'
3535
param networkIsolation = true
3636

37+
// Coordinate PostgreSQL networking with the overall isolation flag by default.
38+
param postgreSqlNetworkIsolation = networkIsolation
39+
// Skip this if a PostgreSQL private DNS zone is already linked to the VNet.
40+
param deployPostgreSqlPrivateDnsLink = true
41+
// Optional: use an existing VNet link name to avoid conflicts.
42+
param postgreSqlPrivateDnsLinkNameOverride = ''
43+
44+
// ========================================
45+
// POSTGRESQL FLEXIBLE SERVER (Optional)
46+
// ========================================
47+
48+
var postgreSqlEnvNameLower = toLower(environmentName)
49+
var postgreSqlEnvNameSanitized = replace(replace(replace(replace(replace(replace(replace(postgreSqlEnvNameLower, ' ', '-'), '_', '-'), '.', ''), '/', ''), '\\', ''), ':', ''), ',', '')
50+
var postgreSqlEnvNameTrimmed = substring(postgreSqlEnvNameSanitized, 0, min(50, length(postgreSqlEnvNameSanitized)))
51+
var postgreSqlServerNameBase = !empty(postgreSqlEnvNameTrimmed)
52+
? 'pg-${postgreSqlEnvNameTrimmed}'
53+
: 'pg${uniqueString(readEnvironmentVariable('AZURE_SUBSCRIPTION_ID', ''), environmentName, location)}'
54+
55+
param deployPostgreSql = true
56+
param postgreSqlServerName = substring(postgreSqlServerNameBase, 0, min(63, length(postgreSqlServerNameBase)))
57+
param postgreSqlAdminLogin = 'pgadmin'
58+
param postgreSqlAdminPassword = readEnvironmentVariable('POSTGRES_ADMIN_PASSWORD', '$(secretOrRandomPassword)')
59+
param enablePostgreSqlKeyVaultSecret = true
60+
param postgreSqlAdminSecretName = 'postgres-admin-password'
61+
param postgreSqlFabricUserName = 'fabric_user'
62+
param postgreSqlFabricUserSecretName = 'postgres-fabric-user-password'
63+
param postgreSqlSkuName = 'Standard_D2s_v3'
64+
param postgreSqlTier = 'GeneralPurpose'
65+
param postgreSqlAvailabilityZone = 1
66+
param postgreSqlHighAvailability = 'Disabled'
67+
param postgreSqlHighAvailabilityZone = -1
68+
param postgreSqlVersion = '16'
69+
param postgreSqlStorageSizeGB = 32
70+
3771
// ========================================
3872
// FEATURE TOGGLES
3973
// ========================================
@@ -44,7 +78,7 @@ param deployAiFoundrySubnet = true
4478
param deployAppConfig = true
4579
param deployKeyVault = true
4680
param deployVmKeyVault = readEnvironmentVariable('DEPLOY_VM_KEY_VAULT', 'true') == 'true'
47-
param deployLogAnalytics = true
81+
param deployLogAnalytics = false
4882
param deployAppInsights = true
4983
param deploySearchService = true
5084
param deployStorageAccount = true

0 commit comments

Comments
 (0)