2023 Beginner’s Guide To Create An Azure Devops Pipeline to deploy Azure Resources in Terraform

I will give a walk through of a simple starter approach to building an Azure Devops Pipeline to deploy an Azure resource in Terraform. I will deploy an Azure Function as an example.

By automating, you promote consistency, repeatability, collaboration, overall productivity and more efficiency in operating your Azure infrastructure.


Demo Design

  • AZDO Repo
    • pipeline
    • terraform module
  • Azure
    • Subscription
    • Resource Group
    • Storage account for terraform backend state file(s)
    • Azure App Plan
    • Azure Function App
    • Storage account for app plan
    • Service principal for AZDO service connection to Contributor role to the subscription

  1. I created a new project as such

2. My AZDO repo contains a starter YAML pipeline and I had created a sub folder called terraform.

YAML pipelines provide a more efficient and scalable way to manage your build and release pipelines in Azure DevOps.

In a YAML pipeline, you define the tasks, steps, and stages of your pipeline in a YAML file, and then check it into your source control repository. The terraform folder is to create your terraform script that the pipeline will use to execute.

3. In order for your pipeline to create resource into your azure subscription, you need to create an Azure service principal that has the permissions to create appropriate resources such as resource group, storage account and azure function app.

An Azure AD service principal is a security identity that represents an application or service in Azure and enables it to interact with Azure resources in a secure and controlled manner. It is used to authenticate and authorize applications and services that need to access Azure resources, and to manage their permissions and access to those resources.

I created a service principal to be used by Azure Devops Service Connection by going to Azure AD > App registrations

When creating, take note of the client ID and client secret as this is the password as it is used in creating the AZDO service connection.
Clicking into the details you can see the following:

Next, in my demo, I have granted this service principal the Contributor role to the subscription. This will enable the service principal to create any azure resource. As a security best practice, you should adopt the least privilege principal by providing specific Azure AD RBAC roles.

5. You need a service connection that the pipeline will refer to so that various pipeline tasks can execute.

In project settings > Service Connections

I provided these properties

Note that another option is to use Azure Managed Identities as you don’t need to worry about the client secret being expired. But this does not work with AZDO pipelines in the MS hosted build agent as I am using in this demo. Managed Identities will work in self hosted agents in Azure VMs in your subscription.

5. I have prepared these terraform files which are used by the pipeline.

You can develop these in visual studio code by using running them locally with terraform command lines such as terraform init, terraform plan and terraform apply. Or you can develop and test these through building your pipeline by calling those same terraform commands via pipeline tasks.

Here are the terraform file contents:


