Managing infrastructure with Terraform is one of the most common practices in modern DevOps workflows. But when it comes to automation and governance, Azure DevOps pipelines make the process much smoother.
In this blog, Iβll walk you through a real-world Azure DevOps pipeline that deploys Terraform infrastructure to Dev and Prod environments with manual approvals in between.
π Prerequisites
Before setting up this pipeline, make sure you have the following in place:
- Active Azure Subscription You need a valid Azure subscription where your Terraform resources will be deployed.
- Azure DevOps Account An Azure DevOps project should be created to host your pipeline and repository.
- Self-Hosted Agent Pool A self-hosted agent is required to run Terraform tasks. This gives more control and avoids limitations of Microsoft-hosted agents.
- App Registration with Federated Credentials Create an App Registration in Azure AD and configure it with federated credentials. This will be used as the Service Connection (SPN) in Azure DevOps to authenticate Terraform with Azure.
-
Terraform Code
A working Terraform configuration (with
.tf
files and.tfvars
for Dev and Prod) must be present in your repository.
π Pipeline Overview
This pipeline is designed with two major stages:
-
Dev Stage β Runs Terraform
init
,plan
, requires manual approval, and then applies changes on the Dev environment. - Prod Stage β After Dev is successful, it requires another manual approval and then applies changes to the Prod environment.
By using manual validations, we ensure that no changes are applied accidentally in production without human approval.
π§ Key Features of the Pipeline
- Multi-stage deployment β Dev first, then Prod.
- Terraform AzureRM backend β Stores the Terraform state in an Azure Storage Account.
- Manual validations β Adds an approval step before applying changes.
- Reusable variables β Service connection, resource group, storage account, and container details are stored as variables to avoid repetition.
π Pipeline YAML
Hereβs the YAML code for the pipeline:
trigger:
- master
pool:
name: mademitech
variables:
serviceConnection: 'mademi-spn'
rgName: 'mademi-rg'
storageAccount: 'mademisg'
containerName: 'mademistatefiles'
stages:
- stage: Dev
jobs:
- job: Validate_Dev
displayName: Validate on Dev
steps:
- task: TerraformTask@5
displayName: Init Dev
inputs:
provider: 'azurerm'
command: 'init'
backendServiceArm: $(serviceConnection)
backendAzureRmResourceGroupName: $(rgName)
backendAzureRmStorageAccountName: $(storageAccount)
backendAzureRmContainerName: $(containerName)
backendAzureRmKey: 'dev.tfstate'
- task: TerraformTask@5
displayName: Plan Dev
inputs:
provider: 'azurerm'
command: 'plan'
commandOptions: '-var-file=dev.tfvars --lock=false'
environmentServiceNameAzureRM: $(serviceConnection)
- job: ManualValidation
displayName: Manual Approval for Dev Deployment
dependsOn: Validate_Dev
pool: server
steps:
- task: ManualValidation@1
inputs:
notifyUsers: 'deepak83s143@gmail.com'
onTimeout: 'resume'
- job: Deploy_Dev
displayName: Deploy on Dev
dependsOn: ManualValidation
steps:
- task: TerraformTask@5
displayName: Apply Dev
inputs:
provider: 'azurerm'
command: 'apply'
commandOptions: '-var-file=dev.tfvars --auto-approve --lock=false'
environmentServiceNameAzureRM: $(serviceConnection)
- stage: Prod
dependsOn: Dev
jobs:
- job: ManualValidation_Prod
displayName: Manual Approval for Prod
pool: server
steps:
- task: ManualValidation@1
inputs:
notifyUsers: 'deepak83s143@gmail.com'
onTimeout: 'resume'
- job: Deploy_Prod
displayName: Deploy on Prod
dependsOn: ManualValidation_Prod
steps:
- task: TerraformTask@5
displayName: Init Prod
inputs:
provider: 'azurerm'
command: 'init'
backendServiceArm: $(serviceConnection)
backendAzureRmResourceGroupName: $(rgName)
backendAzureRmStorageAccountName: $(storageAccount)
backendAzureRmContainerName: $(containerName)
backendAzureRmKey: 'prod.tfstate'
- task: TerraformTask@5
displayName: Apply Prod
inputs:
provider: 'azurerm'
command: 'apply'
commandOptions: '-var-file=prod.tfvars --auto-approve'
environmentServiceNameAzureRM: $(serviceConnection)
π Step-by-Step Explanation
1. Trigger & Pool
trigger:
- master
This pipeline runs automatically whenever code is pushed to the master branch.
The pool is set to mademitech, which means it will run on a self-hosted agent.
2. Variables
variables:
serviceConnection: 'mademi-spn'
rgName: 'mademi-rg'
storageAccount: 'mademisg'
containerName: 'mademistatefiles'
These variables store reusable values for the backend configuration.
If tomorrow you change your storage account or service connection, you only need to update it once here.
3. Dev Stage
-
Init
: Initializes Terraform with AzureRM backend and points to the Dev state file. -
Plan
: Runs terraform plan using dev.tfvars. -
Manual Validation
: Waits for approval before applying changes. -
Apply
: Applies the infrastructure changes on Dev.
4. Prod Stage
- Depends on Dev β Ensures that only after successful Dev deployment, Prod stage runs.
- Manual Validation: Extra safety check before deploying to Prod.
- Init + Apply: Runs Terraform init and applies changes using prod.tfvars.
β Why This Approach?
- Governance β Manual approvals ensure no accidental changes hit production.
- Reusability β Variables make the pipeline maintainable.
- Safety β Dev is always tested first before Prod.
- Automation + Control β Combines Terraform automation with human oversight.
π― Final Thoughts
This pipeline strikes a balance between automation and manual governance. Itβs a solid starting point for teams looking to deploy infrastructure with Terraform while keeping production safe.
From here, you can also enhance it by:
- Adding linting & validation steps before plan.
- Integrating policy checks with tools like Terraform Cloud or OPA.
- Using templates for even more reusability.
π Follow me on
Top comments (0)