DEV Community

Cover image for How to create an Azure API Management Service with Developer Portal with CI/CD using Azure DevOps
Markus Meyer
Markus Meyer

Posted on • Originally published at markusmeyer.hashnode.dev

How to create an Azure API Management Service with Developer Portal with CI/CD using Azure DevOps

Table of Contents

Prerequisites
Step 1: Create an Azure API Management Service
Step 2: Set Up the App Registration for the Developer Portal
Step 3: Register the app registration with the API Management service
Complete Code

In this blog post, we will walk through the steps to create an Azure API Management service and set up the Developer Portal. Azure API Management is a fully managed service that enables organizations to publish, secure, and manage APIs at scale. The Developer Portal is a customizable self-service portal where developers can discover, learn, and consume your APIs. Azure DevOps CI/CD pipeline will be used to automate the deployment of the API Management service and the Developer Portal. Bicep will be used to define the infrastructure as code for the API Management service and the Developer Portal.

Azure Portal - API Management Service

The Azure DevOps pipeline uses three steps to complete the deployment:

Flow

The Developer Portal with Azure Active Directory authentication:

Developer Portal - Sign in

Prerequisites

Before we begin, make sure you have the following:

  • Azure DevOps service principal with the necessary permissions to create and manage resources in Azure.

  • this service principal should have at least the following permissions:

Step 1: Create an Azure API Management Service

The following Bicep code defines an Azure API Management service with the required configuration options, such as the publisher email and name. The apim resource creates an API Management service with the specified properties, including the pricing tier and location.

@description('The email address of the owner of the service')
@minLength(1)
param publisherEmail string

@description('The name of the owner of the service')
@minLength(1)
param publisherName string

@description('Location for all resources.')
param location string = resourceGroup().location

var apimName = 'apim-eval-mm-${uniqueString(resourceGroup().id)}'
resource apim 'Microsoft.ApiManagement/service@2023-05-01-preview' = {
  name: apimName
  location: location
  sku: {
    name: 'Developer'
    capacity: 1
  }
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    publisherEmail: publisherEmail
    publisherName: publisherName
  }
}
Enter fullscreen mode Exit fullscreen mode

Configure App Insights

App Insights is a monitoring and diagnostics service that helps you understand how your application is performing and how it's being used. You can use App Insights to monitor the performance and usage of your APIs in the API Management service. The following Bicep code creates an App Insights resource and configures it to send telemetry data to a Log Analytics workspace.

var logAnalyticsWorkspaceName = 'log-eval-mm-${uniqueString(resourceGroup().id)}'
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
  name: logAnalyticsWorkspaceName
  location: location
  properties: any({
    retentionInDays: 90
    features: {
      searchVersion: 1
    }
    sku: {
      name: 'Standalone'
    }
  })
}

var appInsightsName = 'appi-eval-mm-${uniqueString(resourceGroup().id)}'
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: appInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: logAnalyticsWorkspace.id
    publicNetworkAccessForIngestion: 'Enabled'
    publicNetworkAccessForQuery: 'Enabled'
  }
}

resource namedValueAppInsightsKey 'Microsoft.ApiManagement/service/namedValues@2023-05-01-preview' = {
  parent: apim
  name: 'instrumentationKey'
  properties: {
    tags: []
    secret: false
    displayName: 'instrumentationKey'
    value: appInsights.properties.InstrumentationKey
  }
}

resource apimLogger 'Microsoft.ApiManagement/service/loggers@2023-05-01-preview' = {
  parent: apim
  name: 'apimlogger'
  properties: {
    resourceId: appInsights.id
    description: 'Application Insights for APIM'
    loggerType: 'applicationInsights'
    credentials: {
      instrumentationKey: '{{instrumentationKey}}'
    }
  }
  dependsOn: [
    namedValueAppInsightsKey
  ]
}
Enter fullscreen mode Exit fullscreen mode

API Management - Logger

Step 2: Set Up the App Registration for the Developer Portal

The Developer Portal will use Azure Active Directory (Azure AD / Microsoft Entra ID) for authentication and authorization. You can configure the Developer Portal to use Microsoft Entra ID by creating an app registration with Powershell and granting it the necessary permissions to access the API Management service.

Create App Registration

# Create the app registration. $appName is the same name as the API Management service.
$newApp = az ad app create --display-name $appName --sign-in-audience AzureADMyOrg
Enter fullscreen mode Exit fullscreen mode

App registration

Create client secret

The app registration needs a client secret to authenticate with the API Management service. You can use the following command to create a client secret for the app registration.

