Expose a subset of your openapi spec on azure APIM

Sometimes you don’t want to expose all the operations of your API on APIM. For example, you might have an API that has a lot of operations, but you only want to expose a subset of those operations on APIM. Or you might have an API that has operations that are not meant to be called by external consumers. In this post, I’ll show you how you can expose a subset of your openapi spec via Azure devops on APIM.

First we want to generate the full openapi spec from your dotnet core application in your CI/CD pipeline.

Install the swashbuckle tool

In the root of your dotnet core project, add a .config folder with a dotnet-tools.json file:

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "swashbuckle.aspnetcore.cli": {
      "version": "6.2.3",
      "commands": [
        "swagger"
      ]
    }
  }
}

To use the swashbuckle tool on your local machine, you need to restore the tool via the dotnet command:

dotnet tool restore

Generate the full openapi spec

My project structure looks like this:

  • alm (files needed for the CI/CD pipeline)
  • src/Dpr.Demo.Api (this is the API project)
  • src/Dpr.Demo.Apim (here are the files needed for APIM)

When a release build is triggered, we want a postbuild event to generate the full openapi spec. The generated file needs to be placed in the Dpr.Demo.Apim folder where all the files for APIM are. We can do this by adding the following to the .csproj file of the API project:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
    <Exec Command="dotnet swagger tofile --output ../Dpr.Demo.Apim/v1/openapi_full.json $(OutputPath)$(AssemblyName).dll v1" />
  </Target>
</Project>

The generated openapi file will look like:

openapi: 3.0.1
info:
  title: My Demo API
  description: The demo API to showcase a subset on APIM
  version: v1
paths:
  /authors:
    get:
      tags:
      - Authors
      summary: Get all authors
      operationId: GetAuthors
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Author'
  /books:
    get:
      tags:
      - Books
      summary: Get all books
      operationId: GetBooks
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Book'
components:
  schemas:
    Author:
      type: object
      properties:
        id:
          type: integer
          format: int32
        name:
          type: string
    Book: ...

Create a openapi template file

Now we have the full openapi spec, we can create a template file that will be used to generate the subset of the openapi spec. If we only want to expose the /authors operation the template file will look like:

openapi: 3.0.1
info:
  title: My Demo API
  description: The demo API to showcase a subset on APIM
  version: v1
servers:
- url: 'https://{service-host}/demo/v1'
paths:
  /authors:
    $ref: './openapi_full.json#/paths/~1authors'

In the template file I’ve added the servers section. On APIM I have a named value “service-host” that contains the hostname of the APIM instance. This way I can use the same template file for all my environments.

The path section contains only the /authors operation. The $ref points to the full openapi spec and the path of the operation.

So the structure of the APIM folder in my project looks like:

The azure devops CI/CD pipeline

In the CI/CD pipeline I have defined 3 stages:

The stages

  • build stage
  • build openapi stage
  • deploy stage

The build openapi stage is placed in een different stage because it allows you to run this stage on a different image. The build stage is running on the hosted windows-latest image. The build openapi stage is running on a hosted ubuntu-latest image. The swashbuckle tool is not available on the windows image.

The build stage

  • restore the dotnet tool
  • build the solution
  • copy the files from the Dpr.Demo.Apim folder to the artifact staging directory
  • publish the artifact
- task: DotNetCoreCLI@2
    displayName: Restore local tools
    inputs:
      command: 'custom'
      custom: 'tool'
      arguments: 'restore --tool-manifest $(Build.SourcesDirectory)\src\Dpr.Demo.Api\.config\dotnet-tools.json'

- task: DotNetCoreCLI@2
    displayName: Build Solution
    inputs:
      command: 'build'
      projects: '**/*.sln'
      arguments: '--configuration $(BuildConfiguration)'

- task: CopyFiles@2
    displayName: 'Copy apim files'
    inputs:
      SourceFolder: 'src/Dpr.Demo.Apim'
      TargetFolder: '$(build.artifactstagingdirectory)/apim'
  
