Skip to content

Commit b8078d7

Browse files
feat(new): Added Azure.AppService.AvailabilityZone (Azure#2984)
* feat(new): Added Azure.AppService.AvailabilityZone * Update docs/en/rules/Azure.AppService.AvailabilityZone.md Co-authored-by: Bernie White <[email protected]> * Update docs/en/rules/Azure.AppService.AvailabilityZone.md Co-authored-by: Bernie White <[email protected]> * Update docs/en/rules/Azure.AppService.AvailabilityZone.md Co-authored-by: Bernie White <[email protected]> * Update src/PSRule.Rules.Azure/rules/Azure.AppService.Rule.ps1 Co-authored-by: Bernie White <[email protected]> * fix: Fix test * fix: Fix localized string and test * fix: Fix grammar * fix: lowercase * fix: lowercase --------- Co-authored-by: Bernie White <[email protected]>
1 parent 478c527 commit b8078d7

File tree

6 files changed

+279
-7
lines changed

6 files changed

+279
-7
lines changed

docs/CHANGELOG-v1.md

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
3636
- Azure SQL Managed Instance:
3737
- Verify that Azure SQL Managed Instances have a customer-controlled maintenance window configured by @BenjaminEngeset.
3838
[#2979](https://github.com/Azure/PSRule.Rules.Azure/issues/2979)
39+
- App Service:
40+
- Verify that app service plans have availability zones configured by @BenjaminEngeset.
41+
[#2964](https://github.com/Azure/PSRule.Rules.Azure/issues/2964)
3942

4043
## v1.38.0
4144

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
severity: Important
3+
pillar: Reliability
4+
category: RE:05 Regions and availability zones
5+
resource: App Service
6+
online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.AppService.AvailabilityZone/
7+
---
8+
9+
# Deploy app service plan instances using availability zones
10+
11+
## SYNOPSIS
12+
13+
Deploy app service plan instances using availability zones in supported regions to ensure high availability and resilience.
14+
15+
## DESCRIPTION
16+
17+
App Service plans support zone redundancy, which distributes your application running within the plan across Availablity Zones.
18+
Each Availability Zone is a group of phyiscally separated data centers.
19+
Deploying your application with zone redundancy:
20+
21+
- Scales your plan to a minimum of 3 instances in a highly available configuration.
22+
Additional instances can be added manually or on-demand by using autoscale.
23+
- Improves the resiliency against service disruptions or issues affecting a single zone.
24+
25+
Additionally:
26+
27+
- **Even Distribution**: If the instance count is larger than 3 and divisible by 3, instances are evenly distributed across the three zones.
28+
- **Partial Distribution**: Instance counts beyond 3*N are spread across the remaining one or two zones to ensure balanced distribution.
29+
30+
**Important** Configuring zone redundancy with per-application scaling is possible but may increase costs and administrative overhead.
31+
When `perSiteScaling` is enabled, each application can have its own scaling rules and run on dedicated instances.
32+
To maintain zone redundancy, it is crucial that each application’s scaling rules ensure a minimum of 3 instances.
33+
Without explicitly configuring this minimum, the application may not meet the zone redundancy requirement.
34+
35+
## RECOMMENDATION
36+
37+
Consider using enabling zone redundancy using availability zones to improve the resiliency of your solution.
38+
39+
## EXAMPLES
40+
41+
### Configure with Azure template
42+
43+
To configure a zone-redundant app service plan:
44+
45+
- Set the `properties.zoneRedundant` property to `true`.
46+
47+
For example:
48+
49+
```json
50+
{
51+
"type": "Microsoft.Web/serverfarms",
52+
"apiVersion": "2022-09-01",
53+
"name": "[parameters('planName')]",
54+
"location": "[parameters('location')]",
55+
"sku": {
56+
"name": "P1v3",
57+
"tier": "PremiumV3",
58+
"size": "P1v3",
59+
"family": "Pv3",
60+
"capacity": 3
61+
},
62+
"properties": {
63+
"zoneRedundant": true
64+
}
65+
}
66+
```
67+
68+
### Configure with Bicep
69+
70+
To configure a zone-redundant app service plan:
71+
72+
- Set the `properties.zoneRedundant` property to `true`.
73+
74+
For example:
75+
76+
```bicep
77+
resource plan 'Microsoft.Web/serverfarms@2022-09-01' = {
78+
name: name
79+
location: location
80+
sku: {
81+
name: 'P1v3'
82+
tier: 'PremiumV3'
83+
size: 'P1v3'
84+
family: 'Pv3'
85+
capacity: 3
86+
}
87+
properties: {
88+
zoneRedundant: true
89+
}
90+
}
91+
```
92+
93+
## NOTES
94+
95+
Zone-redundancy is only supported for the `PremiumV2`, `PremiumV3` and `ElasticPremium` SKU tiers.
96+
97+
## LINKS
98+
99+
- [RE:05 Regions and availability zones](https://learn.microsoft.com/azure/well-architected/reliability/regions-availability-zones)
100+
- [Reliability in Azure App Service](https://learn.microsoft.com/azure/reliability/reliability-app-service)
101+
- [Availability zone support](https://learn.microsoft.com/azure/reliability/reliability-app-service#availability-zone-support)
102+
- [Azure resource deployment](https://learn.microsoft.com/azure/templates/microsoft.web/serverfarms)

src/PSRule.Rules.Azure/en/PSRule-rules.psd1

+2
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,6 @@
112112
InsecureParameterType = "The parameter '{0}' with type '{1}' is not secure."
113113
AzureSQLMIMaintenanceWindow = "The managed instance ({0}) should have a customer-controlled maintenance window configured."
114114
AzureSQLDatabaseMaintenanceWindow = "The {0} ({1}) should have a customer-controlled maintenance window configured."
115+
AppServiceAvailabilityZoneSKU = "The app service plan ({0}) is not deployed with a SKU that supports zone-redundancy."
116+
AppServiceAvailabilityZone = "The app service plan ({0}) deployed to region ({1}) should use three availability zones from the following [{2}]."
115117
}

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

+25
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,31 @@ Rule 'Azure.AppService.NodeJsVersion' -Ref 'AZR-000428' -Type 'Microsoft.Web/sit
189189
}
190190
}
191191

192+
# Synopsis: Deploy app service plan instances using availability zones in supported regions to ensure high availability and resilience.
193+
Rule 'Azure.AppService.AvailabilityZone' -Ref 'AZR-000442' -Type 'Microsoft.Web/serverfarms' -Tag @{ release = 'GA'; ruleSet = '2024_09 '; 'Azure.WAF/pillar' = 'Reliability'; } {
194+
# Check if the region supports availability zones.
195+
$provider = [PSRule.Rules.Azure.Runtime.Helper]::GetResourceType('Microsoft.Compute', 'virtualMachineScaleSets') # Use VMSS provider for availability zones as the App Service provider does not provide this information.
196+
$availabilityZones = GetAvailabilityZone -Location $TargetObject.Location -Zone $provider.ZoneMappings
197+
198+
# Don't flag if the region does not support availability zones.
199+
if (-not $availabilityZones) {
200+
return $Assert.Pass()
201+
}
202+
203+
# Availability zones are only supported for these Premium SKUs.
204+
$Assert.In($TargetObject, 'sku.tier', @('PremiumV2', 'PremiumV3', 'ElasticPremium')).Reason(
205+
$LocalizedData.AppServiceAvailabilityZoneSKU,
206+
$TargetObject.name
207+
)
208+
209+
$Assert.HasFieldValue($TargetObject, 'properties.zoneRedundant', $true).Reason(
210+
$LocalizedData.AppServiceAvailabilityZone,
211+
$TargetObject.name,
212+
$TargetObject.location,
213+
($availabilityZones -join ', ')
214+
)
215+
}
216+
192217
#endregion Web Apps
193218

194219
#region Helper functions

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

+26-6
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ Describe 'Azure.AppService' -Tag 'AppService' {
4242
# Fail
4343
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
4444
$ruleResult | Should -Not -BeNullOrEmpty;
45-
$ruleResult.Length | Should -Be 1;
46-
$ruleResult.TargetName | Should -Be 'plan-B';
45+
$ruleResult.Length | Should -Be 3;
46+
$ruleResult.TargetName | Should -Be 'plan-B', 'plan-C', 'plan-D';
4747
$ruleResult.Detail.Reason.Path | Should -BeIn 'sku.capacity';
4848

4949
# Pass
5050
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
5151
$ruleResult | Should -Not -BeNullOrEmpty;
52-
$ruleResult.Length | Should -Be 1;
53-
$ruleResult.TargetName | Should -Be 'plan-A';
52+
$ruleResult.Length | Should -Be 2;
53+
$ruleResult.TargetName | Should -Be 'plan-A', 'plan-E';
5454
}
5555

5656
It 'Azure.AppService.MinPlan' {
@@ -66,8 +66,8 @@ Describe 'Azure.AppService' -Tag 'AppService' {
6666
# Pass
6767
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
6868
$ruleResult | Should -Not -BeNullOrEmpty;
69-
$ruleResult.Length | Should -Be 1;
70-
$ruleResult.TargetName | Should -Be 'plan-A';
69+
$ruleResult.Length | Should -Be 4;
70+
$ruleResult.TargetName | Should -Be 'plan-A', 'plan-C', 'plan-D', 'plan-E';
7171
}
7272

7373
It 'Azure.AppService.ARRAffinity' {
@@ -255,6 +255,26 @@ Describe 'Azure.AppService' -Tag 'AppService' {
255255
$ruleResult.Length | Should -Be 13;
256256
$ruleResult.TargetName | Should -BeIn 'site-A', 'site-A/staging', 'site-B', 'site-B/staging', 'fn-app', 'site-c', 'site-d', 'site-f', 'site-h', 'site-j', 'site-l/web', 'site-n/web', 'site-p/appsettings';
257257
}
258+
259+
It 'Azure.AppService.AvailabilityZone' {
260+
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.AppService.AvailabilityZone' };
261+
262+
# Fail
263+
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
264+
$ruleResult.Length | Should -Be 2;
265+
$ruleResult.TargetName | Should -Be 'plan-A', 'plan-D';
266+
267+
$ruleResult[0].Reason | Should -BeExactly @(
268+
"The app service plan (plan-A) is not deployed with a SKU that supports zone-redundancy."
269+
"The app service plan (plan-A) deployed to region (eastus) should use three availability zones from the following [1, 2, 3]."
270+
);
271+
$ruleResult[1].Reason | Should -BeExactly "The app service plan (plan-D) deployed to region (eastus) should use three availability zones from the following [1, 2, 3].";
272+
273+
# Pass
274+
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
275+
$ruleResult.Length | Should -Be 4;
276+
$ruleResult.TargetName | Should -Be 'plan-B', 'plan-C', 'plan-E', 'plan-F';
277+
}
258278
}
259279

260280
Context 'With Template' {

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

+121-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"ResourceType": "Microsoft.Web/serverfarms",
77
"Kind": "app",
88
"ResourceGroupName": "test-rg",
9-
"Location": "region",
9+
"Location": "eastus",
1010
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
1111
"Tags": null,
1212
"Properties": {
@@ -608,6 +608,126 @@
608608
"capacity": 1
609609
}
610610
},
611+
{
612+
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/plan-C",
613+
"ResourceName": "plan-C",
614+
"Name": "plan-C",
615+
"ResourceType": "Microsoft.Web/serverfarms",
616+
"Kind": "app",
617+
"ResourceGroupName": "test-rg",
618+
"Location": "notregion",
619+
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
620+
"Tags": null,
621+
"Properties": {
622+
"perSiteScaling": false,
623+
"elasticScaleEnabled": false,
624+
"maximumElasticWorkerCount": 1,
625+
"isSpot": false,
626+
"reserved": true,
627+
"isXenon": false,
628+
"hyperV": false,
629+
"targetWorkerCount": 0,
630+
"targetWorkerSizeId": 0,
631+
"zoneRedundant": false
632+
},
633+
"Sku": {
634+
"name": "P1v3",
635+
"tier": "PremiumV3",
636+
"size": "P1v3",
637+
"family": "Pv3",
638+
"capacity": 1
639+
}
640+
},
641+
{
642+
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/plan-D",
643+
"ResourceName": "plan-D",
644+
"Name": "plan-D",
645+
"ResourceType": "Microsoft.Web/serverfarms",
646+
"Kind": "app",
647+
"ResourceGroupName": "test-rg",
648+
"Location": "eastus",
649+
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
650+
"Tags": null,
651+
"Properties": {
652+
"perSiteScaling": false,
653+
"elasticScaleEnabled": false,
654+
"maximumElasticWorkerCount": 1,
655+
"isSpot": false,
656+
"reserved": true,
657+
"isXenon": false,
658+
"hyperV": false,
659+
"targetWorkerCount": 0,
660+
"targetWorkerSizeId": 0,
661+
"zoneRedundant": false
662+
},
663+
"Sku": {
664+
"name": "P1v3",
665+
"tier": "PremiumV3",
666+
"size": "P1v3",
667+
"family": "Pv3",
668+
"capacity": 1
669+
}
670+
},
671+
{
672+
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/plan-E",
673+
"ResourceName": "plan-E",
674+
"Name": "plan-E",
675+
"ResourceType": "Microsoft.Web/serverfarms",
676+
"Kind": "app",
677+
"ResourceGroupName": "test-rg",
678+
"Location": "eastus",
679+
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
680+
"Tags": null,
681+
"Properties": {
682+
"perSiteScaling": false,
683+
"elasticScaleEnabled": false,
684+
"maximumElasticWorkerCount": 1,
685+
"isSpot": false,
686+
"reserved": true,
687+
"isXenon": false,
688+
"hyperV": false,
689+
"targetWorkerCount": 0,
690+
"targetWorkerSizeId": 0,
691+
"zoneRedundant": true
692+
},
693+
"Sku": {
694+
"name": "P1v3",
695+
"tier": "PremiumV3",
696+
"size": "P1v3",
697+
"family": "Pv3",
698+
"capacity": 3
699+
}
700+
},
701+
{
702+
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/plan-F",
703+
"ResourceName": "plan-F",
704+
"Name": "plan-F",
705+
"ResourceType": "Microsoft.Web/serverfarms",
706+
"Kind": "elastic",
707+
"ResourceGroupName": "test-rg",
708+
"Location": "eastus",
709+
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
710+
"Tags": null,
711+
"Properties": {
712+
"perSiteScaling": false,
713+
"elasticScaleEnabled": false,
714+
"maximumElasticWorkerCount": 1,
715+
"isSpot": false,
716+
"reserved": true,
717+
"isXenon": false,
718+
"hyperV": false,
719+
"targetWorkerCount": 0,
720+
"targetWorkerSizeId": 0,
721+
"zoneRedundant": true
722+
},
723+
"Sku": {
724+
"name": "EP1",
725+
"tier": "ElasticPremium",
726+
"size": "EP1",
727+
"family": "EP",
728+
"capacity": 3
729+
}
730+
},
611731
{
612732
"Name": "site-B",
613733
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/site-B",

0 commit comments

Comments
 (0)