Managing Environments in CI/CD Pipelines

Environment management is the part of CI/CD that prevents dev changes from reaching production before they are ready. Done well, it gives you fast feedback loops in lower environments and solid confidence gates before production. This page covers the patterns that work at scale: environment isolation, variable scoping, approval gates, and pipeline stages that enforce sequential promotion.

Environment isolation strategies

The level of isolation between environments trades cost for safety. Three common patterns:

  • Shared subscription, separate resource groups. Dev, staging, and prod all exist in the same Azure subscription but in separate resource groups with different IAM permissions. Cheapest option, lowest isolation. A misconfigured subscription-level policy or an admin mistake can affect all environments. Acceptable for small teams with low-risk applications.
  • Separate subscriptions per environment. Dev, staging, and prod each get their own Azure subscription. Strong isolation boundary — subscription-level quotas, policies, and billing are completely independent. The correct approach for production workloads. Use Azure Management Groups to apply governance policies across all subscriptions consistently.
  • Separate Azure AD tenants. Extreme isolation for compliance-heavy scenarios where even admin access must be separated. Rarely needed, significant management overhead. Typically reserved for scenarios where regulatory requirements mandate complete tenant-level separation.

Variable groups per environment

Environment-specific configuration (connection strings, API endpoints, feature flags, resource names) should come from variable groups, not from hardcoded pipeline YAML. Each environment gets its own variable group containing values specific to that environment:

# Pipeline references environment-specific variable groups
stages:
  - stage: DeployDev
    variables:
      - group: vg-myapp-dev    # Contains: APP_URL, DB_CONNECTION_STRING, etc.
    jobs:
      - deployment: DeployApp
        environment: env-dev
        strategy:
          runOnce:
            deploy:
              steps:
                - script: echo "Deploying to $(APP_URL)"

  - stage: DeployStaging
    dependsOn: DeployDev
    variables:
      - group: vg-myapp-staging
    jobs:
      - deployment: DeployApp
        environment: env-staging
        strategy:
          runOnce:
            deploy:
              steps:
                - script: echo "Deploying to $(APP_URL)"

  - stage: DeployProd
    dependsOn: DeployStaging
    variables:
      - group: vg-myapp-prod
    jobs:
      - deployment: DeployApp
        environment: env-prod
        strategy:
          runOnce:
            deploy:
              steps:
                - script: echo "Deploying to $(APP_URL)"

The same pipeline YAML deploys to all three environments. Only the variable group changes per stage. This pattern ensures you are deploying the same artifact with environment-appropriate configuration, not different code per environment.

Azure DevOps Environments with approvals

Azure DevOps Environments are the mechanism for approval gates. Create an environment for each deployment target, then configure checks on that environment:

In Azure DevOps, navigate to Pipelines > Environments, create an environment named env-staging and another named env-prod. On env-prod, add an Approvals check requiring sign-off from specific users or groups before any pipeline can deploy to it.

When the pipeline reaches the stage that deploys to env-prod, it pauses and notifies the designated approvers. The deployment does not proceed until someone approves it. This creates a human gate between staging validation and production deployment.

Note

Beyond manual approvals, Azure DevOps Environments support other check types: Invoke Azure Function (run custom validation logic), Query Azure Monitor Alerts (block if there are active alerts on a resource), Branch control (only allow deployments from specific branches), and Business hours checks (only deploy during approved time windows).

Service connections per environment

Each environment should use a separate Azure service connection configured with a separate service principal or managed identity. The prod service connection should have minimal permissions — only what is needed to deploy, nothing else.

stages:
  - stage: DeployDev
    jobs:
      - deployment: DeployApp
        environment: env-dev
        pool:
          vmImage: ubuntu-latest
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: 'sc-myapp-dev'    # Dev service connection
                    appName: 'app-myapp-dev'
                    package: '$(Pipeline.Workspace)/drop/*.zip'

  - stage: DeployProd
    dependsOn: DeployStaging
    jobs:
      - deployment: DeployApp
        environment: env-prod
        pool:
          vmImage: ubuntu-latest
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: 'sc-myapp-prod'   # Separate prod service connection
                    appName: 'app-myapp-prod'
                    package: '$(Pipeline.Workspace)/drop/*.zip'

Building once, deploying everywhere

A critical principle: build the application artifact once in a build stage, publish it, and then deploy that same artifact to dev, staging, and production in sequence. Never rebuild the artifact per environment. If you rebuild, you risk subtle differences introduced by dependency version resolution or environment-specific build settings. The artifact that passed staging tests must be byte-for-byte identical to what goes to production.

stages:
  - stage: Build
    jobs:
      - job: BuildApp
        pool:
          vmImage: ubuntu-latest
        steps:
          - task: DotNetCoreCLI@2
            inputs:
              command: publish
              publishWebProjects: true
              arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)'
          - publish: $(Build.ArtifactStagingDirectory)
            artifact: drop

  - stage: DeployDev
    dependsOn: Build
    jobs:
      - deployment: Deploy
        environment: env-dev
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: drop
                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: 'sc-myapp-dev'
                    appName: 'app-myapp-dev'
                    package: '$(Pipeline.Workspace)/drop/**/*.zip'

Common mistakes

  1. Putting environment-specific configuration in pipeline YAML. Hardcoding values like database hostnames or API keys in YAML means that YAML must be modified to change any environment’s configuration. Every config change becomes a code change, creating noise in commit history and requiring a deployment to take effect. Store all environment-specific values in variable groups and reference them by group name in the pipeline.
  2. Building the application separately for each environment. Rebuilding from source for each environment introduces risk. A dependency that resolves differently, a build tool version bump, or a timestamp embedded in the binary will produce a different artifact for each environment. Build once, publish the artifact, and deploy the same artifact through all environments.
  3. Skipping staging and deploying directly from dev to prod under time pressure. The most common cause of production incidents is bypassing the staging environment during urgent deployments. If staging approval gates feel like a blocker during an incident, the right solution is to speed up staging validation (automate smoke tests) — not to remove the gate. A failed deployment to production under pressure is worse than the original incident.

Frequently asked questions

What is an Azure DevOps Environment?

An Azure DevOps Environment is a resource that represents a deployment target — it can contain Azure VMs, Kubernetes namespaces, or be empty (used purely as a checkpoint). Environments track deployment history, showing which pipeline run deployed what version and when. They also host approval checks and branch control policies that gate deployments to that environment.

How do I prevent a pipeline from deploying to production without going through staging first?

Use the dependsOn keyword in your pipeline YAML so the production stage only runs after the staging stage succeeds. Add a branch control check on the production environment in Azure DevOps requiring the source branch to be main. This prevents feature branches from bypassing staging and deploying directly to production.

Can different environments use different Azure subscriptions?

Yes, and this is often a good practice. Keeping dev, staging, and prod in separate subscriptions creates a hard isolation boundary. A misconfigured Azure Policy, accidental deletion, or runaway script in dev cannot affect prod. Configure a separate Azure DevOps service connection per subscription and reference the correct service connection in each stage.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.