Shared Azure API Management Service Design

PR Code - Christopher Pateman
Version 1
Published in
9 min readNov 30, 2023

--

Azure API Management Services (APIM) are a powerful, flexible, and well-equipped product in Azure, but they are also expensive. There are reasons for this, and ways in general you can reduce the cost with SKUs, but another way is to share it with other products within your organisation instead of having a dedicated APIM. I will guide you through a shared APIM governed implemented design, that will empower all your teams to work on the same APIM.

What is an API Management Service?

The APIM in general is an API gateway, but with a lot more powers to make it an expensive service. Other than just routing API requests to other services, you can use the XML based schema and built-in functions to manipulate the inbound and outbound data called Policies. This is great for doing conversions of data formats, injecting additional information, and even merging multiple sources into one, plus you can also use C#.NET language to do advanced logic within the API request.

These endpoints, called Operations in the APIM, sit within APIs which is the logical grouping of methods like GET, POST, DELETE. As well as the grouping, it is where you set the APIs URI, Authentication, and monitoring for the API. You can also set more Policies at this level that will affect all the endpoints within the API. This is ideal for authentication, general error handling and shared functions.

To separate these APIs and give the ability to support multiple teams, you can group these APIs into Products. These can have Policies assigned, which will affect all APIs and all endpoints within it. At this level you can set things like call rates to affect all child APIs, but if you want some services to have different settings you can override them lower in the chain.

The above feature is called a Product for a reason as this service comes with a shop floor. You can configure a Developers Website built into the APIM that will dynamically take the Products, APIs, and Operations to turn it into a website where you can test the services and see documentation. This feature can have a custom domain, style customizations and authentication like Active Directory.

As well as all the features above you can set custom SSL certificates, networking, reusable variables and so much more, which is why it is so expensive and having an easy to manage ability to share APIMs is important.

What are we aiming to do?

We would like to give the ability for the APIM to become a hosting platform. Instead of it being a service that takes APIs in, we want the APIM to become part of the service. If you think about a micro-service on Kubernetes, you will have the code, service specific infrastructure and configuration all tightly coupled. This means if you wanted to target another hosting subscription everything related to that service would all go together.

This is the same goal we would want, so if we put our service in another subscription and targeted another APIM, all the services related Terraform and configuration would go with it to set the service up correctly.

Solution

These are the building blocks to achieve the goal and create a managed service offering within your company to host multiple API services on your APIM. We will start with a centrally governed APIM then design the ability for others to build a Product with multiple APIs and Operations.

Central APIM

As this is a shared service, you would want the APIM to be managed by the central DevOps/Platform team. From here they can setup governance on the APIM level policies that will affect the downstream products and services. They can also setup and configure the network, custom domain, and security aspects. Setting these up here means the hosted APIs do not need to worry about them.

You can also setup common helpers for when engineers are setting up their services like variables, certificates, and endpoints. These can save on the duplication of code and create a managed set of details to updated when required. For example, if there is a backend service you can set this up for others to consume.

These are some of the services you can add into the central APIM:

  • Networking
  • Custom Domain
  • Shared Certificates
  • Names values
  • Backends
  • Policy Fragments
  • API Tags
  • Authentication
  • Application Insights
  • Logging
  • Alerts

As per below, you can set this up to come from a single repository to build the APIM and its resources. If you were to have multiple APIMs, you may want to abstract the APIM code so others can use a central resource for it.

Central APIM design

Products

Products should be used as grouping for projects or products within the company. This will depend on how your company is structured and how many of these centrally governed APIMs you are going to have. For instance, if you have a single product as a company, you may split that into projects that work on certain areas of the product. This would make sense to have an APIM Product per project. However, you might also have different products within your company. If that is the case then depending on the number of APIs you could split the APIM Products by the company’s products, or you might have an APIM per product.

Either way your split the APIM Products, you should consider that the APIM Product Policy will apply to all children APIs, and you will set security at this level. You should use a logical and future proof choice that will last as the product grows.

The management of these products for the Terraform should be contained within a shared repository for the project or product depending on the split decided. This will make it easier to maintain and manage going forward, with each of the APIs referencing this centrally located source. Within here you can also update the other shared items that your APIs might use. For instance, you may want to create some Names Values sets containing resources information that are created in the shared repository. This will save effort later when your APIs need to pull the information during build time, and instead they can access it at runtime.

A recommendation for consistency and the ability to improve security, would be to contain the Terraform to create common resources in an external repository. This will allow all services to utilize this to setup the resources in a predefined format. For example, naming conventions for products, the Terraform module can accept the project prefix as a parameter and set the name as `<project prefix>-product` each time so they all look the same. You could also add in some predefined Policies that have parameters to override like how many requests per minute.

APIM Product

API Services

API Services should be handled like microservices, and with that they should be kept with the API code. This will mean as the developers work with the code, they can update the Open API Swagger that will be used to build the API Service in the APIM.

To make it easier, this process below will use Terraform to build the API Service then draw its information for the Policies and Open API Swagger from definition files.

API Swagger

The API Swagger will be used to import all the operations (GET, POST, PUT etc.) to the API. This is a well-known format that the code developers should know, which makes it easier for them to be able to edit and update the file with no input from the DevOps Engineer. We could dynamically source this from the running API, but there might be cases where you don’t want all the Operations on the APIM, or you want a custom operation that utilizes multiple operations. This will be stored in `./resources/api_service/api-service.json`. Below is a basic example but notice the “OperationId” field and its value as it will be used later.

{ 
"openapi":"3.0.3",
"info":{
"title":"API Service",
"description":"Use this service call other APIs",
"contact":{
"name":"Development Team",
"url":"https://support.com",
"email":"support@email.com"
},
"version":"1.0.0"
},
"paths":{
"/operation-one":{
"post":{
"summary":"My Operation",
"operationId":"OperationOne",
"parameters":[],
"requestBody":{
"content":{}
},
"responses":{
"200":{
"description":"OK",
"content":{}
}
}
}
}
}
}

API Terraform

We now have enough information to build the API Service. This will build the API in the APIM, with its operations. Before creating the API, we will use the data source to get the APIM Product so we can need to link them.

data "azurerm_api_management_product" "apim_product" { 
product_id = var.product_name
resource_group_name = var.api_mgmt _rg
api_management_name = var.api_mgmt _name
}

Once we have the Product, we can create the API. Two things to note is the `path` is made up of the Product name and the API name. This will create a consistent pattern for all API services URLs and easily identify the API source. The other part is the `import` where we are using the `openapi+json` format and passing in the Swagger JSON content, that describes the API and Operations to setup.

resource "azurerm_api_management_api" "api_service" { 
name = var.name
display_name = var.display_name


resource_group_name = var.api_mgmt_rg
api_management_name = var.api_mgmt_name


path = "${data.azurerm_api_management_product.apim_product.name}/${var.name}"


protocols = ["http", "https"]


subscription_required = false


import {
content_format = "openapi+json"
content_value = file("./resources/swagger/api-swagger.json")
}
}

From there we can link the API Service to the Product, so it is Policy’s will affect them.

resource "azurerm_api_management_product_api" "link_to_product" { 
api_name = azurerm_api_management_api.api_service.name
api_management_name = var.api_mgmt_name
resource_group_name = var.api_mgmt_rg


product_id = data.azurerm_api_management_product.apim_product.id


depends_on = [
azurerm_api_management_api.api
]
}

API Policy

Here we create the API Policy to be consumed, which will be an XML file in the directory `./resources/api_service/api-policy.xml`. For this example, we have a simple policy that will be the base for all Policies and can be expanded on.

<policies> 
<inbound>
<base />
<cors>
<allowed-origins>
<origin>*</origin>
</allowed-origins>
<allowed-methods>
<method>GET</method>
<method>POST</method>
</allowed-methods>
</cors>
<choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>

Within the Terraform we can then use the files content to apply to the API Service.

resource "azurerm_api_management_api_policy" "this" { 
api_name = data.azurerm_api_management_api.api_service.name
api_management_name = data.azurerm_api_management_api.api_service.api_management_name
resource_group_name = data.azurerm_api_management_api.api_service.resource_group_name


xml_content = file("./resources/api_service/api-policy.xml")
}

Operations Policies

Just like the API Policy you can create the Operations Policy within the folder `./resources/operations/`. For each operation you have in the APIs Swagger, you can create a policy file, but you don’t need to if the Policy would be empty. The file will have the name of the Operation ID you set in the Open API Swagger. This is the ‘operationId’ field as we mentioned before.

This is how we can relate the Policy file to the deployed Operation as you can see from the Terraform. We use the locals to pull in all the operation files from the folder, then loop through to create an array of operation objects. These objects contain the Operation ID from the file name and the Operation Policy XML from the files content. This means it will only apply the policies for the referenced Operations.

locals { 
operation_policies_files = fileset("./resources/operations/*.xml")
operation_policies = {
for operation_policies_file in local.operation_policies_files :
basename(operation_policies_file) => {
operation_id = replace(basename(operation_policies_file), ".xml", "")
xml_content = file("${path.module}/${operation_policies_file}")
}
}
}

We can then loop through the operations with the for_each on the Operations Terraform. The ‘operation_id’ is used to link to the Operation and the content is deployed.

resource "azurerm_api_management_api_operation_policy" "this" { 
for_each = { for operation in local.operation_policies : operation.operation_id => operation }


operation_id = each.value.operation_id


api_name = data.azurerm_api_management_api.api_service.name
api_management_name = data.azurerm_api_management_api.api_service.api_management_name
resource_group_name = data.azurerm_api_management_api.api_service.resource_group_name


xml_content = each.value.xml_content
}

Extras

All the above will create the API Service with their Operations, which I would advise having the Terraform in a module to be reusable and create a secure consistent pattern. This will reduce the number of inputs required and means all other API Services will follow the same format.

API Design

How it will be used?

To give you a visualization on how this can then be use by multiple teams and APIs, there is the below diagram.

As you can see the central DevOps Team are managing the Central Repository for the APIM Terraform. Then ‘proj1’ and ‘proj2’ team have their own Shared Repository that references the APIM Product Terraform Module to create it in the APIM. Each API Service then has its own repository managed by the same team and all calling the APIM Terraform module for the API Service.

APIM Shared Usage

About the Author:
Christopher Pateman is a Senior DevOps Azure Engineer at Version 1.

--

--

PR Code - Christopher Pateman
Version 1

I’m a Azure DevOps Engineer with a wide knowledge of the digital landscape. I enjoy sharing hard to find fixes and solutions for the wider community to use.