Skip to content

Commit d8d0972

Browse files
feat: Added Azure.PostgreSQL.ZoneRedundantHA (Azure#2933)
* feat: Added Azure.PostgreSQL.ZoneRedundantHA * Minor updates --------- Co-authored-by: Bernie White <[email protected]>
1 parent 69ef58a commit d8d0972

File tree

5 files changed

+263
-18
lines changed

5 files changed

+263
-18
lines changed

docs/CHANGELOG-v1.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ What's changed since v1.37.0:
3737
[#2916](https://github.com/Azure/PSRule.Rules.Azure/issues/2916)
3838
- Verify that servers have zone-redundant high availability (HA) configured by @BenjaminEngeset.
3939
[#2914](https://github.com/Azure/PSRule.Rules.Azure/issues/2914)
40-
4140
- Azure Database for PostgreSQL:
4241
- Verify that Azure Database for PostgreSQL servers have a customer-controlled maintenance window configured by @BenjaminEngeset.
4342
[#2927](https://github.com/Azure/PSRule.Rules.Azure/issues/2927)
43+
- Verify that servers have zone-redundant high availability (HA) configured by @BenjaminEngeset.
44+
[#2932](https://github.com/Azure/PSRule.Rules.Azure/issues/2932)
4445
- Azure Firewall:
4546
- Verify that firewalls have availability zones configured by @BenjaminEngeset.
4647
[#2909](https://github.com/Azure/PSRule.Rules.Azure/issues/2909)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
severity: Important
3+
pillar: Reliability
4+
category: RE:05 Regions and availability zones
5+
resource: Azure Database for PostgreSQL
6+
online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.PostgreSQL.ZoneRedundantHA/
7+
---
8+
9+
# Zone-Redundant High Availability
10+
11+
## SYNOPSIS
12+
13+
Deploy Azure Database for PostgreSQL servers using zone-redundant high availability (HA) in supported regions to ensure high availability and resilience.
14+
15+
## DESCRIPTION
16+
17+
Azure Database for PostgreSQL flexible servers allows configuration high availability (HA) across availability zones in supported regions.
18+
Using availability zones improves resiliency of your solution to failures or disruptions isolated to a zone or data center.
19+
20+
Zone-redundant HA works by:
21+
22+
- Deploying two servers; a primary in one zone, and a secondary in a physically separate zone.
23+
- Daily Data Backups: Performed from the primary server and stored using zone-redundant backup storage.
24+
- Transaction Logs: Continuously archived in zone-redundant backup storage from the standby replica.
25+
26+
The failover process ensures continuous operation by switching from the primary server to the standby replica server.
27+
This process can be:
28+
29+
- Manual (Planned) Failover: Initiated by the user for maintenance or other operational reasons.
30+
- Automatic (Unplanned) Failover: Triggered by Azure in response to failures such as hardware or network issues affecting the primary server.
31+
32+
Before opting for the zone-redundant HA model, review the documentation for additional limitations and critical information.
33+
This includes understanding the latency impact between zones, cost implications, and any specific regional support constraints.
34+
35+
## RECOMMENDATION
36+
37+
Consider deploying flexible servers using zone-redundant high-availability to improve the resiliency of your databases.
38+
39+
## EXAMPLES
40+
41+
### Configure with Azure template
42+
43+
To configure servers that pass this rule:
44+
45+
- Set the `properties.highAvailability.mode` property to `ZoneRedundant`.
46+
47+
For example:
48+
49+
```json
50+
{
51+
"type": "Microsoft.DBforPostgreSQL/flexibleServers",
52+
"apiVersion": "2023-03-01-preview",
53+
"name": "[parameters('serverName')]",
54+
"location": "[parameters('location')]",
55+
"sku": {
56+
"name": "Standard_D16as",
57+
"tier": "GeneralPurpose"
58+
},
59+
"properties": {
60+
"administratorLogin": "[parameters('administratorLogin')]",
61+
"administratorLoginPassword": "[parameters('administratorLoginPassword')]",
62+
"createMode": "Default",
63+
"version": "[parameters('postgresqlVersion')]",
64+
"availabilityZone": "1",
65+
"highAvailability": {
66+
"mode": "ZoneRedundant",
67+
"standbyAvailabilityZone": "2"
68+
}
69+
}
70+
}
71+
```
72+
73+
### Configure with Bicep
74+
75+
To configure servers that pass this rule:
76+
77+
- Set the `properties.highAvailability.mode` property to `ZoneRedundant`.
78+
79+
For example:
80+
81+
```bicep
82+
resource postgresqlDbServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
83+
name: serverName
84+
location: location
85+
sku: {
86+
name: 'Standard_D16as'
87+
tier: 'GeneralPurpose'
88+
}
89+
properties: {
90+
administratorLogin: administratorLogin
91+
administratorLoginPassword: administratorLoginPassword
92+
createMode: 'Default'
93+
version: postgresqlVersion
94+
availabilityZone: 1
95+
highAvailability: {
96+
mode: 'ZoneRedundant'
97+
standbyAvailabilityZone: 2
98+
}
99+
}
100+
}
101+
```
102+
103+
<!-- external:avm avm/res/db-for-postgre-sql/flexible-server highAvailability -->
104+
105+
## NOTES
106+
107+
The `Burstable` SKU tier is not supported.
108+
109+
The zone-redundancy HA model is only available in regions that support availability zones.
110+
111+
## LINKS
112+
113+
- [RE:05 Regions and availability zones](https://learn.microsoft.com/azure/well-architected/reliability/regions-availability-zones)
114+
- [High availability concepts in Azure Database for PostgreSQL](https://learn.microsoft.com/azure/reliability/reliability-postgresql-flexible-server)
115+
- [Zone-redundant HA architecture](https://learn.microsoft.com/azure/reliability/reliability-postgresql-flexible-server#availability-zone-support)
116+
- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.dbforpostgresql/flexibleservers)

src/PSRule.Rules.Azure/rules/Azure.PostgreSQL.Rule.ps1

+19
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,25 @@ Rule 'Azure.PostgreSQL.AAD' -Ref 'AZR-000389' -Type 'Microsoft.DBforPostgreSQL/f
8282
}
8383
}
8484

85+
# Synopsis: Deploy Azure Database for PostgreSQL servers using zone-redundant high availability (HA) in supported regions to ensure high availability and resilience.
86+
Rule 'Azure.PostgreSQL.ZoneRedundantHA' -Ref 'AZR-000434' -Type 'Microsoft.DBforPostgreSQL/flexibleServers' -Tag @{ release = 'GA'; ruleSet = '2024_06 '; 'Azure.WAF/pillar' = 'Reliability'; } {
87+
# Check if the region supports availability zones.
88+
$provider = [PSRule.Rules.Azure.Runtime.Helper]::GetResourceType('Microsoft.DBforPostgreSQL', 'flexibleServers')
89+
$availabilityZones = GetAvailabilityZone -Location $TargetObject.Location -Zone $provider.ZoneMappings
90+
91+
# Don't flag if the region does not support availability zones.
92+
if (-not $availabilityZones) {
93+
return $Assert.Pass()
94+
}
95+
96+
$supportedSku = @('GeneralPurpose', 'MemoryOptimized')
97+
if ($TargetObject.sku.tier -notin $supportedSku) {
98+
return $Assert.In($TargetObject, 'sku.tier', $supportedSku) # Zone-redundant HA is only supported for the GeneralPurpose and MemoryOptimized SKU tiers.
99+
}
100+
101+
$Assert.HasFieldValue($TargetObject, 'properties.highAvailability.mode', 'ZoneRedundant')
102+
}
103+
85104
#endregion SQL Managed Instance
86105

87106
#region Helper functions

tests/PSRule.Rules.Azure.Tests/Azure.PostgreSQL.Tests.ps1

+27-8
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ Describe 'Azure.PostgreSQL' -Tag 'PostgreSQL' {
128128
# Fail
129129
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
130130
$ruleResult | Should -Not -BeNullOrEmpty;
131-
$ruleResult.Length | Should -Be 5;
132-
$ruleResult.TargetName | Should -BeIn 'server-A', 'server-B', 'server-E', 'server-F', 'server-G';
131+
$ruleResult.Length | Should -Be 7;
132+
$ruleResult.TargetName | Should -BeIn 'server-A', 'server-B', 'server-E', 'server-F', 'server-G', 'server-H', 'server-I';
133133

134134
$ruleResult[0].Reason | Should -BeExactly "The Azure Database for PostgreSQL 'server-B' should have geo-redundant backup configured.";
135135
$ruleResult[1].Reason | Should -BeExactly "The Azure Database for PostgreSQL 'server-A' should have geo-redundant backup configured.";
@@ -168,8 +168,8 @@ Describe 'Azure.PostgreSQL' -Tag 'PostgreSQL' {
168168
# Fail
169169
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
170170
$ruleResult | Should -Not -BeNullOrEmpty;
171-
$ruleResult.Length | Should -Be 7;
172-
$ruleResult.TargetName | Should -BeIn 'server-A', 'server-B', 'server-D', 'server-E', 'server-G', 'ActiveDirectoryAdmin-A', 'ActiveDirectoryAdmin-C';
171+
$ruleResult.Length | Should -Be 9;
172+
$ruleResult.TargetName | Should -BeIn 'server-A', 'server-B', 'server-D', 'server-E', 'server-G', 'server-H', 'server-I', 'ActiveDirectoryAdmin-A', 'ActiveDirectoryAdmin-C';
173173

174174
$ruleResult[0].Reason | Should -BeIn 'Path properties.administratorType: Is null or empty.', 'Path properties.login: Is null or empty.', 'Path properties.sid: Is null or empty.';
175175
$ruleResult[1].Reason | Should -BeIn "A sub-resource of type 'Microsoft.DBforPostgreSQL/servers/administrators' has not been specified.";
@@ -196,17 +196,17 @@ Describe 'Azure.PostgreSQL' -Tag 'PostgreSQL' {
196196
# Pass
197197
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
198198
$ruleResult | Should -Not -BeNullOrEmpty;
199-
$ruleResult.Length | Should -Be 2;
200-
$ruleResult.TargetName | Should -BeIn 'server-F', 'server-G';
199+
$ruleResult.Length | Should -Be 4;
200+
$ruleResult.TargetName | Should -BeIn 'server-F', 'server-G', 'server-H', 'server-I';
201201
}
202202

203203
It 'Azure.PostgreSQL.MaintenanceWindow' {
204204
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.PostgreSQL.MaintenanceWindow' };
205205

206206
# Fail
207207
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
208-
$ruleResult.Length | Should -Be 3;
209-
$ruleResult.TargetName | Should -BeIn 'server-D', 'server-E', 'server-F';
208+
$ruleResult.Length | Should -Be 5;
209+
$ruleResult.TargetName | Should -BeIn 'server-D', 'server-E', 'server-F', 'server-H', 'server-I';
210210

211211
$ruleResult[0].Reason | Should -BeExactly "Path properties.maintenanceWindow.customWindow: The field 'properties.maintenanceWindow.customWindow' does not exist.";
212212
$ruleResult[1].Reason | Should -BeExactly "Path properties.maintenanceWindow.customWindow: Is set to 'notset'.";
@@ -217,6 +217,25 @@ Describe 'Azure.PostgreSQL' -Tag 'PostgreSQL' {
217217
$ruleResult.Length | Should -Be 1;
218218
$ruleResult.TargetName | Should -BeIn 'server-G';
219219
}
220+
221+
It 'Azure.PostgreSQL.ZoneRedundantHA' {
222+
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.PostgreSQL.ZoneRedundantHA' };
223+
224+
# Fail
225+
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
226+
$ruleResult.Length | Should -Be 4;
227+
$ruleResult.TargetName | Should -BeIn 'server-D', 'server-E', 'server-F', 'server-G';
228+
229+
$ruleResult[0].Reason | Should -BeExactly "Path sku.tier: The field value 'Burstable' was not included in the set.";
230+
$ruleResult[1].Reason | Should -BeExactly "Path properties.highAvailability.mode: Is set to 'Disabled'."
231+
$ruleResult[2].Reason | Should -BeExactly "Path properties.highAvailability.mode: Is set to 'SameZone'.";
232+
$ruleResult[3].Reason | Should -BeExactly "Path sku.tier: The field value 'Burstable' was not included in the set.";
233+
234+
# Pass
235+
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
236+
$ruleResult.Length | Should -Be 2;
237+
$ruleResult.TargetName | Should -BeIn 'server-H', 'server-I';
238+
}
220239
}
221240

222241
Context 'Resource name - Azure.PostgreSQL.ServerName' {

tests/PSRule.Rules.Azure.Tests/Resources.PostgreSQL.json

+99-9
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@
325325
{
326326
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-D",
327327
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-D",
328-
"Location": "region",
328+
"Location": "westeurope",
329329
"ResourceName": "server-D",
330330
"Name": "server-D",
331331
"Properties": {
@@ -337,9 +337,6 @@
337337
"backupRetentionDays": 35,
338338
"geoRedundantBackup": "Enabled"
339339
},
340-
"highAvailability": {
341-
"mode": "Disabled"
342-
},
343340
"version": "14",
344341
"createMode": "Default"
345342
},
@@ -356,7 +353,7 @@
356353
{
357354
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-E",
358355
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-E",
359-
"Location": "region",
356+
"Location": "westeurope",
360357
"ResourceName": "server-E",
361358
"Name": "server-E",
362359
"Properties": {
@@ -378,6 +375,7 @@
378375
"startHour": "notset",
379376
"startMinute": "notset"
380377
},
378+
"availabilityZone": "1",
381379
"highAvailability": {
382380
"mode": "Disabled"
383381
},
@@ -412,7 +410,7 @@
412410
{
413411
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-F",
414412
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-F",
415-
"Location": "region",
413+
"Location": "westeurope",
416414
"ResourceName": "server-F",
417415
"Name": "server-F",
418416
"Properties": {
@@ -435,8 +433,10 @@
435433
"startHour": 1,
436434
"startMinute": 0
437435
},
436+
"availabilityZone": "1",
438437
"highAvailability": {
439-
"mode": "Disabled"
438+
"mode": "SameZone",
439+
"standbyAvailabilityZone": "1"
440440
},
441441
"version": "14",
442442
"createMode": "Default"
@@ -469,7 +469,7 @@
469469
{
470470
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-G",
471471
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-G",
472-
"Location": "region",
472+
"Location": "westeurope",
473473
"ResourceName": "server-G",
474474
"Name": "server-G",
475475
"Properties": {
@@ -492,8 +492,98 @@
492492
"startHour": 1,
493493
"startMinute": 0
494494
},
495+
"availabilityZone": "1",
495496
"highAvailability": {
496-
"mode": "Disabled"
497+
"mode": "ZoneRedundant",
498+
"standbyAvailabilityZone": "2"
499+
},
500+
"version": "14",
501+
"createMode": "Default"
502+
},
503+
"ResourceGroupName": "test-rg",
504+
"Type": "Microsoft.DBforPostgreSQL/flexibleServers",
505+
"ResourceType": "Microsoft.DBforPostgreSQL/flexibleServers",
506+
"Sku": {
507+
"Name": "Standard_B2s",
508+
"Tier": "Burstable"
509+
},
510+
"Tags": null,
511+
"SubscriptionId": "00000000-0000-0000-0000-000000000000"
512+
},
513+
{
514+
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-H",
515+
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-H",
516+
"Location": "notregion",
517+
"ResourceName": "server-H",
518+
"Name": "server-H",
519+
"Properties": {
520+
"administratorLogin": "db-admin",
521+
"authConfig": {
522+
"activeDirectoryAuth": "Enabled",
523+
"passwordAuth": "Disabled",
524+
"tenantId": "00000000-0000-0000-0000-000000000000"
525+
},
526+
"storage": {
527+
"storageSizeGB": 20
528+
},
529+
"backup": {
530+
"backupRetentionDays": 7,
531+
"geoRedundantBackup": "Disabled"
532+
},
533+
"maintenanceWindow": {
534+
"customWindow": "Disabled",
535+
"dayOfWeek": 0,
536+
"startHour": 1,
537+
"startMinute": 0
538+
},
539+
"availabilityZone": "1",
540+
"highAvailability": {
541+
"mode": "ZoneRedundant",
542+
"standbyAvailabilityZone": "2"
543+
},
544+
"version": "14",
545+
"createMode": "Default"
546+
},
547+
"ResourceGroupName": "test-rg",
548+
"Type": "Microsoft.DBforPostgreSQL/flexibleServers",
549+
"ResourceType": "Microsoft.DBforPostgreSQL/flexibleServers",
550+
"Sku": {
551+
"Name": "E64",
552+
"Tier": "MemoryOptimized"
553+
},
554+
"Tags": null,
555+
"SubscriptionId": "00000000-0000-0000-0000-000000000000"
556+
},
557+
{
558+
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-I",
559+
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/server-I",
560+
"Location": "westeurope",
561+
"ResourceName": "server-I",
562+
"Name": "server-I",
563+
"Properties": {
564+
"administratorLogin": "db-admin",
565+
"authConfig": {
566+
"activeDirectoryAuth": "Enabled",
567+
"passwordAuth": "Disabled",
568+
"tenantId": "00000000-0000-0000-0000-000000000000"
569+
},
570+
"storage": {
571+
"storageSizeGB": 20
572+
},
573+
"backup": {
574+
"backupRetentionDays": 7,
575+
"geoRedundantBackup": "Disabled"
576+
},
577+
"maintenanceWindow": {
578+
"customWindow": "Disabled",
579+
"dayOfWeek": 0,
580+
"startHour": 1,
581+
"startMinute": 0
582+
},
583+
"availabilityZone": "1",
584+
"highAvailability": {
585+
"mode": "ZoneRedundant",
586+
"standbyAvailabilityZone": "2"
497587
},
498588
"version": "14",
499589
"createMode": "Default"

0 commit comments

Comments
 (0)