How to parameterize Azure API management policy files when using Bicep

I recently started working with Bicep to setup deployments of APIs to Azure. Bicep modules provide an easy-to-manage process when single-API deployment is needed.

In most cases, we need to customize the policy files (API level, or operation level) to accommodate different environments. For example, in the following simple policy, we might want to replace the value with “abc” in development environment, but use “def” in production environment.

<policies>
    <inbound>
        <base />
        <find-and-replace from="xyz" to="abc" />
    </inbound>
</policies>

Imagine our Bicep looks like this:

param apiName string

module api 'api.module.bicep' = {
  name: '${apiName}-API'
  params: {
    apiManagementServiceName: apimServiceName
    name: apiName
    ...
    policy: {
      format: 'rawxml'
      value: loadTextContent('policies/apiPolicy.xml')
    }
  }
}

…and the API module looks like this:

param policy object = {}
...
// API policy
resource api_policy 'Microsoft.ApiManagement/service/apis/policies@2021-08-01' = if (!empty(policy)) {
  name: 'policy'
  parent: api
  properties: {
    format: contains(policy, 'format') ? policy.format : 'rawxml'
    value: policy.value
  }
}

The simplest way would be to create a policy.xml file for each environment and then use a conditional in the Bicep to select which policy to use, i.e.

<!-- DEV policy-->
<policies>
    <inbound>
        <base />
        <find-and-replace from="xyz" to="abc" />
    </inbound>
</policies>
<!-- PROD policy-->
<policies>
    <inbound>
        <base />
        <find-and-replace from="xyz" to="def" />
    </inbound>
</policies>
param apiName string
@allowed([
  'DEV'
  'PROD'
])
param environment string

module api 'api.module.bicep' = {
  name: '${apiName}-API'
  params: {
    apiManagementServiceName: apimServiceName
    name: apiName
    ...
    policy: {
      format: 'rawxml'
      value: environment == 'DEV' ? loadTextContent('policies/apiPolicy.DEV.xml') : loadTextContent('policies/apiPolicy.PROD.xml')
    }
  }
}

However this doesn’t scale well, and you have to create a policy.xml file for each environment, even when they don’t differ by much.

A better way is to introduce parameterization within the policy file itself, i.e.

<policies>
    <inbound>
        <base />
        <find-and-replace from="xyz" to="$(replacement)" />
    </inbound>
</policies>

Then we can introduce the replacement parameter when we pass the module parameters for the policy, i.e.

param apiName string
param replacement string

module api 'api.module.bicep' = {
  name: '${apiName}-API'
  params: {
    apiManagementServiceName: apimServiceName
    name: apiName
    ...
    policy: {
      format: 'rawxml'
      value: loadTextContent('policies/apiPolicy.xml')
      params: {
        replacement: replacement
      }
    }
  }
}

…and then use the reduce function in the module to do the string replacement:

param policy object = {}
...
// API policy
resource api_policy 'Microsoft.ApiManagement/service/apis/policies@2021-08-01' = if (!empty(policy)) {
  name: 'policy'
  parent: api
  properties: {
    format: contains(policy, 'format') ? policy.format : 'rawxml'
    value: !contains(policy, 'params') ? policy.value : reduce(items(policy.params), policy.value, (result, param) => replace(string(result), '\$(${param.key})', param.value))
  }
}

Hope this helps, thanks for reading!

Subscribe to receive notifications about new blog posts by email

* indicates required