Versions: Terraform 1.13.3 (HCP Terraform agent) and 1.13.5 (local); hashicorp/azurerm 4.59.0.
(I did try it with Terraform 1.16.1 with no success)
The situation
A security policy resource references a for_each map of custom domains through a whole-resource reference in a dynamic block. Azure refuses to delete a custom domain while a security policy still references it, so removing a key from the map requires the policy’s in-place update to land before the orphaned domain’s destroy.
Terraform schedules them the other way round, because the policy’s update node waits on every instance node of the referenced resource — including the orphan being destroyed:
"terraform_data.policy" -> "terraform_data.domain[\"b\"] (destroy)" # A -> B means A waits for B
create_before_destroy on the domain reverses that edge, which is the documented behaviour and is what part 1 below demonstrates. It does not take effect against the real provider, which is what I’m trying to understand.
Part 1 — provider-free repro (works as documented)
No credentials needed; uses the built-in terraform_data.
variable "keys" { type = list(string) }
resource "terraform_data" "domain" {
for_each = toset(var.keys)
input = each.key
lifecycle {
create_before_destroy = true # remove this to see the other ordering
}
}
resource "terraform_data" "policy" {
input = [for k, v in terraform_data.domain : v.output]
}
terraform init
terraform apply -auto-approve -var='keys=["a","b"]'
terraform apply -auto-approve -var='keys=["a"]'
With create_before_destroy:
terraform_data.policy: Modifying...
terraform_data.domain["b"]: Destroying...
Without it:
terraform_data.domain["b"]: Destroying...
terraform_data.policy: Modifying...
So core reverses the destroy edge exactly as expected. This holds under every variant I tried: the resources nested inside a module, count on the policy, and sibling resources that also reference the domain and are destroyed in the same apply.
Part 2 — the same shape with azurerm (CBD has no effect)
Requires an Azure subscription and a DNS zone you control. The custom domains do not need to finish certificate validation to reproduce.
provider "azurerm" {
features {}
}
variable "domains" {
type = list(string)
default = ["one", "two"]
}
variable "zone_name" {
type = string # e.g. "example.com" - a zone you own
}
resource "azurerm_resource_group" "this" {
name = "rg-afd-cbd-repro"
location = "eastus"
}
resource "azurerm_cdn_frontdoor_profile" "this" {
name = "afd-cbd-repro"
resource_group_name = azurerm_resource_group.this.name
sku_name = "Premium_AzureFrontDoor" # Premium required for security policies
}
resource "azurerm_cdn_frontdoor_firewall_policy" "this" {
name = "afdcbdrepowaf"
resource_group_name = azurerm_resource_group.this.name
sku_name = azurerm_cdn_frontdoor_profile.this.sku_name
enabled = true
mode = "Detection"
}
resource "azurerm_cdn_frontdoor_custom_domain" "this" {
for_each = toset(var.domains)
name = each.key
cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.this.id
host_name = "${each.key}.${var.zone_name}"
tls {
certificate_type = "ManagedCertificate"
}
lifecycle {
create_before_destroy = true
}
}
resource "azurerm_cdn_frontdoor_security_policy" "this" {
name = "repro-security-policy"
cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.this.id
security_policies {
firewall {
cdn_frontdoor_firewall_policy_id = azurerm_cdn_frontdoor_firewall_policy.this.id
association {
patterns_to_match = ["/*"]
dynamic "domain" {
for_each = azurerm_cdn_frontdoor_custom_domain.this
content {
cdn_frontdoor_domain_id = domain.value.id
}
}
}
}
}
}
terraform apply # creates both domains, both in the policy
terraform apply -var='domains=["one"]' # remove "two"
Expected (per part 1): the security policy is modified first, then the domain is destroyed.
Actual: the security policy is never modified at all, the domain destroy is attempted first, and Azure rejects it:
Error: deleting Front Door Custom Domain: (Custom Domain Name "two" / Profile Name "afd-cbd-repro"
/ Resource Group "rg-afd-cbd-repro"): cdn.AFDCustomDomainsClient#Delete: Failure sending request:
StatusCode=400 -- Original Error: Code="BadRequest"
Message="Resource is associated with security policy 'repro-security-policy'."
Confirmed after the failed apply that the domain is still listed in the security policy’s associations, i.e. the update genuinely did not run rather than running and not taking effect.
The question
Why does create_before_destroy reverse the destroy edge in part 1 but appear to have no effect in part 2? The plan is identical either way — a lifecycle block produces no plan diff — so there is no way to tell from a plan whether it is in effect.
Things already ruled out
| Attempt | Result |
|---|---|
Referencing instances individually, custom_domain[k].id |
references resolve at resource level; identical edge |
| Dropping the reference, building domain IDs as strings | edge is rebuilt from the dependency recorded in state |
terraform apply -target=<security policy> |
targeting pulls in the target’s dependencies, so the orphan destroy comes too |
depends_on added to the policy |
duplicates the existing reference edge; no change |
depends_on inverted (domain depends on policy, reference broken) |
still destroys first, and breaks creation — the policy would be created referencing domains that do not exist yet |
| Terraform version | nothing in the 1.14, 1.15 or 1.16 changelogs touches destroy ordering except the 1.16.0 regression fixed in 1.16.1, which postdates 1.13.3 |