# Create client secret
$clientSecret = az ad app credential reset --id $appId --years 42 --display-name eval-secret --append --output tsv --query "password"
Enter fullscreen mode Exit fullscreen mode

Certificates & secrets

Configure authentication

The app registration needs to be configured to enable implicit grant settings and redirect URIs for the Developer Portal. You can use the following commands to configure the app registration for the Developer Portal.

$webJson = @{
    implicitGrantSettings = @{
        enableIdTokenIssuance     = $true
        enableAccessTokenIssuance = $true
    }
    redirectUris          = @()
} | ConvertTo-Json -d 4 -Compress
if ($IsWindows -eq $true) {
    $webJson = $webJson | ConvertTo-Json -d 4
}

az ad app update --id $appId --set web=$webJson

$replyUrlSignInAad = "https://$appName.portal.azure-api.net/signin-aad"
$replyUrlSignIn = "https://$appName.developer.azure-api.net/signin"

$spaJson = @{
    redirectUris = @(
        "$replyUrlSignInAad"
        "$replyUrlSignIn"
    )
} | ConvertTo-Json -d 4 -Compress

if ($IsWindows -eq $true) {
    $spaJson = $spaJson | ConvertTo-Json -d 4
}
az ad app update --id $appId --set spa=$spaJson
Enter fullscreen mode Exit fullscreen mode

Authentication

Grant permissions

The app registration needs to be granted the necessary permissions to access the API Management service. You can use the following commands to grant the app registration the necessary permissions to access the API Management service.

  • Azure Active Directory Graph

    • Directory.Read.All
    • User.Read
  • Microsoft Graph

    • Directory.Read.All
    • User.Read
az ad app permission add --id $appId --api 00000003-0000-0000-c000-000000000000 --api-permissions 7ab1d382-f21e-4acd-a863-ba3e13f7da61=Role
az ad app permission add --id $appId --api 00000003-0000-0000-c000-000000000000 --api-permissions e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope
az ad app permission add --id $appId --api 00000002-0000-0000-c000-000000000000 --api-permissions 5778995a-e1bf-45b8-affa-663a9f3f4d04=Role
az ad app permission add --id $appId --api 00000002-0000-0000-c000-000000000000 --api-permissions 311a71cc-e848-46a1-bdf8-97ff7156d8e6=Scope
Enter fullscreen mode Exit fullscreen mode

API permissions

To finalize the configuration, you need to grant admin consent to the app registration.

Note: As mentioned in the screenshot, Azure Active Directory Graph is obsolete. If you enabling the Developer Portal in the Azure Portal, the same permissions will be used.

Enable Azure AD

Step 3: Register the app registration with the API Management service

The app registration needs to be registered with the API Management service to enable the Developer Portal. This can be done using the following Bicep code to add the Aad identity provider to the API Management service.

var tenantId = subscription().tenantId
resource developerPortalIdentityMicrosoftEntraId 'Microsoft.ApiManagement/service/identityProviders@2023-05-01-preview' = {
  parent: apim
  name: 'Aad'
  properties: {
    clientId: apimAppId
    clientSecret: apimAppClientSecret
    signinTenant: tenantId
    allowedTenants: [
      tenantId
    ]
    type: 'aad'
    clientLibrary: 'MSAL-2'
  }
}
Enter fullscreen mode Exit fullscreen mode

Update identity provider

Complete Code

The complete code consists of the following files:

  • Add-DeveloperPortal-Microsoft-Entra-Id.ps1

    • Powershell script to create the app registration and configure it for the Developer Portal.
  • apim.bicep

    • Bicep file to create the API Management service and configure App Insights.
  • apim.developer.bicep

    • Bicep file to register the app registration with the API Management service.
  • azure-pipeline.yaml

    • Azure DevOps pipeline to automate the deployment of the API Management service and the Developer Portal.

Bicep API Management service

Bicep file to create the API Management service and configure App Insights apim.bicep.

@description('The email address of the owner of the service')
@minLength(1)
param publisherEmail string

@description('The name of the owner of the service')
@minLength(1)
param publisherName string

@description('Location for all resources.')
param location string = resourceGroup().location

var apimName = 'apim-eval-mm-${uniqueString(resourceGroup().id)}'
resource apim 'Microsoft.ApiManagement/service@2023-05-01-preview' = {
  name: apimName
  location: location
  sku: {
    name: 'Developer'
    capacity: 1
  }
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    publisherEmail: publisherEmail
    publisherName: publisherName
  }
}

