Menu

Creating Azure resources through Azure DevOps (VSTS) CI/CD pipeline

Some Devs prefer to create resources manually on Azure and some prefer to automate their build/releases using CI/CD. But some Devs prefer to automate everything, Yes – including creating resources in Azure are such as Storage,  Azure Functions, etc. I cant see a huge difference in automating the resource creation from CI/CD pipeline, you would not make changes to resources that frequently as you do with your code.

We will use ARM templates to, I also have a function app written using typescript in the same repository. Folder structure as below

repository root
– arm
— azuredeploy.json
— deploy.ps1
– build
— azure-pipelines.yml
– src
— package.json
— ……..

below is the azuredeploy.json file

{
	"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
	"contentVersion": "1.0.0.0",
	"parameters": {
		"appName": {
			"defaultValue": "MYAPPNAME",
			"type": "string",
			"metadata": {
				"description": "The name of the app that you wish to create."
			}
		},
		"environment": {
			"defaultValue": "dev",
			"allowedValues": ["dev", "test", "preprod", "prod"],
			"type": "string",
			"metadata": {
				"description": "The name of the environment you wish to release."
			}
		},
		"location": {
			"type": "string",
			"defaultValue": "[resourceGroup().location]",
			"metadata": {
				"description": "Location for all resources."
			}
		},
		"runtime": {
			"type": "string",
			"defaultValue": "node",
			"allowedValues": ["node", "dotnet", "java"],
			"metadata": {
				"description": "The language worker runtime to load in the function app."
			}
		}
	},
	"variables": {
		"functionAppName": "[concat('azfun-', parameters('appName'), '-', toLower(parameters('environment'))) ]",
		"hostingPlanName": "[concat('azapp-', parameters('appName'), '-', toLower(parameters('environment'))) ]",
		"applicationInsightsName": "[concat('appins-', parameters('appName'), '-', toLower(parameters('environment'))) ]",
		"storageAccountName": "[concat('stor', parameters('appName'), toLower(parameters('environment'))) ]",
		"storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]",
		"functionWorkerRuntime": "[parameters('runtime')]"
	},
	"resources": [

		{
			"name": "[variables('storageAccountName')]",
			"type": "Microsoft.Storage/storageAccounts",
			"apiVersion": "2019-06-01",
			"location": "[parameters('location')]",
			"kind": "StorageV2",
			"sku": {
				"name": "Standard_LRS"
			}
		},


		{
			"type": "Microsoft.Web/serverfarms",
			"apiVersion": "2016-09-01",
			"name": "[variables('hostingPlanName')]",
			"location": "[resourceGroup().location]",
			"properties": {
				"name": "[variables('hostingPlanName')]",
				"computeMode": "Dynamic"
			},
			"sku": {
				"name": "Y1",
				"tier": "Dynamic",
				"size": "Y1",
				"family": "Y",
				"capacity": 0
			}
		},


		{
			"name": "[variables('functionAppName')]",
			"type": "Microsoft.Web/sites",
			"apiVersion": "2018-11-01",
			"location": "[parameters('location')]",
			"kind": "functionapp",
			"dependsOn": [
				"[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
				"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
			],
			"properties": {
				"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
				"siteConfig": {
					"appSettings": [{
							"name": "AzureWebJobsDashboard",
							"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
						},
						{
							"name": "AzureWebJobsStorage",
							"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
						},
						{
							"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
							"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
						},
						{
							"name": "WEBSITE_CONTENTSHARE",
							"value": "[toLower(variables('functionAppName'))]"
						},
						{
							"name": "FUNCTIONS_EXTENSION_VERSION",
							"value": "~2"
						},
						{
							"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
							"value": "[reference(resourceId('microsoft.insights/components/', variables('applicationInsightsName')), '2015-05-01').InstrumentationKey]"
						},
						{
							"name": "FUNCTIONS_WORKER_RUNTIME",
							"value": "[variables('functionWorkerRuntime')]"
						}
					]
				}
			}
		},


		{
			"name": "[concat(variables('functionAppName'),'/', 'staging')]",
			"type": "Microsoft.Web/sites/slots",
			"apiVersion": "2018-11-01",
			"location": "[parameters('location')]",
			"kind": "functionapp",
			"dependsOn": [
				"[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
				"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
				"[resourceId('Microsoft.Web/Sites', variables('functionAppName'))]"
			],
			"properties": {
				"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
				"siteConfig": {
					"appSettings": [{
							"name": "AzureWebJobsDashboard",
							"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
						},
						{
							"name": "AzureWebJobsStorage",
							"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
						},
						{
							"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
							"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
						},
						{
							"name": "WEBSITE_CONTENTSHARE",
							"value": "[toLower(variables('functionAppName'))]"
						},
						{
							"name": "FUNCTIONS_EXTENSION_VERSION",
							"value": "~2"
						},
						{
							"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
							"value": "[reference(resourceId('microsoft.insights/components/', variables('applicationInsightsName')), '2015-05-01').InstrumentationKey]"
						},
						{
							"name": "FUNCTIONS_WORKER_RUNTIME",
							"value": "[variables('functionWorkerRuntime')]"
						}
					]
				}
			}
		},


		{
			"apiVersion": "2015-05-01",
			"name": "[variables('applicationInsightsName')]",
			"type": "Microsoft.Insights/components",
			"kind": "web",
			"location": "[parameters('location')]",
			"tags": {
				"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', variables('functionAppName'))]": "Resource"
			},
			"properties": {
				"Application_Type": "web",
				"ApplicationId": "[variables('applicationInsightsName')]"
			}
		}
	]
}

