Automating Azure DevOps Service Connections

Azure DevOps, Azure Infrastructure, DevOps, Windows PowerShell
Azure Pipelines Icon

Overview

Recently I was working on doing infrastructure deployments using Azure DevOps Pipelines. One of the first things that needs to be done is to create a Service Connection to the target environment. In my case, my target environment is an Azure Subscription and I’ll use a Service Principal with an ID and Key (versus a Certificate) for authentication. However, we want to avoid storing this authentication information and we want it automated.

I also don’t want my Service Principal to have broad privileges so it’s scope will be limited to a single Resource Group. Let’s create several Resource Groups, a Service Principal for each one, assign it privileges and create a corresponding Service Connection in Azure Dev Ops

Oh, so many parameters

First, let’s define some parameters and set some variables. Most of these are self-explanatory:

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
#region Parameters
$cloud = "AzureCloud"
$location = 'eastus'
$tagDept = "specialprojects"
$tagEnv = "dev"
$devOpsUrl = 'https://dev.azure.com/M365x'
$devOpsProject = 'infra'
$resourceUrl = https://management.core.windows.net/$apps = @{
     'logging'="2";
     'devops'="2";
     'domain'="1";
     }
#endregion

The $apps parameter is a hashtable that will be used in the name of the Resource Group name and Service Principal. The number next to each one is part of my Resource Tags.

Next, we’re going to login while saving the context so we can gather more variables:

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
Clear-AzContext -Force
Save-AzContext -Profile (Add-AzAccount -Environment $cloud) -Path $env:TEMP\az.json -Force
 
#Get variables
$az = Get-Content -Path $env:TEMP\az.json | ConvertFrom-Json
$tenantId = $az.Contexts.Default.Tenant.TenantId
$subId = $az.Contexts.Default.Subscription.SubscriptionId
$subName = $az.Contexts.Default.Subscription.Name
$cloudEnv = $az.Contexts.Default.Environment.Name
$cloudUrl = $az.Contexts.Default.Environment.ResourceManagerUrl
$createdBy = $az.Contexts.Default.Account.Id

We also need the Access Token that we’ll use later to make a REST API call to Azure DevOps:

?View Code POWERSHELL
1
2
3
$ctx = Get-AzContext
$cacheItems = $ctx.TokenCache.ReadItems()
$token = ($cacheItems | where { $_.Resource -eq $resourceUrl }).AccessToken

Loop it

We’re almost ready to get to work. Since we have 3 Resource Groups defined in $apps (and could easily expand this to several dozen), we need a foreach loop. We’ll also set some additional variables:

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
foreach ($app in $apps.GetEnumerator())
    {
        $appName = $app.Name
        $rgName = "rg-$appName-$tagEnv"
        $spName = "sp-$appName-$tagEnv"
        $scope = "/subscriptions/$subId/resourceGroups/$rgName"
        $tags = @{
                App=$appName;
                Department=$tagDept;
                Environment=$tagEnv
                Tier=$app.Value
                CreatedBy=$createdBy
        }

These variables are just setting the names of the Resource Groups and Service Principal. If you have a different standard, modify these. Also, this is where we set the scope to the Resource Group. If you prefer to have a Subscription-wide scope, just remove /resourceGroups/$rgName.

Next, we’ll create the Resource Group and Service Principal using these values:

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        #Create Resource Group
        If ((Get-AzResourceGroup -Name $rgName -ErrorAction SilentlyContinue) -eq $null)
            {
            New-AzResourceGroup -Location $location -Name $rgName -Tag $tags | Out-Null
            }
 
        #Create Service Principal
        If ((Get-AzADServicePrincipal -DisplayName $spName) -eq $null)
            {
            #Create Service Principal and assign rights. This can take a minute.
            $sp = New-AzADServicePrincipal -DisplayName $spName -Scope $scope  `
-Role Contributor -WarningAction SilentlyContinue
            }
 
        $spNameId = $sp.ServicePrincipalNames | ? {$_ -notlike "http*"} | select -First 1
        $spkey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($sp.Secret))

Let me explain the last two parameters:

  • $spNameId – This is the Service Principal ID and will look like a GUID
  • $spkey – This is the password for the Service Principal. We need to use this later to create the Service Connection later but after that, we don’t really need to know it. It’s available in memory for a short time, until the next item in the loop or once the session is closed but we don’t have to store it anywhere (thus improving security).

The last thing we need to do is to create the Service Connection in Azure DevOps. I’m not aware of an official PowerShell module but there is an Azure CLI extension. The problem with the Azure CLI is that it has limited support for creating Service Endpoints (Service Connections) and is in preview. Therefore, we’ll call the REST API directly, using PowerShell:

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        #Set variables for request body
        $params = @{
            data=@{
                SubscriptionId=$subId;SubscriptionName=$subName;environment=$cloudEnv;
scopeLevel="Subscription";creationMode="Manual"}
            name=$spName;type="azurerm";url=$cloudUrl;
            authorization=@{
                scheme="ServicePrincipal";
                parameters=@{
                    tenantid=$tenantId;serviceprincipalid=$spNameId;
authenticationType="spnKey";serviceprincipalkey=$spKey}
                }
            }
        $body = $params| ConvertTo-Json
 
        #Set headers and send request
        $headers = @{"Authorization" = "Bearer " + $token;"Content-Type" = "application/json"}
        $baseUri = "$devOpsUrl/$devOpsProject/_apis/serviceendpoint/endpoints?api-version=5.0-preview.2"
        $req = Invoke-RestMethod -Method POST -Uri $baseUri -Headers $headers -Body $body -ErrorAction SilentlyContinue
    }

Because that was in a foreach loop, we can easily create many Resource Groups, a Service Principal for each one, assign it Contributor rights on the Resource Group, and create a Service Connection.

Conclusion

Now, we can create Pipelines that use these Service Connections to connect to Azure Resource Manager. No passwords or secrets are kept insecurely and our Service Principals are using limited rights.

I mentioned that there was not an official PowerShell module for Azure DevOps. However, here are some community projects; I have not tried any of these:

For more information on using Pipelines for infrastructure, check out these great posts from Barbara 4bes:

Is there a better way to do this, got any ideas? Post a comment below.

0 comments… add one

Leave a Reply

%d bloggers like this: