Skip to main content

Command Palette

Search for a command to run...

A Guide to Azure DevOps - Environment Protection

Updated
8 min read
A Guide to Azure DevOps - Environment Protection
Y

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

ChatGPT Image Dec 20, 2025, 05_49_41 PM

Implementing Steps

  • Create two different Azure Subscriptions
    • One for Dev
    • One for Pre-Prod

create dev subscripition

  • 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.

create service princple

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

certificate secrets

📝 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 create role for preprod

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

file 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 service connection tap
  • From the list, select:

    • Azure Resource Manager
  • Click Next

service connection type

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

configure connection type

Create Azure DevOps Environments

  • Go to Pipelines
  • Environments

    pipeline-enviroments

  • 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/*

feature branch

2- Push to main

pushing to the main

3- Approve

after approval

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.

More from this blog

Youssef Blog

12 posts

Cloud & DevOps Engineer, AWS, Linux Sysadmin, Terraform, Kubernetes, Bash & Python scripting, Passionate about DevOps, automation and continuous self-improvement, being a DevOps Expert is my Aim.