Owns the global policy of an Azure API Management service β the document that applies to every API, at the broadest of the five policy scopes. Targets
hashicorp/azurerm ~> 4.0.
- π Owns the global policy of an API Management service β one document, applying to every API.
- 𧨠States the behaviour no other module in this family has: creating it overwrites whatever global policy already existed, with no warning at plan or apply.
- π Corrects a reasonable assumption: this resource's Resource ID is the service's own ID, byte for byte.
- πͺ Surfaces the bypass β a narrower policy that drops
<base />silently opts out of everything here β and names the Azure Policy definition that can enforce it. - π Explains that
xml_linkis fetched once and never read back, so it is not a live link. - π Emits no policy content β only a fingerprint, a length and a match flag.
- π·οΈ Carries no
tagsβ the resource has none. The universal tail istimeoutsonly.
π‘ Why it matters: a global policy is where organisations put the controls they most want to be universal β authentication, logging, rate limiting, IP filtering. Two things undermine that: the first apply replaces whatever was already there, and any API team can step out of it by deleting one element. Neither is visible in a plan, so both are stated here as outputs.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
flowchart TB
rg["terraform-azurerm-resource-group"]
apim["terraform-azurerm-api-management"]
nv["terraform-azurerm-api-management-named-value"]
frag["terraform-azurerm-api-management-policy-fragment"]
cache["terraform-azurerm-api-management-redis-cache"]
this["terraform-azurerm-api-management-policy"]
api["terraform-azurerm-api-management-api"]
pa["terraform-azurerm-subscription-policy-assignment"]
rg -->|"name"| apim
apim -->|"id, which is also this record's own id"| this
nv -->|"referenced as a double-brace token"| this
frag -->|"name, pasted into an include-fragment element"| this
cache -->|"makes an external cache available to a cache-store element"| this
this -->|"applies to every API, unless one drops the base element"| api
api -->|"an API policy without base bypasses this one entirely"| this
pa -->|"audits or denies a missing base element"| api
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,nv,frag,cache,api,pa sib;
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 the edge running back from the API module: an API-scoped policy that omits <base /> bypasses this document entirely, which makes the relationship bidirectional in effect even though only one direction is a Terraform reference. The policy-assignment edge is the only mechanism that turns this global policy into a floor rather than a default.
flowchart TB
subgraph inputs["Inputs"]
svc["api_management_id, the only force-new field"]
content["xml_content, the document inline"]
link["xml_link, fetched once by Azure"]
end
this["azurerm_api_management_policy.this"]
prop["the service's global policy, one per service"]
subgraph outputs["Outputs"]
oid["id, which IS the service id"]
ofp["policy_fingerprint, policy_length_in_characters, policy_matches_configuration"]
oshape["has_policies_root, contains_an_inert_base_element, declares_forward_request_in_backend"]
ofact["creating_this_overwrites_any_existing_global_policy, a_narrower_policy_without_base_bypasses_this_one"]
end
svc --> this
content -->|"exactly one of the two"| this
link -->|"exactly one of the two"| this
this -->|"replaces whatever was there"| prop
this --> oid
this --> ofp
this --> oshape
this --> ofact
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 prop keystone;
class svc,content,link,oid,ofp,oshape,ofact sib;
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_api_management_policy.this |
1 | The keystone, and the only resource. One per service; its Resource ID is the service's own. |
| 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
- No import-exists check, by design. Every other resource in this family refuses to adopt an existing object; this one replaces it. The provider's own source comment explains why: the API always returns a policy, so "unset" and "set by someone else" are indistinguishable.
- The Resource ID is the service's Resource ID.
idandapi_management_idare the same string. api_management_idis the only force-new field. Both document fields update in place.xml_contentandxml_linkareExactlyOneOfandConflictsWith; the create function additionally errors witheither `xml_content` or `xml_link` must be setat apply when neither is given.xml_linkis never read back. Azure downloads the document and stores its content;xml_contentis Optional and Computed, so it fills with whatever was downloaded.- Switching from
xml_linktoxml_contentclears the link inside the update function, with ad.Set("xml_link", "")on a non-new resource. - The diff suppression strips all whitespace and unescapes the five XML entities before comparing β so reformatting and re-escaping are both invisible.
- Read applies HTML unescaping, which is wider than XML's five entities.
SchemaVersionis 3, with three state upgraders for state written by older provider versions.- No
tags, nolocation. The universal tail istimeoutsonly.
Microsoft.ApiManagement/service/policies/*β write and delete, scoped to the parent API Management service. API Management Service Contributor on the service is the smallest built-in role that covers it; Contributor on the resource group covers it more broadly than necessary.Microsoft.ApiManagement/service/readon the service, which the read path uses before fetching the policy.
π Plan access is policy-content access. Refreshing this resource returns the whole global policy document, which routinely carries backend URLs and named-value references. That is a reason to keep credentials out of the document β reference a named value β rather than a reason to restrict the plan.
- An existing API Management service.
- Knowledge of the service's current global policy. Creating this resource replaces it without warning, so a service managed by hand or by another pipeline must have its policy exported and reconciled into the configuration first.
- Any named value or policy fragment the document references must already exist on the same service. Nothing validates either β a missing one fails at request time.
- For
xml_link, a URL Azure can reach anonymously. - The caller configures
provider "azurerm" { features {} }, authentication and subscription.
terraform-azurerm-api-management-policy/
βββ providers.tf # required_version + pinned azurerm; no provider block
βββ variables.tf # deeply-typed inputs; the unambiguous document mistakes rejected at plan time
βββ main.tf # the single keystone resource
βββ outputs.tf # id first (with its caveat), then the fingerprint, then the silent behaviours
βββ README.md # this document
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore # the canonical library ignore set
provider "azurerm" {
features {}
}
module "apim_global_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy.git?ref=v1.0.0"
api_management_id = module.apim.id
xml_content = file("${path.module}/policies/global.xml")
}βΉοΈ The caller configures the provider, including the mandatory
features {}block. This module declares none.
β οΈ This replaces the service's existing global policy on the first apply. Export the current one first if the service was not created by this configuration.
Consumes
| Input | Type | Source |
|---|---|---|
api_management_id |
string |
terraform-azurerm-api-management output id |
xml_content |
string |
a local .xml file read with file() |
xml_link |
string |
a publicly reachable URL, hosted outside this library |
fragment names inside xml_content |
string |
terraform-azurerm-api-management-policy-fragment output include_fragment_snippet |
named-value tokens inside xml_content |
string |
terraform-azurerm-api-management-named-value β referenced by name in the XML, not wired |
Emits
| Output | Consumed by |
|---|---|
policy_fingerprint |
cross-environment comparison and out-of-band change detection |
policy_matches_configuration |
drift review |
a_narrower_policy_without_base_bypasses_this_one |
control review β the reason a global policy is a default, not a floor |
azure_policy_can_enforce_the_base_element |
terraform-azurerm-subscription-policy-assignment, which can assign the built-in definition |
1 Β· Minimal global policy
module "global_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy.git?ref=v1.0.0"
api_management_id = module.apim.id
xml_content = file("${path.module}/policies/global.xml")
}π‘ Read the document from a file rather than a heredoc. Terraform's own interpolation syntax collides with policy expressions, and
file()interpolates nothing at all.
2 Β· The document itself
<policies>
<inbound>
<ip-filter action="allow">
<address-range from="10.0.0.0" to="10.255.255.255" />
</ip-filter>
</inbound>
<backend>
<forward-request />
</backend>
<outbound />
<on-error />
</policies>βΉοΈ A global policy is a full document with the
<policies>root and its four sections β unlike a policy fragment, which carries bare statements. Note the absence of<base />: at the global scope it does nothing, because there is no parent scope to inherit from.
β οΈ <forward-request />is what Azure puts in the global backend section by default. A document that omits it changes how every request reaches its backend.
3 Β· Including a policy fragment
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")
}
module "global_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy.git?ref=v1.0.0"
api_management_id = module.apim.id
xml_content = <<-XML
<policies>
<inbound>
${module.cors_fragment.include_fragment_snippet}
</inbound>
<backend><forward-request /></backend>
<outbound />
<on-error />
</policies>
XML
}π‘ Interpolating
include_fragment_snippetis the one way to make the fragment a genuine Terraform dependency β otherwise the include is a bare string and nothing orders the two.
4 Β· Referencing a named value instead of a literal secret
module "global_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy.git?ref=v1.0.0"
api_management_id = module.apim.id
# The XML references {{backend-api-key}} rather than carrying the key.
xml_content = file("${path.module}/policies/global.xml")
depends_on = [module.backend_key]
}π Policy XML is stored in Terraform state in plaintext, and marking a value sensitive would redact plan output without encrypting state. Reference a named value β ideally Key Vault-backed β and keep the secret out of the document.
references_a_named_valuereports whether a document does this.
5 Β· Fetching the document from a URL
module "global_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy.git?ref=v1.0.0"
api_management_id = module.apim.id
xml_link = "https://policies.contoso.com/apim/global.xml"
}
β οΈ This is a fetch-once instruction, not a live link. Azure downloads the document at apply time and stores its content; changing the file afterwards changes nothing until Terraform applies again, and no plan will propose that apply.π The URL must be reachable by Azure anonymously, which means the document β and whatever backend topology it reveals β is public. Prefer
xml_contentwith the document held alongside the Terraform configuration.
6 Β· Adopting a service whose policy already exists
# Export the current global policy BEFORE the first apply. This resource has no
# import-exists check: applying without this step replaces it silently.
az apim api policy show --resource-group rg-platform-apim \
--service-name apim-platform-prod --api-id '*' 2>/dev/null
terraform import 'module.global_policy.azurerm_api_management_policy.this' \
"/subscriptions/$SUB/resourceGroups/rg-platform-apim/providers/Microsoft.ApiManagement/service/apim-platform-prod"
β οΈ Note the import ID: it is the service's Resource ID, with no policy segment. That is not a shorthand β it is genuinely this resource's identifier.
β οΈ creating_this_overwrites_any_existing_global_policystates the risk as a constant so it survives in every instance's output.
7 Β· Detecting an out-of-band change
output "global_policy_matches_terraform" {
value = module.global_policy.policy_matches_configuration
}
output "global_policy_fingerprint" {
value = module.global_policy.policy_fingerprint
}π‘
policy_matches_configurationcompares Azure's document against the configuration the way the provider does β whitespace stripped and entities unescaped β so it reports a substantive change rather than a reformatting. The fingerprint is what to compare across environments.
8 Β· Comparing the policy across environments
output "policy_fingerprints_by_environment" {
value = { for k, m in module.global_policies : k => m.policy_fingerprint }
}policy_fingerprints_by_environment = {
"dev" = "b1e4...c92a"
"prod" = "b1e4...c92a"
"uat" = "7f30...11de" # <- UAT has drifted
}
π‘ The document itself is never emitted, so this comparison leaks nothing β which is what makes it safe to publish as a CI output.
9 Β· Reviewing document shape across an estate
output "policies_missing_a_policies_root" {
value = [for k, m in module.global_policies : k if m.has_policies_root == false]
}
output "policies_with_an_inert_base_element" {
value = [for k, m in module.global_policies : k if m.contains_an_inert_base_element == true]
}
output "policies_not_forwarding_requests" {
value = [for k, m in module.global_policies : k if m.declares_forward_request_in_backend == false]
}
β οΈ All three readnullon axml_linkpolicy, because the document is not visible from the configuration β hence the explicit== falseand== truecomparisons rather than a bare truthiness test.
10 Β· Enforcing that narrower scopes cannot bypass this policy
module "enforce_base_element" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subscription-policy-assignment.git?ref=v1.0.0"
name = "apim-policies-must-inherit-base"
subscription_id = data.azurerm_subscription.current.id
policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/d5448c98-e503-4fdd-bcd2-784960c00d04"
display_name = "API Management policies should inherit parent scope policies using <base />"
}π This is the only mechanism that makes a global policy a floor rather than a default. Microsoft describes omitting
<base />as leading to "bypassing shared rules such as authentication, logging, rate limits, and other critical controls", and ships the built-in definition above with Audit, Deny and Disabled effects.
11 Β· Custom timeouts
module "global_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy.git?ref=v1.0.0"
api_management_id = module.apim.id
xml_content = file("${path.module}/policies/global.xml")
timeouts = {
create = "45m"
read = "10m"
update = "45m"
delete = "45m"
}
}
β οΈ A key that is notcreate,read,updateordeleteis silently discarded by Terraform's type conversion β no error, and no timeout. Note that the write here is fast: Microsoft's guidance for the global scope is that saving propagates to the gateway immediately. The risk is not duration; it is that the change takes effect on every API at once.
12 Β· for_each across several services
variable "api_management_ids" {
description = "Service Resource IDs, keyed by environment."
type = map(string)
}
module "global_policies" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy.git?ref=v1.0.0"
for_each = var.api_management_ids
api_management_id = each.value
xml_content = file("${path.module}/policies/global.xml")
}π‘ One document, several services. Because
api_management_idis force-new and a destroy reverts a service to its default policy, removing an entry from this map is a control change on that service β not a bookkeeping change.
13 Β· ποΈ 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"
}
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 "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 global policy."
}
module "global_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-policy.git?ref=v1.0.0"
api_management_id = module.apim.id
xml_content = <<-XML
<policies>
<inbound>
${module.cors_fragment.include_fragment_snippet}
<set-header name="X-Backend-Key" exists-action="override">
<value>{{backend-api-key}}</value>
</set-header>
</inbound>
<backend><forward-request /></backend>
<outbound />
<on-error />
</policies>
XML
depends_on = [module.backend_key]
}
# Without this, any API can step out of the global policy by deleting <base />.
module "enforce_base_element" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subscription-policy-assignment.git?ref=v1.0.0"
name = "apim-policies-must-inherit-base"
subscription_id = data.azurerm_subscription.current.id
policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/d5448c98-e503-4fdd-bcd2-784960c00d04"
display_name = "API Management policies should inherit parent scope policies using <base />"
}π The backend key never appears in the policy document β it lives in a named value marked secret and is referenced by token, which keeps it out of the document, out of state, and out of every plan that refreshes this resource.
β οΈ depends_onis needed because the named-value reference is a string inside XML, not a Terraform reference. The fragment include, by contrast, gets its ordering for free from the interpolation.
β οΈ The policy assignment is not decoration. Without it, the controls in this document are a default that any API team can decline.
| Group | Variables |
|---|---|
| Identity (force-new) | api_management_id |
| The document (exactly one) | xml_content, xml_link |
| Universal tail | timeouts |
Full input schemas
api_management_id = string # force-new; the SERVICE id -- and also this resource's own id
# Exactly one of the two. Neither is force-new.
xml_content = optional(string) # the full <policies> document, inline
xml_link = optional(string) # a publicly reachable URL; fetched once, never read back
timeouts = optional(object({ create, read, update, delete }))See variables.tf for the full descriptions and every validation.
| Output | Description | Notes |
|---|---|---|
id |
Resource ID of the global policy | Equals the service's own ID |
api_management_id |
Resource ID of the parent service | Same string as id |
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 |
policy_source |
xml_content or xml_link |
Derived |
policy_fingerprint |
SHA-256 of the document as Azure holds it | The document is not emitted |
policy_length_in_characters |
Length of the document as Azure holds it | Derived |
policy_matches_configuration |
True when Azure's document matches the configuration | Null on the xml_link path |
xml_link |
The configured URL | Azure never reads it back |
xml_link_uses_plaintext_http |
True when fetched over HTTP | Derived |
has_policies_root |
True when the document carries a <policies> root |
Null on the xml_link path |
contains_an_inert_base_element |
True when <base /> appears, where it does nothing |
Null on the xml_link path |
declares_forward_request_in_backend |
True when the document forwards requests | Null on the xml_link path |
references_a_named_value |
True when the document uses a {{...}} token |
Null on the xml_link path |
creating_this_overwrites_any_existing_global_policy |
Constant true | The adoption risk |
id_is_the_api_management_service_id |
Constant true | |
this_record_is_a_singleton_per_service |
Constant true | |
a_narrower_policy_without_base_bypasses_this_one |
Constant true | The control risk |
azure_policy_can_enforce_the_base_element |
Constant true | The remedy |
xml_link_is_fetched_once_and_never_read_back |
Constant true | |
whitespace_and_entity_changes_never_produce_a_diff |
Constant true | Cuts both ways |
policy_xml_is_stored_in_state_in_plaintext |
Constant true | |
destroying_this_reverts_the_service_to_its_default_policy |
Constant true | |
force_new_fields |
The single force-new field | |
fields_azure_returns_on_read |
The only fields in which drift can be detected | |
this_resource_supports_no_azure_resource_tags |
Constant true |
No policy content is emitted β only a fingerprint, a length and a match flag. This follows the convention the
api-management-api-operationmodule already sets for policy XML.
The first apply is the dangerous one. Every other resource in this family refuses to adopt an existing object and tells you to import it. This one replaces it, and the provider's own source comment explains that it has no choice: the API always returns a policy β a default one when nobody has set one β so "unset" and "set by someone else" are indistinguishable. Export the current global policy before adopting a service you did not create.
The Resource ID is not an identifier for this record. It is the API Management service's ID, byte for byte, because there is one global policy per service and it has no identity of its own. Using it as a policy identifier, or as a lock scope intended for the policy, silently addresses the whole service.
A global policy is a default, not a floor. Microsoft: "If you remove the base element at the API scope, only policies configured at the API scope will be applied. Policies configured at product and broader scopes won't be applied." So an API team can step out of global authentication, logging and rate limiting by deleting one element β no failure, no warning, and nothing here reporting which scopes have done so. The built-in Azure Policy definition that audits or denies a missing <base /> is the only mechanism that closes it.
<base /> in this document is inert. Microsoft again: "A globally scoped policy has no parent scope, and using the base element in it has no effect." Harmless, and worth surfacing, because its presence suggests inheritance that is not happening.
A linked document is fetched once. Azure downloads it at apply time and stores the content; the link is never read back, and no plan depends on the file's contents. Drift between the URL and the service is invisible in both directions, which makes xml_link a poor fit for anything that changes.
The diff suppression is generous in both directions. It parses both sides as XML, then falls back to a string comparison with all whitespace stripped and the five XML entities unescaped. Reformatting produces no plan, which is convenient. A change purely in escaping, or in literal whitespace inside a value, is equally invisible β which is not.
The content stays out of the outputs. Policy documents carry backend URLs, subscription keys and authorization headers. This module emits a fingerprint, a length and a match flag, which support every comparison a consumer needs without copying the document into another configuration's state.
| Concern | Default in this module | Opt-out |
|---|---|---|
| Adoption safety | none available β the provider has no import-exists check, so the module states the risk as an output and documents the export-first workflow instead | none |
| Document source | not defaulted; every example uses xml_content, and xml_link carries an explicit public-exposure warning |
use xml_link |
| Link scheme | not defaulted; xml_link_uses_plaintext_http reports HTTP rather than refusing it |
none |
| Document shape | a missing <policies> root, an inert <base /> and a missing <forward-request /> are reported through outputs, not refused, because the provider validates nothing and a validation failure blocks destroy |
none |
| Content emission | no policy content is emitted β fingerprint, length and match flag only | none |
| Secrets | not withheld by marking sensitive, which would redact plan output without encrypting state; the module says plainly that credentials belong in a named value | none |
| Tagging | no tags variable β the resource exposes none |
tag the parent API Management service |
terraform init -backend=false
terraform validate
terraform fmt -checkPin 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. Treat the first apply against an existing service as a control change, not a bookkeeping one.
terraform validate covers the service-ID shape (including an ID that addresses a narrower scope), the exactly-one-of rule, and the document checks β non-empty, contains XML, is not plainly a file path β plus the link checks: it must be an http/https URL, and must not be a policy document pasted into the wrong field.
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, and note the third case this resource needs: an xml_link fixture, where every content-derived output must read null rather than false. 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, and whether Azure accepts the XML. What nothing reaches, at any stage: what the service's global policy was before this configuration replaced it, whether any narrower scope has dropped <base />, and whether a referenced named value or fragment exists.
id = "/subscriptions/.../providers/Microsoft.ApiManagement/service/apim-platform-prod"
api_management_id = "/subscriptions/.../providers/Microsoft.ApiManagement/service/apim-platform-prod"
api_management_name = "apim-platform-prod"
resource_group_name = "rg-platform-apim"
policy_source = "xml_content"
policy_fingerprint = "b1e4c0a97f2d5e8143aa6c0b7e91d2f4c3358a10bb47e6d9f0c2a5b83e71d94c"
policy_length_in_characters = 412
policy_matches_configuration = true
xml_link = null
xml_link_uses_plaintext_http = false
has_policies_root = true
contains_an_inert_base_element = false
declares_forward_request_in_backend = true
references_a_named_value = true
force_new_fields = [
"api_management_id",
]
creating_this_overwrites_any_existing_global_policy = true
id_is_the_api_management_service_id = true
a_narrower_policy_without_base_bypasses_this_one = true
| Symptom | Cause | Fix |
|---|---|---|
| The service's existing global policy vanished on the first apply | This resource has no import-exists check; it replaces whatever was there | Export the policy before adopting a service. creating_this_overwrites_any_existing_global_policy states it |
| A control in the global policy is not applying to one API | That API's policy omits <base />, which drops every broader scope |
Add <base /> to the API policy, and assign the built-in Azure Policy definition to catch the next one |
<base /> in the global policy appears to do nothing |
It genuinely does nothing β a globally scoped policy has no parent | Remove it. contains_an_inert_base_element reports it |
| Requests stopped reaching the backend after a policy change | The new document's <backend> section omits <forward-request />, which Azure puts there by default at the global scope |
Restore it, or make the alternative deliberate. declares_forward_request_in_backend reports it |
The file at xml_link changed and nothing happened |
The link is fetched once; Azure stores the content, and no plan depends on the file | Apply again, or move the document to xml_content where Terraform can see it change |
xml_link shows as null in state after an apply |
The provider never reads it back β Azure stores the downloaded content, not the link | Expected. The module emits the configured value so it is recorded somewhere |
Using id as a lock scope locked the whole service |
This resource's ID is the service's ID | Use it knowingly, or scope the lock deliberately |
| A reformatted policy file produces no plan | The diff suppression strips all whitespace and unescapes entities | Expected. Note it also hides a genuine whitespace or escaping change |
Apply fails with either `xml_content` or `xml_link` must be set |
Neither was supplied; the provider raises this in the create function, so the plan looked clean | Supply exactly one. This module refuses the same case at plan time, so seeing this means the call bypassed the module |
A {{named-value}} in the policy fails at request time |
The named value does not exist on the service; nothing validates the reference | Create it with the named-value module and order it with depends_on |
azurerm_api_management_policyβ provider resource reference- Policies in Azure API Management β the five scopes and how they combine
- How to set or edit Azure API Management policies β the
baseelement and policy evaluation order - Azure Policy built-in definitions for API Management β including the definition that enforces
<base /> - Sibling modules:
terraform-azurerm-api-management,terraform-azurerm-api-management-policy-fragment,terraform-azurerm-api-management-named-value,terraform-azurerm-api-management-api,terraform-azurerm-subscription-policy-assignment - This module's
SCOPE.mdβ the cross-module contract, permissions and prerequisites
π "Infrastructure as Code should be standardized, consistent, and secure."