Deploying Terraform via Azure Pipelines
Overview
Automating Terraform through Azure DevOps pipelines ensures consistent, repeatable infrastructure deployments with full audit trails and approval controls.
Prerequisites
- Azure DevOps project with a Git repository
- Service Connection to Azure (Workload Identity Federation recommended)
- Storage Account for Terraform remote state
- Terraform extension installed from the Azure DevOps Marketplace
Remote State Configuration
Store your Terraform state in Azure Blob Storage:
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "stterraformstate"
container_name = "tfstate"
key = "infrastructure.tfstate"
}
}
Create the storage account beforehand (you cannot use Terraform to create its own state storage).
Pipeline Definition
trigger:
branches:
include:
- main
paths:
include:
- infra/*
variables:
- group: terraform-vars
- name: workingDirectory
value: '$(System.DefaultWorkingDirectory)/infra'
stages:
- stage: Plan
jobs:
- job: TerraformPlan
pool:
vmImage: 'ubuntu-latest'
steps:
- task: TerraformInstaller@1
inputs:
terraformVersion: 'latest'
- task: TerraformTaskV4@4
displayName: 'Terraform Init'
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: '$(workingDirectory)'
backendServiceArm: 'terraform-service-connection'
backendAzureRmResourceGroupName: 'rg-terraform-state'
backendAzureRmStorageAccountName: 'stterraformstate'
backendAzureRmContainerName: 'tfstate'
backendAzureRmKey: 'infrastructure.tfstate'
- task: TerraformTaskV4@4
displayName: 'Terraform Plan'
inputs:
provider: 'azurerm'
command: 'plan'
workingDirectory: '$(workingDirectory)'
environmentServiceNameAzureRM: 'terraform-service-connection'
commandOptions: '-out=tfplan'
- publish: '$(workingDirectory)/tfplan'
artifact: 'tfplan'
- stage: Apply
dependsOn: Plan
condition: succeeded()
jobs:
- deployment: TerraformApply
environment: 'infrastructure-production'
strategy:
runOnce:
deploy:
steps:
- checkout: self
- download: current
artifact: 'tfplan'
- task: TerraformInstaller@1
inputs:
terraformVersion: 'latest'
- task: TerraformTaskV4@4
displayName: 'Terraform Init'
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: '$(workingDirectory)'
backendServiceArm: 'terraform-service-connection'
backendAzureRmResourceGroupName: 'rg-terraform-state'
backendAzureRmStorageAccountName: 'stterraformstate'
backendAzureRmContainerName: 'tfstate'
backendAzureRmKey: 'infrastructure.tfstate'
- task: TerraformTaskV4@4
displayName: 'Terraform Apply'
inputs:
provider: 'azurerm'
command: 'apply'
workingDirectory: '$(workingDirectory)'
environmentServiceNameAzureRM: 'terraform-service-connection'
commandOptions: '$(Pipeline.Workspace)/tfplan/tfplan'
Service Connection Setup
For the most secure approach, use Workload Identity Federation:
- Go to Project Settings → Service Connections
- Choose Azure Resource Manager → Workload Identity Federation (automatic)
- Select scope (subscription or resource group)
- Name it consistently (e.g.,
terraform-service-connection)
This eliminates the need for client secrets.
State Locking
Azure Blob Storage provides automatic state locking via blob leases. This prevents concurrent modifications that could corrupt your state file.
Best Practices
- Always run plan before apply — never skip the review step
- Use approval gates on the Apply stage
- Pin Terraform versions to avoid unexpected changes
- Use variable groups for environment-specific values
- Store sensitive values in Key Vault referenced via variable groups
- Use
-detailed-exitcodein plan to detect actual changes - Tag all resources with environment, owner, and cost centre