below is the deploy.ps1 PowerShell script to create resources using the above template. Few points we will pass in the subscriptionId, environment, templateFilePath as input parameters from DevOps,  I have set the location as “Australia Southeast”. Also, all the resources created under a single resource group for that environment. Ex: RG_MYDEMOAPP_Dev will have database, stage, azure function, etc. This will help the organization with billing, and tracking of cost.

Quick note: alternatively you can use the “Azure Resource Group Deployment” task instead of the PowerShell script.

<#
 .SYNOPSIS
	Deploys a template to Azure

 .DESCRIPTION
	Deploys an Azure Resource Manager template

 .PARAMETER subscriptionId
	The subscription id where the template will be deployed.
	
 .PARAMETER environment
	Environment Name. Valid values: dev, test, preprod prod

 .PARAMETER templateFilePath
	Path for the template.json
#>

param(	
    [Parameter(Mandatory = $True)]
    [string]
    $subscriptionId,


    [Parameter(Mandatory = $True)]
    [ValidateSet('dev', 'test', 'preprod', 'prod')]
    [string]
    $environment,


    [Parameter(Mandatory = $True)]
    [string]
    $templateDir
)

<#
.SYNOPSIS
	Registers RPs
#>
Function RegisterRP {
    Param(
        [string]$ResourceProviderNamespace
    )

    Write-Host "Registering resource provider '$ResourceProviderNamespace'";
    Register-AzResourceProvider -ProviderNamespace $ResourceProviderNamespace;
}

$applicationName = "MYAPPNAME"
$resourceGroupName = "RG_${applicationName}_${environment}"
$resourceGroupLocation = 'Australia Southeast'

# select subscription
Write-Host "Selecting subscription '$subscriptionId'";
Select-AzSubscription -SubscriptionID $subscriptionId;

# Register RPs
$resourceProviders = @("microsoft.insights", "microsoft.web", "microsoft.storage");
if ($resourceProviders.length) {
    Write-Host "Registering resource providers"
    foreach ($resourceProvider in $resourceProviders) {
        RegisterRP($resourceProvider);
    }
}


#Create or check for existing resource group
$resourceGroup = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue

if (!$resourceGroup) {
    Write-Host "Resource group '$resourceGroupName' does not exist.";
    Write-Host "Creating resource group '$resourceGroupName' in location '$resourceGroupLocation'";
    New-AzResourceGroup -Name $resourceGroupName -Location $resourceGroupLocation -Tag @{Application = $applicationName; Team = "Digital"; Environment = $environment }
}
else {
    Write-Host "Using existing resource group '$resourceGroupName'";
    Set-AzResourceGroup -Name $resourceGroupName -Tag @{Application = $applicationName; Team = "Digital"; Environment = $environment }
}

# Start the deployment
Write-Host "Starting deployment...";
Write-Host "Template directory is '$templateDir'"

$templateFile = -join ($templateDir, "azuredeploy.json");
Write-Host "Deploying '$templateFile'"

$result = New-AzResourceGroupDeployment -Name "Initial-template" -ResourceGroupName $resourceGroupName -TemplateFile $templateFile -TemplateParameterObject @{environment = $environment; appName = $applicationName } -Verbose -Mode Incremental
	
Write-Host $result

Now the build .YML file (azure-pipelines.yml).

trigger:
  branches:
    include:
    - master
    - hotfix/*
    - feature/*
    - release/*
	
pool:
  vmImage: 'vs2017-win2016'  

steps:
- task: Npm@1
  displayName: 'Install node modules on server'
  inputs:
    command: 'install'
    workingDir: '$(Build.SourcesDirectory)/src'
    verbose: true

- task: Npm@1
  displayName: 'Build server'
  inputs:
    command: 'custom'
    workingDir: '$(Build.SourcesDirectory)/src'
    customCommand: 'run build'
    verbose: true

- task: CopyFiles@2
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)/src'
    Contents: |
      **
      !**/*.ts
      !node_modules/**
    TargetFolder: '$(Build.ArtifactStagingDirectory)/dist'
    OverWrite: true

- task: CopyFiles@2
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)/arm'
    Contents: '**'
    TargetFolder: '$(Build.ArtifactStagingDirectory)/arm'
    OverWrite: true

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact'
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

Build

We will use this build file on our build profile, let login to Azure DevOps and set up new build from your repo, and discover the .yml file. Now your build pipeline is done, kick off a build to see if all the steps pass. Remember build will only create the artefacts not create resources.

Release

Let’s create a new release pipeline, create an “Azure Powershell Script” and set up fields for deployment.

 

 

 

 

 

Leave a comment