Automating your OpenAPI updates to API Management through your CI/CD pipeline
Microservices are the trend in today’s day and age, even if you may have read that some are going back the monolith way. Most microservices architectures are built to communicate through REST: each service is an API that shares a contract for other services to consume. Since your product ecosystem will (hopefully) evolve over time, your contracts are to evolve overtime as well. If you decided to consolidate your contracts consumption into one point of entry (using the API Gateway pattern), how do you actually make sure that those contracts are properly updated in your gateway for each of your environments up to your production environment?
In this post, I will show you how you can update your APIs contracts using the API Management service, while maintaining a segregation between your teams, using the Azure DevOps platform.
Note: this posts assumes that you have basic familiarity with Azure DevOps, API Management and ASP.NET Core.
The basics
I will start by creating the Weather API, in ASP.NET Core, using the template. You can create it using the dotnet new webapi command. This API is setup with Swashbuckle. How to setup Swashbuckle can be found in the documentation or in the repository directly.
As shown below, the API has the following contract:
API Management
My API Management is also configured with the proper version set, along with the operations associated to my v1 version.
CI configuration
The magic here, to be able to update the API Management, is that I need the OpenAPI spec file. I will generate it and add it to my artifacts package that will be used for deployment.
To do this, I will use the Swashbuckle CLI which can be use to retrieve the Swagger/OpenAPI JSON directly from the application’s startup assembly, and write it to a file. Follow the configuration to add the CLI to your source control repository so you can restore the tool and use it in your CI build definition.
I’m now ready to setup the API in my CI build definition. I will be using a YAML pipeline and I am using the following template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
trigger: - master pool: vmImage: 'ubuntu-latest' variables: buildConfiguration: 'Release' appName: 'WeatherAPI' stages: - stage: CI displayName: Continuous Integration jobs: - job: Build steps: - task: DotNetCoreCLI@2 displayName: 'Restore tools' inputs: command: custom custom: tool arguments: 'restore' - task: DotNetCoreCLI@2 displayName: 'Build API' inputs: command: build arguments: '--configuration $(buildConfiguration)' - task: DotNetCoreCLI@2 displayName: 'Publish API' inputs: command: publish arguments: '--configuration $(buildConfiguration) --no-restore --output $(Build.ArtifactStagingDirectory)/app' zipAfterPublish: false - task: CmdLine@2 displayName: Create specs directory inputs: script: 'mkdir $(Build.ArtifactStagingDirectory)/specs' - task: DotNetCoreCLI@2 displayName: 'Generate OpenAPI spec document' inputs: command: custom custom: swagger arguments: 'tofile --output $(Build.ArtifactStagingDirectory)/specs/$(appName).v1.json $(Build.ArtifactStagingDirectory)/app/$(appName)/$(appName).dll v1' - task: ArchiveFiles@2 displayName: 'Zip API' inputs: archiveType: Zip rootFolderOrFile: $(Build.ArtifactStagingDirectory)/app/$(appName) includeRootFolder: false archiveFile: '$(Build.ArtifactStagingDirectory)/$(appName).$(Build.BuildId).zip' - task: CopyFiles@2 displayName: Copy build PowerShell scripts inputs: contents: '$(System.DefaultWorkingDirectory)/build/*.ps1' targetFolder: $(Build.ArtifactStagingDirectory) cleanTargetFolder: false - task: PublishPipelineArtifact@1 displayName: Publish artifacts inputs: targetPath: '$(Build.ArtifactStagingDirectory)' |
This will restore the swagger tool, build our API, publish our API into the artifacts folder, generate the specification document, copy the necessary build scripts (more on this later) and publish our artifacts.
The swagger tool last parameter is the name I specified when I setup my SwaggerGen:
1 2 3 4 |
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Weather API", Version = "v1" }); }); |
In my case, it is v1.
Making modifications
Lets update the API to add an extra route, /WeatherForecast/Today. In the WeatherForecastController, I add the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
[HttpGet(nameof(Today))] public WeatherForecast Today() { int temperatureC; string summary; switch (DateTime.Today.DayOfWeek) { case DayOfWeek.Monday: temperatureC = 0; summary = Summaries[2]; break; case DayOfWeek.Tuesday: temperatureC = 8; summary = Summaries[3]; break; case DayOfWeek.Wednesday: temperatureC = 15; summary = Summaries[4]; break; case DayOfWeek.Thursday: temperatureC = 20; summary = Summaries[5]; break; case DayOfWeek.Friday: temperatureC = 24; summary = Summaries[6]; break; case DayOfWeek.Saturday: temperatureC = 35; summary = Summaries[7]; break; default: temperatureC = 42; summary = Summaries[8]; break; } var weatherForecast = new WeatherForecast { Date = DateTime.Today, TemperatureC = temperatureC, Summary = summary }; return weatherForecast; } |
As you can now see, it appears in the OpenAPI definition
CD Configuration
To deploy the API into an App Service and update the API Management, I add a CD stage into my YAML file located at the root of my API solution. It includes the deployment to the App Service (Web App) along with updating the API Management.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
trigger: - master pool: vmImage: 'ubuntu-latest' variables: buildConfiguration: 'Release' appName: 'WeatherAPI' azureSubscriptionEndpoint: 'SET_ME' webAppName: 'SET_ME' apimResourceGroupName: 'SET_ME' apimName: 'SET_ME' apiName: 'Weather API' apiVersion: 'v1' stages: # CI stage here... - stage: CD displayName: Continuous Deployment jobs: - deployment: Deploy environment: 'demo' strategy: runOnce: deploy: steps: - task: AzurePowerShell@4 displayName: Update API Management inputs: azureSubscription: $(azureSubscriptionEndpoint) scriptType: 'FilePath' scriptPath: $(Pipeline.Workspace)/CI.Build/build/Update-ApimOpenAPISpec.ps1 scriptArguments: '-ResourceGroupName $(apimResourceGroupName) -ServiceName $(apimName) -ApiName "$(apiName)" -ApiVersion $(apiVersion) -SpecificationFilePath $(Pipeline.Workspace)/CI.Build/specs/$(appName).$(apiVersion).json' errorActionPreference: 'stop' azurePowerShellVersion: 'latestVersion' - task: AzureRMWebAppDeployment@4 displayName: Deploy to App Service inputs: WebAppKind: webApp WebAppName: $(webAppName) ConnectedServiceName: $(azureSubscriptionEndpoint) Package: $(Pipeline.Workspace)/CI.Build/$(appName).$(Build.BuildId).zip |
Now remember when I spoke about copying the build scripts, there’s a PowerShell script you need to have which will help you update your API in the service. Here’s the content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
[CmdletBinding()] Param( [string] [Parameter(Mandatory=$true)] $ResourceGroupName, [string] [Parameter(Mandatory=$true)] $ServiceName, [string] [Parameter(Mandatory=$true)] $ApiName, [string] [Parameter(Mandatory=$true)] $ApiVersion, [string] [Parameter(Mandatory=$true)] $SpecificationFilePath ) $apiMgmtContext = New-AzApiManagementContext -ResourceGroupName $ResourceGroupName -ServiceName $ServiceName $api = Get-AzApiManagementApi -Context $apiMgmtContext -Name $ApiName if ($null -eq $api) { Write-Error "Failed to get API with name $ApiName" exit(1) } $apiVersionSetId = $api.ApiVersionSetId.Substring($api.ApiVersionSetId.LastIndexOf("/")+1) $apiVersionSet = Get-AzApiManagementApiVersionSet -Context $apiMgmtContext -ApiVersionSetId $apiVersionSetId Write-Host "Importing Spec file" -ForegroundColor Green Import-AzApiManagementApi -Context $apiMgmtContext ` -SpecificationPath $SpecificationFilePath ` -SpecificationFormat 'OpenApi' ` -Path $api.Path ` -ApiId $api.ApiId ` -ApiVersionSetId $apiVersionSet.Id ` -ApiVersion $ApiVersion |
This script basically will go and fetch the required variables of your API to be able to update your API operations.
As you can see the pipeline ran successfully
Looking into the API Management service, under my API, the new operation was added.
If I navigate to the Swagger-UI of the Web App, I also see that it has the proper OpenAPI definition.
Securing your updates to a specific API
Yea but Dom, now that we’ve been able to update the API, what if there are other teams that uses the API Management resource, and I don’t want their pipeline to go and modify my API (intentionally or unintentionally)?
That’s an excellent question. In this case, what you want is to use RBAC. Microsoft has some good documentation about this. The idea is to actually create a custom role that is only able to modify your API. The teams who deploy their APIs into the service, have to create a service connection, with a service principal that has this role assigned to it.
Here’s a the content of this script written using PowerShell:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$resourceGroup = "<your resource group name>" $apiMgmtName = "<your api management service name>" $apiMgmtContext = New-AzApiManagementContext -ResourceGroupName $resourceGroup -ServiceName $apiMgmtName $apiName = "<your api name>" $api = Get-AzApiManagementApi -Context $ApiMgmtContext -Name $apiName $role = Get-AzRoleDefinition "API Management Service Reader Role" $role.Id = $null $role.Name = "Weather API Contributor" $role.Description = "Has read access to Contoso APIM instance and write access to the Weather API." $role.Actions.Add("Microsoft.ApiManagement/service/apis/write") $role.Actions.Add("Microsoft.ApiManagement/service/apis/*/write") $role.AssignableScopes.Clear() $role.AssignableScopes.Add($api.Id) New-AzRoleDefinition -Role $role New-AzRoleAssignment -ObjectId <object ID of the user account> -RoleDefinitionName "Weather API Contributor" -Scope $api.Id |
To get the API name, you can run the following command:
1 2 3 4 |
$resourceGroup = "<your resource group name>" $apiMgmtName = "<your api management service name>" $apiMgmtContext = New-AzApiManagementContext -ResourceGroupName $resourceGroup -ServiceName $apiMgmtName Get-AzApiManagementApi -Context $apiMgmtContext | Select-Object ApiId,Name |
Replace the ResourceGroup and ApiMgmtName variable.