A Guide to Azure DevOps - Environment Protection

Cloud & DevOps Engineer skilled in AWS, Linux/Windows, Bash, PowerShell & Python. Passionate about automation, CI/CD, and continuous learning toward DevOps mastery.
Inception
Hello everyone, this post is aimed at guiding you to protect your environment from deployment across (dev, Pre-Prod, production) by segregating environments, building a secure pipeline, and approval gates using Azure DevOps Environment.
You will learn how to segregate environments, configure approval gates, and template-driven YAML pipelines come together to create a controlled, predictable path from development to production.
implementing a lap simulating the situation to explain the concept.
Overview
In DevOps, an environment is a separate and isolated space where an application and its infrastructure run. Environments exist to support the software lifecycle from development to production. Each environment usually has its own cloud resources, configurations, access permissions, and deployment rules. Common environments include Dev, Pre-Prod, and Production. The main goal of having multiple environments is to reduce risk by ensuring that changes are tested and validated before reaching real users.
Why its matter?
Multiple environments are necessary because software is not always stable or safe at every stage of development. Developers constantly make changes, test ideas, and fix bugs. If all changes were deployed directly to production, the risk of outages and failures would be very high. By using multiple environments, teams can safely experiment in Dev, validate in Pre-Prod, and only release fully approved changes to Production. This separation creates a controlled and predictable delivery process.
What is Environment segregation?
Environment segregation means keeping each environment completely independent from the others. This includes using separate Azure subscriptions, separate Terraform state files, and separate deployment configurations. Segregation ensures that actions in one environment do not accidentally affect another. For example, a mistake in Dev should never be able to delete or modify resources in Pre-Prod. This isolation is a key principle in enterprise cloud architecture and governance.
This could be achieved by branch-based deployment control.
Branch-based deployment control ensures that only approved and stable code can be deployed to sensitive environments. In this task, Pre-Prod deployments are allowed only from the main branch. This means all code must be reviewed, tested, and merged before it can reach Pre-Prod. This rule prevents unfinished or experimental features from being deployed and ensures consistency between Pre-Prod and what will eventually go to Production.
Approval gates play a vital role here
Approval gates introduce a human checkpoint before executing risky actions such as Terraform Apply. Infrastructure changes can create, modify, or destroy cloud resources, so automatic execution in sensitive environments is dangerous. Requiring manual approval for Pre-Prod ensures that someone reviews the planned changes, understands their impact, and takes responsibility before they are applied. This is a standard governance practice in professional DevOps teams.
All this could be easily maintained by separating your YAML files for pipelines.
Separating pipelines into multiple YAML files improves clarity, reusability, and safety. A main pipeline file orchestrates the workflow, while separate YAML templates handle specific jobs or environments. This structure makes it easier to apply different rules for Dev and Pre-Prod, reduces duplication, and lowers the risk of configuration mistakes. It also reflects real-world DevOps standards used in large organizations.
All these concepts together form what is known as environment governance. Governance ensures that deployments are predictable, secure, and compliant with organizational policies. By segregating environments, enforcing branch rules, requiring approvals, structuring pipelines properly, and securing authentication, organizations ensure that infrastructure deployments are safe and production-ready.
Workflow Diagram
Implementing Steps
- Create two different Azure Subscriptions
- One for Dev
- One for Pre-Prod
Create Service Principals for each subscription
- Go to Azure Portal
- Azure Active Directory
- Click App registrations
- Click + New registration
- Fill in:
- Name: 1- sp-terraform-dev 2- sp-terraform-preprod
- Supported account types: Single tenant (default)
- Click Register
You created an application but it still cannot do anything.
On the app overview page, you now see:
- Application (client) ID
- Directory (tenant) ID
⚠️ Important:
This is NOT yet allowed to deploy resources
You need to create a client secret from the right menu
- Create Client Secret
- Go to Certificates & secrets
- Click + New client secret
- Description: terraform-secret
- Expiry: 12 months for example
- Click Add
📝 Note:
Copy the Value immediately as this is the Client Secret and will not appear again.
- Assign Contributor Role on the Subscription
- In Azure Portal, search for Subscriptions
- Click the Dev subscription (example: FrogTech-Dev-Subscription)
- Inside the subscription:
- Click Access control (IAM)
- Click + Add
- Choose Add role assignment
- In Role tab:
- Search for Contributor
- Select Contributor
- Click Next This allows create/update/delete resources.
- Select the Service Principal
- In Members tab:
- Choose User, group, or service principal
- Click + Select members
- Search for:
- sp-terraform-dev
- Select it
- Click Next
- Click Review + assign
- In Members tab:
We will now create an Azure DevOps Organization, Project, Repos, and build the pipelines logic
- Create an Azure DevOps Organization
- Create a Project (e.g. frogtech-terraform)
- Go to Repos
- Initialize a Git repository
- Create branch (main)
- Prepare Terraform Project Structure
Each folder deploys to one environment and has its own backend (state)
📝 Note :
You can use HCP Cloud for storing your state for each environment.
Create Azure Service Connections
In Azure DevOps:
- Click Project Settings
- Service connections
From the list, select:
- Azure Resource Manager
- Click Next
You will see two main options in the authentication method:
- Select Service principal (manual)
Fill in Service Principal Details
- Subscription ID
- Subscription Name
- Tenant ID
- Client ID
- Client Secret
Choose Scope Level
- Subscription
- Click Verify
- name
- sc-terraform-dev
- sc-terraform-preprod
- Click Save
Create Azure DevOps Environments
- Go to Pipelines
Environments
- create:
- dev
- preprod
- Add Approval for preprod
- Select approver (you, as Tech Lead)
Design Pipeline Structure
pipelines/
azure-pipelines.yml # main
templates/
terraform-plan.yml
terraform-apply.yml
deploy-dev.yml
deploy-preprod.yml
Main Pipeline YAML (Entry Point)
This includes Pipeline triggers, if branch = main it will deploy on Dev normally, Pre-Prod require approval.
if not = it will deploy only on Dev.
stages:
- stage: Dev
variables:
- group: azure-dev
jobs:
- template: pipelines/templates/deploy-dev.yml
- stage: PreProd
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
variables:
- group: azure-preprod
jobs:
- template: pipelines/templates/deploy-preprod.yml
Dev Deployment Flow
jobs:
- job: DeployDev
displayName: Terraform Deploy Dev
pool:
vmImage: ubuntu-latest
variables:
- group: terraform-cloud
- group: azure-dev
steps:
- template: terraform-init.yml
parameters:
terraformDirectory: terraform/dev
- template: terraform-plan.yml
parameters:
terraformDirectory: terraform/dev
- template: terraform-apply.yml
parameters:
terraformDirectory: terraform/dev
- template: terraform-destroy.yml
parameters:
terraformDirectory: terraform/dev
Pre-Prod Deployment Flow
jobs:
- deployment: Deploypreprod
displayName: Terraform Deploy Pre-Prod
pool:
vmImage: ubuntu-latest
environment: preprod
variables:
- group: terraform-cloud
- group: azure-preprod
strategy:
runOnce:
deploy:
steps:
- checkout: self
- template: terraform-init.yml
parameters:
terraformDirectory: terraform/preprod
- template: terraform-plan.yml
parameters:
terraformDirectory: terraform/preprod
- template: terraform-apply.yml
parameters:
terraformDirectory: terraform/preprod
- template: terraform-destroy.yml
parameters:
terraformDirectory: terraform/preprod
terraform-init.yml Flow
parameters:
terraformDirectory: ""
steps:
- bash: |
set -e
echo "Installing Terraform via APT..."
sudo apt-get update -y
sudo apt-get install -y gnupg software-properties-common curl
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update -y
sudo apt-get install -y terraform
terraform version
displayName: Install Terraform
- bash: |
echo "Checking Terraform Cloud token..."
if [ -z "$TF_TOKEN_app_terraform_io" ]; then
echo " TF_API_TOKEN is missing"
exit 1
else
echo " TF_API_TOKEN is present"
fi
terraform init
displayName: Terraform Init
workingDirectory: ${{ parameters.terraformDirectory }}
env:
TF_TOKEN_app_terraform_io: $(TF_TOKEN_app_terraform_io)
terraform-plan.yml Flow
parameters:
terraformDirectory: ""
steps:
- bash: |
echo "Checking Terraform Cloud token..."
if [ -z "$TF_TOKEN_app_terraform_io" ]; then
echo " TF_API_TOKEN is missing"
exit 1
else
echo " TF_API_TOKEN is present"
fi
echo "Azure auth mode:"
echo "ARM_USE_AZURECLI_AUTH=$ARM_USE_AZURECLI_AUTH"
terraform plan
displayName: Terraform Plan
workingDirectory: ${{ parameters.terraformDirectory }}
env:
TF_TOKEN_app_terraform_io: $(TF_TOKEN_app_terraform_io)
TF_VAR_client_id: $(ARM_CLIENT_ID)
TF_VAR_client_secret: $(ARM_CLIENT_SECRET)
TF_VAR_tenant_id: $(ARM_TENANT_ID)
TF_VAR_subscription_id: $(ARM_SUBSCRIPTION_ID)
ARM_USE_AZURECLI_AUTH: "false"
terraform-apply.yml Flow
parameters:
terraformDirectory: ""
steps:
- bash: |
echo "Checking Terraform Cloud token..."
if [ -z "$TF_TOKEN_app_terraform_io" ]; then
echo " TF_API_TOKEN is missing"
exit 1
else
echo " TF_API_TOKEN is present"
fi
echo "Azure auth mode:"
echo "ARM_USE_AZURECLI_AUTH=$ARM_USE_AZURECLI_AUTH"
terraform apply -auto-approve
displayName: Terraform Apply
workingDirectory: ${{ parameters.terraformDirectory }}
env:
TF_TOKEN_app_terraform_io: $(TF_TOKEN_app_terraform_io)
TF_VAR_client_id: $(ARM_CLIENT_ID)
TF_VAR_client_secret: $(ARM_CLIENT_SECRET)
TF_VAR_tenant_id: $(ARM_TENANT_ID)
TF_VAR_subscription_id: $(ARM_SUBSCRIPTION_ID)
ARM_USE_AZURECLI_AUTH: "false"
terraform-destroy.yml Flow
parameters:
terraformDirectory: ""
steps:
- bash: |
echo "Checking Terraform Cloud token..."
if [ -z "$TF_TOKEN_app_terraform_io" ]; then
echo " TF_API_TOKEN is missing"
exit 1
else
echo " TF_API_TOKEN is present"
fi
echo "Azure auth mode:"
echo "ARM_USE_AZURECLI_AUTH=$ARM_USE_AZURECLI_AUTH"
terraform destroy -auto-approve
displayName: Terraform Destroy
workingDirectory: ${{ parameters.terraformDirectory }}
env:
TF_TOKEN_app_terraform_io: $(TF_TOKEN_app_terraform_io)
TF_VAR_client_id: $(ARM_CLIENT_ID)
TF_VAR_client_secret: $(ARM_CLIENT_SECRET)
TF_VAR_tenant_id: $(ARM_TENANT_ID)
TF_VAR_subscription_id: $(ARM_SUBSCRIPTION_ID)
ARM_USE_AZURECLI_AUTH: "false"
Test Cases
1- Push to feature/*
2- Push to main
3- Approve
References
https://eraki.hashnode.dev/building-secure-deployment-pipelines-with-azure-devops-environments-approvals
https://learn.microsoft.com/en-us/azure/devops/pipelines/process/environments?view=azure-devops
https://learn.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&tabs=check-pass
Thanks for reading.



