diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 1bcf0f3d88..368785def8 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -32,6 +32,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since v1.35.2: + +- Bug fixes: + - Fixed false positive with load balancers that use a public IP by @BernieWhite. + [#2814](https://github.com/Azure/PSRule.Rules.Azure/issues/2814) + ## v1.35.2 What's changed since v1.35.1: diff --git a/docs/en/rules/Azure.LB.AvailabilityZone.md b/docs/en/rules/Azure.LB.AvailabilityZone.md index 73bade7787..40826ce9f6 100644 --- a/docs/en/rules/Azure.LB.AvailabilityZone.md +++ b/docs/en/rules/Azure.LB.AvailabilityZone.md @@ -1,4 +1,5 @@ --- +reviewed: 2024-04-11 severity: Important pillar: Reliability category: RE:05 Regions and availability zones @@ -6,7 +7,7 @@ resource: Load Balancer online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.LB.AvailabilityZone/ --- -# Load balancers should be zone-redundant +# Internal load balancers should be zone-redundant ## SYNOPSIS @@ -14,20 +15,20 @@ Load balancers deployed with Standard SKU should be zone-redundant for high avai ## DESCRIPTION -Load balancers using availability zones improve reliability and ensure availability during failure scenarios affecting a data center within a region. -A single zone redundant frontend IP address will survive zone failure. -The frontend IP may be used to reach all (non-impacted) backend pool members no matter the zone. -One or more availability zones can fail and the data path survives as long as one zone in the region remains healthy. +A load balancer is an Azure service that distributes traffic among instances of a service in a backend pool (such as VMs). +Load balancers route traffic to healthy instances in the backend pool based on configured rules. +However if the load balancer itself becomes unavailable, traffic sent through the load balancer may become disrupted. -## RECOMMENDATION +In a region that supports availability zones, Standard Load Balancers can be deployed across multiple zones (zone-redundant). +A zone-redundant Load Balancer allows traffic to be served by a single frontend IP address that can survive zone failure. -Consider using zone-redundant load balancers deployed with Standard SKU. +Also consider the data path to the backend pool, and ensure that the backend pool is deployed with zone-redundancy in mind. -## NOTES +In a region that supports availability zones, Standard Load Balancers should be deployed with zone-redundancy. -This rule applies when analyzing resources deployed to Azure using *pre-flight* and *in-flight* data. +## RECOMMENDATION -This rule fails when `"zones"` is constrained to a single(zonal) zone or is not configured, and passes when set to `["1", "2", "3"]`. +Consider using load balancers deployed across at least two availability zones. ## EXAMPLES @@ -35,48 +36,40 @@ This rule fails when `"zones"` is constrained to a single(zonal) zone or is not To configure zone-redundancy for a load balancer. -- Set `sku.name` to `Standard`. -- Set `properties.frontendIPConfigurations[*].zones` to `["1", "2", "3"]`. +- Set the `sku.name` property to `Standard`. +- Set the `properties.frontendIPConfigurations[*].zones` property to at least two availability zones. + e.g. `1`, `2`, `3`. For example: ```json { - "apiVersion": "2020-07-01", - "name": "[parameters('name')]", - "type": "Microsoft.Network/loadBalancers", - "location": "[parameters('location')]", - "dependsOn": [], - "tags": {}, - "properties": { - "frontendIPConfigurations": [ - { - "name": "frontend-ip-config", - "properties": { - "privateIPAddress": null, - "privateIPAddressVersion": "IPv4", - "privateIPAllocationMethod": "Dynamic", - "subnet": { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/virtualNetworks/lb-vnet/subnets/default" - } - }, - "zones": [ - "1", - "2", - "3" - ] - } - ], - "backendAddressPools": [], - "probes": [], - "loadBalancingRules": [], - "inboundNatRules": [], - "outboundRules": [] - }, - "sku": { - "name": "[parameters('sku')]", - "tier": "[parameters('tier')]" - } + "type": "Microsoft.Network/loadBalancers", + "apiVersion": "2023-09-01", + "name": "[parameters('lbName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "properties": { + "frontendIPConfigurations": [ + { + "name": "frontendIPConfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[reference(resourceId('Microsoft.Network/virtualNetworks', parameters('name')), '2023-09-01').subnets[1].id]" + } + }, + "zones": [ + "1", + "2", + "3" + ] + } + ] + } } ``` @@ -84,17 +77,19 @@ For example: To configure zone-redundancy for a load balancer. -- Set `sku.name` to `Standard`. -- Set `properties.frontendIPConfigurations[*].zones` to `['1', '2', '3']`. +- Set the `sku.name` property to `Standard`. +- Set the `properties.frontendIPConfigurations[*].zones` property to at least two availability zones. + e.g. `1`, `2`, `3`. For example: ```bicep -resource lb_001 'Microsoft.Network/loadBalancers@2021-02-01' = { +resource internal_lb 'Microsoft.Network/loadBalancers@2023-09-01' = { name: lbName location: location sku: { name: 'Standard' + tier: 'Regional' } properties: { frontendIPConfigurations: [ @@ -117,8 +112,26 @@ resource lb_001 'Microsoft.Network/loadBalancers@2021-02-01' = { } ``` + + +## NOTES + +This rule applies to internal load balancers deployed with Standard SKU. +Internal load balancers do not have a public IP address and are used to load balance traffic inside a virtual network. + +The `zones` property is not supported with: + +- Public load balancers, which are load balancers with a public IP address. + To address availability zones for public load balancers, use a Standard tier zone-redundant public IP address. +- Load balancers deployed with Basic SKU, which are not zone-redundant. + +For regions that support availability zones, the `zones` property should be set to at least two zones. + ## LINKS - [RE:05 Regions and availability zones](https://learn.microsoft.com/azure/well-architected/reliability/regions-availability-zones) +- [What is Azure Load Balancer?](https://learn.microsoft.com/azure/load-balancer/load-balancer-overview) +- [Azure Load Balancer components](https://learn.microsoft.com/azure/load-balancer/components#frontend-ip-configurations) - [Reliability in Load Balancer](https://learn.microsoft.com/azure/reliability/reliability-load-balancer) +- [Zone redundant load balancer](https://learn.microsoft.com/azure/reliability/reliability-load-balancer#zone-redundant-load-balancer) - [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.network/loadbalancers) diff --git a/docs/examples-vnet.bicep b/docs/examples-vnet.bicep index baedb884ab..7577f173ed 100644 --- a/docs/examples-vnet.bicep +++ b/docs/examples-vnet.bicep @@ -9,8 +9,6 @@ param name string @description('The location resources will be deployed.') param location string = resourceGroup().location -param asgName string = 'asg-001' -param nsgName string = 'nsg-001' param lbName string = 'lb-001' // An example virtual network (VNET) with NSG configured. @@ -139,12 +137,13 @@ resource asg 'Microsoft.Network/applicationSecurityGroups@2023-09-01' = { properties: {} } -// An example internal load balancer. -resource lb_001 'Microsoft.Network/loadBalancers@2023-09-01' = { +// An example internal load balancer with availability zones configured. +resource internal_lb 'Microsoft.Network/loadBalancers@2023-09-01' = { name: lbName location: location sku: { name: 'Standard' + tier: 'Regional' } properties: { frontendIPConfigurations: [ @@ -166,6 +165,48 @@ resource lb_001 'Microsoft.Network/loadBalancers@2023-09-01' = { } } +// An example zone redundant public IP address. +resource pip 'Microsoft.Network/publicIPAddresses@2023-09-01' = { + name: 'pip-001' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAddressVersion: 'IPv4' + publicIPAllocationMethod: 'Static' + idleTimeoutInMinutes: 4 + } + zones: [ + '1' + '2' + '3' + ] +} + +// An example public load balancer. +resource public_lb 'Microsoft.Network/loadBalancers@2023-09-01' = { + name: lbName + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + frontendIPConfigurations: [ + { + name: 'frontendIPConfig' + properties: { + publicIPAddress: { + id: pip.id + } + } + } + ] + } +} + // An example VNET with a GatewaySubnet, AzureBastionSubnet, and AzureBastionSubnet. resource spoke 'Microsoft.Network/virtualNetworks@2023-09-01' = { name: name diff --git a/docs/examples-vnet.json b/docs/examples-vnet.json index 5a157caa4b..090ba40b49 100644 --- a/docs/examples-vnet.json +++ b/docs/examples-vnet.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.25.53.49325", - "templateHash": "15957041710021771979" + "version": "0.26.54.24096", + "templateHash": "10466611904245566781" } }, "parameters": { @@ -22,14 +22,6 @@ "description": "The location resources will be deployed." } }, - "asgName": { - "type": "string", - "defaultValue": "asg-001" - }, - "nsgName": { - "type": "string", - "defaultValue": "nsg-001" - }, "lbName": { "type": "string", "defaultValue": "lb-001" @@ -171,7 +163,8 @@ "name": "[parameters('lbName')]", "location": "[parameters('location')]", "sku": { - "name": "Standard" + "name": "Standard", + "tier": "Regional" }, "properties": { "frontendIPConfigurations": [ @@ -195,6 +188,51 @@ "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" ] }, + { + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2023-09-01", + "name": "pip-001", + "location": "[parameters('location')]", + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "properties": { + "publicIPAddressVersion": "IPv4", + "publicIPAllocationMethod": "Static", + "idleTimeoutInMinutes": 4 + }, + "zones": [ + "1", + "2", + "3" + ] + }, + { + "type": "Microsoft.Network/loadBalancers", + "apiVersion": "2023-09-01", + "name": "[parameters('lbName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "properties": { + "frontendIPConfigurations": [ + { + "name": "frontendIPConfig", + "properties": { + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', 'pip-001')]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses', 'pip-001')]" + ] + }, { "type": "Microsoft.Network/virtualNetworks", "apiVersion": "2023-09-01", diff --git a/src/PSRule.Rules.Azure/rules/Azure.LB.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Azure.LB.Rule.ps1 index 2cd2e0fe55..c3edeb4950 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.LB.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Azure.LB.Rule.ps1 @@ -36,12 +36,18 @@ Rule 'Azure.LB.AvailabilityZone' -Ref 'AZR-000127' -Type 'Microsoft.Network/load return $Assert.Pass(); } - foreach ($ipConfig in $TargetObject.Properties.frontendIPConfigurations) { - $Assert.SetOf($ipConfig, 'zones', @('1', '2', '3')).Reason( - $LocalizedData.LBAvailabilityZone, - $TargetObject.name, - $ipConfig.name - ) + foreach ($ipconfig in $TargetObject.properties.frontendIPConfigurations) { + # The zones property only applies to internal load balancers. + if ($Assert.HasFieldValue($ipconfig, 'properties.publicIPAddress.id').Result) { + $Assert.Pass() + } + else { + $Assert.GreaterOrEqual($ipconfig, 'zones', 2).Reason( + $LocalizedData.LBAvailabilityZone, + $TargetObject.name, + $ipConfig.name + ) + } } } diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.LB.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.LB.Tests.ps1 index accbd0e979..91c7adb810 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.LB.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.LB.Tests.ps1 @@ -66,16 +66,16 @@ Describe 'Azure.LB' -Tag 'Network', 'LB' { $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); $ruleResult | Should -Not -BeNullOrEmpty; $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -Be 'lb-A'; + $ruleResult.TargetName | Should -Be 'lb-B'; $ruleResult[0].Reason | Should -Not -BeNullOrEmpty; - $ruleResult[0].Reason | Should -BeExactly "The load balancer (lb-A) frontend IP configuration (frontend-A) should be zone-redundant."; + $ruleResult[0].Reason | Should -BeExactly "The load balancer (lb-B) frontend IP configuration (frontend-A) should be zone-redundant."; # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; $ruleResult.Length | Should -Be 3; - $ruleResult.TargetName | Should -Be 'kubernetes', 'lb-B', 'lb-C'; + $ruleResult.TargetName | Should -Be 'kubernetes', 'lb-A', 'lb-C'; # None $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' -and $_.TargetType -eq 'Microsoft.Network/loadBalancers' }); diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json b/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json index 7876c67faa..ed584ea382 100644 --- a/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json +++ b/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json @@ -1736,10 +1736,7 @@ } ], "privateIPAddressVersion": "IPv4" - }, - "zones": [ - "3" - ] + } } ], "backendAddressPools": [ @@ -1898,9 +1895,6 @@ "properties": { "provisioningState": "Succeeded", "privateIPAllocationMethod": "Dynamic", - "publicIPAddress": { - "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/publicIPAddresses/lb-B-ip-A" - }, "loadBalancingRules": [ { "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/loadBalancers/lb-B/loadBalancingRules/rule-TCP-80" @@ -1912,8 +1906,6 @@ "privateIPAddressVersion": "IPv4" }, "zones": [ - "1", - "2", "3" ] } @@ -2072,9 +2064,6 @@ "properties": { "provisioningState": "Succeeded", "privateIPAllocationMethod": "Dynamic", - "publicIPAddress": { - "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/publicIPAddresses/lb-C-ip-A" - }, "loadBalancingRules": [ { "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/loadBalancers/lb-C/loadBalancingRules/rule-TCP-80"