Automating Azure API Management Policies with Terraform
Azure API Management (APIM) is a powerful and flexible service that enhances the operability of REST APIs. Whether you are managing a well-architected set of microservices or dealing with a collection of legacy APIs that need modernization, APIM provides a centralized way to manage, secure, and optimize API interactions.
One of its most distinctive features is its policy engine, which allows fine-grained control over API behavior using layered XML-based policies. However, from an Infrastructure as Code (IaC) perspective, these XML policies introduce unique challenges due to their embedded nature and the need for dynamic string concatenation when integrating with Terraform. This article explores the structure of Azure API Management and demonstrates how to automate its policies using Terraform.
Defining the API Management Service
The first step in configuring APIM with Terraform is setting up the API Management service itself. I think of this as the “endpoint” because it serves as such — the central endpoint for API requests and where policies will be enforced.
resource "azurerm_api_management" "main" {
name = var.name
location = var.location
resource_group_name = var.resource_group_name
publisher_name = var.publisher_name
publisher_email = var.publisher_email
sku_name = var.service_settings.sku_name
policy {
xml_content = <<XML
<policies>
<inbound>
${var.policies.inbound}
</inbound>
<backend>
${var.policies.backend}
</backend>
<outbound >
${var.policies.outbound}
</outbound >
<on-error>
${var.policies.error}
</on-error>
</policies>
XML
}
}
This Terraform configuration initializes the APIM service and embeds XML-based policies that define how API requests and responses should be processed.
Defining the API
Next, we define an API within the API Management service and apply policies at the API level.
resource "azurerm_api_management_api" "api" {
resource_group_name = var.resource_group_name
api_management_name = var.endpoint_name
revision = var.revision
name = var.name
display_name = var.description
path = var.path
protocols = [var.primary_protocol]
subscription_required = var.subscription_required
}
This step ensures that API definitions align with the organization’s requirements while maintaining governance through APIM.
Defining Policy Components
APIM policies often include authentication, rate limiting, transformations, and error handling. Here, we define a local variable that incorporates JWT authentication as a policy element:
locals {
jwt_authentication = <<XML
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
<openid-config url="https://login.microsoftonline.com/${data.azurerm_client_config.current.tenant_id}/.well-known/openid-configuration" />
<required-claims>
<claim name="aud">
<value>${var.aad_settings.scope}</value>
</claim>
</required-claims>
</validate-jwt>
XML
}
Merging Policies with Local Variables
One of the key techniques in this configuration is dynamically merging externally provided policy variables with locally defined XML using newline character concatenation. While this approach may not be the prettiest, it is highly effective in allowing flexible, parameterized policies while maintaining readability.
merged_policies = {
inbound = "${var.policies.inbound} \n ${local.jwt_authentication}"
outbound = var.policies.outbound
backend = var.policies.backend
error = var.policies.error
}
This ensures that authentication policies are seamlessly integrated while still allowing modular policy components to be defined elsewhere.
Building the Final XML Payload
Once the policy components are merged, we construct the final XML payload:
locals {
policy_xml = <<XML
<policies>
<inbound>
${var.policies.inbound}
<set-backend-service id="tf-generated-policy" backend-id="${var.service_settings.backend_name}" />
<base />
</inbound>
<backend>
${var.policies.backend}
<base />
</backend>
<outbound>
${var.policies.outbound}
<base />
</outbound>
<on-error>
${var.policies.error}
<base />
</on-error>
</policies>
XML
}
Readability Considerations in Azure Portal
Even though we are managing APIM policies through Infrastructure as Code, we must ensure that the final XML output remains readable in the Azure Portal. Read-only operators who inspect policies within the APIM UI should still have a clear and structured view of the policy logic. The approach taken here ensures that:
- Policies remain modular and parameterized.
- The final XML structure is preserved for easy inspection.
- The management experience for operators in the Azure Portal is not diminished.
Attaching the Policy to the API
Finally, the constructed policy is applied to the API:
resource "azurerm_api_management_api_policy" "api_policy" {
resource_group_name = var.context.resource_group_name
api_name = azurerm_api_management_api.api.name
api_management_name = var.service_settings.endpoint_name
xml_content = local.policy_xml
}
Conclusion
Azure API Management provides an incredibly powerful way to control and optimize APIs, but its XML-based policies introduce challenges when automating infrastructure with Terraform. By carefully structuring and parameterizing XML artifacts, we can overcome these challenges and dynamically configure API behavior in a repeatable and scalable manner.
The technique of merging variable-driven policies with local XML definitions ensures that policies remain both flexible, accommodating the inherited nature of API policies, and readable within the Azure Portal.
I think this scenario highlights the diversity of automation challenges across Azure services — proving that cloud infrastructure is not just about managing virtual machines folks!!!