Azure API Management - Developer Portal
How to create an Azure API Management Service with Developer Portal with CI/CD using Azure DevOps
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.
The Azure DevOps pipeline uses three steps to complete the deployment:
The Developer Portal with Azure Active Directory
authentication:
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
}
}
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
]
}
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
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"
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
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
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.
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'
}
}
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
]
}
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'
}
}
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)
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)'