Terraform with Azure DevOps
Introductionā
Infrastructure as Code (IaC) has revolutionized how we manage cloud resources, and Terraform has emerged as one of the leading tools in this space. While Terraform allows us to define infrastructure in a declarative way, integrating it into a CI/CD pipeline brings additional benefits like automation, consistency, and collaboration.
Azure DevOps provides a robust platform for implementing CI/CD pipelines for Terraform, enabling teams to automate infrastructure deployments with proper testing and approval processes. This guide will walk you through setting up Terraform with Azure DevOps pipelines to create an efficient infrastructure deployment workflow.
Prerequisitesā
Before we begin, make sure you have:
- An Azure subscription
- An Azure DevOps organization and project
- Basic familiarity with Terraform concepts
- Basic understanding of YAML and Azure DevOps pipelines
Setting Up Your Repositoryā
First, we need to set up a repository structure that works well for Terraform projects in Azure DevOps:
š terraform-project/
āāā š environments/
ā āāā š dev/
ā ā āāā main.tf
ā ā āāā variables.tf
ā ā āāā terraform.tfvars
ā āāā š staging/
ā ā āāā main.tf
ā ā āāā variables.tf
ā ā āāā terraform.tfvars
ā āāā š prod/
ā āāā main.tf
ā āāā variables.tf
ā āāā terraform.tfvars
āāā š modules/
ā āāā š networking/
ā āāā š compute/
ā āāā š database/
āāā azure-pipelines.yml
āāā README.md
This structure separates configuration by environment while reusing common modules, which is a best practice for Terraform projects.
Storing Terraform State in Azureā
When working with Terraform in a team environment, storing state remotely is crucial. Azure Storage provides a perfect backend for Terraform state:
# In each environment's main.tf file
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "terraformstate12345"
container_name = "tfstate"
key = "dev.terraform.tfstate"
}
}
provider "azurerm" {
features {}
}
Creating Service Connectionsā
Before setting up our pipeline, we need to create a service connection in Azure DevOps to allow our pipeline to interact with Azure:
- In your Azure DevOps project, go to Project Settings > Service connections
- Click New service connection > Azure Resource Manager
- Select Service principal (automatic)
- Fill in the required details and give it a name like
terraform-service-connection
Creating Azure DevOps Pipelineā
Now, let's create our pipeline. Create an azure-pipelines.yml
file in your repository:
trigger:
branches:
include:
- main
paths:
include:
- environments/dev/**
pool:
vmImage: 'ubuntu-latest'
variables:
environment: 'dev'
workingDirectory: '$(System.DefaultWorkingDirectory)/environments/$(environment)'
stages:
- stage: Validate
jobs:
- job: ValidateTerraform
steps:
- task: TerraformInstaller@0
displayName: 'Install Terraform'
inputs:
terraformVersion: '1.3.7'
- task: TerraformTaskV4@4
displayName: 'Terraform Init'
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: '$(workingDirectory)'
backendServiceArm: 'terraform-service-connection'
backendAzureRmResourceGroupName: 'terraform-state-rg'
backendAzureRmStorageAccountName: 'terraformstate12345'
backendAzureRmContainerName: 'tfstate'
backendAzureRmKey: 'dev.terraform.tfstate'
- task: TerraformTaskV4@4
displayName: 'Terraform Validate'
inputs:
provider: 'azurerm'
command: 'validate'
workingDirectory: '$(workingDirectory)'
- task: TerraformTaskV4@4
displayName: 'Terraform Plan'
inputs:
provider: 'azurerm'
command: 'plan'
workingDirectory: '$(workingDirectory)'
environmentServiceNameAzureRM: 'terraform-service-connection'
- stage: Apply
dependsOn: Validate
condition: succeeded()
jobs:
- deployment: DeployTerraform
environment: 'dev'
strategy:
runOnce:
deploy:
steps:
- task: TerraformInstaller@0
displayName: 'Install Terraform'
inputs:
terraformVersion: '1.3.7'
- task: TerraformTaskV4@4
displayName: 'Terraform Init'
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: '$(workingDirectory)'
backendServiceArm: 'terraform-service-connection'
backendAzureRmResourceGroupName: 'terraform-state-rg'
backendAzureRmStorageAccountName: 'terraformstate12345'
backendAzureRmContainerName: 'tfstate'
backendAzureRmKey: 'dev.terraform.tfstate'
- task: TerraformTaskV4@4
displayName: 'Terraform Apply'
inputs:
provider: 'azurerm'
command: 'apply'
workingDirectory: '$(workingDirectory)'
environmentServiceNameAzureRM: 'terraform-service-connection'
Pipeline Workflowā
Let's understand what our pipeline does:
The pipeline consists of two main stages:
- Validate Stage: Initializes Terraform, validates the syntax, and creates an execution plan
- Apply Stage: Applies the changes to create/update the infrastructure
Understanding Key Pipeline Componentsā
Terraform Tasksā
Azure DevOps provides built-in tasks for working with Terraform:
- TerraformInstaller: Installs the specified version of Terraform
- TerraformTaskV4: Executes Terraform commands (init, validate, plan, apply)
Environment Approvalsā
To add a manual approval step before deploying to production, you can configure approvals:
- Go to Pipelines > Environments
- Select or create your environment (e.g.,
prod
) - Click on the three dots (...) and select Approvals and checks
- Add approvers who need to authorize deployments
Parameterizing the Pipelineā
To make our pipeline more flexible, we can use parameters to specify the environment:
parameters:
- name: environment
displayName: Environment
type: string
default: dev
values:
- dev
- staging
- prod
variables:
workingDirectory: '$(System.DefaultWorkingDirectory)/environments/${{ parameters.environment }}'
Creating a Multi-Environment Pipelineā
For a complete CI/CD solution, we can extend our pipeline to handle multiple environments:
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: DeployDev
displayName: 'Deploy to Development'
jobs:
- template: pipeline-templates/terraform-deploy.yml
parameters:
environment: 'dev'
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: DeployDev
jobs:
- template: pipeline-templates/terraform-deploy.yml
parameters:
environment: 'staging'
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: DeployStaging
jobs:
- template: pipeline-templates/terraform-deploy.yml
parameters:
environment: 'prod'
Best Practices for Terraform with Azure DevOpsā
-
Use Remote State: Always store Terraform state in Azure Storage.
-
State Locking: Ensure state locking is enabled to prevent concurrent modifications.
-
Separate Environments: Maintain separate state files for each environment.
-
Module Reusability: Create reusable modules for common infrastructure components.
-
Variable Management: Use variable files and Azure DevOps variable groups for configuration.
-
Plan Output as Artifact: Save the Terraform plan output as a pipeline artifact for review.
-
Include Automated Testing: Add infrastructure testing using tools like Terratest.
-
Use Workspaces: Leverage Terraform workspaces for managing multiple environments with the same configuration.
Practical Example: Deploying a Web Appā
Let's create a practical example that deploys an Azure Web App using Terraform and Azure DevOps:
First, our Terraform configuration in environments/dev/main.tf
:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "terraformstate12345"
container_name = "tfstate"
key = "dev.terraform.tfstate"
}
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "East US"
}
resource "azurerm_app_service_plan" "example" {
name = "example-appserviceplan"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
kind = "Linux"
reserved = true
sku {
tier = "Basic"
size = "B1"
}
}
resource "azurerm_app_service" "example" {
name = "example-webapp-12345"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
app_service_plan_id = azurerm_app_service_plan.example.id
site_config {
linux_fx_version = "NODE|14-lts"
}
}
output "webapp_url" {
value = "https://${azurerm_app_service.example.default_site_hostname}"
}
For variables, in environments/dev/variables.tf
:
variable "location" {
description = "The Azure region where resources will be created"
type = string
default = "East US"
}
variable "app_name" {
description = "Name of the web application"
type = string
default = "example-webapp"
}
Advanced: Using Variable Groupsā
To better manage environment-specific variables, we can use Azure DevOps variable groups:
- Go to Pipelines > Library > Variable groups
- Create a new variable group named
terraform-dev-variables
- Add variables like
resource_group_name
,location
, etc.
Then, reference them in your pipeline:
variables:
- group: terraform-dev-variables
- name: workingDirectory
value: '$(System.DefaultWorkingDirectory)/environments/$(environment)'
steps:
- task: TerraformTaskV4@4
displayName: 'Terraform Apply'
inputs:
provider: 'azurerm'
command: 'apply'
workingDirectory: '$(workingDirectory)'
environmentServiceNameAzureRM: 'terraform-service-connection'
commandOptions: '-var="resource_group_name=$(resource_group_name)" -var="location=$(location)"'
Troubleshooting Common Issuesā
Authentication Failuresā
If your pipeline fails with authentication errors:
- Check that your service connection has the right permissions
- Verify that the service principal hasn't expired
- Ensure the service principal has required roles (Contributor) on your subscription
State Locking Issuesā
If you encounter state locking problems:
- Check if a previous pipeline run is still holding the lock
- Verify that the storage account allows the service principal to create blobs and leases
- As a last resort, use the Azure portal to delete the lease on the state blob
Failed Deploymentsā
For failed Terraform deployments:
- Review the detailed logs from the Terraform apply step
- Check Azure Activity Log for specific resource creation failures
- Try running the same commands locally to see if you can reproduce the issue
Summaryā
Integrating Terraform with Azure DevOps provides a powerful combination for implementing Infrastructure as Code with proper CI/CD practices. In this guide, we covered:
- Setting up a Terraform repository structure
- Configuring Azure Storage for remote state management
- Creating an Azure DevOps pipeline for Terraform deployment
- Implementing a multi-environment deployment strategy
- Best practices for managing Terraform in a team environment
By following these practices, you can create reliable, repeatable infrastructure deployments that scale with your organization's needs.
Additional Resourcesā
- Terraform Documentation
- Azure DevOps Documentation
- Terraform Provider for Azure
- HashiCorp Learn - Terraform on Azure
Exercisesā
- Create a Terraform configuration that deploys a virtual network with subnets and integrate it into an Azure DevOps pipeline.
- Modify the pipeline to support deployment to multiple environments (dev, staging, prod) with different configurations.
- Add a manual approval step before deploying to the production environment.
- Configure a policy that requires all Terraform changes to be reviewed before merging to the main branch.
- Implement a backend service for remote state with proper state locking and versioning.
If you spot any mistakes on this website, please let me know at [email protected]. Iād greatly appreciate your feedback! :)