var tenantId = subscription().tenantId

var keyVaultName = 'kv-eval-mm-${uniqueString(resourceGroup().id)}'
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: keyVaultName
  location: location
  properties: {
    enabledForTemplateDeployment: true
    enablePurgeProtection: true
    enableRbacAuthorization: true
    enableSoftDelete: true
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: tenantId
  }
}

var logAnalyticsWorkspaceName = 'log-eval-mm-${uniqueString(resourceGroup().id)}'
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
  name: logAnalyticsWorkspaceName
  location: location
  properties: any({
    retentionInDays: 90
    features: {
      searchVersion: 1
    }
    sku: {
      name: 'Standalone'
    }
  })
}

var appInsightsName = 'appi-eval-mm-${uniqueString(resourceGroup().id)}'
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: appInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: logAnalyticsWorkspace.id
    publicNetworkAccessForIngestion: 'Enabled'
    publicNetworkAccessForQuery: 'Enabled'
  }
}

resource namedValueAppInsightsKey 'Microsoft.ApiManagement/service/namedValues@2023-05-01-preview' = {
  parent: apim
  name: 'instrumentationKey'
  properties: {
    tags: []
    secret: false
    displayName: 'instrumentationKey'
    value: appInsights.properties.InstrumentationKey
  }
}

resource apimLogger 'Microsoft.ApiManagement/service/loggers@2023-05-01-preview' = {
  parent: apim
  name: 'apimlogger'
  properties: {
    resourceId: appInsights.id
    description: 'Application Insights for APIM'
    loggerType: 'applicationInsights'
    credentials: {
      instrumentationKey: '{{instrumentationKey}}'
    }
  }
  dependsOn: [
    namedValueAppInsightsKey
  ]
}
Enter fullscreen mode Exit fullscreen mode

Bicep API Management Developer Portal

Bicep file to register the app registration with the API Management service apim.developer.bicep.

@description('Client Secret of App Registration for APIM Developer Portal')
@secure()
@minLength(1)
param apimAppClientSecret string

@description('AppId of App Registration for APIM Developer Portal')
@minLength(1)
param apimAppId string

var apimName = 'apim-eval-mm-${uniqueString(resourceGroup().id)}'
resource apim 'Microsoft.ApiManagement/service@2023-05-01-preview' existing = {
  name: apimName
}

var tenantId = subscription().tenantId

resource developerPortalIdentityMicrosoftEntraId 'Microsoft.ApiManagement/service/identityProviders@2023-05-01-preview' = {
  parent: apim
  name: 'Aad'
  properties: {
    clientId: apimAppId
    clientSecret: apimAppClientSecret
    signinTenant: tenantId
    allowedTenants: [
      tenantId
    ]
    type: 'aad'
    clientLibrary: 'MSAL-2'
  }
}
Enter fullscreen mode Exit fullscreen mode

Powershell

Powershell script to create the app registration and configure it for the Developer Portal Add-DeveloperPortal-Microsoft-Entra-Id.ps1.

<#
.SYNOPSIS
This code adds a new App Registration to Microsoft Entra Id.

.DESCRIPTION
This code adds the App Registration, creates a service principal and return the ClientSecret.

.PARAMETER ApimName
Specify the API Management Service name.

.OUTPUTS
Returns the ClientSecret and AppId.

.EXAMPLE
Example usage of the code:
 .\Add-DeveloperPortal-Microsoft-Entra-Id.ps1 -ApimName apim-eval-mm-ta4jvourkbccs

.NOTES
  The user / service principal running this script must have the necessary permissions to create an App Registration in the Azure AD tenant:
  At least: Application.ReadWrite.OwnedBy
#>

[CmdletBinding()]
param (
    [Parameter(Mandatory = $true)]
    [string]
    $ApimName
)

$appName = $ApimName
Write-Host "Creating app registration for '$appName'..."


