Skip to content

Commit

Permalink
feat(new): Added Azure.AppService.AvailabilityZone (Azure#2984)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
BenjaminEngeset and BernieWhite authored Jul 13, 2024
1 parent 478c527 commit b8078d7
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 7 deletions.
3 changes: 3 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
- Azure SQL Managed Instance:
- Verify that Azure SQL Managed Instances have a customer-controlled maintenance window configured by @BenjaminEngeset.
[#2979](https://github.com/Azure/PSRule.Rules.Azure/issues/2979)
- App Service:
- Verify that app service plans have availability zones configured by @BenjaminEngeset.
[#2964](https://github.com/Azure/PSRule.Rules.Azure/issues/2964)

## v1.38.0

Expand Down
102 changes: 102 additions & 0 deletions docs/en/rules/Azure.AppService.AvailabilityZone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
severity: Important
pillar: Reliability
category: RE:05 Regions and availability zones
resource: App Service
online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.AppService.AvailabilityZone/
---

# Deploy app service plan instances using availability zones

## SYNOPSIS

Deploy app service plan instances using availability zones in supported regions to ensure high availability and resilience.

## DESCRIPTION

App Service plans support zone redundancy, which distributes your application running within the plan across Availablity Zones.
Each Availability Zone is a group of phyiscally separated data centers.
Deploying your application with zone redundancy:

- Scales your plan to a minimum of 3 instances in a highly available configuration.
Additional instances can be added manually or on-demand by using autoscale.
- Improves the resiliency against service disruptions or issues affecting a single zone.

Additionally:

- **Even Distribution**: If the instance count is larger than 3 and divisible by 3, instances are evenly distributed across the three zones.
- **Partial Distribution**: Instance counts beyond 3*N are spread across the remaining one or two zones to ensure balanced distribution.

**Important** Configuring zone redundancy with per-application scaling is possible but may increase costs and administrative overhead.
When `perSiteScaling` is enabled, each application can have its own scaling rules and run on dedicated instances.
To maintain zone redundancy, it is crucial that each application’s scaling rules ensure a minimum of 3 instances.
Without explicitly configuring this minimum, the application may not meet the zone redundancy requirement.

## RECOMMENDATION

Consider using enabling zone redundancy using availability zones to improve the resiliency of your solution.

## EXAMPLES

### Configure with Azure template

To configure a zone-redundant app service plan:

- Set the `properties.zoneRedundant` property to `true`.

For example:

```json
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2022-09-01",
"name": "[parameters('planName')]",
"location": "[parameters('location')]",
"sku": {
"name": "P1v3",
"tier": "PremiumV3",
"size": "P1v3",
"family": "Pv3",
"capacity": 3
},
"properties": {
"zoneRedundant": true
}
}
```

### Configure with Bicep

To configure a zone-redundant app service plan:

- Set the `properties.zoneRedundant` property to `true`.

For example:

```bicep
resource plan 'Microsoft.Web/serverfarms@2022-09-01' = {
name: name
location: location
sku: {
name: 'P1v3'
tier: 'PremiumV3'
size: 'P1v3'
family: 'Pv3'
capacity: 3
}
properties: {
zoneRedundant: true
}
}
```

## NOTES

Zone-redundancy is only supported for the `PremiumV2`, `PremiumV3` and `ElasticPremium` SKU tiers.

## LINKS

- [RE:05 Regions and availability zones](https://learn.microsoft.com/azure/well-architected/reliability/regions-availability-zones)
- [Reliability in Azure App Service](https://learn.microsoft.com/azure/reliability/reliability-app-service)
- [Availability zone support](https://learn.microsoft.com/azure/reliability/reliability-app-service#availability-zone-support)
- [Azure resource deployment](https://learn.microsoft.com/azure/templates/microsoft.web/serverfarms)
2 changes: 2 additions & 0 deletions src/PSRule.Rules.Azure/en/PSRule-rules.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,6 @@
InsecureParameterType = "The parameter '{0}' with type '{1}' is not secure."
AzureSQLMIMaintenanceWindow = "The managed instance ({0}) should have a customer-controlled maintenance window configured."
AzureSQLDatabaseMaintenanceWindow = "The {0} ({1}) should have a customer-controlled maintenance window configured."
AppServiceAvailabilityZoneSKU = "The app service plan ({0}) is not deployed with a SKU that supports zone-redundancy."
AppServiceAvailabilityZone = "The app service plan ({0}) deployed to region ({1}) should use three availability zones from the following [{2}]."
}
25 changes: 25 additions & 0 deletions src/PSRule.Rules.Azure/rules/Azure.AppService.Rule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,31 @@ Rule 'Azure.AppService.NodeJsVersion' -Ref 'AZR-000428' -Type 'Microsoft.Web/sit
}
}

# Synopsis: Deploy app service plan instances using availability zones in supported regions to ensure high availability and resilience.
Rule 'Azure.AppService.AvailabilityZone' -Ref 'AZR-000442' -Type 'Microsoft.Web/serverfarms' -Tag @{ release = 'GA'; ruleSet = '2024_09 '; 'Azure.WAF/pillar' = 'Reliability'; } {
# Check if the region supports availability zones.
$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.
$availabilityZones = GetAvailabilityZone -Location $TargetObject.Location -Zone $provider.ZoneMappings

# Don't flag if the region does not support availability zones.
if (-not $availabilityZones) {
return $Assert.Pass()
}

# Availability zones are only supported for these Premium SKUs.
$Assert.In($TargetObject, 'sku.tier', @('PremiumV2', 'PremiumV3', 'ElasticPremium')).Reason(
$LocalizedData.AppServiceAvailabilityZoneSKU,
$TargetObject.name
)

$Assert.HasFieldValue($TargetObject, 'properties.zoneRedundant', $true).Reason(
$LocalizedData.AppServiceAvailabilityZone,
$TargetObject.name,
$TargetObject.location,
($availabilityZones -join ', ')
)
}

#endregion Web Apps

#region Helper functions
Expand Down
32 changes: 26 additions & 6 deletions tests/PSRule.Rules.Azure.Tests/Azure.AppService.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ Describe 'Azure.AppService' -Tag 'AppService' {
# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 1;
$ruleResult.TargetName | Should -Be 'plan-B';
$ruleResult.Length | Should -Be 3;
$ruleResult.TargetName | Should -Be 'plan-B', 'plan-C', 'plan-D';
$ruleResult.Detail.Reason.Path | Should -BeIn 'sku.capacity';

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

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

It 'Azure.AppService.ARRAffinity' {
Expand Down Expand Up @@ -255,6 +255,26 @@ Describe 'Azure.AppService' -Tag 'AppService' {
$ruleResult.Length | Should -Be 13;
$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';
}

It 'Azure.AppService.AvailabilityZone' {
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.AppService.AvailabilityZone' };

# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult.Length | Should -Be 2;
$ruleResult.TargetName | Should -Be 'plan-A', 'plan-D';

$ruleResult[0].Reason | Should -BeExactly @(
"The app service plan (plan-A) is not deployed with a SKU that supports zone-redundancy."
"The app service plan (plan-A) deployed to region (eastus) should use three availability zones from the following [1, 2, 3]."
);
$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].";

# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult.Length | Should -Be 4;
$ruleResult.TargetName | Should -Be 'plan-B', 'plan-C', 'plan-E', 'plan-F';
}
}

Context 'With Template' {
Expand Down
122 changes: 121 additions & 1 deletion tests/PSRule.Rules.Azure.Tests/Resources.AppService.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"ResourceType": "Microsoft.Web/serverfarms",
"Kind": "app",
"ResourceGroupName": "test-rg",
"Location": "region",
"Location": "eastus",
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
"Tags": null,
"Properties": {
Expand Down Expand Up @@ -608,6 +608,126 @@
"capacity": 1
}
},
{
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/plan-C",
"ResourceName": "plan-C",
"Name": "plan-C",
"ResourceType": "Microsoft.Web/serverfarms",
"Kind": "app",
"ResourceGroupName": "test-rg",
"Location": "notregion",
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
"Tags": null,
"Properties": {
"perSiteScaling": false,
"elasticScaleEnabled": false,
"maximumElasticWorkerCount": 1,
"isSpot": false,
"reserved": true,
"isXenon": false,
"hyperV": false,
"targetWorkerCount": 0,
"targetWorkerSizeId": 0,
"zoneRedundant": false
},
"Sku": {
"name": "P1v3",
"tier": "PremiumV3",
"size": "P1v3",
"family": "Pv3",
"capacity": 1
}
},
{
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/plan-D",
"ResourceName": "plan-D",
"Name": "plan-D",
"ResourceType": "Microsoft.Web/serverfarms",
"Kind": "app",
"ResourceGroupName": "test-rg",
"Location": "eastus",
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
"Tags": null,
"Properties": {
"perSiteScaling": false,
"elasticScaleEnabled": false,
"maximumElasticWorkerCount": 1,
"isSpot": false,
"reserved": true,
"isXenon": false,
"hyperV": false,
"targetWorkerCount": 0,
"targetWorkerSizeId": 0,
"zoneRedundant": false
},
"Sku": {
"name": "P1v3",
"tier": "PremiumV3",
"size": "P1v3",
"family": "Pv3",
"capacity": 1
}
},
{
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/plan-E",
"ResourceName": "plan-E",
"Name": "plan-E",
"ResourceType": "Microsoft.Web/serverfarms",
"Kind": "app",
"ResourceGroupName": "test-rg",
"Location": "eastus",
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
"Tags": null,
"Properties": {
"perSiteScaling": false,
"elasticScaleEnabled": false,
"maximumElasticWorkerCount": 1,
"isSpot": false,
"reserved": true,
"isXenon": false,
"hyperV": false,
"targetWorkerCount": 0,
"targetWorkerSizeId": 0,
"zoneRedundant": true
},
"Sku": {
"name": "P1v3",
"tier": "PremiumV3",
"size": "P1v3",
"family": "Pv3",
"capacity": 3
}
},
{
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/plan-F",
"ResourceName": "plan-F",
"Name": "plan-F",
"ResourceType": "Microsoft.Web/serverfarms",
"Kind": "elastic",
"ResourceGroupName": "test-rg",
"Location": "eastus",
"SubscriptionId": "00000000-0000-0000-0000-000000000000",
"Tags": null,
"Properties": {
"perSiteScaling": false,
"elasticScaleEnabled": false,
"maximumElasticWorkerCount": 1,
"isSpot": false,
"reserved": true,
"isXenon": false,
"hyperV": false,
"targetWorkerCount": 0,
"targetWorkerSizeId": 0,
"zoneRedundant": true
},
"Sku": {
"name": "EP1",
"tier": "ElasticPremium",
"size": "EP1",
"family": "EP",
"capacity": 3
}
},
{
"Name": "site-B",
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Web/sites/site-B",
Expand Down

0 comments on commit b8078d7

Please sign in to comment.