Using Terraform in Azure DevOps Pipelines
This guide explains the basic steps to getting started with Terraform and Azure DevOps Pipelines
1. Introduction: IaC and the Power of Pairing IaC with Azure DevOps
Maintaining infrastructure consistency and minimizing configuration drift is paramount. Trying to manage cloud resources manually is a recipe for chaos, leading to slow deployments and unreliable environments.
This is where Infrastructure as Code (IaC) becomes non-negotiable. IaC allows you to manage and provision your cloud infrastructure using code and software engineering practices.
Why Terraform/OpenTofu? Both Terraform and its open-source fork, OpenTofu, are leading declarative IaC tools. They allow you to define the desired state of your Azure infrastructure—from Resource Groups and Virtual Networks to App Services—in HashiCorp Configuration Language (HCL). This code can be version-controlled, reviewed, and tested.
Why Azure DevOps Pipelines? Azure DevOps provides a robust, native CI/CD platform to act as the orchestration engine. By integrating your IaC code into YAML pipelines, you can automate the entire lifecycle: validating changes, creating execution plans, and applying those changes across your environments in a safe, repeatable manner. The result is infrastructure deployments that are auditable and traceable.
2. Prerequisites: Getting Your Environment Ready
Before diving into the pipeline code, several foundational elements must be in place.
Infrastructure Code
Your Terraform or OpenTofu files (.tf) should be committed to an Azure Repos Git repository. Ensure your code is configured to use the Azure Provider (azurerm) or any other provider you might be using.
Remote State Management (Crucial for CI/CD)
The state file tracks the real-world resources managed by your configuration. In a CI/CD environment, this file must be stored securely and centrally to enable collaboration and state locking. You have two main approaches:
- Option 1: Azure Blob Storage (Self-Managed)
- Requirement: You must use Azure Blob Storage for your remote backend.
- Setup: Manually create a dedicated Azure Storage Account and a container to host your state files before your pipeline runs. Add the AzureRM backend block to your code.
- Option 2: Scalr (Centralized Governance)
- Scalr provides a centralized platform for managing Terraform/OpenTofu state, runs, policy enforcement, and drift detection. This is a powerful alternative for organizations requiring advanced governance features. It's not just a backend for state storage; it orchestrated the entire Terraform/OpenTofu ecosystem.
- When using Scalr, you authenticate the agent by providing a dedicated API token. This is typically achieved using the
terraform loginortofu logincommand in an initial script step before theinitcommand runs. The token must be stored securely as an Azure DevOps secret variable.
Scalr Configuration Example:
terraform {
backend "remote" {
hostname = "your-account.scalr.io"
organization = "your-organization-name"
workspaces {
name = "project-name-environment-name"
}
}
}
Permissions and Authentication
Your pipeline needs permission to create and manage resources in your Azure subscription.
- Service Connection: Create an Azure Resource Manager Service Connection in Azure DevOps. This Service Principal (SPN) is the identity that the IaC tool will use to authenticate with Azure.
- Security Principle: Strictly adhere to the principle of least privilege. Grant the SPN only the permissions it needs (e.g., Contributor rights to a specific subscription or Resource Group).
The Pipeline Agent
For deployment, we will utilize Microsoft-hosted agents (e.g., vmImage: 'ubuntu-latest' or 'windows-latest'). These agents come pre-installed with many necessary tools, though we will ensure the IaC tool is available if needed.
3. Designing the Pipeline: A Multi-Stage CI/CD Workflow
The safest way to run IaC in a pipeline is to separate the process into two stages: Plan and Apply. This split guarantees that a human operator can review the proposed changes before they are executed.
We recommend using native script: blocks with standard CLI commands (e.g., terraform init) instead of relying exclusively on marketplace tasks. This gives you maximum control and ensures compatibility across both Terraform and OpenTofu.
4. Full YAML Pipeline Example
To visualize the multi-stage workflow, review the full azure-pipelines.yml provided below. It showcases the necessary steps for initialization, planning, artifact publishing, and application using variables for secure backend configuration.
# This YAML defines a multi-stage pipeline for deploying infrastructure using Terraform or OpenTofu.
# It enforces a best practice: separate stages for Plan and Apply,
# with an optional manual approval gate between them.
trigger:
- main
# Define variables needed for configuration and authentication
variables:
# The name of the folder containing your .tf files in the repository root
- name: terraformDirectory
value: '$(System.DefaultWorkingDirectory)/infra'
# The version of Terraform/OpenTofu to use (e.g., 'latest' or '1.7.0')
- name: iacVersion
value: 'latest'
# Link to a variable group for secrets like storage account name, container name, etc.
# IMPORTANT: Secrets for Azure authentication (ARM_CLIENT_ID, etc.) are usually
# provided securely via the Service Connection (see Stage 1/Job 1).
# Replace 'tf-backend-secrets' with your actual variable group name.
- group: tf-backend-secrets
stages:
# ------------------------------
# Stage 1: Terraform Plan (CI)
# ------------------------------
- stage: Plan
displayName: '1. Plan Infrastructure Changes'
jobs:
- job: Plan
displayName: 'Generate Execution Plan'
pool:
vmImage: 'ubuntu-latest' # Use a Microsoft-hosted agent
steps:
# This task automatically downloads and installs the specified version of Terraform/Tofu.
- task: TerraformInstaller@1
displayName: 'Install Terraform/Tofu'
inputs:
terraformVersion: $(iacVersion)
# 2. Terraform/OpenTofu Init
# Initializes the backend configuration (Azure Storage Account)
- script: |
# Use 'terraform' or 'tofu' as needed
terraform init \
-backend-config="resource_group_name=$(RG_NAME)" \
-backend-config="storage_account_name=$(SA_NAME)" \
-backend-config="container_name=$(CONTAINER_NAME)" \
-backend-config="key=production.tfstate"
displayName: 'Terraform/Tofu Init'
workingDirectory: $(terraformDirectory)
# 3. Terraform/OpenTofu Plan & Save
# Creates the execution plan file (tfplan)
- script: |
# Use 'terraform' or 'tofu' as needed
terraform plan \
-out=tfplan
displayName: 'Terraform/Tofu Plan'
workingDirectory: $(terraformDirectory)
# 4. Publish the Plan as a Pipeline Artifact
# The plan artifact is required for the Apply stage.
- task: PublishPipelineArtifact@1
displayName: 'Publish Plan Artifact'
inputs:
targetPath: '$(terraformDirectory)/tfplan'
artifact: 'tfplan-artifact'
# ------------------------------
# Stage 2: Terraform Apply
# ------------------------------
- stage: Apply
displayName: '2. Apply Infrastructure Changes'
# This targets a protected environment, triggering a Manual Approval Check if configured
environment: 'production-infra'
dependsOn: Plan
jobs:
- job: Apply
displayName: 'Execute Apply'
pool:
vmImage: 'ubuntu-latest' # Use the same agent pool as the Plan stage
steps:
# 1. Install Terraform/OpenTofu (again, required for the new job)
- task: TerraformInstaller@1
displayName: 'Install Terraform/OpenTofu'
inputs:
terraformVersion: $(iacVersion)
# 2. Download the Plan Artifact
- task: DownloadPipelineArtifact@2
displayName: 'Download Plan Artifact'
inputs:
artifactName: 'tfplan-artifact'
# The artifact is downloaded to $(Pipeline.Workspace)/tfplan-artifact
targetPath: '$(System.DefaultWorkingDirectory)/tfplan-artifact'
# 3. Terraform/OpenTofu Init
# Re-initialize the backend to ensure connectivity for the apply command
- script: |
# Use 'terraform' or 'tofu' as needed
terraform init \
-backend-config="resource_group_name=$(RG_NAME)" \
-backend-config="storage_account_name=$(SA_NAME)" \
-backend-config="container_name=$(CONTAINER_NAME)" \
-backend-config="key=production.tfstate"
displayName: 'Terraform/Tofu Init for Apply'
workingDirectory: $(terraformDirectory)
# 4. Terraform/OpenTofu Apply
# Apply the downloaded plan file.
- script: |
# Use 'terraform' or 'tofu' as needed
terraform apply -auto-approve \
$(Pipeline.Workspace)/tfplan-artifact/infra/tfplan
displayName: 'Terraform/Tofu Apply'
workingDirectory: $(terraformDirectory)5.Validate and Plan
This stage’s primary goal is to validate the code, generate the execution plan, and secure that plan as an artifact for the next stage.
Step | Command/Action | Purpose |
|---|---|---|
1. Initialize |
| Initializes the working directory and configures the Azure Backend, connecting to the remote state file. |
2. Validate Code |
| Performs a quick syntax and configuration check. |
3. Static Analysis (Best Practice) |
| Runs security and best-practice checks against the code. |
4. Generate Plan |
| Creates the execution plan file ( |
5. Output Management |
| Publishes the generated |
6. Manual Approval Gate
After the plan is generated, your pipeline should pause. To prevent accidental resource destruction or unintended changes when deploying infrastructure with Terraform, a manual approval check should be implemented. This process involves a designated DevOps Engineer reviewing the tfplan output, typically by executing terraform show tfplan in a pre-approval job or via a pull request comment integration, before the infrastructure is applied. This critical control is configured in Azure DevOps by defining a protected environment and adding the manual approval check to it, ensuring that the Terraform Apply stage is explicitly configured to target this protected environment.
7. Continuous Deployment - Apply
Once the plan is approved, this stage executes the change.
Step 1 Download Artifacts: Use the DownloadPipelineArtifact@2 task to retrieve the saved tfplan file from Stage 1. This helps to ensure idempotence and immutability between stages.
Step 2: Apply Plan: The agent executes the apply command using the previously generated plan file.
Step 3 Output Management: If your IaC creates resources whose outputs are needed by downstream application deployments (e.g., a database connection string), you can extract them:
8. Best Practices for Production Readiness
To truly productionize your IaC workflow, consider these best practices:
- Secret Management: NEVER hardcode secrets in your repo or YAML. Use Azure Key Vault integration with Azure DevOps Variable Groups to securely fetch and inject sensitive data (like authentication tokens or environment-specific values) at runtime.
- Workspace Strategy: For true isolation between development and production, use separate configuration directories or utilize Terraform/OpenTofu Workspaces (
terraform workspace new <environment>) combined with dynamic pipeline variables. This ensures that a bug in yourdevcode cannot accidentally affect yourprodstate. - GitFlow Integration: Configure a pipeline trigger on pull requests. Whenever a developer opens a PR with infrastructure changes, automatically run Stage 1 (
validateandplan). Displaying theplanoutput directly in the PR helps reviewers understand the impact before any code is merged to the main branch.
9. Conclusion and Next Steps
Integrating Terraform or OpenTofu with Azure DevOps Pipelines transforms infrastructure management from a manual chore into a reliable, automated engineering discipline. By following the multi-stage plan/apply workflow, you ensure every infrastructure change is reviewed, tested, and deployed with confidence.