terraform {
  required_version = ">=1.0" # https://github.com/hashicorp/terraform/releases

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>3.0" # https://registry.terraform.io/providers/hashicorp/azurerm/latest
    random = {
      source  = "hashicorp/random"
      version = "~>3.0"

  # If a configuration includes no backend block, Terraform defaults to using the local backend, which stores state as a plain file in the current working directory.
  backend "azurerm" {


provider "azurerm" {
  features {}


variable "environment" {
  type = string

variable "resource_group_name" {
  type = string

variable "location" {
  type = string
  default = "canada central"

variable "functionapp_storage_account_name" {
  type = string

variable "azurerm_windows_function_app_name" {
  type = string


data "azurerm_resource_group" "rg" {
  name = var.resource_group_name

resource "azurerm_storage_account" "example" {
  name                     = var.functionapp_storage_account_name
  resource_group_name      = data.azurerm_resource_group.rg.name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"

resource "azurerm_service_plan" "example" {
  name                = "rk-app-service-plan01"
  resource_group_name = data.azurerm_resource_group.rg.name
  location            = var.location
  os_type             = "Windows"
  sku_name            = "Y1"

resource "azurerm_windows_function_app" "example" {
  name                = var.azurerm_windows_function_app_name
  resource_group_name = data.azurerm_resource_group.rg.name
  location            = var.location

  storage_account_name       = azurerm_storage_account.example.name
  storage_account_access_key = azurerm_storage_account.example.primary_access_key
  service_plan_id            = azurerm_service_plan.example.id

  site_config {}

6. Before developing the pipline yaml file, since I am running terraform tasks, I need to install the Visual Studio Extension from the marketplace found at https://marketplace.visualstudio.com/items?itemName=ms-devlabs.custom-terraform-tasks

7. The YAML pipeline was developed as follows and see the inline comments for educational commentary.

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

- main

  vmImage: ubuntu-latest

# Define variables
  - name: environment
    value: dev
  - name: location
    value: canadacentral
  - name: subscriptionId
    value: d<redacted>
  - name: serviceConnectionName
    value: 'Enterprise Az Subscription'
  - name: resource_group_name
    value: appservice
  - name: storage_account_name
    value: rkazdostfstorage
  - name: functionapp_storage_account_name
    value: rkfunctionappstor01
  - name: azurerm_windows_function_app_name
    value: rkfunctionapp01

# Part of the Starter YAML pipeline template and is good example for debugging.
- script: echo Hello, world!
  displayName: 'Run a one-line script'

# Install Terraform
# Since using MS hosted agent, need to install the terraform and indicate the version.
- task: TerraformInstaller@0
  displayName: install terraform
    terraformVersion: latest

# Configure Azure Provider
# This is optional but just for debugging purposes and test the AzureCLI task
- task: AzureCLI@2
  condition: false
    azureSubscription: $(serviceConnectionName)
    scriptType: 'pscore'
    scriptLocation: 'inlineScript'
    inlineScript: |
      az account show

# Need to create a resource group for the solution to be deployed. At the minimum need a resource group that is containing the essential storage account for the terraform statefile.
- task: AzureCLI@2
  displayName: 'Create resource group $(resource_group_name)'
  condition: false
    azureSubscription: $(serviceConnectionName)
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: |
      az group create -n $(resource_group_name) -l $(location)

# A storage account is needed to store the terraform statefile. A Terraform state file is a # file that Terraform uses to store the state of the resources that it manages. The state 
# file keeps track of the current state of the infrastructure, and Terraform uses this 
# information to determine the changes that need to be made to the infrastructure to 
# bring it to the desired state.
# By placing it in a storage account, promotes collaboration so that other devops engineers can update and run the terraform code.
- task: AzureCLI@2
  displayName: 'Create storage account $(storage_account_name) for terraform state files'
  condition: false
    azureSubscription: $(serviceConnectionName)
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: |
      az storage account create -n $(storage_account_name) -g $(resource_group_name) -l $(location) --sku Standard_LRS

# Initialize Terraform 
# This initializes ensuring that Terraform has the necessary information to manage the # infrastructure, including the plugins for the providers that you are using, the state of 
# your resources, and the backend to store the state file.
- task: TerraformTaskV2@2
  displayName: 'Terraform init'
    command: 'init'
    provider: 'azurerm'
    workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
    backendServiceArm: '$(serviceConnectionName)'
    backendAzureRmResourceGroupName: '$(resource_group_name)'
    backendAzureRmResourceGroupLocation: '$(location)'
    backendAzureRmStorageAccountName: '$(storage_account_name)'
    backendAzureRmContainerName: 'terraform-state'
    backendAzureRmKey: 'terraform.tfstate'
    commandOptions: '-lock=false'
# The terraform plan command takes your Terraform configuration as input and 
# compares the desired state specified in the configuration to the current state stored in # the Terraform state file. It then generates an execution plan, which is a summary of 
# the changes that Terraform will make to your infrastructure in order to bring it in line # with the desired state specified in the configuration.
- task: TerraformTaskV3@0
  displayName: 'Terraform plan'
    command: 'plan'
    workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
    environmentServiceNameAzureRM: '$(serviceConnectionName)' # Service connection or to override the subscription id defined in a Subscription scoped service connection
    commandOptions: '-var "environment=$(environment)" -var "resource_group_name=$(resource_group_name)" -var "location=$(location)" -var "functionapp_storage_account_name=$(functionapp_storage_account_name)"  -var "azurerm_windows_function_app_name=$(azurerm_windows_function_app_name)" -input=false'
    # The -input=false option indicates that Terraform should not attempt to prompt for input, and instead expect all necessary values to be provided by either configuration files or the command line

# Provisions the specified resources in your infrastructure, updates existing resources 
# as necessary, and deletes any resources that are no longer specified in your Terraform # configuration.
- task: TerraformTaskV3@0
  displayName: 'Terraform apply'
    command: 'apply'
    workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
    environmentServiceNameAzureRM: '$(serviceConnectionName)' # Service connection or to override the subscription id defined in a Subscription scoped service connection
    commandOptions: '-var "environment=$(environment)" -var "resource_group_name=$(resource_group_name)" -var "location=$(location)" -var "functionapp_storage_account_name=$(functionapp_storage_account_name)" -var "azurerm_windows_function_app_name=$(azurerm_windows_function_app_name)" -input=false'

# Delete the azure resources under terraform management. This has a conditional flag to manually enable when need.
- task: TerraformTaskV3@0
  displayName: 'Terraform destroy'
  condition: false # disable destroying
    command: 'destroy'
    workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
    environmentServiceNameAzureRM: '$(serviceConnectionName)' # Service connection or to override the subscription id defined in a Subscription scoped service connection
    commandOptions: '-var "environment=$(environment)" -var "location=$(location)" -var "functionapp_storage_account_name=$(functionapp_storage_account_name)  -var "azurerm_windows_function_app_name=$(azurerm_windows_function_app_name)" "'

- script: |
    echo Finished AZDO pipeline demo execution
    echo See https://aka.ms/yaml
  displayName: 'Run a multi-line script'

Upon running the pipeline we get:

This is the storage container and the terraform state file in the storage account container that stores the current state of an infrastructure managed by Terraform. It contains information about the resources that Terraform has created, such as the type of resource, its ID, and its current state.

Here’s a snippet of the file contents.

Here are the resources that have been created by the pipeline.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s