Skip to content

Commit dba0f54

Browse files
lonegunmanbCopilot
andauthored
fix: resolve FC1 (Flex Consumption) SKU plan drift for kind, capacity, and maximumElasticWorkerCount (#128)
* fix: resolve FC1 (Flex Consumption) SKU plan drift for kind, capacity, and maximumElasticWorkerCount Fixes #125 Three root causes of perpetual plan drift with FC1 SKU: 1. kind: FC1 requires 'functionapp' but module always sent 'linux'/'windows' based on os_type. Azure returns 'functionapp' causing drift. 2. sku.capacity: FC1 capacity is managed by Azure (always 0), but module sent var.worker_count (default 3) causing capacity drift. 3. maximumElasticWorkerCount: FC1's value is managed by Azure (resets to 1). Using merge() to conditionally omit the property for FC1, relying on azapi's ignore_missing_property to prevent drift. Changes: - locals.tf: Add is_flex_consumption flag, fix kind for FC1, add sku_capacity local, include FC1 in elastic worker count regex - main.tf: Use merge() for properties to conditionally omit maximumElasticWorkerCount for FC1, use local.sku_capacity for capacity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: resolve WS SKU kind and elasticScaleEnabled drift, add multi_kind example - Fix WS (Workflow Standard) SKU kind drift: Azure returns 'elastic' but module was sending 'linux'/'windows' based on os_type - Fix WS SKU elasticScaleEnabled drift: Azure enforces true for WS SKUs but module was sending false (regex only matched EP/P) - Add examples/multi_kind/ to verify all kind variants (S1/P1v2/FC1/WS1) deploy without configuration drift Fixes #125 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add .e2eignore for multi_kind example, fix P1v2 zone balancing - Add .e2eignore to skip APRL well-architected checks for multi_kind example (FC1/WS1 are specialized SKUs that can't pass 'standard or premium tier' check) - Set zone_balancing=true for P1v2 plan (best practice) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: replace .e2eignore with rego exception for multi_kind example Delete .e2eignore which skipped all conftest checks for multi_kind. Instead, add a targeted APRL exception for service_plan_use_standard_or_premium_tier since FC1 (Flex Consumption) and WS1 (Workflow Standard) are specialized SKUs that intentionally do not fall into standard/premium/isolated tier categories. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: set worker_count=3 for zone-redundant P1v2 in multi_kind example Azure enforces minimum capacity for zone-redundant App Service Plans. With worker_count=1 and zone_balancing=true, Azure adjusts capacity to 2, causing idempotency drift. Setting worker_count=3 (one per availability zone) avoids this drift. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove zone_balancing from P1v2 in multi_kind example P1v2 + Availability Zones is not reliably available in australiaeast. Zone balancing is already tested in the complete example. The multi_kind example focuses on testing different SKU kinds (S1/P1v2/FC1/WS1), not zone redundancy. Added migrate_service_plan_to_availability_zone_support to APRL exceptions since P1v2 with zone_balancing=false triggers it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8f83da0 commit dba0f54

10 files changed

Lines changed: 301 additions & 9 deletions

File tree

examples/multi_kind/README.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<!-- BEGIN_TF_DOCS -->
2+
<!-- Code generated by terraform-docs. DO NOT EDIT. -->
3+
# Multi-kind example
4+
5+
This example deploys multiple App Service Plans with different SKU kinds to verify that the module correctly handles the `kind` property for each SKU type without configuration drift.
6+
7+
The following plan types are tested:
8+
9+
- **Standard (S1)**: Linux, kind = `"linux"`
10+
- **Premium (P1v2)**: Windows, kind = `"windows"`
11+
- **Flex Consumption (FC1)**: Linux, kind = `"functionapp"`, capacity managed by Azure
12+
- **Workflow Standard (WS1)**: Windows, kind = `"elastic"`, elasticScaleEnabled forced true
13+
14+
```hcl
15+
terraform {
16+
required_version = ">= 1.9, < 2.0"
17+
18+
required_providers {
19+
azapi = {
20+
source = "Azure/azapi"
21+
version = "~> 2.4"
22+
}
23+
random = {
24+
source = "hashicorp/random"
25+
version = ">= 3.5.0, < 4.0.0"
26+
}
27+
}
28+
}
29+
30+
provider "azapi" {}
31+
32+
resource "random_integer" "region_index" {
33+
max = length(local.test_regions) - 1
34+
min = 0
35+
}
36+
37+
## End of section to provide a random Azure region for the resource group
38+
39+
# This ensures we have unique CAF compliant names for our resources.
40+
module "naming" {
41+
source = "Azure/naming/azurerm"
42+
version = "0.4.2"
43+
}
44+
45+
# This is required for resource modules
46+
# Hardcoding location due to quota constraints
47+
resource "azapi_resource" "resource_group" {
48+
location = "australiaeast"
49+
name = module.naming.resource_group.name_unique
50+
type = "Microsoft.Resources/resourceGroups@2024-03-01"
51+
response_export_values = []
52+
}
53+
54+
# Deploy multiple App Service Plans with different SKU kinds
55+
module "test" {
56+
source = "../.."
57+
for_each = local.plans
58+
59+
location = azapi_resource.resource_group.location
60+
name = "${module.naming.app_service_plan.name_unique}-${each.key}"
61+
os_type = each.value.os_type
62+
parent_id = azapi_resource.resource_group.id
63+
enable_telemetry = var.enable_telemetry
64+
sku_name = each.value.sku_name
65+
worker_count = each.value.worker_count
66+
zone_balancing_enabled = each.value.zone_balancing
67+
}
68+
```
69+
70+
<!-- markdownlint-disable MD033 -->
71+
## Requirements
72+
73+
The following requirements are needed by this module:
74+
75+
- <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) (>= 1.9, < 2.0)
76+
77+
- <a name="requirement_azapi"></a> [azapi](#requirement\_azapi) (~> 2.4)
78+
79+
- <a name="requirement_random"></a> [random](#requirement\_random) (>= 3.5.0, < 4.0.0)
80+
81+
## Resources
82+
83+
The following resources are used by this module:
84+
85+
- [azapi_resource.resource_group](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) (resource)
86+
- [random_integer.region_index](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/integer) (resource)
87+
88+
<!-- markdownlint-disable MD013 -->
89+
## Required Inputs
90+
91+
No required inputs.
92+
93+
## Optional Inputs
94+
95+
The following input variables are optional (have default values):
96+
97+
### <a name="input_enable_telemetry"></a> [enable\_telemetry](#input\_enable\_telemetry)
98+
99+
Description: This variable controls whether or not telemetry is enabled for the module.
100+
For more information see <https://aka.ms/avm/telemetryinfo>.
101+
If it is set to false, then no telemetry will be collected.
102+
103+
Type: `bool`
104+
105+
Default: `true`
106+
107+
## Outputs
108+
109+
The following outputs are exported:
110+
111+
### <a name="output_plan_names"></a> [plan\_names](#output\_plan\_names)
112+
113+
Description: Names of the deployed app service plans
114+
115+
### <a name="output_plan_resource_ids"></a> [plan\_resource\_ids](#output\_plan\_resource\_ids)
116+
117+
Description: Resource IDs of the deployed app service plans
118+
119+
## Modules
120+
121+
The following Modules are called:
122+
123+
### <a name="module_naming"></a> [naming](#module\_naming)
124+
125+
Source: Azure/naming/azurerm
126+
127+
Version: 0.4.2
128+
129+
### <a name="module_test"></a> [test](#module\_test)
130+
131+
Source: ../..
132+
133+
Version:
134+
135+
<!-- markdownlint-disable-next-line MD041 -->
136+
## Data Collection
137+
138+
The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.
139+
<!-- END_TF_DOCS -->

examples/multi_kind/_footer.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<!-- markdownlint-disable-next-line MD041 -->
2+
## Data Collection
3+
4+
The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.

examples/multi_kind/_header.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Multi-kind example
2+
3+
This example deploys multiple App Service Plans with different SKU kinds to verify that the module correctly handles the `kind` property for each SKU type without configuration drift.
4+
5+
The following plan types are tested:
6+
7+
- **Standard (S1)**: Linux, kind = `"linux"`
8+
- **Premium (P1v2)**: Windows, kind = `"windows"`
9+
- **Flex Consumption (FC1)**: Linux, kind = `"functionapp"`, capacity managed by Azure
10+
- **Workflow Standard (WS1)**: Windows, kind = `"elastic"`, elasticScaleEnabled forced true
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package Azure_Proactive_Resiliency_Library_v2
2+
3+
import rego.v1
4+
5+
# FC1 (Flex Consumption) and WS1 (Workflow Standard) are specialized SKUs
6+
# that do not fall into standard/premium/isolated tier categories.
7+
# This exception is required for the multi_kind example which intentionally
8+
# tests these non-standard SKU types.
9+
#
10+
# Zone redundancy is not tested in this example (it is covered by the
11+
# complete example). The APRL zone check only applies to Premium/Isolated
12+
# tiers, so P1v2 with zone_balancing=false would trigger it.
13+
exception contains rules if {
14+
rules = ["service_plan_use_standard_or_premium_tier", "migrate_service_plan_to_availability_zone_support"]
15+
}

examples/multi_kind/locals.tf

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
## Section to provide a random Azure region for the resource group
2+
# This allows us to randomize the region for the resource group.
3+
locals {
4+
plans = {
5+
linux_standard = {
6+
os_type = "Linux"
7+
sku_name = "S1"
8+
worker_count = 1
9+
zone_balancing = false
10+
}
11+
windows_premium = {
12+
os_type = "Windows"
13+
sku_name = "P1v2"
14+
worker_count = 1
15+
zone_balancing = false
16+
}
17+
flex_consumption = {
18+
os_type = "Linux"
19+
sku_name = "FC1"
20+
worker_count = 0
21+
zone_balancing = false
22+
}
23+
workflow_standard = {
24+
os_type = "Windows"
25+
sku_name = "WS1"
26+
worker_count = 1
27+
zone_balancing = false
28+
}
29+
}
30+
test_regions = [
31+
"centralus",
32+
"southcentralus",
33+
"canadacentral",
34+
"eastus",
35+
"eastus2",
36+
"westus2",
37+
"westus3"
38+
]
39+
}

examples/multi_kind/main.tf

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
terraform {
2+
required_version = ">= 1.9, < 2.0"
3+
4+
required_providers {
5+
azapi = {
6+
source = "Azure/azapi"
7+
version = "~> 2.4"
8+
}
9+
random = {
10+
source = "hashicorp/random"
11+
version = ">= 3.5.0, < 4.0.0"
12+
}
13+
}
14+
}
15+
16+
provider "azapi" {}
17+
18+
resource "random_integer" "region_index" {
19+
max = length(local.test_regions) - 1
20+
min = 0
21+
}
22+
23+
## End of section to provide a random Azure region for the resource group
24+
25+
# This ensures we have unique CAF compliant names for our resources.
26+
module "naming" {
27+
source = "Azure/naming/azurerm"
28+
version = "0.4.2"
29+
}
30+
31+
# This is required for resource modules
32+
# Hardcoding location due to quota constraints
33+
resource "azapi_resource" "resource_group" {
34+
location = "australiaeast"
35+
name = module.naming.resource_group.name_unique
36+
type = "Microsoft.Resources/resourceGroups@2024-03-01"
37+
response_export_values = []
38+
}
39+
40+
# Deploy multiple App Service Plans with different SKU kinds
41+
module "test" {
42+
source = "../.."
43+
for_each = local.plans
44+
45+
location = azapi_resource.resource_group.location
46+
name = "${module.naming.app_service_plan.name_unique}-${each.key}"
47+
os_type = each.value.os_type
48+
parent_id = azapi_resource.resource_group.id
49+
enable_telemetry = var.enable_telemetry
50+
sku_name = each.value.sku_name
51+
worker_count = each.value.worker_count
52+
zone_balancing_enabled = each.value.zone_balancing
53+
}

examples/multi_kind/outputs.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
output "plan_names" {
2+
description = "Names of the deployed app service plans"
3+
value = { for k, v in module.test : k => v.name }
4+
}
5+
6+
output "plan_resource_ids" {
7+
description = "Resource IDs of the deployed app service plans"
8+
value = { for k, v in module.test : k => v.resource_id }
9+
}

examples/multi_kind/variables.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
variable "enable_telemetry" {
2+
type = bool
3+
default = true
4+
description = <<DESCRIPTION
5+
This variable controls whether or not telemetry is enabled for the module.
6+
For more information see <https://aka.ms/avm/telemetryinfo>.
7+
If it is set to false, then no telemetry will be collected.
8+
DESCRIPTION
9+
}

locals.tf

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
locals {
2-
# Elastic scale is only applicable to Elastic Premium and Premium SKUs
3-
elastic_scale_enabled = can(regex("^(EP|P)", var.sku_name)) ? var.premium_plan_auto_scale_enabled : false
2+
# Elastic scale: WS SKUs are always elastic (Azure enforces elasticScaleEnabled=true),
3+
# EP/P SKUs are user-controllable, others are false.
4+
# Reference: https://github.com/Azure/terraform-azurerm-avm-res-web-serverfarm/issues/125
5+
elastic_scale_enabled = local.is_workflow_standard ? true : (can(regex("^(EP|P)", var.sku_name)) ? var.premium_plan_auto_scale_enabled : false)
6+
# Flex Consumption (FC1) requires special handling for kind, capacity, and maximumElasticWorkerCount
7+
is_flex_consumption = var.sku_name == "FC1"
8+
# Workflow Standard (WS1/WS2/WS3) requires kind="elastic" and elasticScaleEnabled=true
9+
is_workflow_standard = can(regex("^WS", var.sku_name))
410
# Determine the kind property based on os_type and sku_name
511
# Reference: https://github.com/Azure/app-service-linux-docs/blob/master/Things_You_Should_Know/kind_property.md
6-
kind = var.os_type == "Linux" ? "linux" : "windows"
7-
# Maximum elastic worker count is only applicable to Elastic Premium and WS SKUs
8-
maximum_elastic_worker_count = can(regex("^(EP|WS)", var.sku_name)) ? var.maximum_elastic_worker_count : var.worker_count
12+
# FC1 (Flex Consumption) always uses "functionapp" regardless of os_type
13+
# WS (Workflow Standard) always uses "elastic" regardless of os_type
14+
kind = local.is_flex_consumption ? "functionapp" : (local.is_workflow_standard ? "elastic" : (var.os_type == "Linux" ? "linux" : "windows"))
15+
# Maximum elastic worker count is applicable to Elastic Premium, WS, and Flex Consumption SKUs
16+
maximum_elastic_worker_count = can(regex("^(EP|WS|FC)", var.sku_name)) ? var.maximum_elastic_worker_count : var.worker_count
17+
# FC1 capacity is managed by Azure (always 0), other SKUs use worker_count
18+
sku_capacity = local.is_flex_consumption ? 0 : var.worker_count
919
}

main.tf

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ resource "azapi_resource" "this" {
1919
type = var.server_farm_resource_type
2020
body = {
2121
kind = local.kind
22-
properties = {
22+
properties = merge({
2323
asyncScalingEnabled = null
2424
freeOfferExpirationTime = null
2525
isCustomMode = var.os_type == "WindowsManagedInstance"
@@ -29,7 +29,6 @@ resource "azapi_resource" "this" {
2929
elasticScaleEnabled = local.elastic_scale_enabled
3030
hostingEnvironmentProfile = var.app_service_environment_id != null ? { id = var.app_service_environment_id } : null
3131
hyperV = var.os_type == "WindowsContainer"
32-
maximumElasticWorkerCount = local.maximum_elastic_worker_count
3332
installScripts = var.os_type == "WindowsManagedInstance" && var.install_scripts != null ? [
3433
for script in var.install_scripts : {
3534
name = script.name
@@ -74,10 +73,15 @@ resource "azapi_resource" "this" {
7473
targetWorkerSizeId = null
7574
workerTierName = null
7675
zoneRedundant = var.zone_balancing_enabled
77-
}
76+
},
77+
# FC1 (Flex Consumption) maximumElasticWorkerCount is managed by Azure; omit it so ignore_missing_property handles drift
78+
local.is_flex_consumption ? {} : {
79+
maximumElasticWorkerCount = local.maximum_elastic_worker_count
80+
}
81+
)
7882
sku = {
7983
name = var.sku_name
80-
capacity = var.worker_count
84+
capacity = local.sku_capacity
8185
family = null
8286
size = null
8387
tier = null

0 commit comments

Comments
 (0)