Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure API Management Policy Fragment Terraform Module

Stores one reusable XML policy snippet on an Azure API Management service, for other policies to include by name. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat


🧩 Overview

  • 🧩 Creates one azurerm_api_management_policy_fragment β€” a reusable snippet of policy statements.
  • πŸ”— Emits the exact <include-fragment .../> element to paste, because the reference is hand-written XML that Terraform cannot track.
  • πŸ” Explains the perpetual-diff trap: a wrong format loops rather than failing, because it decides how value is read back as well as sent.
  • πŸ“₯ Warns that an imported fragment always arrives as xml, whatever it was authored as.
  • πŸ”’ States plainly that the snippet is stored and returned in clear β€” credentials belong in a named value.
  • 🏷️ Carries no tags β€” the resource has none. The universal tail is timeouts only.

πŸ’‘ Why it matters: a policy fragment's only interface is its name, used as a bare string inside someone else's XML. Terraform creates no dependency, enforces no ordering, and reports nothing when a rename orphans an include. This module makes that link as explicit as it can be made.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


πŸ—ΊοΈ Where this fits in the family

flowchart TB
  rg["terraform-azurerm-resource-group"]
  kv["terraform-azurerm-key-vault"]
  apim["terraform-azurerm-api-management"]
  nv["terraform-azurerm-api-management-named-value"]
  this["terraform-azurerm-api-management-policy-fragment"]
  api["terraform-azurerm-api-management-api"]
  svcpol["terraform-azurerm-api-management-policy"]

  rg -->|"name"| apim
  apim -->|"id"| this
  apim -->|"name"| nv
  kv -->|"secret behind a named value"| nv
  nv -->|"referenced from the fragment as a double-brace token"| this
  this -->|"name, pasted into an include-fragment element"| api
  this -->|"name, pasted into an include-fragment element"| svcpol

  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef keystone fill:#004578,stroke:#002b4d,color:#ffffff;
  classDef sib fill:#eef3f8,stroke:#b9c7d6,color:#1b2a3a;
  class this me;
  class apim keystone;
  class rg,kv,nv,api,svcpol sib;
Loading

The API Management family is large, so this diagram names only this module's direct neighbours rather than every api-management-* module in the library. Note that both downstream edges are dashed in practice rather than in Terraform: a policy includes this fragment by writing its name into XML, so nothing in the graph above is an actual resource reference until include_fragment_snippet is interpolated into the consuming document.


🧬 What this module builds

flowchart TB
  subgraph inputs["Inputs"]
    ident["name plus api_management_id, both force-new"]
    content["value, the XML snippet"]
    fmt["format, xml or rawxml"]
    desc["description"]
  end

  this["azurerm_api_management_policy_fragment.this"]

  subgraph outputs["Outputs"]
    oid["id, name, include_fragment_snippet"]
    oval["value, format, description"]
    oshape["value_looks_like_a_full_policy_document, references_a_named_value"]
    ofact["the_include_is_a_string_terraform_cannot_see, a_wrong_format_loops_instead_of_failing"]
  end

  ident --> this
  content --> this
  fmt -->|"decides how value is sent AND read back"| this
  desc --> this
  this --> oid
  this --> oval
  this --> oshape
  this --> ofact

  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef sib fill:#eef3f8,stroke:#b9c7d6,color:#1b2a3a;
  class this me;
  class ident,content,fmt,desc,oid,oval,oshape,ofact sib;
Loading

Resource inventory

Resource Count Notes
azurerm_api_management_policy_fragment.this 1 The keystone, and the only resource.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module β€” the caller configures provider "azurerm" { features {} }, auth and subscription

