Industrial Scale Onboarding in Microsoft Azure

In my last post on Industrial Scale RBAC, I alluded to the fact that in an enterprise context you'll likely need some special processes and tools for onboarding new projects into Azure. In this post, I'm going to break this down. As with my last post, the advice in this one isn't likely to be applicable in small organisations or where a team has a subscription to themselves. But in a large enterprise with many teams working on different projects while sharing subscriptions, things start to get interesting.

To see why, let's pay a visit to everyone's favourite multinational, Contoso. After following the guidance in the Azure Enterprise Scaffold, Contoso's central cloud team has already set up their foundational Azure environment, by setting up subscriptions for production and non-production work, VNETs and subnets, connectivity to their on-premises networks, defining naming standards and writing polices on what can be deployed in which regions. They also have defined a chargeback regime, whereby each cost centre needs to pay for the resources used by their projects.

What they don't have yet is any actual workloads running in Azure. This is not due to lack of demand—in fact there are a whole bunch of teams itching to deploy resources to the cloud. But the cloud team is fearful that once they start granting access to the Azure subscription it will become a free-for-all, where loads of resources are deployed with little regard to their careful planning, and no way of knowing which resources are owned by which team and used for what purpose.

What we need here is some governance! Contoso's central cloud team needs to know when new projects are being established, ensure that the right people have access to the resources, and have a way of tracking which resources are owned by which team and cost centre for chargeback purposes. They also need to be able to enforce additional rules such as restricting resources to certain locations or preventing people from creating things that should be centrally managed (like VNET gateways or Key Vaults). The Contoso project teams probably don't care much about any of these things, but they know that unless the central cloud team is happy they aren't going to get access to the platform.

The solution here is to define a process that uses Azure Resource Groups, RBAC and Resource Policy to onboard new projects in a predictable and compliant manner. At the centre of the solution is the idea that Resource Groups are created and configured centrally, after which project teams are free to deploy what they want, subject to the constraints baked into the resource group.

How you implement this process will depend on your organisation's maturity, toolsets and the frequency which new projects are onboarded. If you only get a new project every few months, you could do all of this manually using the portal. If projects are springing up randomly and frequently, you could implement a self-service request portal backed by a workflow with approvals and automation. In this post I've put together a starter PowerShell script that demonstrates the key steps, but note that this is not production quality and you'll need to think about what is appropriate in your onboarding process.

My starter process does the following:

  1. The script is invoked, specifying the project name and additional metadata (cost centre, deployment location, etc).
  2. Two AAD groups are created, one for team contributors and one for team readers. Note the script doesn't add users to these groups, so you'll need to do that separately.
  3. A new resource group is created in accordance with a simple naming convention
  4. Tags are applied to the resource group, recording the project name, cost centre, etc.
  5. RBAC is applied to the resource group, assigning the Contributor and Reader role to each of the two new AAD groups
  6. Resource Policy is applied to the resource group. I've got two policies, but you could add more:
    1. A policy that applies the resource group's Tags to each resource that is added to the group
    2. A policy that prevents resources from being deployed outside of a list of approved locations

The code for my starter process is below. Note that the PowerShell script requires that you have a custom policy definition called taggingPolicy already created. The XML and PowerShell to create this is in the following section.

[caption id="attachment_2635" align="alignright" width="300"] Azure Policy allows you to automatically set the correct tags on resources created in the resource group.[/caption]

Onboarding PowerShell Script

[powershell]param(
    [string]$projectName,
    [string]$projectContact,
    [string]$costCentre,
    [string]$environment,
    [string]$subscriptionId,
    [array]$locations # First location is where the RG will be created.
)

# TODO: Validate the inputs

# Log in
Login-AzureRmAccount
$sub = Select-AzureRmSubscription -SubscriptionId $subscriptionId
Connect-AzureAD -TenantId $sub.Tenant.TenantId

# Create AAD groups
# TODO: Check if they already exist
$contributorsGroupName = "$projectName-$environment-Contributors"
$readersGroupName = "$projectName-$environment-Readers"
New-AzureADGroup -DisplayName $contributorsGroupName -MailEnabled $false -SecurityEnabled $true -MailNickName $contributorsGroupName
New-AzureADGroup -DisplayName $readersGroupName -MailEnabled $false -SecurityEnabled $true -MailNickName $readersGroupName

# Create Azure Resource Group
# TODO: Check if it already exists
$rgName = "$projectName-$environment-RG" # or whatever your naming convention is
$tags = @{"ProjectName"=$projectName; ProjectContact=$projectContact; CostCentre=$costCentre; Environment=$environment}
$rg = New-AzureRmResourceGroup -Name $rgName -Location $locations[0] -Tag $tags

# Set RBAC for the Resource Group
$contributorsGroup = Get-AzureRmADGroup -SearchString $contributorsGroupName
$readersGroup = Get-AzureRmADGroup -SearchString $readersGroupName
New-AzureRmRoleAssignment -ObjectId $contributorsGroup.Id -RoleDefinitionName "Contributor" -Scope $rg.ResourceId
New-AzureRmRoleAssignment -ObjectId $readersGroup.Id -RoleDefinitionName "Reader" -Scope $rg.ResourceId

# Assign policies
$tagPolicy = Get-AzureRmPolicyDefinition -Name taggingPolicy
New-AzureRMPolicyAssignment -Name "$rgname-TagPolicy" -Scope $rg.ResourceId -PolicyDefinition $tagPolicy -PolicyParameterObject $tags # Assumes policy already created with this name
$locationPolicy =  Get-AzureRmPolicyDefinition | where-object { $_.Name -eq "e56962a6-4747-49cd-b67b-bf8b01975c4c" } # Built-in location policy
New-AzureRMPolicyAssignment -Name "$rgname-LocationPolicy" -Scope $rg.ResourceId -PolicyDefinition $locationPolicy -PolicyParameterObject @{listOfAllowedLocations=$locations}[/powershell]

Tagging Policy

Policy file

[xml]{
"if": {
"field": "tags",
"exists": "false"
},
"then": {
"effect": "append",
"details": [
{
"field": "tags",
"value": {
"ProjectName": "[parameters('projectName')]",
"ProjectContact": "[parameters('projectContact')]",
"CostCentre": "[parameters('costCentre')]",
"Environment": "[parameters('environment')]"
}
}
]
}
}
[/xml]

Parameters file

[xml]{
"projectName": {
"type": "string",
"metadata": {
"description": "The short name of the project",
"displayName": "Project Name"
}
},
"projectContact": {
"type": "string",
"metadata": {
"description": "The username of the main contact for this project",
"displayName": "Project Contact"
}
},
"costCentre": {
"type": "string",
"metadata": {
"description": "The cost centre that usage will be charged to",
"displayName": "Cost Centre"
}
},
"environment": {
"type": "string",
"metadata": {
"description": "The code for the environment for this deployment",
"displayName": "Environment"
}
}
}
[/xml]

PowerShell to create the Policy Definition

[powershell]$policy = New-AzureRmPolicyDefinition -Name taggingPolicy -Description "Policy to append default tags to all resources" -Policy ".\TagPolicy.json" -Parameter ".\TagPolicyParameters.json"[/powershell]