How to create an Azure API Management Service with Developer Portal with CI/CD using Azure DevOps
Tags: azure, powershell, apim, devops
Published: 2024-02-28 13:51:12

## Table of Contents
[Prerequisites](#prerequisites)
[Step 1: Create an Azure API Management Service](#step1-create-an-azure-api-management-service)
[Step 2: Set Up the App Registration for the Developer Portal](#step2-set-up-the-app-registration-for-the-developer-portal)
[Step 3: Register the app registration with the API Management service](#step3-register-the-app-registration-with-the-api-management-service) 
[Complete Code](#complete-code) 

In this blog post, we will walk through the steps to create an [Azure API Management service](https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts) and set up the [Developer Portal](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-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](https://cdn.hashnode.com/res/hashnode/image/upload/v1708494396416/658259fb-98d6-4f76-b4b4-07e353f2925a.png)

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

![Flow](https://cdn.hashnode.com/res/hashnode/image/upload/v1708494641352/0d09bced-5d4d-45c7-bb4b-25fe1423b10c.png)

The Developer Portal with `Azure Active Directory` authentication:

![Developer Portal - Sign in](https://cdn.hashnode.com/res/hashnode/image/upload/v1708496855008/ed6cc0e5-9a89-4068-84c1-c06e32c8417b.png)

## Prerequisites <a id="prerequisites"></a>

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:
    
    * [Application.ReadWrite.OwnedBy](https://learn.microsoft.com/en-us/graph/permissions-reference#applicationreadwriteownedby)

## Step 1: Create an Azure API Management Service <a id="step1-create-an-azure-api-management-service"></a>

The following [Bicep code defines an Azure API Management service](https://learn.microsoft.com/en-us/azure/templates/microsoft.apimanagement/service?pivots=deployment-language-bicep) 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.

```json
@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.

```json
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
  ]
}
```

![API Management - Logger](https://cdn.hashnode.com/res/hashnode/image/upload/v1708491522385/432b13c0-5127-4f8a-9e04-33067d9b259d.png )

## Step 2: Set Up the App Registration for the Developer Portal <a id="step2-set-up-the-app-registration-for-the-developer-portal"></a>


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

```powershell
# 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
```

![App registration](https://cdn.hashnode.com/res/hashnode/image/upload/v1708491130616/7eefae3f-54af-445f-bf74-dac52fc59619.png)

### 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.

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

![Certificates & secrets](https://cdn.hashnode.com/res/hashnode/image/upload/v1708490956096/62d0bb8a-1476-4e63-8da5-878632cecde9.png)

### 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.

```powershell
$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
```

![Authentication](https://cdn.hashnode.com/res/hashnode/image/upload/v1708491012071/7cc77e60-d3f8-48cd-9a24-0c84c1c770b8.png)

### 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
        

```powershell
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
```

![API permissions](https://cdn.hashnode.com/res/hashnode/image/upload/v1708492459363/004e1170-e82b-4f65-8044-97b02cb2dbf3.png)

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1708492698668/c4c65c5b-c3e9-426e-912c-8d953aa4f48e.png)

## Step 3: Register the app registration with the API Management service<a id="step3-register-the-app-registration-with-the-api-management-service"></a>


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.

```json
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'
  }
}
```

![Update identity provider](https://cdn.hashnode.com/res/hashnode/image/upload/v1708494222261/e9356325-9d33-498c-bcd9-fd5886049aa1.png)

## Complete Code<a id="complete-code"></a>


![](https://cdn.hashnode.com/res/hashnode/image/upload/v1708494774354/d0b121f9-d3e9-4184-88c6-eedf5b6be1eb.png align="center")

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`.

```json
@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`.

```json
@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`.

```powershell
<#
.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.

```yaml
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)'
```