Automate your API deployments with Azure API Management and Bicep modules
Published Nov 28 2022 10:12 AM 3,876 Views
Microsoft

I think most practitioners can agree that making the jump from ad-hoc code deployments to a continuous integration (CI/CD) deployment model can seem complex.  There are so many things that you need to learn - Azure pipelines, Azure Resource Manager (ARM) templates, the Azure CLI, and a whole new system to set up, secure, and monitor for deployments.  It was daunting to learn and apply multiple new technologies at the same time when I learned the process.

 

We've provided various architectures and tools to work with Azure API Management.  However, the best tool for deploying a single API is one that is readily available to you - Bicep modules.  Bicep is a language that compiles to ARM, so it can deploy anything that ARM can deploy.  However, it's easier to read and maintain, mostly because of the declarative nature and the authoring support in Visual Studio Code.  A bicep module is a bicep file that describes a part of your infrastructure in a reusable way.  You can start by having a library of bicep modules in a git repository that you can just use, or you can publish the bicep modules to a private repository for sharing among multiple projects.  There is also a public repository that Microsoft maintains.  If you are starting from ARM templates, you can easily decompile the ARM template into bicep, which aids in conversion.

 

Let's take an example.  Let's say that I want to create an API on Azure API Management driven by an OpenAPI specification file.  To do this, I have to create an API resource, policy resource and potentially named values and policy fragments that the API policy depends on.  I've got an example of this for the petstore API:

 

@description('The name of the API Management instance to deploy this API to.')
param serviceName string

resource apimService 'Microsoft.ApiManagement/service@2022-04-01-preview' existing = {
  name: serviceName
}

resource apiDefinition 'Microsoft.ApiManagement/service/apis@2022-04-01-preview' = {
  name: 'petstore'
  parent: apimService
  properties: {
    path: 'petstore'
    description: 'See petstore3.swagger.io'
    displayName: 'petstore'
    format: 'openapi+json'
    value: loadTextContent('./petstore3.json')
    subscriptionRequired: true
    type: 'http'
    protocols: [ 'https' ]
    serviceUrl: 'https://petstore3.swagger.io/api/v3'
  }
}

resource apiPolicy 'Microsoft.ApiManagement/service/apis/policies@2022-04-01-preview' = {
  name: 'policy'
  parent: apiDefinition
  properties: {
    format: 'rawxml'
    value: loadTextContent('./policy.xml')
  }
}

 

A lot of the bicep code is repeatable, which makes it an ideal candidate for modularization.  Let's look at an alternative using bicep modules, starting with what I might want to write in bicep:

 

@description('The name of the API Management instance to deploy this API to.')
param serviceName string

resource apimService 'Microsoft.ApiManagement/service@2022-04-01-preview' existing = {
  name: serviceName
}

module petstoreApi '../modules/api-management-openapi.bicep' = {
  name: 'petstoreApi'
  params: {
    serviceName: serviceName
    apiName: 'petstore'
    apiDescription: 'See petstore3.swagger.io'
    serviceUrl: 'https://petstore3.swagger.io/api/v3'
    openApiSpecification: loadTextContent('./petstore/petstore.json')
    policyDocument: loadTextContent('./petstore/petstore-api.xml')
  }
  dependsOn: [
    apimService
  ]
}

 

Instead of referencing a resource, I reference a module to define this API.  The module takes certain parameters that describe the API.  The OpenAPI specification and policy document for the API are loaded from text files on disk.  Now, take a look at the bicep module:

 

@description('The name of the API')
@minLength(1)
@maxLength(63)
param apiName string

@description('The contents of the OpenAPI definition')
@minLength(1)
param openApiSpecification string

@description('The XML Policy document for the API')
@minLength(1)
param policyDocument string

@description('The name of the API Management service to deploy the API to.')
@minLength(1)
param serviceName string

@description('The API description (if blank, use the name of the API)')
param apiDescription string = ''

@description('The relative path for the API (if different to the API name)')
param path string = ''

@description('The (optional) service URL')
param serviceUrl string = ''

@description('Set to true if a subscription is required')
param subscriptionRequired bool = false

// Assume the content format is JSON format if the ending is .json - otherwise, it's YAML
var contentFormat = startsWith(openApiSpecification, '{') ? 'openapi+json' : 'openapi'

resource apimService 'Microsoft.ApiManagement/service@2022-04-01-preview' existing = {
  name: serviceName
}

resource apiDefinition 'Microsoft.ApiManagement/service/apis@2022-04-01-preview' = {
  name: apiName
  parent: apimService
  properties: {
    path: (path == '') ? apiName : path
    description: (apiDescription == '') ? apiName : apiDescription
    displayName: apiName
    format: contentFormat
    value: openApiSpecification
    subscriptionRequired: subscriptionRequired
    type: 'http'
    protocols: [ 'https' ]
    serviceUrl: (serviceUrl == '') ? null : serviceUrl
  }
}

resource apiPolicy 'Microsoft.ApiManagement/service/apis/policies@2022-04-01-preview' = {
  name: 'policy'
  parent: apiDefinition
  properties: {
    format: 'rawxml'
    value: policyDocument
  }
}

 

You can (hopefully) see that the bicep module looks remarkably similar to the original API definition.  Indeed, I copied the code over to the module and used that as my starting point.  I then generalized the module by using parameters for things I wanted to pass in and variables for things I didn't need to pass in because they can be inferred.  An example of and inferred value is the content type.  When you define an API based on OpenAPI, you need to select between two formats - JSON and YAML.  Since all JSON-based specifications start with an open-brace, I used that as the distinguisher between the two.

 

Obviously, this simple example of a module does not support many of the features that are supported by Azure API Management.  However, it can easily be expanded to support those features.  I have a version that supports dependent named values and policy fragments, for example, so those are deployed when my API is deployed.

 

Yes, bicep is one more thing to learn along the way to adopting safe deployment practices with CI/CD pipelines.  However, it is a technology that can make your life simpler in the long run by focusing your attention on what matters about the API.  Leaving the implementation details to a module ensures that you can focus on those things that change between APIs and drives consistency between deployments.

 

My final point is that bicep helps you describe cloud applications that rely on multiple Azure services.  A typical single page web application written in React using a web API as a backend might use Static Web Apps, API Management, App Service or Functions, KeyVault, Azure Active Directory, and a database like Cosmos DB.  You can define this system as a single bicep file, but it will be large.  It's better to put every service in its own module and bring them together with another bicep file.  This allows you to re-use your service definitions easily.  The smaller file sizes provide better readability and maintainability for your infratructure, and the modular code allows you to deploy a part of the infrastructure or the whole infrastructure at once.

2 Comments
Co-Authors
Version history
Last update:
‎Nov 22 2022 02:22 PM
Updated by: