From a751f27ef075fe24f639e5e40dce1b52d4f45ed6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:58:09 +0000 Subject: [PATCH 1/7] Initial plan From c47df30b8e554dd610eb83d75998e96f1431e1da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:17:32 +0000 Subject: [PATCH 2/7] Add Azure.ContainerApp.HealthProbe rule (AZR-000535) for HTTP health probe checks Co-authored-by: BernieWhite <13513058+BernieWhite@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/PSRule.Rules.Azure/sessions/9bd40ff2-e52b-4e37-8cd4-2f3a82a979db --- docs/changelog.md | 4 + .../rules/Azure.ContainerApp.HealthProbe.md | 180 ++++++++++++++++++ docs/en/rules/index.md | 1 + .../rules/Azure.ContainerApp.Rule.ps1 | 28 +++ .../Azure.ContainerApp.Tests.ps1 | 44 +++-- .../Resources.ContainerApp.json | 180 ++++++++++++++++++ 6 files changed, 423 insertions(+), 14 deletions(-) create mode 100644 docs/en/rules/Azure.ContainerApp.HealthProbe.md diff --git a/docs/changelog.md b/docs/changelog.md index 953a25e7049..01078974b81 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -30,6 +30,10 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +- New rules: + - Container Apps: + - Check that liveness and readiness health probes use HTTP checks for HTTP-based ingress. + [#3714](https://github.com/Azure/PSRule.Rules.Azure/issues/3714) - Updated rules: - Azure Kubernetes Service: - Updated `Azure.AKS.Version` to use `1.33.7` as the minimum version by @BernieWhite. diff --git a/docs/en/rules/Azure.ContainerApp.HealthProbe.md b/docs/en/rules/Azure.ContainerApp.HealthProbe.md new file mode 100644 index 00000000000..a4981edf093 --- /dev/null +++ b/docs/en/rules/Azure.ContainerApp.HealthProbe.md @@ -0,0 +1,180 @@ +--- +reviewed: 2026-03-25 +severity: Important +pillar: Reliability +category: RE:07 Self-preservation +resource: Container App +resourceType: Microsoft.App/containerApps +online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ContainerApp.HealthProbe/ +--- + +# Use HTTP health probes for HTTP-based ingress + +## SYNOPSIS + +Container app ingress that uses HTTP should have HTTP health probes configured for liveness and readiness checks. + +## DESCRIPTION + +Azure Container Apps supports health probes to determine the health and readiness of your containers. +Health probes can be configured as HTTP, TCP, or gRPC checks, and support liveness, readiness, and startup probe types. + +When a container app uses HTTP-based ingress (transport is `http` or `http2`, or the target port is `80`, `8080`, or `443`), +health probes should use HTTP checks (`httpGet`) for liveness and readiness probes. +HTTP health probes provide granular feedback by checking the HTTP response status code, +which gives more accurate information about whether a replica is available and ready to receive traffic +compared to a TCP port check which only determines if a port is open or closed. + +The default health probes use TCP port checks when no probes are explicitly configured. +Configuring HTTP health probes instead allows the platform to better detect and respond to application-level failures. + +Startup probes are excluded from this check as they are commonly configured as TCP for initial container startup purposes. + +## RECOMMENDATION + +Consider configuring HTTP health probes (`httpGet`) for liveness and readiness probes on containers +that use HTTP-based ingress. + +## EXAMPLES + +### Configure with Azure template + +To deploy Container Apps that pass this rule: + +- For each container in `properties.template.containers`: + - Configure a `Liveness` probe with `httpGet` in the `probes` array. + - Configure a `Readiness` probe with `httpGet` in the `probes` array. + +For example: + +```json +{ + "type": "Microsoft.App/containerApps", + "apiVersion": "2024-03-01", + "name": "[parameters('appName')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "environmentId": "[resourceId('Microsoft.App/managedEnvironments', parameters('envName'))]", + "configuration": { + "ingress": { + "external": false, + "targetPort": 8080, + "transport": "http" + } + }, + "template": { + "containers": [ + { + "name": "app", + "image": "[parameters('image')]", + "probes": [ + { + "type": "Liveness", + "httpGet": { + "path": "/healthz", + "port": 8080 + }, + "initialDelaySeconds": 5, + "periodSeconds": 10 + }, + { + "type": "Readiness", + "httpGet": { + "path": "/healthz/ready", + "port": 8080 + }, + "initialDelaySeconds": 5, + "periodSeconds": 10 + } + ] + } + ], + "scale": { + "minReplicas": 2 + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', parameters('envName'))]" + ] +} +``` + +### Configure with Bicep + +To deploy Container Apps that pass this rule: + +- For each container in `properties.template.containers`: + - Configure a `Liveness` probe with `httpGet` in the `probes` array. + - Configure a `Readiness` probe with `httpGet` in the `probes` array. + +For example: + +```bicep +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { + name: appName + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + environmentId: containerEnv.id + configuration: { + ingress: { + external: false + targetPort: 8080 + transport: 'http' + } + } + template: { + containers: [ + { + name: 'app' + image: image + probes: [ + { + type: 'Liveness' + httpGet: { + path: '/healthz' + port: 8080 + } + initialDelaySeconds: 5 + periodSeconds: 10 + } + { + type: 'Readiness' + httpGet: { + path: '/healthz/ready' + port: 8080 + } + initialDelaySeconds: 5 + periodSeconds: 10 + } + ] + } + ] + scale: { + minReplicas: 2 + } + } + } +} +``` + +## NOTES + +This rule applies to container apps where the ingress transport is `http` or `http2`, +or where the target port is `80`, `8080`, or `443`. + +Startup probes are excluded from this check. + +When multiple containers are defined, each container must have both liveness and readiness HTTP probes configured. + +## LINKS + +- [RE:07 Self-preservation](https://learn.microsoft.com/azure/well-architected/reliability/self-preservation) +- [Health probes in Azure Container Apps](https://learn.microsoft.com/azure/container-apps/health-probes) +- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.app/containerapps#containerappprobe) diff --git a/docs/en/rules/index.md b/docs/en/rules/index.md index 12f6e475686..0d7b83c1274 100644 --- a/docs/en/rules/index.md +++ b/docs/en/rules/index.md @@ -553,5 +553,6 @@ AZR-000531 | [Azure.ServiceFabric.ManagedNaming](Azure.ServiceFabric.ManagedNami AZR-000532 | [Azure.EventHub.AvailabilityZone](Azure.EventHub.AvailabilityZone.md) | Use zone redundant Event Hub namespaces in supported regions to improve reliability. | GA AZR-000533 | [Azure.Redis.MigrateAMR](Azure.Redis.MigrateAMR.md) | Azure Cache for Redis is being retired. Migrate to Azure Managed Redis. | GA AZR-000534 | [Azure.RedisEnterprise.MigrateAMR](Azure.RedisEnterprise.MigrateAMR.md) | Azure Cache for Redis Enterprise and Enterprise Flash are being retired. Migrate to Azure Managed Redis. | GA +AZR-000535 | [Azure.ContainerApp.HealthProbe](Azure.ContainerApp.HealthProbe.md) | Container apps using HTTP-based ingress should use HTTP health probes for liveness and readiness checks. | GA *[GA]: Generally Available — Rules related to a generally available Azure features. diff --git a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.ps1 index 799a8c8f9ab..629aba4e2a7 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.ps1 @@ -48,6 +48,24 @@ Rule 'Azure.ContainerApp.JobNaming' -Ref 'AZR-000512' -Type 'Microsoft.App/jobs' $Assert.Match($PSRule, 'TargetName', $Configuration.AZURE_CONTAINER_APP_JOB_NAME_FORMAT, $True); } +# Synopsis: Container app ingress that uses HTTP should have HTTP health probes configured for liveness and readiness checks. +Rule 'Azure.ContainerApp.HealthProbe' -Ref 'AZR-000535' -Type 'Microsoft.App/containerApps' -If { IsHttpIngress } -Tag @{ release = 'GA'; ruleSet = '2025_12'; 'Azure.WAF/pillar' = 'Reliability'; } { + $containers = @($TargetObject.properties.template.containers) + foreach ($container in $containers) { + foreach ($probeType in 'Liveness', 'Readiness') { + $probes = @($container.probes | Where-Object { $_.type -eq $probeType }) + if ($probes.Length -eq 0) { + $Assert.Fail() + } + else { + foreach ($probe in $probes) { + $Assert.HasFieldValue($probe, 'httpGet') + } + } + } + } +} + #endregion Rules #region Helper functions @@ -60,4 +78,14 @@ function global:HasIngress { } } +function global:IsHttpIngress { + [CmdletBinding()] + param () + process { + $ingress = $TargetObject.properties.configuration.ingress + if ($null -eq $ingress) { return $False } + ($ingress.transport -in 'http', 'http2') -or ($ingress.targetPort -in 80, 8080, 443) + } +} + #endregion Helper functions diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.ContainerApp.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.ContainerApp.Tests.ps1 index 9724e104590..f0f407bd864 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.ContainerApp.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.ContainerApp.Tests.ps1 @@ -51,8 +51,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 2; - $ruleResult.TargetName | Should -BeIn 'capp-A', 'capp-C'; + $ruleResult.Length | Should -Be 4; + $ruleResult.TargetName | Should -BeIn 'capp-A', 'capp-C', 'capp-E', 'capp-F'; } It 'Azure.ContainerApp.ManagedIdentity' { @@ -68,8 +68,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 2; - $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D'; + $ruleResult.Length | Should -Be 4; + $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D', 'capp-E', 'capp-F'; } It 'Azure.ContainerApp.PublicAccess' { @@ -102,8 +102,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 2; - $ruleResult.TargetName | Should -BeIn 'capp-A', 'capp-D'; + $ruleResult.Length | Should -Be 4; + $ruleResult.TargetName | Should -BeIn 'capp-A', 'capp-D', 'capp-E', 'capp-F'; } It 'Azure.ContainerApp.Storage' { @@ -136,8 +136,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 3; - $ruleResult.TargetName | Should -BeIn 'capp-B', 'capp-C', 'capp-D'; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -BeIn 'capp-B', 'capp-C', 'capp-D', 'capp-E', 'capp-F'; } It 'Azure.ContainerApp.RestrictIngress' { @@ -155,8 +155,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -BeIn 'capp-D'; + $ruleResult.Length | Should -Be 3; + $ruleResult.TargetName | Should -BeIn 'capp-D', 'capp-E', 'capp-F'; } It 'Azure.ContainerApp.APIVersion' { @@ -171,8 +171,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); - $ruleResult.Length | Should -Be 2; - $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D'; + $ruleResult.Length | Should -Be 4; + $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D', 'capp-E', 'capp-F'; } It 'Azure.ContainerApp.MinReplicas' { @@ -186,8 +186,24 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); - $ruleResult.Length | Should -Be 2; - $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D'; + $ruleResult.Length | Should -Be 4; + $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D', 'capp-E', 'capp-F'; + } + + It 'Azure.ContainerApp.HealthProbe' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.ContainerApp.HealthProbe' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -BeIn 'capp-A', 'capp-B', 'capp-C', 'capp-D', 'capp-F'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 1; + $ruleResult.TargetName | Should -BeIn 'capp-E'; } It 'Azure.ContainerApp.AvailabilityZone' { diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.ContainerApp.json b/tests/PSRule.Rules.Azure.Tests/Resources.ContainerApp.json index 7c77b774eeb..8e47bab9919 100644 --- a/tests/PSRule.Rules.Azure.Tests/Resources.ContainerApp.json +++ b/tests/PSRule.Rules.Azure.Tests/Resources.ContainerApp.json @@ -485,5 +485,185 @@ "Sku": null, "Tags": null, "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.App/containerApps/capp-E", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.App/containerApps/capp-E", + "Identity": { + "type": "SystemAssigned", + "userAssignedIdentities": null + }, + "Kind": null, + "Location": "Canada Central", + "ManagedBy": null, + "ResourceName": "capp-E", + "Name": "capp-E", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "provisioningState": "Succeeded", + "configuration": { + "activeRevisionsMode": "Single", + "ingress": { + "fqdn": "capp-E.env-A.canadacentral.azurecontainerapps.io", + "allowInsecure": false, + "external": false, + "ipSecurityRestrictions": [ + { + "action": "Allow", + "description": "ClientIPAddress_1", + "ipAddressRange": "10.1.1.1/32", + "name": "ClientIPAddress_1" + } + ], + "targetPort": 443, + "transport": "http", + "traffic": [ + { + "weight": 100, + "latestRevision": true + } + ] + } + }, + "template": { + "revisionSuffix": "", + "containers": [ + { + "image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", + "name": "simple-hello-world-container", + "resources": { + "cpu": 0.25, + "memory": ".5Gi" + }, + "probes": [ + { + "type": "Liveness", + "httpGet": { + "path": "/healthz", + "port": 443 + }, + "initialDelaySeconds": 3, + "periodSeconds": 3 + }, + { + "type": "Readiness", + "httpGet": { + "path": "/healthz", + "port": 443 + }, + "initialDelaySeconds": 3, + "periodSeconds": 3 + }, + { + "type": "Startup", + "tcpSocket": { + "port": 443 + }, + "initialDelaySeconds": 3, + "periodSeconds": 3 + } + ] + } + ], + "scale": { + "minReplicas": 2, + "maxReplicas": 10 + } + } + }, + "ResourceGroupName": "test-rg", + "Type": "Microsoft.App/containerApps", + "ResourceType": "Microsoft.App/containerApps", + "apiVersion": "2024-03-01", + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.App/containerApps/capp-F", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.App/containerApps/capp-F", + "Identity": { + "type": "SystemAssigned", + "userAssignedIdentities": null + }, + "Kind": null, + "Location": "Canada Central", + "ManagedBy": null, + "ResourceName": "capp-F", + "Name": "capp-F", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "provisioningState": "Succeeded", + "configuration": { + "activeRevisionsMode": "Single", + "ingress": { + "fqdn": "capp-F.env-A.canadacentral.azurecontainerapps.io", + "allowInsecure": false, + "external": false, + "ipSecurityRestrictions": [ + { + "action": "Allow", + "description": "ClientIPAddress_1", + "ipAddressRange": "10.1.1.1/32", + "name": "ClientIPAddress_1" + } + ], + "targetPort": 8080, + "transport": "http", + "traffic": [ + { + "weight": 100, + "latestRevision": true + } + ] + } + }, + "template": { + "revisionSuffix": "", + "containers": [ + { + "image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", + "name": "simple-hello-world-container", + "resources": { + "cpu": 0.25, + "memory": ".5Gi" + }, + "probes": [ + { + "type": "Liveness", + "tcpSocket": { + "port": 8080 + }, + "initialDelaySeconds": 3, + "periodSeconds": 3 + }, + { + "type": "Readiness", + "tcpSocket": { + "port": 8080 + }, + "initialDelaySeconds": 3, + "periodSeconds": 3 + } + ] + } + ], + "scale": { + "minReplicas": 2, + "maxReplicas": 10 + } + } + }, + "ResourceGroupName": "test-rg", + "Type": "Microsoft.App/containerApps", + "ResourceType": "Microsoft.App/containerApps", + "apiVersion": "2024-03-01", + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" } ] From 988f3c5aec2bfa29a096e80d31d307c6659e44ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:02:44 +0000 Subject: [PATCH 3/7] Convert Azure.ContainerApp.HealthProbe rule from PowerShell to YAML Co-authored-by: BernieWhite <13513058+BernieWhite@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/PSRule.Rules.Azure/sessions/2e3e1e50-6ac6-4f43-ad05-caabf4076f28 --- .../rules/Azure.ContainerApp.Rule.ps1 | 28 --------- .../rules/Azure.ContainerApp.Rule.yaml | 59 +++++++++++++++++++ 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.ps1 index 629aba4e2a7..799a8c8f9ab 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.ps1 @@ -48,24 +48,6 @@ Rule 'Azure.ContainerApp.JobNaming' -Ref 'AZR-000512' -Type 'Microsoft.App/jobs' $Assert.Match($PSRule, 'TargetName', $Configuration.AZURE_CONTAINER_APP_JOB_NAME_FORMAT, $True); } -# Synopsis: Container app ingress that uses HTTP should have HTTP health probes configured for liveness and readiness checks. -Rule 'Azure.ContainerApp.HealthProbe' -Ref 'AZR-000535' -Type 'Microsoft.App/containerApps' -If { IsHttpIngress } -Tag @{ release = 'GA'; ruleSet = '2025_12'; 'Azure.WAF/pillar' = 'Reliability'; } { - $containers = @($TargetObject.properties.template.containers) - foreach ($container in $containers) { - foreach ($probeType in 'Liveness', 'Readiness') { - $probes = @($container.probes | Where-Object { $_.type -eq $probeType }) - if ($probes.Length -eq 0) { - $Assert.Fail() - } - else { - foreach ($probe in $probes) { - $Assert.HasFieldValue($probe, 'httpGet') - } - } - } - } -} - #endregion Rules #region Helper functions @@ -78,14 +60,4 @@ function global:HasIngress { } } -function global:IsHttpIngress { - [CmdletBinding()] - param () - process { - $ingress = $TargetObject.properties.configuration.ingress - if ($null -eq $ingress) { return $False } - ($ingress.transport -in 'http', 'http2') -or ($ingress.targetPort -in 80, 8080, 443) - } -} - #endregion Helper functions diff --git a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml index 1e69c1306b3..93e54c152ce 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml +++ b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml @@ -206,6 +206,40 @@ spec: field: properties.template.scale.minReplicas greaterOrEquals: 2 +--- +# Synopsis: Container app ingress that uses HTTP should have HTTP health probes configured for liveness and readiness checks. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Azure.ContainerApp.HealthProbe + ref: AZR-000535 + tags: + release: GA + ruleSet: 2025_12 + Azure.WAF/pillar: Reliability +spec: + with: + - Azure.ContainerApp.IsHttpIngress + condition: + field: properties.template.containers + allOf: + - field: probes + anyOf: + - allOf: + - field: type + equals: Liveness + - field: httpGet + hasValue: true + count: 1 + - field: probes + anyOf: + - allOf: + - field: type + equals: Readiness + - field: httpGet + hasValue: true + count: 1 + #endregion Rules #region Selectors @@ -224,4 +258,29 @@ spec: - field: properties.configuration.ingress exists: true +--- +# Synopsis: Get container apps with HTTP-based ingress. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Azure.ContainerApp.IsHttpIngress +spec: + if: + allOf: + - type: '.' + equals: Microsoft.App/containerApps + - field: properties.configuration.ingress + exists: true + - anyOf: + - field: properties.configuration.ingress.transport + in: + - http + - http2 + - field: properties.configuration.ingress.targetPort + equals: 80 + - field: properties.configuration.ingress.targetPort + equals: 8080 + - field: properties.configuration.ingress.targetPort + equals: 443 + #endregion Selectors From f2da7118130abaaa973d6eff8a73125b601b0b0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:31:14 +0000 Subject: [PATCH 4/7] Add capp-G test resource with http2 ingress transport and httpGet probes Co-authored-by: BernieWhite <13513058+BernieWhite@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/PSRule.Rules.Azure/sessions/193049a1-b5b9-4715-ae49-a41c2411fa36 --- .../Azure.ContainerApp.Tests.ps1 | 32 +++---- .../Resources.ContainerApp.json | 87 +++++++++++++++++++ 2 files changed, 103 insertions(+), 16 deletions(-) diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.ContainerApp.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.ContainerApp.Tests.ps1 index f0f407bd864..41ae7e90246 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.ContainerApp.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.ContainerApp.Tests.ps1 @@ -51,8 +51,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 4; - $ruleResult.TargetName | Should -BeIn 'capp-A', 'capp-C', 'capp-E', 'capp-F'; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -BeIn 'capp-A', 'capp-C', 'capp-E', 'capp-F', 'capp-G'; } It 'Azure.ContainerApp.ManagedIdentity' { @@ -68,8 +68,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 4; - $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D', 'capp-E', 'capp-F'; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D', 'capp-E', 'capp-F', 'capp-G'; } It 'Azure.ContainerApp.PublicAccess' { @@ -102,8 +102,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 4; - $ruleResult.TargetName | Should -BeIn 'capp-A', 'capp-D', 'capp-E', 'capp-F'; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -BeIn 'capp-A', 'capp-D', 'capp-E', 'capp-F', 'capp-G'; } It 'Azure.ContainerApp.Storage' { @@ -136,8 +136,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 5; - $ruleResult.TargetName | Should -BeIn 'capp-B', 'capp-C', 'capp-D', 'capp-E', 'capp-F'; + $ruleResult.Length | Should -Be 6; + $ruleResult.TargetName | Should -BeIn 'capp-B', 'capp-C', 'capp-D', 'capp-E', 'capp-F', 'capp-G'; } It 'Azure.ContainerApp.RestrictIngress' { @@ -155,8 +155,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 3; - $ruleResult.TargetName | Should -BeIn 'capp-D', 'capp-E', 'capp-F'; + $ruleResult.Length | Should -Be 4; + $ruleResult.TargetName | Should -BeIn 'capp-D', 'capp-E', 'capp-F', 'capp-G'; } It 'Azure.ContainerApp.APIVersion' { @@ -171,8 +171,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); - $ruleResult.Length | Should -Be 4; - $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D', 'capp-E', 'capp-F'; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D', 'capp-E', 'capp-F', 'capp-G'; } It 'Azure.ContainerApp.MinReplicas' { @@ -186,8 +186,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); - $ruleResult.Length | Should -Be 4; - $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D', 'capp-E', 'capp-F'; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -BeIn 'capp-C', 'capp-D', 'capp-E', 'capp-F', 'capp-G'; } It 'Azure.ContainerApp.HealthProbe' { @@ -202,8 +202,8 @@ Describe 'Azure.ContainerApp' -Tag 'ContainerApp' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -BeIn 'capp-E'; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'capp-E', 'capp-G'; } It 'Azure.ContainerApp.AvailabilityZone' { diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.ContainerApp.json b/tests/PSRule.Rules.Azure.Tests/Resources.ContainerApp.json index 8e47bab9919..ad966872d29 100644 --- a/tests/PSRule.Rules.Azure.Tests/Resources.ContainerApp.json +++ b/tests/PSRule.Rules.Azure.Tests/Resources.ContainerApp.json @@ -665,5 +665,92 @@ "Sku": null, "Tags": null, "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.App/containerApps/capp-G", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.App/containerApps/capp-G", + "Identity": { + "type": "SystemAssigned", + "userAssignedIdentities": null + }, + "Kind": null, + "Location": "Canada Central", + "ManagedBy": null, + "ResourceName": "capp-G", + "Name": "capp-G", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "provisioningState": "Succeeded", + "configuration": { + "activeRevisionsMode": "Single", + "ingress": { + "fqdn": "capp-G.env-A.canadacentral.azurecontainerapps.io", + "allowInsecure": false, + "external": false, + "ipSecurityRestrictions": [ + { + "action": "Allow", + "description": "ClientIPAddress_1", + "ipAddressRange": "10.1.1.1/32", + "name": "ClientIPAddress_1" + } + ], + "targetPort": 443, + "transport": "http2", + "traffic": [ + { + "weight": 100, + "latestRevision": true + } + ] + } + }, + "template": { + "revisionSuffix": "", + "containers": [ + { + "image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", + "name": "simple-hello-world-container", + "resources": { + "cpu": 0.25, + "memory": ".5Gi" + }, + "probes": [ + { + "type": "Liveness", + "httpGet": { + "path": "/healthz", + "port": 443 + }, + "initialDelaySeconds": 3, + "periodSeconds": 3 + }, + { + "type": "Readiness", + "httpGet": { + "path": "/healthz", + "port": 443 + }, + "initialDelaySeconds": 3, + "periodSeconds": 3 + } + ] + } + ], + "scale": { + "minReplicas": 2, + "maxReplicas": 10 + } + } + }, + "ResourceGroupName": "test-rg", + "Type": "Microsoft.App/containerApps", + "ResourceType": "Microsoft.App/containerApps", + "apiVersion": "2024-03-01", + "Sku": null, + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" } ] From 25d07c5e3f7f7effe180fcdc551e0dea9881b28b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:34:57 +0000 Subject: [PATCH 5/7] Update Azure.ContainerApp.HealthProbe ruleSet to 2026_03 Co-authored-by: BernieWhite <13513058+BernieWhite@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/PSRule.Rules.Azure/sessions/5bdacc1d-dd37-4fff-b0e2-023d06a15bd2 --- src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml index 93e54c152ce..c4179717fd0 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml +++ b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml @@ -215,7 +215,7 @@ metadata: ref: AZR-000535 tags: release: GA - ruleSet: 2025_12 + ruleSet: 2026_03 Azure.WAF/pillar: Reliability spec: with: From 7728154170965e4585f9817cf8823528120e0bb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:49:56 +0000 Subject: [PATCH 6/7] Update rule ID to AZR-000537 and ruleSet to 2026_06 Co-authored-by: BernieWhite <13513058+BernieWhite@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/PSRule.Rules.Azure/sessions/4af2dd6f-c955-45c0-9fd3-7cf0fd1d4e6b --- docs/en/rules/index.md | 2 +- src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/rules/index.md b/docs/en/rules/index.md index 0d7b83c1274..920b315c5fb 100644 --- a/docs/en/rules/index.md +++ b/docs/en/rules/index.md @@ -553,6 +553,6 @@ AZR-000531 | [Azure.ServiceFabric.ManagedNaming](Azure.ServiceFabric.ManagedNami AZR-000532 | [Azure.EventHub.AvailabilityZone](Azure.EventHub.AvailabilityZone.md) | Use zone redundant Event Hub namespaces in supported regions to improve reliability. | GA AZR-000533 | [Azure.Redis.MigrateAMR](Azure.Redis.MigrateAMR.md) | Azure Cache for Redis is being retired. Migrate to Azure Managed Redis. | GA AZR-000534 | [Azure.RedisEnterprise.MigrateAMR](Azure.RedisEnterprise.MigrateAMR.md) | Azure Cache for Redis Enterprise and Enterprise Flash are being retired. Migrate to Azure Managed Redis. | GA -AZR-000535 | [Azure.ContainerApp.HealthProbe](Azure.ContainerApp.HealthProbe.md) | Container apps using HTTP-based ingress should use HTTP health probes for liveness and readiness checks. | GA +AZR-000537 | [Azure.ContainerApp.HealthProbe](Azure.ContainerApp.HealthProbe.md) | Container apps using HTTP-based ingress should use HTTP health probes for liveness and readiness checks. | GA *[GA]: Generally Available — Rules related to a generally available Azure features. diff --git a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml index c4179717fd0..27d8deb29ca 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml +++ b/src/PSRule.Rules.Azure/rules/Azure.ContainerApp.Rule.yaml @@ -212,10 +212,10 @@ apiVersion: github.com/microsoft/PSRule/v1 kind: Rule metadata: name: Azure.ContainerApp.HealthProbe - ref: AZR-000535 + ref: AZR-000537 tags: release: GA - ruleSet: 2026_03 + ruleSet: 2026_06 Azure.WAF/pillar: Reliability spec: with: From e83fcb4e591cd03952b130b6293a9f1b7ef77daf Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 25 Mar 2026 20:24:43 +0100 Subject: [PATCH 7/7] Doc updates --- docs/en/rules/Azure.ContainerApp.ExternalIngress.md | 2 +- docs/en/rules/Azure.ContainerApp.HealthProbe.md | 12 +++++++----- docs/examples/resources/containerapp.bicep | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/en/rules/Azure.ContainerApp.ExternalIngress.md b/docs/en/rules/Azure.ContainerApp.ExternalIngress.md index 026a6bf04bd..b9c88202160 100644 --- a/docs/en/rules/Azure.ContainerApp.ExternalIngress.md +++ b/docs/en/rules/Azure.ContainerApp.ExternalIngress.md @@ -114,7 +114,7 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { } ``` - + ## NOTES diff --git a/docs/en/rules/Azure.ContainerApp.HealthProbe.md b/docs/en/rules/Azure.ContainerApp.HealthProbe.md index a4981edf093..45e0bead81a 100644 --- a/docs/en/rules/Azure.ContainerApp.HealthProbe.md +++ b/docs/en/rules/Azure.ContainerApp.HealthProbe.md @@ -17,13 +17,13 @@ Container app ingress that uses HTTP should have HTTP health probes configured f ## DESCRIPTION Azure Container Apps supports health probes to determine the health and readiness of your containers. -Health probes can be configured as HTTP, TCP, or gRPC checks, and support liveness, readiness, and startup probe types. +Health probes can be configured as HTTP or TCP checks and support liveness, readiness, and startup probe types. When a container app uses HTTP-based ingress (transport is `http` or `http2`, or the target port is `80`, `8080`, or `443`), health probes should use HTTP checks (`httpGet`) for liveness and readiness probes. HTTP health probes provide granular feedback by checking the HTTP response status code, -which gives more accurate information about whether a replica is available and ready to receive traffic -compared to a TCP port check which only determines if a port is open or closed. +which gives more accurate information about whether a replica is available and ready to receive traffic compared to +a TCP port check which only determines if a port is open or closed. The default health probes use TCP port checks when no probes are explicitly configured. Configuring HTTP health probes instead allows the platform to better detect and respond to application-level failures. @@ -50,7 +50,7 @@ For example: ```json { "type": "Microsoft.App/containerApps", - "apiVersion": "2024-03-01", + "apiVersion": "2025-07-01", "name": "[parameters('appName')]", "location": "[parameters('location')]", "identity": { @@ -114,7 +114,7 @@ To deploy Container Apps that pass this rule: For example: ```bicep -resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { +resource containerApp 'Microsoft.App/containerApps@2025-07-01' = { name: appName location: location identity: { @@ -164,6 +164,8 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { } ``` + + ## NOTES This rule applies to container apps where the ingress transport is `http` or `http2`, diff --git a/docs/examples/resources/containerapp.bicep b/docs/examples/resources/containerapp.bicep index 4446a9c8cf4..e8c48e372d8 100644 --- a/docs/examples/resources/containerapp.bicep +++ b/docs/examples/resources/containerapp.bicep @@ -80,7 +80,7 @@ resource containerEnv 'Microsoft.App/managedEnvironments@2025-01-01' = { } // An example Container App using a minimum of 2 replicas. -resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { +resource containerApp 'Microsoft.App/containerApps@2025-07-01' = { name: name location: location identity: {