Sharing templates seems like a really simple thing to want to do especially when you have similar projects that use the same steps to build or deploy code. In this post I am going to go through how does Azure Pipelines support this and how can we version shared templates.
As a C# developer I build code the same way in multiple projects, I use GitVersion for versioning my code and the DotNetCoreCLI tasks to restore, build and publish my code into and artifact, for example
There is probably a dozen projects/repositories where this exists now and say I wanted to add a security scan to all my projects that may take some time. A shared template would allow this to be maintained and updated in one place.
So how can we create a shared template that is used across multiple repositories in Azure DevOps?
Firstly we need to create a repository that contains the shared template code, I called my repository ‘shared-templates’, I added a folder called ‘code’ and added the above code into a template called ‘build-code.yml’ and added a parameter to pass in the name of the artifact
parameters:
- name: artifactName
type: string
Now I have added the first version of the code I am going to tag the repo with a version
The shared templates repository is now ready for use, let’s update a pipeline to now use this. The current pipeline looks something like this:
In order to use the external repository we need to add a resources section to the pipeline to look for the shared-templates repository and which branch to look at
This will get the latest version of the shared-templates when it is ran, if we wanted to get a specific version we can reference a tag like one we created earlier. The above would then become
If you are new to Pipelines and Bicep then checkout this Microsoft Learn course to give an introduction.
So, on to creating the AKS cluster using Bicep.
The resources we are going to deploy are:
Virtual Network
Log Analytics Workspace
AKS Cluster
Container Registry
We are also going to add Azure AD groups to lockdown the cluster administration and connect the container registry to allow AKS to pull containers from the registry.
Bicep
So let’s start with creating a module for the Virtual network, we need a name for the network and subnet as well as some address prefixes and tags.
The next module then is the AKS cluster itself, there is a lot of settings that you might want to control but I’ve added defaults for some of them. This module also includes creation of an Log Analytics workspace and the renaming of the AKS resource group that normally is prefixed with MC_ to something inline with the used naming convention.
@description('The environment prefix of the Managed Cluster resource e.g. dev, prod, etc.')
param prefix string
@description('The name of the Managed Cluster resource')
param clusterName string
@description('Resource location')
param location string = resourceGroup().location
@description('Kubernetes version to use')
param kubernetesVersion string = '1.20.7'
@description('The VM Size to use for each node')
param nodeVmSize string
@minValue(1)
@maxValue(50)
@description('The number of nodes for the cluster.')
param nodeCount int
@maxValue(100)
@description('Max number of nodes to scale up to')
param maxNodeCount int
@description('The node pool name')
param nodePoolName string = 'linux1'
@minValue(0)
@maxValue(1023)
@description('Disk size (in GB) to provision for each of the agent pool nodes. This value ranges from 0 to 1023. Specifying 0 will apply the default disk size for that agentVMSize')
param osDiskSizeGB int
param nodeAdminUsername string
@description('Availability zones to use for the cluster nodes')
param availabilityZones array = [
'1'
'2'
'3'
]
@description('Allow the cluster to auto scale to the max node count')
param enableAutoScaling bool = true
@description('SSH RSA public key for all the nodes')
@secure()
param sshPublicKey string
@description('Tags for the resources')
param tags object
@description('Log Analytics Workspace Tier')
@allowed([
'Free'
'Standalone'
'PerNode'
'PerGB2018'
'Premium'
])
param workspaceTier string
@allowed([
'azure'
])
@description('Network plugin used for building Kubernetes network')
param networkPlugin string = 'azure'
@description('Subnet id to use for the cluster')
param subnetId string
@description('Cluster services IP range')
param serviceCidr string = '10.0.0.0/16'
@description('DNS Service IP address')
param dnsServiceIP string = '10.0.0.10'
@description('Docker Bridge IP range')
param dockerBridgeCidr string = '172.17.0.1/16'
@description('An array of AAD group object ids for administration')
param adminGroupObjectIDs array = []
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-10-01' = {
name: '${prefix}-oms-${clusterName}-${resourceGroup().location}'
location: location
properties: {
sku: {
name: workspaceTier
}
}
tags: tags
}
resource aksCluster 'Microsoft.ContainerService/managedClusters@2021-03-01' = {
name: '${prefix}-aks-${clusterName}-${location}'
location: location
identity: {
type: 'SystemAssigned'
}
tags: tags
properties: {
nodeResourceGroup: 'rg-${prefix}-aks-nodes-${clusterName}-${location}'
kubernetesVersion: kubernetesVersion
dnsPrefix: '${clusterName}-dns'
enableRBAC: true
agentPoolProfiles: [
{
name: nodePoolName
osDiskSizeGB: osDiskSizeGB
osDiskType: 'Ephemeral'
count: nodeCount
enableAutoScaling: enableAutoScaling
minCount: nodeCount
maxCount: maxNodeCount
vmSize: nodeVmSize
osType: 'Linux'
type: 'VirtualMachineScaleSets'
mode: 'System'
availabilityZones: availabilityZones
enableEncryptionAtHost: true
vnetSubnetID: subnetId
}
]
networkProfile: {
loadBalancerSku: 'standard'
networkPlugin: networkPlugin
serviceCidr: serviceCidr
dnsServiceIP: dnsServiceIP
dockerBridgeCidr: dockerBridgeCidr
}
aadProfile: !empty(adminGroupObjectIDs) ? {
managed: true
adminGroupObjectIDs: adminGroupObjectIDs
} : null
addonProfiles: {
azurepolicy: {
enabled: false
}
omsAgent: {
enabled: true
config: {
logAnalyticsWorkspaceResourceID: logAnalyticsWorkspace.id
}
}
}
linuxProfile: {
adminUsername: nodeAdminUsername
ssh: {
publicKeys: [
{
keyData: sshPublicKey
}
]
}
}
}
dependsOn: [
logAnalyticsWorkspace
]
}
output controlPlaneFQDN string = reference('${prefix}-aks-${clusterName}-${location}').fqdn
output clusterPrincipalID string = aksCluster.properties.identityProfile.kubeletidentity.objectId
The final module is building an Azure Container Registry and assigning the ACR Pull role for the cluster
@description('The name of the container registry')
param registryName string
@description('The principal ID of the AKS cluster')
param aksPrincipalId string
@description('Tags for the resources')
param tags object
@allowed([
'b24988ac-6180-42a0-ab88-20f7382dd24c' // Contributor
'acdd72a7-3385-48ef-bd42-f606fba81ae7' // Reader
])
param roleAcrPull string = 'b24988ac-6180-42a0-ab88-20f7382dd24c'
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2019-05-01' = {
name: registryName
location: resourceGroup().location
sku: {
name: 'Standard'
}
properties: {
adminUserEnabled: true
}
tags: tags
}
resource assignAcrPullToAks 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
name: guid(resourceGroup().id, registryName, aksPrincipalId, 'AssignAcrPullToAks')
scope: containerRegistry
properties: {
description: 'Assign AcrPull role to AKS'
principalId: aksPrincipalId
principalType: 'ServicePrincipal'
roleDefinitionId: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/${roleAcrPull}'
}
}
output name string = containerRegistry.name
So now we have the all the modules lets setup the main bicep file to put it all together
@description('Naming prefix for the resources e.g. dev, test, prod')
param prefix string
@description('The public SSH key')
@secure()
param publicsshKey string
@description('The name of the cluster')
param clusterName string
@description('The location of the resources')
param location string = resourceGroup().location
@description('The admin username for the nodes in the cluster')
param nodeAdminUsername string
@description('An array of AAD group object ids to give administrative access.')
param adminGroupObjectIDs array = []
@description('The VM size to use in the cluster')
param nodeVmSize string
@minValue(1)
@maxValue(50)
@description('The number of nodes for the cluster.')
param nodeCount int = 1
@maxValue(100)
@description('Max number of nodes to scale up to')
param maxNodeCount int = 3
@description('Disk size (in GB) to provision for each of the agent pool nodes. This value ranges from 0 to 1023. Specifying 0 will apply the default disk size for that agentVMSize')
param osDiskSizeGB int
@description('Log Analytics Workspace Tier')
@allowed([
'Free'
'Standalone'
'PerNode'
'PerGB2018'
'Premium'
])
param workspaceTier string
@description('The virtual network address prefixes')
param vnetAddressPrefixes array
@description('The subnet address prefix')
param subnetAddressPrefix string
@description('Tags for the resources')
param tags object
module vnet 'vnet.bicep' = {
name: 'vnetDeploy'
params: {
vnetName: '${prefix}-vnet-${clusterName}-${location}'
subnetName: '${prefix}-snet-${clusterName}-${location}'
vnetAddressPrefixes: vnetAddressPrefixes
subnetAddressPrefix: subnetAddressPrefix
tags: tags
}
}
module aks 'aks.bicep' = {
name: 'aksDeploy'
params: {
prefix: prefix
clusterName: clusterName
subnetId: vnet.outputs.subnetId
nodeAdminUsername: nodeAdminUsername
adminGroupObjectIDs: adminGroupObjectIDs
nodeVmSize: nodeVmSize
nodeCount: nodeCount
maxNodeCount: maxNodeCount
osDiskSizeGB: osDiskSizeGB
sshPublicKey: publicsshKey
workspaceTier: workspaceTier
tags: tags
}
dependsOn: [
vnet
]
}
module registry 'registry.bicep' = {
name: 'registryDeploy'
params: {
registryName: 'acr${clusterName}'
aksPrincipalId: aks.outputs.clusterPrincipalID
tags: tags
}
dependsOn: [
aks
]
}
As with ARM templates you can use a Json File to configure the parameters in bicep and so I’ve added one for this
Now we have all the Bicep files and a parameters file, we can create an Azure Pipeline but first we are going to need an SSH Key and upload it to Azure Pipelines, one way to generate an SSH key is to use the ssh-keygen command in Bash (I used Ubuntu in WSL) e.g.
ssh-keygen -q -t rsa -b 4096 -N '' -f aksKey
This will generate a private and public key pair, you can then upload the public key file e.g. aksKey.pub to Secure Files in Azure DevOps Pipelines (Pipelines->Library->Secure files)
We are going to add Azure AD Groups in this deployment and will need to assign the role ‘Azure Kubernetes Service Cluster User Role’ to each group, the Microsoft Docs detail how to do this.
Now we have the SSH key uploaded we can configure the parameters we want to set for our AKS cluster and network.
trigger: none
pr: none
pool:
vmImage: ubuntu-latest
parameters:
- name: azureSubscription
type: string
default: 'Sandbox'
- name: location
displayName: 'Resource Location'
type: string
default: 'uksouth'
- name: prefix
displayName: 'Environment Prefix'
type: string
default: 'prod'
- name: clusterName
displayName: 'Name of the AKS Cluster'
type: string
default: 'demo'
- name: nodeVmSize
displayName: 'VM Size for the Nodes'
type: string
default: 'Standard_D2s_V3'
values:
- 'Standard_D2s_V3'
- 'Standard_DS2_v2'
- 'Standard_D4s_V3'
- 'Standard_DS3_v2'
- 'Standard_DS4_v2'
- 'Standard_D8s_v3'
- name: osDiskSizeGB
displayName: 'Size of OS disk (0 means use vm size)'
type: number
default: 50
- name: nodeCount
displayName: 'The number of nodes'
type: number
default: 3
- name: maxNodeCount
displayName: 'Max node to scale out to'
type: number
default: 10
- name: workspaceTier
displayName: Log Analytics Workspace Tier
type: string
default: 'PerGB2018'
values:
- 'Free'
- 'Standalone'
- 'PerNode'
- 'PerGB2018'
- 'Premium'
- name: tags
displayName: 'Tags'
type: object
default:
Environment: "prod"
Resource: "AKS"
Project: "Demo"
- name: nodeAdminUsername
displayName: 'Admin username for the nodes'
type: string
default: 'adminUserName'
- name: vnetAddressPrefixes
displayName: 'Virtual Network Address Prefixes'
type: object
default:
- '10.240.0.0/16'
- name: subnetAddressPrefix
displayName: 'Subnet Address Prefix'
type: string
default: '10.240.0.0/20'
- name: adGroupNames
type: object
default:
- 'demo-group'
variables:
resourceGroupName: 'rg-${{ parameters.prefix }}-${{ parameters.clusterName }}-${{ parameters.location }}'
With the parameters set the next part is to build up the steps, starting with downloading the SSH key from the secure files using the DownloadSecureFile task
If you need to debug the transform then you can add another step to output the file contents, I find this a useful technique to make sure the transform worked as expected
- bash: |
cat deploy.parameters.json
displayName: "Debug show parameters file"
If we put all that together then the final pipeline looks like this:
Now we have an AKS cluster setup we might want to deploy some applications to the cluster. CoderDave has a great video tutorial to do this with Azure Pipelines.
All the files shown above can be found on my GitHub