try {
    $existingApp = az ad app list --display-name $appName --query "[0]"

    if (-not $existingApp) {


        Write-Host "App does not exist. Creating app registration..."

        # Create the app registration
        $newApp = az ad app create --display-name $appName --sign-in-audience AzureADMyOrg 

        if (-not $newApp) {
            Write-Error -Message "Cannot add App Registration" -ErrorAction Stop
            return;
        }
        $appId = az ad app list --display-name $appName --query "[].[appId]" --output tsv

        # Create client secret
        $clientSecret = az ad app credential reset --id $appId --years 42 --display-name eval-secret --append --output tsv --query "password"

        $webJson = @{
            implicitGrantSettings = @{
                enableIdTokenIssuance     = $true
                enableAccessTokenIssuance = $true
            }
            redirectUris          = @()
        } | ConvertTo-Json -d 4 -Compress
        if ($IsWindows -eq $true) {
            $webJson = $webJson | ConvertTo-Json -d 4
        }

        az ad app update --id $appId --set web=$webJson

        $replyUrlSignInAad = "https://$appName.portal.azure-api.net/signin-aad"
        $replyUrlSignIn = "https://$appName.developer.azure-api.net/signin"

        $spaJson = @{
            redirectUris = @(
                "$replyUrlSignInAad"
                "$replyUrlSignIn"
            )
        } | ConvertTo-Json -d 4 -Compress

        if ($IsWindows -eq $true) {
            $spaJson = $spaJson | ConvertTo-Json -d 4
        }
        az ad app update --id $appId --set spa=$spaJson

        # requiredResourceAccess
        az ad app permission add --id $appId --api 00000003-0000-0000-c000-000000000000 --api-permissions 7ab1d382-f21e-4acd-a863-ba3e13f7da61=Role
        az ad app permission add --id $appId --api 00000003-0000-0000-c000-000000000000 --api-permissions e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope
        az ad app permission add --id $appId --api 00000002-0000-0000-c000-000000000000 --api-permissions 5778995a-e1bf-45b8-affa-663a9f3f4d04=Role
        az ad app permission add --id $appId --api 00000002-0000-0000-c000-000000000000 --api-permissions 311a71cc-e848-46a1-bdf8-97ff7156d8e6=Scope
    }
    else {
        Write-Output "App registration with the name '$appName' already exists."
        Write-Output "No new app registration created."
    }
}
catch {
    Write-Output $_
    Write-Error -Message "Error on adding App Registration" -ErrorAction Stop
}

# Return the client secret and app id
return @($clientSecret, $appId)
Enter fullscreen mode Exit fullscreen mode

Pipeline

Azure DevOps pipeline to automate the deployment of the API Management service and the Developer Portal azure-pipeline.yaml.

The Powershell script returns the app registration details, such as the app ID and client secret, which are used in the Bicep files to configure the API Management service and the Developer Portal. Write-Host "##vso[task.setvariable variable=apimClientSecret;issecret=true]$appRegistrationClientSecret" is used to store this information as a secret in the Azure DevOps pipeline which will be passed later to the Bicep file.

trigger:
  branches:
    include:
    - feature/*

pool:
  vmImage: ubuntu-latest

variables:
  ServiceConnectionName: 'Azure Service Connection'
  ResourceGroupName: 'eval.webapp'
  DeploymentDefaultLocation: 'westeurope'

jobs:
- job:
  steps:
  - task: AzureResourceManagerTemplateDeployment@3
    inputs:
      connectedServiceName: $(ServiceConnectionName)
      location: $(DeploymentDefaultLocation)
      resourceGroupName: $(ResourceGroupName)
      csmFile: apim.bicep
      overrideParameters: -publisherEmail lorem@ipsum -publisherName 'Lorem'
  - task: AzureCLI@2
    displayName: Deploy DEV - App Registration Script
    inputs:
      azureSubscription: $(ServiceConnectionName)
      scriptType: pscore
      scriptLocation: inlineScript
      inlineScript: |
        Write-Host "Create App Registration"
        $appRegistration=$(Build.SourcesDirectory)/Add-DeveloperPortal-Microsoft-Entra-Id.ps1 -ApimName apim-eval-mm-ta4jvourkbccs
        $appRegistrationClientSecret = $appRegistration[0]
        $appRegistrationAppId = $appRegistration[1]
        Write-Host "##vso[task.setvariable variable=apimClientSecret;issecret=true]$appRegistrationClientSecret"
        Write-Host "##vso[task.setvariable variable=apimAppId;issecret=false]$appRegistrationAppId"

  - task: AzureResourceManagerTemplateDeployment@3
    condition: and(succeeded(), ne(variables['apimClientSecret'], ''), ne(variables['apimAppId'], ''))
    inputs:
      connectedServiceName: $(ServiceConnectionName)
      location: $(DeploymentDefaultLocation)
      resourceGroupName: $(ResourceGroupName)
      csmFile: apim.developer.bicep
      overrideParameters: '-apimAppClientSecret $(apimClientSecret) -apimAppId $(apimAppId)'
Enter fullscreen mode Exit fullscreen mode

Top comments (0)