- Azure subscription with appropriate permissions
- Azure DevOps project
- Azure Resource Manager service connection
- Backend storage account for Terraform state (optional)
- Pipeline variables configured
- Environment created for approvals (optional)
# Create a service principal
az ad sp create-for-rbac --name "terraform-sp" --role Contributor --scopes /subscriptions/{subscription-id}Save the output - you'll need:
appId(Client ID)password(Client Secret)tenant(Tenant ID)
- Navigate to: Project Settings → Service connections → New service connection
- Select "Azure Resource Manager"
- Choose "Service principal (manual)"
- Fill in the details from the service principal
- Name it (e.g., "azure-terraform-connection")
- Grant access permissions to all pipelines
#!/bin/bash
# Set variables
RESOURCE_GROUP_NAME="tfstate-rg"
STORAGE_ACCOUNT_NAME="tfstate$(date +%s)" # Must be globally unique
CONTAINER_NAME="tfstate"
LOCATION="eastus"
# Create resource group
az group create --name $RESOURCE_GROUP_NAME --location $LOCATION
# Create storage account
az storage account create \
--name $STORAGE_ACCOUNT_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--location $LOCATION \
--sku Standard_LRS \
--encryption-services blob \
--https-only true \
--min-tls-version TLS1_2
# Create blob container
az storage container create \
--name $CONTAINER_NAME \
--account-name $STORAGE_ACCOUNT_NAME
echo "Backend configuration:"
echo "resource_group_name: $RESOURCE_GROUP_NAME"
echo "storage_account_name: $STORAGE_ACCOUNT_NAME"
echo "container_name: $CONTAINER_NAME"- Go to Azure Portal → Storage Accounts → Create
- Fill in:
- Resource Group: tfstate-rg
- Storage Account Name: tfstatestorage (must be unique)
- Region: East US
- Performance: Standard
- Redundancy: LRS
- Review + Create
- After creation, go to Containers → Add Container
- Name: tfstate
Update variables section in the pipeline file:
variables:
- name: azureServiceConnection
value: 'azure-terraform-connection' # Your service connection name
- name: backendResourceGroupName
value: 'tfstate-rg'
- name: backendStorageAccountName
value: 'tfstatestorage' # Your storage account name
- name: backendContainerName
value: 'tfstate'
- name: backendKey
value: 'terraform.tfstate'- Go to: Pipelines → Library → Variable groups
- Create a new variable group named "terraform-config"
- Add variables:
azureServiceConnectionbackendResourceGroupNamebackendStorageAccountNamebackendContainerNamebackendKey
- In your pipeline, reference the variable group:
variables:
- group: terraform-config# Copy example file
cp terraform/terraform.tfvars.example terraform/terraform.tfvars
# Edit with your values
# DO NOT commit this fileAdd variables in Azure DevOps:
TF_VAR_resource_group_nameTF_VAR_locationTF_VAR_storage_account_name
These automatically become Terraform variables.
Create environment-specific variable files:
terraform/dev.tfvarsterraform/prod.tfvars
Update pipeline to pass the file:
- template: templates/terraform-plan.yml
parameters:
workingDirectory: $(workingDirectory)
commandOptions: '-var-file="dev.tfvars"'- Go to: Pipelines → Environments
- Click "New environment"
- Name: production
- Click "Create"
- Click on the environment
- Click "..." → Approvals and checks
- Add approvals:
- Add yourself or team members as approvers
- Configure timeout settings
- Optionally add other checks:
- Business hours
- Invoke REST API
- Azure function
# Get storage account id
STORAGE_ID=$(az storage account show \
--name $STORAGE_ACCOUNT_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--query id --output tsv)
# Grant service principal access
az role assignment create \
--assignee $SP_APP_ID \
--role "Storage Blob Data Contributor" \
--scope $STORAGE_IDService Principal needs:
- Contributor role on the subscription or resource groups
- Storage Blob Data Contributor role on the state storage account
Instead of Contributor at subscription level:
# Create resource group first
az group create --name myapp-rg --location eastus
# Grant permissions only to specific resource group
az role assignment create \
--assignee $SP_APP_ID \
--role "Contributor" \
--scope "/subscriptions/{subscription-id}/resourceGroups/myapp-rg"# In Azure DevOps pipeline, add a test stage
- stage: Test
jobs:
- job: TestConnection
steps:
- task: AzureCLI@2
inputs:
azureSubscription: '$(azureServiceConnection)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: 'az account show'# Test storage account access
az storage blob list \
--container-name tfstate \
--account-name tfstatestorage# Clone repo and test locally
cd terraform
terraform init \
-backend-config="resource_group_name=tfstate-rg" \
-backend-config="storage_account_name=tfstatestorage" \
-backend-config="container_name=tfstate" \
-backend-config="key=terraform.tfstate"
terraform plan# In your pipeline
- script: |
terraform workspace select $(environment) || terraform workspace new $(environment)
terraform planUpdate pipeline per environment:
# Dev pipeline
variables:
- name: backendKey
value: 'dev/terraform.tfstate'
# Prod pipeline
variables:
- name: backendKey
value: 'prod/terraform.tfstate'Cause: Backend not properly configured Solution: Ensure all backend variables are set correctly
Cause: Service principal lacks permissions Solution:
# Check current role assignments
az role assignment list --assignee $SP_APP_ID --output table
# Add missing permissions
az role assignment create --assignee $SP_APP_ID --role "Contributor" --scope "/subscriptions/{sub-id}"Cause: Storage account doesn't exist or wrong name Solution: Verify storage account exists:
az storage account show --name $STORAGE_ACCOUNT_NAME --resource-group $RESOURCE_GROUP_NAMEUpdate service connection to use managed identity if Azure DevOps agent is Azure-hosted.
Azure Storage automatically provides state locking. Verify it's enabled:
az storage account show \
--name $STORAGE_ACCOUNT_NAME \
--query "properties.supportsHttpsTrafficOnly"Enable encryption at rest (default for Azure Storage):
az storage account update \
--name $STORAGE_ACCOUNT_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--encryption-services blob- task: AzureKeyVault@2
inputs:
azureSubscription: '$(azureServiceConnection)'
KeyVaultName: 'mykeyvault'
SecretsFilter: '*'
RunAsPreJob: trueAfter configuration:
- Run the validate stage to test Terraform syntax
- Run the plan stage to preview changes
- Review the plan output carefully
- Approve and run the apply stage
- Verify resources in Azure Portal
- Azure DevOps Documentation: https://docs.microsoft.com/azure/devops/
- Terraform Azure Provider: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs
- Azure CLI Reference: https://docs.microsoft.com/cli/azure/