Deploying to Azure from an Azure DevOps Pipeline starts with a Service Connection. If you're using Azure DevOps Services (aka. the cloud version of Azure DevOps), it's easy to create the service connection because a wizard walks you through it and does the majority of the work for you. But if you're using Azure DevOps Server (aka. the on-premise version), there's no wizard. So how do you create an Azure Resource Manager Service Connection for Azure DevOps Server?
[TL;DR — I created a Powershell script. See below.]
What is an Azure DevOps Service Connection?
What is this Service Connection thing? There are services you might want to connect to and use from an Azure DevOps Pipeline during build and deploy operations. The details of connecting to these services are often non-trivial and rather than embedding those details inside of your pipelines, a Service Connection abstracts away those details and gives you a simplified and reusable way to use those services.
I'm always amazed at the list of services that have integrations with Azure DevOps Server. Here's the list:
Azure Classic
Azure Repos/Team Foundation Server
Azure Resource Manager
Azure Service Bus
Bitbucket Cloud
Chef
Docker Host
Docker Registry
Generic
GitHub
GitHub Enterprise Server
Incoming WebHook
Jenkins
Kubernetes
Maven
NuGet
Other Git
Python package download
Python package upload
SSH
Service Fabric
Subversion
Visual Studio App Center
npm
As you can see, it's not just for connecting to Azure. There's a log of other services, too.
Why is connecting to Azure so hard on Azure DevOps Server?
Back when Azure DevOps was known as Team Foundation Server, I'd written about how to do this and hadn't thought about it for a long time. Then I needed to update some demos and found out that (surprise!) my old script didn't work.
As I was going through the process of making the new Powershell script, I was thinking about why this is so easy in the cloud and so hard on-prem. The answer is that in the cloud, Azure DevOps Services can get access to your resources in Azure and do all the cumbersome setup work for you.
If you're trying to create a service connection for an Azure DevOps Server that's sitting in your datacenter, Azure DevOps doesn't really have a way to access your Azure resources and therefore you need to do it yourself.
And there are a lot of things that need to be done in a lot of different places and a lot of different values that need to be copied and moved around before plugging them into Azure DevOps. What's extra fun is that there isn't a lot of help in the dialog for figuring out what to do or where to find the values. You just see the dialog below and a whole bunch of empty textboxes.
So there's a lot of stuff...but I created a script.
The PowerShell Script
Here's the script to setup the Azure Resource Manager details for the Azure DevOps Service Connection. It assumes that you're trying to deploy to an Azure App Service or an Azure Functions application.
When you run the script, it'll prompt you for the Azure Subscription Id that you're referencing and then the name of the App Service. It'll then pop open a browser window to have you authenticate to your Azure Subscription. After running, it'll display all the values you'll need to set up the service connection in Azure DevOps. Those values will also be saved to a file on disk. Plug those values in to the Create New Service Connection dialog and you should be good to go.
param(
[Parameter(Mandatory=$true, HelpMessage="The guid for the subscription you'll be deploying to.")]
[string]$azureSubscriptionId,
[Parameter(Mandatory=$true, HelpMessage="The name of the azure app service that you'll be deploying to.")]
[string]$appServiceName
)
$ErrorActionPreference = 'Stop'
Import-Module Az
Import-Module Az.Resources
# this is the friendly-ish name and info for your application
# NOTE: this is the name of the Active Directory Application not the AppService
$appServiceDefaultDomain = "$appServiceName.azurewebsites.net"
$activeDirectoryApplicationDisplayName = $appServiceName
$activeDirectoryApplicationHomePage = "https://$appServiceDefaultDomain"
$credentialStartTime = [DateTime]::Now
$credentialEndTime = $credentialStartTime.AddYears(1)
# Login to Azure
Connect-AzAccount
Write-Output "Getting subscription using Get-AzSubscription..."
Write-Output "Requested subscription: $azureSubscriptionId"
$subscription =
(Get-AzSubscription -SubscriptionId $azureSubscriptionId)
if ($null -eq $subscription) {
Write-Output "Subscription is null"
exit
}
else {
Write-Output "Got subscription using Get-AzSubscription..."
}
$subscriptionId = $subscription.Id
$subscriptionName = $subscription.Name
$tenantId = $subscription.tenantId
Write-Output "Subscription Id: $subscriptionId"
Write-Output "Subscription Name: $subscriptionName"
Write-Output "Tenant Id: $tenantId"
Write-Output "Calling Set-AzContext..."
Set-AzContext -SubscriptionId $subscriptionId -TenantId $tenantId
# create application in AAD
Write-Output "Calling New-AzADApplication..."
New-AzADApplication -DisplayName $activeDirectoryApplicationDisplayName -HomePage $activeDirectoryApplicationHomePage -OutVariable app
if ($app -eq $null) {
Write-Output "Call to New-AzADApplication returned null."
exit
}
else {
Write-Output "Got application from New-AzADApplication..."
Write-Output $app
}
$servicePrincipalClientId = $app.AppId
Write-Output "Calling New-AzADAppCredential..."
New-AzADAppCredential -ApplicationId $app.AppId -StartDate $credentialStartTime -EndDate $credentialEndTime -OutVariable generatedCredential
if ($generatedCredential -eq $null) {
Write-Output "Call to New-AzADAppCredential returned null."
exit
}
else {
Write-Output "Got generated credential from New-AzADAppCredential..."
Write-Output $generatedCredential
}
$generatedSecretText = $generatedCredential.SecretText
Write-Output "SecretText: $generatedSecretText"
Write-Output "Calling New-AzADServicePrincipal..."
New-AzADServicePrincipal -ApplicationId $app.AppId -OutVariable servicePrincipal
if ($servicePrincipal -eq $null) {
Write-Output "Call to New-AzADServicePrincipal returned null."
exit
}
else {
Write-Output "Got service principal from New-AzADServicePrincipal..."
Write-Output $servicePrincipal
}
Write-Output $servicePrincipal
Write-Output "Pausing for a bit to let New-AzureRmADServicePrincipal catch up before adding role assignment..."
Start-Sleep -s 10
Write-Output "Calling New-AzRoleAssignment..."
New-AzRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName $app.AppId -OutVariable roleAssignment
if ($roleAssignment -eq $null) {
Write-Output "Call to New-AzRoleAssignment returned null."
exit
}
else {
Write-Output "Got role assignment from New-AzRoleAssignment..."
Write-Output $roleAssignment
}
Write-Output $roleAssignment
Write-Output "Reloading what we just created..."
Get-AzADApplication -DisplayNameStartWith $activeDirectoryApplicationDisplayName -OutVariable reloadedApp
Get-AzADServicePrincipal -ServicePrincipalName $reloadedApp.AppId -OutVariable SPN
Write-Output "Here's the SPN..."
Write-Output $SPN
$nowTicks = [DateTime]::Now.Ticks;
$keyValueFilename = "service-principal-info-$nowTicks.txt"
# create an instance of StringBuilder
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine()
[void]$sb.AppendLine("******************************")
[void]$sb.AppendLine()
[void]$sb.AppendLine("Here's all the info you need.")
[void]$sb.AppendLine()
[void]$sb.AppendLine("Subscription Id:")
[void]$sb.AppendLine("$subscriptionId")
[void]$sb.AppendLine()
[void]$sb.AppendLine("Subscription Name:")
[void]$sb.AppendLine("$subscriptionName")
[void]$sb.AppendLine()
[void]$sb.AppendLine("Service Principal Client Id:")
[void]$sb.AppendLine("$servicePrincipalClientId")
[void]$sb.AppendLine()
[void]$sb.AppendLine("Service Principal Key:")
[void]$sb.AppendLine("$generatedSecretText")
[void]$sb.AppendLine()
[void]$sb.AppendLine("Tenant Id:")
[void]$sb.AppendLine("$tenantId")
[void]$sb.AppendLine()
[void]$sb.AppendLine("******************************")
[void]$sb.AppendLine("Script created by Benjamin Day")
[void]$sb.AppendLine("Benjamin Day Consulting, Inc.")
[void]$sb.AppendLine("https://www.benday.com")
[void]$sb.AppendLine("info@benday.com")
[void]$sb.AppendLine("******************************")
[void]$sb.AppendLine()
$sb.ToString() | Out-File .\$keyValueFilename
Write-Output $sb.ToString()
Write-Output "This info is also written to $keyValueFilename"
Write-Output "Done."
Summary
Setting up a service connection from Azure DevOps to an Azure Subscription can be a huge, confusing hassle...but I created a PowerShell script to make it easier.
Run the script. Plug in the values. Run your Azure DevOps Pipelines and deploy to your heart's content.
I hope this helps.
-Ben
Need help with Azure DevOps or GitHub? Want to get better at delivering software using Scrum or Kanban or Flow Metrics? Looking to solve a sticky architectural problem? We can help. Drop us a line at info@benday.com.