Schema notes that bite

  • name and api_management_id are force-new, and both inherit that from provider helpers rather than from this resource's own field list. Everything else updates in place.
  • format decides how value is read back as well as sent. A mismatch returns a differently-encoded string on every read, so Terraform proposes the same update forever β€” one that applies successfully and comes straight back.
  • An imported fragment always arrives as xml. The provider's import callback sets it unconditionally, because the API only reports a format when one was requested and it is not xml.
  • A format change deliberately re-sends value, because the same string means something different under the other format.
  • The diff suppression strips all whitespace and unescapes XML entities before comparing. Reformatting produces no plan β€” and neither does a genuine change in literal whitespace inside a text value, or a change purely in escaping.
  • Delete has no not-found tolerance, unlike the sibling azurerm_api_management_policy β€” wrapped by the terraform-azurerm-api-management-policy module β€” which does tolerate it. A fragment removed out of band makes terraform destroy fail rather than converge.
  • value is validated by nothing. The provider accepts any string, including one that is not XML at all.
  • No tags, no location. The universal tail is timeouts only.

πŸ”‘ Required Azure RBAC Roles / Permissions

  • Microsoft.ApiManagement/service/policyFragments/* β€” write and delete, scoped to the parent API Management service. A custom role at the service is enough; API Management Service Contributor on the service, or Contributor on its resource group, both cover it more broadly than necessary.
  • Microsoft.ApiManagement/service/policyFragments/read on the service, for the existence check before creating and for every refresh.

πŸ”’ This resource holds no credential β€” provided the fragment does not contain one. Reading it returns the fragment's full content, so plan access is content access: anyone who can refresh this resource can read every policy statement in it. That is a reason to reference a named value rather than paste a key into the XML, not a reason to restrict the plan.


Azure Prerequisites

  • An existing API Management service.
  • Any named value the snippet references must already exist on the same service. Nothing validates the reference β€” a {{missing}} token applies cleanly and fails at request time.
  • A policy that includes the fragment, if it is to do anything at all.
  • The caller configures provider "azurerm" { features {} }, authentication and subscription.

πŸ“ Module Structure

terraform-azurerm-api-management-policy-fragment/
β”œβ”€β”€ providers.tf    # required_version + pinned azurerm; no provider block
β”œβ”€β”€ variables.tf    # deeply-typed inputs; the unambiguous content mistakes rejected at plan time
β”œβ”€β”€ main.tf         # the single keystone resource
β”œβ”€β”€ outputs.tf      # id first, then the include key, the content, and the silent behaviours
β”œβ”€β”€ README.md       # this document
β”œβ”€β”€ SCOPE.md        # the cross-module contract
β”œβ”€β”€ LICENSE         # MIT
└── .gitignore      # the canonical library ignore set

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "cors_fragment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"

  name              = "cors-standard"
  api_management_id = module.apim.id
  value             = file("${path.module}/fragments/cors-standard.xml")
  description       = "Standard CORS. Included by the orders and payments API policies."
}

ℹ️ The caller configures the provider, including the mandatory features {} block. This module declares none.

⚠️ This creates the snippet and nothing more. Until a policy writes <include-fragment fragment-id="cors-standard" />, nothing runs it.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
api_management_id string terraform-azurerm-api-management output id
value string a local .xml file read with file()
named-value tokens inside value string terraform-azurerm-api-management-named-value β€” referenced by name in the XML, not wired

Emits

Output Consumed by
name the XML of every consuming policy β€” this is the real interface
include_fragment_snippet paste directly into a consuming policy
id management locks, role assignments, review
format / value troubleshooting a perpetual diff

πŸ“š Example Library

1 Β· Minimal CORS fragment
module "fragment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"

  name              = "cors-standard"
  api_management_id = module.apim.id
  value             = file("${path.module}/fragments/cors-standard.xml")
}

πŸ’‘ Read the snippet from a file rather than a heredoc. Terraform's own interpolation syntax collides with policy expressions, and file() interpolates nothing at all.

2 Β· With a description that records its consumers
module "fragment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"

  name              = "cors-standard"
  api_management_id = module.apim.id
  value             = file("${path.module}/fragments/cors-standard.xml")
  description       = "Standard CORS. Included by: orders API, payments API, service-wide inbound."
}

πŸ’‘ A fragment does not know which policies include it, and no attribute anywhere lists them. The description is the only place that relationship is written down inside Azure.

3 Β· rawxml for expression-heavy snippets
module "fragment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"

  name              = "rate-limit-by-subscription"
  api_management_id = module.apim.id
  format            = "rawxml"
  value             = file("${path.module}/fragments/rate-limit.xml")
}

⚠️ rawxml takes the value literally, so a policy expression can be written as-is instead of entity-escaped. Both values are lower case and the check is case-sensitive β€” "RawXml" is rejected.

4 Β· Referencing a named value instead of a literal secret
module "backend_auth_fragment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"

  name              = "backend-auth"
  api_management_id = module.apim.id
  format            = "rawxml"

  # The XML references {{backend-api-key}} rather than carrying the key.
  value = file("${path.module}/fragments/backend-auth.xml")
}

πŸ”’ A fragment's content is stored on the service in clear and returned on every read, so anything pasted into it lands in Terraform state and in every plan that refreshes it. Reference a named value β€” ideally one backed by a Key Vault secret β€” and keep the secret out of the snippet entirely. references_a_named_value reports whether a fragment does this.

5 Β· Wiring the include into a policy
module "fragment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"

  name              = "cors-standard"
  api_management_id = module.apim.id
  value             = file("${path.module}/fragments/cors-standard.xml")
}

# The include is a STRING, so build it from the module output rather than
# retyping the name. This also gives Terraform a real dependency edge.
resource "azurerm_api_management_policy" "service" {
  api_management_id = module.apim.id

  xml_content = <<-XML
    <policies>
      <inbound>
        <base />
        ${module.fragment.include_fragment_snippet}
      </inbound>
      <backend><base /></backend>
      <outbound><base /></outbound>
      <on-error><base /></on-error>
    </policies>
  XML
}

πŸ’‘ Interpolating include_fragment_snippet is the one way to make the fragment a genuine Terraform dependency: the policy now cannot be created before the fragment, and a rename propagates instead of orphaning the include.

6 Β· for_each over a directory of fragments
locals {
  fragments = {
    cors        = { file = "cors-standard.xml", format = "xml", note = "Standard CORS." }
    rate_limit  = { file = "rate-limit.xml", format = "rawxml", note = "Per-subscription rate limit." }
    correlation = { file = "correlation-id.xml", format = "xml", note = "Propagates a correlation header." }
  }
}

module "fragments" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"
  for_each = local.fragments

  name              = replace(each.key, "_", "-")
  api_management_id = module.apim.id
  value             = file("${path.module}/fragments/${each.value.file}")
  format            = each.value.format
  description       = each.value.note
}

πŸ’‘ The map key is stable, so adding a fourth fragment never touches the first three. name is force-new, which is exactly why keying on a stable identifier matters here.

7 Β· Emitting the include snippets for a policy library
output "fragment_includes" {
  value = { for k, m in module.fragments : k => m.include_fragment_snippet }
}
fragment_includes = {
  "correlation" = "<include-fragment fragment-id=\"correlation-id\" />"
  "cors"        = "<include-fragment fragment-id=\"cors-standard\" />"
  "rate_limit"  = "<include-fragment fragment-id=\"rate-limit\" />"
}

πŸ’‘ Hand this to whoever writes the policies. Every one of those strings is otherwise typed by hand, and a typo fails at request time rather than at apply.

8 Β· Adopting an existing fragment
module "fragment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"

  name              = "cors-standard"
  api_management_id = module.apim.id
  value             = file("${path.module}/fragments/cors-standard.xml")

  # Import arrives as xml regardless of how the fragment was authored.
  # If it was rawxml, set that AFTER importing and apply the correction.
  format = "xml"
}
terraform import 'module.fragment.azurerm_api_management_policy_fragment.this' \
  "/subscriptions/$SUB/resourceGroups/rg-platform-apim/providers/Microsoft.ApiManagement/service/apim-platform-prod/policyFragments/cors-standard"

⚠️ an_imported_fragment_always_arrives_as_xml states this as a constant. Correcting the format afterwards is an in-place update, not a replacement β€” and the provider re-sends the value alongside it, because the same string means something different under the other format.

9 Β· Diagnosing a diff that will not settle
output "fragment_format_as_azure_reports_it" {
  value = module.fragment.format
}

output "fragment_content_matches_configuration" {
  value = module.fragment.value_matches_configuration
}

output "fragment_content_fingerprint" {
  value = module.fragment.value_fingerprint
}

⚠️ value_matches_configuration reading false while the plan keeps proposing the same update is the signature of a format mismatch, not of a broken value. The provider requests the content in whatever format is configured, so the wrong one returns a differently-encoded string every read. Switch the format rather than reformatting the XML.

10 Β· Custom timeouts
module "fragment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"

  name              = "cors-standard"
  api_management_id = module.apim.id
  value             = file("${path.module}/fragments/cors-standard.xml")

  timeouts = {
    create = "45m"
    read   = "10m"
    update = "45m"
    delete = "45m"
  }
}

⚠️ A key that is not create, read, update or delete is silently discarded by Terraform's type conversion β€” no error, and no timeout.

11 Β· Reviewing fragment hygiene across an estate
output "fragments_wrapped_as_full_documents" {
  value = [for k, m in module.fragments : k if m.value_looks_like_a_full_policy_document]
}

output "fragments_with_an_xml_prolog" {
  value = [for k, m in module.fragments : k if m.value_declares_an_xml_prolog]
}

output "fragments_not_using_a_named_value" {
  value = [for k, m in module.fragments : k if !m.references_a_named_value]
}

πŸ’‘ The first two are shape mistakes this module reports rather than refuses, because the provider accepts any string. The third is a prompt, not a verdict: plenty of fragments legitimately reference nothing β€” but a fragment that talks to a backend and references no named value is worth opening.

12 Β· πŸ—οΈ End-to-end composition
provider "azurerm" {
  features {}
}

module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-platform-apim"
  location = "eastus2"
}

module "apim" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management.git?ref=v1.0.0"

  name                = "apim-platform-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location
  publisher_name      = "Platform Engineering"
  publisher_email     = "platform-engineering@example.com"
  sku_name            = "Developer_1"
}

# The secret the fragment needs, held as a named value rather than in the XML.
module "backend_key" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-named-value.git?ref=v1.0.0"

  name                = "backend-api-key"
  resource_group_name = module.rg.name
  api_management_name = module.apim.name
  display_name        = "backend-api-key"
  secret              = true
  value               = var.backend_api_key
}

module "backend_auth_fragment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy-fragment.git?ref=v1.0.0"

  name              = "backend-auth"
  api_management_id = module.apim.id
  format            = "rawxml"
  description       = "Adds the backend auth header from the backend-api-key named value."

  # References {{backend-api-key}}; carries no secret itself.
  value = file("${path.module}/fragments/backend-auth.xml")

  depends_on = [module.backend_key]
}

# The include is a string, so interpolate it to create a real dependency.
resource "azurerm_api_management_policy" "service" {
  api_management_id = module.apim.id

  xml_content = <<-XML
    <policies>
      <inbound>
        <base />
        ${module.backend_auth_fragment.include_fragment_snippet}
      </inbound>
      <backend><base /></backend>
      <outbound><base /></outbound>
      <on-error><base /></on-error>
    </policies>
  XML
}

πŸ”’ The backend key never appears in the fragment. It lives in a named value marked secret, and the fragment references it by token β€” which keeps it out of the fragment's content, out of this module's state, and out of every plan that refreshes it.

⚠️ depends_on is used because the named-value reference is a string inside XML, not a Terraform reference: without it, nothing orders the two. The include, by contrast, gets its ordering for free from the interpolation.


πŸ“₯ Inputs

Group Variables
Identity (both force-new) name, api_management_id
Content value, format, description
Universal tail timeouts
Full input schemas
name              = string   # force-new; 1-80 chars, alphanumeric plus - and _; also the include key
api_management_id = string   # force-new; the SERVICE id, not its name

value       = string                     # required; the XML snippet, validated by nothing at the provider
format      = optional(string, "xml")    # "xml" | "rawxml", lower case
description = optional(string)

timeouts = optional(object({ create, read, update, delete }))

See variables.tf for the full descriptions and every validation.


🧾 Outputs

Output Description Notes
id Resource ID of the policy fragment Emitted first; little consumes it
name Name of the fragment The real interface β€” the include key
include_fragment_snippet The exact <include-fragment .../> element Derived; paste it
api_management_id Resource ID of the parent service Rebuilt from the Resource ID on read
api_management_name Name of the parent service Derived from the ID
resource_group_name Resource group holding the parent service Derived from the ID
value_fingerprint SHA-256 of the content as Azure returns it The content itself is deliberately not emitted
value_matches_configuration True when Azure's content matches the configuration, whitespace-normalised Derived; false is the format-mismatch signature
value_length_in_characters Length of the content as Azure returns it Derived
format xml or rawxml, as Azure reports it Defaults to xml when Azure says nothing
description The fragment's description as Azure holds it
value_looks_like_a_full_policy_document True when the snippet has a <policies> root Derived; reported, not refused
value_declares_an_xml_prolog True when the snippet starts with <?xml ... ?> Derived; reported, not refused
uses_raw_xml_format True on the rawxml format Derived
references_a_named_value True when the snippet uses a {{...}} token Derived
fragment_alone_runs_nothing Constant true
the_include_is_a_string_terraform_cannot_see Constant true
a_wrong_format_loops_instead_of_failing Constant true The perpetual-diff signature
an_imported_fragment_always_arrives_as_xml Constant true
whitespace_and_element_order_never_produce_a_diff Constant true Cuts both ways
destroy_fails_if_the_fragment_is_already_gone Constant true
put_credentials_in_a_named_value_not_in_the_fragment Constant true
force_new_fields The fields whose change destroys and recreates the record
fields_azure_returns_on_read The only fields in which drift can be detected
this_resource_supports_no_azure_resource_tags Constant true

The fragment's content is emitted, deliberately: it is configuration rather than a credential, Azure returns it on every read, and it is the only drift signal this resource has.


🧠 Architecture Notes

The name is the interface, and Terraform cannot see it being used. A policy includes a fragment by writing <include-fragment fragment-id="NAME" /> into its XML. That is a string, not a reference: no dependency is created, no ordering is enforced, and no plan reports that a rename has orphaned an include. Where the fragment and its consumers share a configuration, interpolating include_fragment_snippet is the one way to turn the string back into a dependency.

A wrong format loops instead of failing. The provider requests the fragment's value from Azure in whatever format is configured, so a mismatch returns a differently-encoded string on every read and Terraform proposes the same update forever β€” one that applies successfully and comes straight back. The published documentation describes the symptom without naming the cause. Switch the format; do not reformat the value.

Import is lossy in exactly one field. The API only reports a format when one was explicitly requested and it is not xml, so the provider's import callback sets xml unconditionally. A rawxml fragment therefore imports as xml and shows a diff until corrected β€” an in-place update, during which the provider deliberately re-sends the value.

The diff suppression is generous in both directions. It parses both sides as XML to absorb element ordering, and falls back to a string comparison with every space, tab, newline and indent stripped and XML entities unescaped. Reformatting a fragment file produces no plan, which is convenient. A genuine change in literal whitespace inside a text value, or in escaping alone, is equally invisible β€” which is not.

Destroy is stricter here than elsewhere in the family. The delete path has no not-found tolerance, so a fragment removed through the portal makes terraform destroy fail rather than converge. terraform state rm is the recovery. Azure may also refuse to delete a fragment a policy still includes; the provider surfaces whatever Azure says and adds no check of its own.

The content is not protected and should not need to be. It is stored on the service in clear and returned on every read, so it lands in state and in every refreshing plan. This module emits it because it is the only drift signal available. The correct response is to keep secrets out of the snippet by referencing a named value.


🧱 Design Principles

Concern Default in this module Opt-out
Format format = "xml", matching the provider and Azure set "rawxml"
Content validation only unambiguous mistakes are refused β€” an empty value, a value with no XML at all, a value that is plainly a file path none
Shape mistakes a <policies> root and an XML prolog are reported through outputs, not refused, because the provider accepts any string and a validation failure blocks destroy none
Secrets not withheld β€” the content is emitted, and the module says plainly that credentials belong in a named value instead none
The include emitted as a ready-to-paste element, so the one string that matters is not retyped none
Tagging no tags variable β€” the resource exposes none; use description tag the parent API Management service

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module by immutable tag β€” ?ref=v1.0.0 β€” never a branch. This module is plan-only from the library's point of view: a human applies from CI against a reviewed plan.


πŸ§ͺ Testing

terraform validate covers the name pattern, the service-ID shape (including an ID that already carries a /policyFragments/ segment), the format enum with its lower-case requirement, and the three content checks β€” non-empty, contains XML at all, and is not plainly a file path.

A variables-only terraform plan fires the same validations against a .tfvars file and is also the cheapest way to confirm the derived flags. Drive both directions: a fully populated positive fixture is what catches a check that is wrong in the accepting direction, and Terraform skips a validation whose referenced variable has already failed, so a short error list is not proof a check is missing.

What only a real plan or apply reaches: whether the service exists, whether the name collides with an existing fragment, and whether Azure accepts the XML. What nothing reaches, at any stage: whether any policy includes the fragment, whether a referenced named value exists, and whether the format matches how the content is actually written β€” that last one surfaces as a diff that will not settle.


πŸ’¬ Example Output

id                                       = "/subscriptions/.../service/apim-platform-prod/policyFragments/backend-auth"
name                                     = "backend-auth"
include_fragment_snippet                 = "<include-fragment fragment-id=\"backend-auth\" />"
api_management_id                        = "/subscriptions/.../service/apim-platform-prod"
api_management_name                      = "apim-platform-prod"
resource_group_name                      = "rg-platform-apim"
format                                   = "rawxml"
description                              = "Adds the backend auth header from the backend-api-key named value."
value                                    = "<set-header name=\"Authorization\" exists-action=\"override\"><value>{{backend-api-key}}</value></set-header>"
value_looks_like_a_full_policy_document  = false
value_declares_an_xml_prolog             = false
uses_raw_xml_format                      = true
references_a_named_value                 = true
force_new_fields                         = [
  "name",
  "api_management_id",
]
the_include_is_a_string_terraform_cannot_see = true
a_wrong_format_loops_instead_of_failing      = true

πŸ” Troubleshooting

Symptom Cause Fix
An update applies successfully and returns on the next plan format does not match how the content is written; the provider reads the value back in the configured format Switch the format. a_wrong_format_loops_instead_of_failing names the signature
A diff appears immediately after terraform import Import always sets format = "xml", whatever the fragment was authored as Set the real format and apply. It is an in-place update, not a replacement
The fragment applies and nothing changes at runtime No policy includes it. An unreferenced fragment is valid and inert Add <include-fragment fragment-id="NAME" /> to a policy. Use include_fragment_snippet
A policy that includes the fragment fails at request time The fragment name in the XML does not match, or a {{named-value}} in the snippet does not exist Compare against name; confirm the named value exists on the same service
terraform destroy fails on a fragment that is already gone The delete path has no not-found tolerance, unlike most of this family terraform state rm this resource, then continue
Azure refuses to delete the fragment A policy still includes it. The provider adds no check and surfaces Azure's error Remove the include first, then destroy
Reformatting the XML produces no plan The diff suppression strips all whitespace and unescapes entities before comparing Expected. Note it also hides a genuine whitespace or escaping change
format = "RawXml" rejected Both values are lower case and the check is case-sensitive Use "rawxml"
value rejected as containing no XML A file path was passed instead of the file's contents value = file("${path.module}/fragments/NAME.xml")
Renaming the fragment broke every policy using it The include is a string Terraform cannot see, so the rename orphaned it Interpolate include_fragment_snippet into the policy so the two move together

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."