Friday, March 4, 2022

Publishing OpenAPI Document from Azure Functions to Azure API Management within CI/CD Pipeline

In my previous post, we've walked through how to run the Azure Functions app as a background process and locally generate the OpenAPI document from it within the GitHub Actions workflow. The OpenAPI document can be stored as an artifact and integrated with Power Platform or Azure API Management (APIM) later on.

 

 

Throughout this post, I'm going to discuss how to publish the OpenAPI document to APIM, after locally running the Azure Functions app as a background process and generating the OpenAPI document within the GitHub Actions workflow.

 

NOTE: Let's use the sample app provided by the Azure Functions OpenAPI Extension repository.

 

Update GitHub Actions Workflow

 

The GitHub Actions workflow built in my previous post looks like the following (line #21-35). Let's assume that we use the Ubuntu runner and PowerShell.

 

name: Build

on:
  push:

jobs:
  build_and_test:
    name: Build
    runs-on: 'ubuntu-latest'

    steps:
    - name: Build solution
      shell: pwsh
      run: |
        pushd MyFunctionApp

        dotnet build . -c Release -v minimal

        popd

    - name: Generate OpenAPI document
      shell: pwsh
      run: |
        cd MyFunctionApp

        Start-Process -NoNewWindow func @("start","--verbose","false")
        Start-Sleep -s 60

        Invoke-RestMethod -Method Get -Uri http://localhost:7071/api/swagger.json | ConvertTo-Json -Depth 100 | Out-File -FilePath outputs/swagger.json -Force

        Get-Content -Path outputs/swagger.json

        cd ..

 

The OpenAPI document generated from the pipeline above has https://localhost:7071/api as the default server URL. However, the actual server URL must be the format of https://<azure-functions-app>.azurewebsites.net/api after deployment. Therefore, although you haven't deployed the function app yet, the generated OpenAPI document must have the deployed app URL. According to the doc, the extension offers the feature to apply the server URL.

 

  • Set the environment variable, OpenApi__HostNames, so the actual server URL is added on top of localhost (line #4).
  • Set the environment variable, AZURE_FUNCTIONS_ENVIRONMENT, to Production so that localhost is omitted and only the server URL is rendered as if the function app is running on Azure (line #5).
  • Generally speaking, local.settings.json is excluded from the repository because it's supposed to use for local development. But, it's required to run the function app locally as a background process. Therefore, it's always a good idea to create one. The revised action below copies the local.settings.sample.json to local.settings.json (line #12).

 

With these three points in mind, let's update the GitHub Action (line #4,5,12).

 

    - name: Generate OpenAPI document
      shell: pwsh
      env:
        OpenApi__HostNames: 'https://<azure-functions-app>.azurewebsites.net/api'
        AZURE_FUNCTIONS_ENVIRONMENT: 'Production'
      run: |
        cd MyFunctionApp

        mkdir outputs

        # Create local.settings.json
        cp ./local.settings.sample.json ./local.settings.json

        Start-Process -NoNewWindow func @("start","--verbose","false")
        Start-Sleep -s 60

        Invoke-RestMethod -Method Get -Uri http://localhost:7071/api/swagger.json | ConvertTo-Json -Depth 100 | Out-File -FilePath outputs/swagger.json -Force

        Get-Content -Path outputs/swagger.json -Raw

        cd ..

 

As mentioned above, this action just copies the existing local.settings.sample.json file to local.settings.json, but it's not common. Therefore, in most cases, you should create the local.settings.json file by yourself, and it MUST include the environment variable, FUNCTIONS_WORKER_RUNTIME (line #3). In other words, the minimal structure of local.settings.json looks like below (assuming you use the in-proc worker):

 

{
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  }
}

 

Finally, you will see the server URL applied from the OpenAPI document (line #4,16).

 

// OpenAPI v2
{
  "swagger": "2.0",
  "host": "<azure-functions-app>.azurewebsites.net",
  "basePath": "/api",
  "schemes": [
    "https"
  ]
}

// OpenAPI v3
{
  "openapi": "3.0.1",
  "servers": [
    {
      "url": "https://<azure-functions-app>.azurewebsites.net/api"
    }
  ]
}

 

Now, we've got the OpenAPI document from the CI/CD pipeline, with the actual server URL, without deploying the function app.

 

Publish to Azure API Management

 

This time, let's publish the OpenAPI document to APIM. Although there are many ways to do it, let's use Azure Bicep and deploy it through Azure CLI in this post. Here's the first part of the bicep file. Use the existing keyword to get the existing APIM resource details.

 

// azuredeploy.bicep
param servicename string

resource apim 'Microsoft.ApiManagement/service@2021-08-01' existing = {
    name: servicename
    scope: resourceGroup(apiManagement.groupName)
}

 

Then, with these APIM details, declare the API resource.

 

// azuredeploy.bicep
param openapidoc string

resource apimapi 'Microsoft.ApiManagement/service/apis@2021-08-01' = {
    name: '${apim.name}/my-api'
    properties: {
        type: 'http'
        displayName: 'My API'
        description: 'This is my API.'
        path: 'myapi'
        subscriptionRequired: true
        format: 'openapi+json-link'
        value: openapidoc
    }
}

 

It looks simple, except those two attributes – format and value (line #12-13). Therefore, it's better to understand what those two attributes are. For more comprehensive details, you can visit the Azure Bicep Template Reference.

 

  1. TO use the JSON string parsed from the OpenAPI document:
    • OpenAPI v2: Set the format value to swagger-json.
    • OpenAPI v3: Set the format value to openapi+json.
    • Assign the JSON string to the value attribute.
  2. TO use the publicly accessible URL for the OpenAPI document:
    • OpenAPI v2: Set the format value to swagger-link-json.
    • OpenAPI v3: Set the format value to openapi+json-link.
    • Assign the publicly accessible URL to the value attribute.

 

Even if the first approach is doable, I wouldn't recommend it because it's too painful to parse the OpenAPI document to the JSON string that the bicep file can understand. Therefore, I would suggest using the second approach by storing the OpenAPI document to Azure Blob Storage right after it's generated from the pipeline.

 

Once you complete authoring the bicep file, run the Azure CLI command to deploy the API to APIM.

 

$servicename = "<my_apim_service_name>"
$openapidoc = https://<my_blob_storage_name>.blob.core.windows.net/<container>/openapi.json

az deployment group create `
    -g <resource_group_name> `
    -n <deployment_name> `
    -f ./azuredeploy.bicep `
    -p servicename=$servicename `
    -p openapidoc=$openapidoc

 

You can confirm that the API is correctly deployed to APIM on Azure Portal.


So far, we've walked through how:

 

  1. To run Azure Functions app as a background process,
  2. To generate the OpenAPI document from the app by applying the deployed server URL without actually deploying it, and
  3. To publish the OpenAPI document to APIM within GitHub Actions workflow.

 

By doing so, you will be able to reduce some maintenance overheads by automating the integration part.

Posted at https://sl.advdat.com/3tqEPeehttps://sl.advdat.com/3tqEPee