Terraform + Azure DevOps CI/CD: Part 3 - Release Pipeline & Approval Gates
2025-11-05 ยท ~13 min read
Build the release pipeline that deploys approved Terraform plans with pre-deployment approval gates and audit trails. This is where governance happens.
Parts 1 and 2 built the pipelines that validate code and create artifacts. Now we build the pipeline that actually deploys infrastructure - with approval gates that prevent unauthorized changes.
What we're building:
- Release pipeline that triggers on new plan artifacts
- Pre-deployment approval gate (human review required)
- Terraform apply using the approved plan file
- Post-deployment notifications
Why Release Pipelines (Not Build Pipelines)
Build pipelines create artifacts. Release pipelines deploy them.
Key difference: Release pipelines support approval gates. You can't click "Deploy" until someone approves it.
The workflow:
1. Code merges to main โ Build pipeline creates .tfplan artifact
2. Artifact published โ Release pipeline queued (not started)
3. Approval gate โ Someone reviews and approves
4. Terraform apply runs โ Infrastructure deployed
This ensures: What gets approved in the PR is what gets deployed. No surprises.
Create the Release Pipeline
Release pipelines use a different interface than build pipelines. They're older but more powerful for deployment workflows.
Step 1: Navigate to Releases
- Go to Pipelines > Releases
- Click New pipeline
- Select template: Empty job (ignore other templates)
Step 2: Configure the Artifact Source
Click Add an artifact (big red box).
Settings:
- Source type: Build
- Project: Your Azure DevOps project
- Source (build pipeline): Terraform Plan (Create Artifact)
- Default version: Latest
- Source alias: _Terraform Plan (Create Artifact) (auto-generated)
Click Add.
What this does: Links this release pipeline to the build pipeline. Every time a new artifact is published, this release pipeline can trigger.
Step 3: Enable Continuous Deployment Trigger
Click the lightning bolt icon on the artifact box.
Toggle on: Continuous deployment trigger
Branch filters:
- Click + Add
- Type: Include
- Branch: main
Click Save (but don't close the trigger pane yet).
What this does: Automatically creates a new release whenever the build pipeline publishes an artifact from main branch.
Why filter by branch? If you later create feature branches with build artifacts (for testing), you don't want them auto-deploying.
Step 4: Rename the Stage
The default stage name is "Stage 1". Let's fix that.
Click Stage 1 and rename to: Deploy Infrastructure
Stages in release pipelines are deployment targets. You might have Dev, Test, Prod. We're starting with one stage.
Step 5: Configure Pre-Deployment Approval
This is the critical governance control. Nobody can deploy infrastructure without approval.
Click the lightning bolt with person icon on the left side of Deploy Infrastructure stage.
Pre-deployment approvals:
- Toggle on: Pre-deployment approvals
- Approvers: Add yourself and any other authorized deployers
- Timeout: 7 days (max time to wait for approval before auto-rejecting)
- Approval policies:
- Check: Approvers receive an email notification
- Uncheck: The user requesting a release should not approve it (optional, but good for separation of duties)
Click Save.
What this does:
- When a release is created, it immediately queues
- An email is sent to approvers: "Release X is waiting for approval"
- Release STOPS and waits
- Only after approval does it proceed to deployment tasks
Step 6: Configure Agent Job
Click 1 job, 0 task inside the Deploy Infrastructure stage.
Agent job settings:
- Display name: Terraform Apply
- Agent pool: Azure Pipelines
- Agent Specification: ubuntu-latest
Click Save.
Add Release Pipeline Tasks
Now we add the actual deployment tasks. These are similar to the build pipeline, but with a critical difference: we use the artifact (not the current Git repo).
Task 1: Download Pipeline Artifact
We need to download the .tfplan artifact from the build pipeline.
Click the + button on Terraform Apply job.
Search for "Download Pipeline Artifact" and add it.
Settings:
- Display name: Download Terraform Artifact
- Download artifacts produced by: Current build
- Matching patterns: **/* (all files)
- Destination directory: $(System.DefaultWorkingDirectory)
What this does: Downloads the entire artifact (the .tar.gz file containing Terraform code and the plan file).
Task 2: Extract Archive
The artifact is compressed. We need to extract it.
Add a Command Line task.
Settings:
- Display name: Extract Terraform Artifact
- Script:
cd $(System.DefaultWorkingDirectory)
tar -xzf *-tfplan.tar.gz -C $(System.DefaultWorkingDirectory)/terraform
What this does:
- Finds the .tar.gz file (named with build ID)
- Extracts it to /terraform folder
- Now we have all Terraform files + the tfplan file
Why wildcard *-tfplan.tar.gz? The build ID changes. This pattern matches any build.
Task 3: Install Terraform
Same as in build pipelines - ensures correct version.
Add the Terraform installer task.
Settings:
- Display name: Install Terraform 1.5.7
- Version: 1.5.7
Task 4: Download Key Vault Secrets
We need storage account keys and service principal credentials.
Add the Azure Key Vault task.
Settings:
- Display name: Download secrets from Key Vault
- Azure subscription: terraform-azure-connection
- Key vault: kv-tfstate-1234 (your Key Vault name)
- Secrets filter: *
Task 5: Terraform Init
We need to initialize Terraform in the release environment (even though we did it in the build pipeline).
Add a Command Line task.
Settings:
- Display name: Terraform Init
- Script:
terraform init \
-backend-config="access_key=$(sttfstate1234-key1)"
Advanced > Working Directory: $(System.DefaultWorkingDirectory)/terraform
Why init again? The release agent is a clean environment. It doesn't have the .terraform directory from the build pipeline.
Task 6: Terraform Apply
This is the deployment task. It applies the approved plan.
Add a Command Line task.
Settings:
- Display name: Terraform Apply (Approved Plan)
- Script:
terraform apply \
-auto-approve \
-input=false \
tfplan
Advanced > Working Directory: $(System.DefaultWorkingDirectory)/terraform
Critical flags:
- -auto-approve - Don't prompt for confirmation (we already approved the release)
- -input=false - No interactive prompts
- tfplan - Apply the EXACT plan file from the artifact (not a new plan)
This is why the workflow is secure:
1. PR reviewer sees the plan output
2. PR gets approved and merged
3. Build pipeline creates artifact with that EXACT plan
4. Release approver reviews again
5. Terraform applies that EXACT plan
No drift. What was reviewed is what deploys.
Task 7: Clean Up Plan File (Optional Security)
After deployment, delete the plan file (it contains sensitive info).
Add a Command Line task.
Settings:
- Display name: Delete Plan File
- Script:
rm -f tfplan
rm -f *-tfplan.tar.gz
Why? Plan files can contain sensitive data. Delete them after use.
Link Variable Group
Click the Variables tab, then Variable groups.
Click Link variable group and select: terraform-keyvault-secrets
Add a regular variable:
- Name: subscription-id
- Value: Your subscription GUID
Save the pipeline.
Configure Notifications (Optional)
Let's send emails when releases succeed or fail.
Enable Email Notifications
- Click the pipeline name at the top (to get to pipeline settings)
- Click Options tab
- Scroll to Integrations
- Check: Send email notifications
Recipients:
- Add yourself
- Add your team
- Add any stakeholders
When to send:
- Check: A deployment completes
- Check: A deployment fails
- Check: A deployment is pending approval
Click Save.
Test the Full Workflow (End-to-End)
Time to test the complete workflow: PR โ Build โ Release โ Approval โ Deploy.
Step 1: Create a Feature Branch
git checkout main
git pull
git checkout -b add-resource-group
Step 2: Add a New Resource
Create or edit terraform/main.tf:
resource "azurerm_resource_group" "demo" {
name = "rsg-terraform-demo-001"
location = "northeurope"
tags = {
Environment = "Test"
ManagedBy = "Terraform"
Purpose = "CI/CD Demo"
}
}
Step 3: Commit and Push
git add terraform/main.tf
git commit -m "Add demo resource group for testing"
git push origin add-resource-group
Step 4: Create Pull Request
- Go to Repos > Pull requests
- Click New pull request
- Source branch:
add-resource-group - Target branch:
main - Title: "Add demo resource group"
- Description: "Testing full CI/CD pipeline with approval gates"
- Click Create
What happens next:
- Status check pipeline should trigger automatically (if you set up branch policies in Part 4)
- Plan output appears in PR comments
- PR requires approval before merge
Step 5: Review and Merge PR
- Click the Checks tab (or Status checks)
- Review the Terraform plan output
- Verify it says:
Plan: 1 to add, 0 to change, 0 to destroy - Click Approve (top right)
- Click Complete (merge options)
- Select: Delete branch after merge (clean up)
- Click Complete merge
What happens next:
- Code merges to main
- Build pipeline (Terraform Plan) triggers automatically
- Build creates artifact and publishes it
- Release pipeline queues (but doesn't start)
Step 6: Approve the Release
You should receive an email: "Release Release-1 is waiting for your approval."
- Go to Pipelines > Releases
- You should see a release in In progress state with a pause icon
- Click into the release
- You'll see:
Deploy Infrastructurestage with Approval pending - Click Approve (or Reject if something looks wrong)
- Comments: "Approved for testing - deploying demo resource group"
- Click Approve
What happens next:
- Release pipeline starts running
- Tasks execute: Download artifact, extract, init, apply
- Terraform deploys the resource group
- You see: Apply complete! Resources: 1 added, 0 changed, 0 destroyed
Step 7: Verify in Azure
Get-AzResourceGroup -Name "rsg-terraform-demo-001"
You should see the resource group with your tags.
Success! Your full CI/CD pipeline works.
The Complete Audit Trail
One of the best features of this setup is the complete audit trail. You can always answer:
"Who deployed this resource?"
- Go to Pipelines > Releases
- Find the release
- Check the Approved by field
- Check the Git commit author
"When was it deployed?"
- Release pipeline shows start time and duration
- Azure Activity Log shows exact deployment time
- Git commit has timestamp
"What exactly was deployed?"
- Download the artifact from the build
- Extract the
tfplanfile - Run:
terraform show tfplan(requires Terraform installed locally)
Or just look at the Git diff in the merged PR.
"Why was it deployed?"
- Read the PR description
- Read the PR comments and reviews
- Read the release approval comments
This satisfies audit requirements for SOC 2, ISO 27001, and most compliance frameworks.
Common Issues & Fixes
Issue: Release doesn't trigger after build completes
Cause: Continuous deployment trigger is disabled or branch filter doesn't match.
Fix:
1. Go to release pipeline
2. Click the artifact lightning bolt
3. Verify: Continuous deployment trigger is ON
4. Verify: Branch filter includes main
Issue: "Artifact not found" error
Cause: The artifact name doesn't match what the build pipeline published.
Fix:
1. Go to the build pipeline run
2. Check the published artifact name (should be something like 123-tfplan)
3. Update the release pipeline download task to match
Or simplify by using **/* wildcard pattern.
Issue: Terraform apply fails with "Invalid plan file"
Cause: The plan file is from a different state version, or the state changed between plan and apply.
Fix:
1. Check if someone manually deployed resources in the portal
2. Reject the release
3. Create a new PR to re-plan with current state
4. Re-approve and merge
Prevention: Don't allow manual Azure Portal changes. Enforce this policy.
Issue: "Backend initialization failed" in release pipeline
Cause: Storage account key is missing or incorrect.
Fix:
1. Verify Key Vault has the storage account key
2. Verify variable group is linked to the release pipeline
3. Check the init task uses the correct variable: $(sttfstate1234-key1)
Issue: Approval email not received
Cause: Notification settings not configured.
Fix:
1. Go to release pipeline Options tab
2. Verify Send email notifications is checked
3. Verify your email is in the recipients list
4. Check your spam folder
Issue: Can't approve own release
Cause: You enabled the policy "The user requesting a release should not approve it."
Fix: Either:
1. Have someone else approve releases you create
2. Disable that policy (if you're a one-person team)
Security Best Practices
1. Rotate Service Principal Secrets
Service principal secrets should rotate regularly (every 90-180 days).
Rotation process:
1. Create new secret for service principal in Azure AD
2. Update Key Vault with new secret
3. Test pipelines
4. Delete old secret
Automation tip: Use Azure Key Vault secret expiration notifications.
2. Limit Approval Permissions
Don't make everyone an approver. Only:
- Infrastructure leads
- Platform team leads
- Security team (for high-risk changes)
How to add approvers:
1. Go to release pipeline
2. Click pre-deployment approval settings
3. Add/remove approvers
3. Use Multiple Approval Gates for Production
For production deployments, add two approval gates:
Gate 1: Technical Review
- Approvers: Infrastructure team
- Purpose: Verify technical correctness
Gate 2: Change Advisory Board (CAB)
- Approvers: CAB members
- Purpose: Verify business justification and timing
How to add:
1. Create two stages in release pipeline: Prod-Technical-Review and Prod-CAB-Approval
2. Both have pre-deployment approvals
3. Second stage depends on first stage completing
4. Implement Deployment Windows
Only deploy during approved maintenance windows.
How:
1. Use scheduled releases (release pipeline option)
2. Set allowed hours: e.g., Saturday 2 AM - 6 AM
3. Auto-reject releases outside the window
5. Archive Release Logs
Keep release logs for at least 1 year (compliance requirement).
Azure DevOps retains:
- Build logs: 30 days (default)
- Release logs: 30 days (default)
Extend retention:
1. Go to Project Settings > Pipelines > Retention
2. Set: Days to keep releases = 365
What We've Built
You now have a production-ready release pipeline with:
โ
Automated artifact deployment - No manual Terraform commands
โ
Human approval gates - No surprise deployments
โ
Email notifications - Stay informed
โ
Audit trails - Full history of approvals and deployments
โ
Consistency - Approved plan is exactly what deploys
โ
Security - Secrets from Key Vault, plan files cleaned up
Key Takeaways
- Release pipelines handle deployment - Build pipelines create artifacts
- Approval gates prevent unauthorized changes - Human review required
- Artifacts preserve intent - No drift between plan and apply
- Notifications keep teams informed - Email on approval, success, failure
- Audit trails satisfy compliance - Who, what, when, why all documented
Next, we wire up pull request automation and branch policies to enforce this workflow.
Next: Part 4 - Branch Policies & Pull Request Automation
All code and pipeline configurations from this series are available in my GitHub repo.