- task: PublishBuildArtifacts@1
    displayName: 'Publish artifact'
    inputs:
      PathtoPublish: '$(build.artifactstagingdirectory)'
      ArtifactName: '$'

The build openapi stage

The generation of the subset of the openapi spec can be done via the swagger-codegen-cli-v3 image.

Create an Azure Container Registry and push the swagger-codegen-cli-v3 image to the registry.

Create a service connection to the Azure Container Registry

  • download the artifact from the build stage
  • login to the Azure Container Registry
  • run the docker command to generate the subset of the openapi spec
  • copy the files to the artifact staging directory
  • copy the alm files to the artifact staging directory
  • publish the artifact
- task: DownloadPipelineArtifact@2
    inputs:
        artifact: '$'
        path: $(Build.SourcesDirectory)/apimsrc

- task: Docker@2
    displayName: Login to ACR
    inputs:
      command: login
      containerRegistry: dpr-spn-devops-acr-tst

  - bash: |
        echo "Executing docker run command"
        docker run --rm -v $(Build.SourcesDirectory)/apimsrc/apim/v1:/var/tmp dprdemotstacr.azurecr.io/swagger-codegen-cli-v3:3.0.29 generate -i /var/tmp/openapi_template.yaml -l openapi-yaml -o /var/tmp -D outputFile=openapi.yaml
    displayName: Run docker inline in pipeline

  - task: CopyFiles@2
    displayName: 'Copy apim files'
    inputs:
      SourceFolder: '$(Build.SourcesDirectory)/apimsrc/apim'
      TargetFolder: '$(build.artifactstagingdirectory)/apim'

  - task: CopyFiles@2
    displayName: 'Copy Files to: $(build.artifactstagingdirectory)/alm'
    inputs:
      SourceFolder: '$(Build.SourcesDirectory)/apimsrc/alm'
      TargetFolder: '$(build.artifactstagingdirectory)/alm'
  
  - task: PublishBuildArtifacts@1
    displayName: 'Publish artifact'
    inputs:
      PathtoPublish: '$(build.artifactstagingdirectory)'
      ArtifactName: '$'

The output file of the docker run command is openapi.yaml. This is your new openapi file with the subset you want to expose via APIM.

The deploy stage

Now your pipeline has 2 artifacts. One artifact contains the code of your API and the other artifact contains the files for APIM.

How to deploy your API is out of scope of this blogpost. I assume you have a release pipeline that deploys your API to an Azure App Service.

The deployment of the APIM files can done via a Powershell task.

The important part of the powershell script is the Import-AzApiManagementApi. With this command you can provide the created subset of the openapi spec via the -SpecificationPath parameter.

Import-AzApiManagementApi -Context $apimCtx -ApiId $apiId -SpecificationFormat $openApiFormat -SpecificationPath $openApiPath -Path $ApiPath

The devops task looks like:

The Import-ApimApi.ps1 script is located in the alm/pipelinescripts folder and is the powershell script I use to deploy the specs to APIM. The important thing to notice is the parameter -OpenApiFile. This is the openapi file containing the subset that we provide to the powershell script to be uploaded to APIM.

- task: AzurePowerShell@5
          displayName: Publish demo Api v1 on APIM
          enabled: $
          inputs:
              azureSubscription: '$'
              ScriptPath: '$(Pipeline.Workspace)/$/alm/pipelinescripts/Import-ApimApi.ps1'
              ScriptArguments: 
                -ResourceGroupName "$" `
                -ApiPath "demo" `
                -ApiVersion "v1" `
                -ApiBackendUrl "https://dpr-demo-$-api.azurewebsites.net/" `
                -ApiDisplayName "Demo API" `
                -OpenApiFile "$(Pipeline.Workspace)/$/apim/v1/openapi.yaml" `
                -ExposeOpenapiSpec $false `
                -Verbose
              azurePowerShellVersion: LatestVersion
              pwsh: true

Conclusion

There are quite a few steps to get this working. But once you have it working you can easily expose your openapi spec on APIM without worries that you expose too much operations. You only need to update the template file and the subset of the openapi spec will be generated automatically.

References

https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.1.md#referenceObject