Back to Blog
AzureDevOpsCI/CDDockerContainer Apps

Automating Ephemeral QA Environments in Azure Container Apps

8 min read

This guide provides a complete end-to-end workflow for setting up a "Vanish" pipeline. The pipeline builds a Docker image, deploys it to a temporary QA environment for testing, and then deletes the resources immediately to ensure zero waste and cost efficiency.

Core Components and Requirements

To successfully implement this workflow, you need the following infrastructure:

  • Azure Container Registry (ACR): A private registry to store your images.
  • Azure Resource Group: A dedicated group to house your Environment (e.g., rg-qa-environments).
  • Azure DevOps Project: A project to host your YAML pipelines.
  • ACR Admin Credentials: In the Azure Portal, go to your ACR > Access Keys and enable Admin User to obtain the username and password.

  • Step 1: Create the Service Connection

    Create a connection that allows Azure DevOps to manage resources in your Azure subscription.

  • 1.In Azure DevOps, go to Project Settings > Service Connections
  • 2.Click New service connection > Azure Resource Manager
  • 3.Select Workload Identity Federation (automatic)
  • 4.Select your Subscription and Resource Group
  • 5.Name it (e.g., azure-qa-connection)
  • 6.Check Grant access permission to all pipelines
  • 7.Save
  • Azure DevOps automatically creates a federated credential - no passwords to manage.


    Step 2: Configure Secrets (Azure DevOps Side)

    Store your ACR Admin password securely so it is never exposed in logs or code.

  • 1.Go to Pipelines > Library
  • 2.Create a new Variable Group named acr-credentials
  • 3.Add a variable named ACR_PASSWORD
  • 4.Paste your ACR Admin Password and click the Padlock icon to make it secret
  • 5.Save the group

  • Step 3: The Main Pipeline (azure-pipelines.yml)

    This is the entry point that triggers on every push and calls the deployment template.

    trigger:
      - main
    
    pool:
      vmImage: 'ubuntu-latest'
    
    variables:
      - group: acr-credentials
      - name: ACR_NAME
        value: 'yourregistry'
      - name: RESOURCE_GROUP
        value: 'rg-qa-environments'
      - name: ENVIRONMENT_NAME
        value: 'qa-environment'
    
    stages:
      - stage: QA
        displayName: 'Deploy & Test QA'
        jobs:
          - job: DeployTest
            steps:
              - template: deploy-template-qa.yml
                parameters:
                  imageName: 'myapp'
                  containerName: 'myapp-qa'
                  port: '3000'

    Step 4: The Deployment Template (deploy-template-qa.yml)

    This template contains the logic for building, deploying, testing, and cleaning up.

    parameters:
      - name: imageName
        type: string
      - name: containerName
        type: string
      - name: port
        type: string
        default: '3000'
    
    steps:
      # 1. Build the image in ACR
      - task: AzureCLI@2
        displayName: 'Build Image in ACR'
        inputs:
          azureSubscription: 'azure-qa-connection'
          scriptType: 'bash'
          scriptLocation: 'inlineScript'
          inlineScript: |
            az acr build \
              --registry $(ACR_NAME) \
              --image ${{ parameters.imageName }}:$(Build.BuildId) .
    
      # 2. Deploy the Container App
      - task: AzureCLI@2
        displayName: 'Deploy Container App'
        inputs:
          azureSubscription: 'azure-qa-connection'
          scriptType: 'bash'
          scriptLocation: 'inlineScript'
          inlineScript: |
            set -e
    
            # Create Environment if it doesn't exist
            if ! az containerapp env show -n $(ENVIRONMENT_NAME) -g $(RESOURCE_GROUP) &>/dev/null; then
              az containerapp env create \
                -n $(ENVIRONMENT_NAME) \
                -g $(RESOURCE_GROUP) \
                --location eastus
            fi
    
            APP_NAME="${{ parameters.containerName }}-$(Build.BuildId)"
    
            az containerapp create \
              --name "$APP_NAME" \
              --resource-group $(RESOURCE_GROUP) \
              --environment $(ENVIRONMENT_NAME) \
              --image "$(ACR_NAME).azurecr.io/${{ parameters.imageName }}:$(Build.BuildId)" \
              --registry-server "$(ACR_NAME).azurecr.io" \
              --registry-username "$(ACR_NAME)" \
              --registry-password "$(ACR_PASSWORD)" \
              --ingress external \
              --target-port ${{ parameters.port }}
    
      # 3. Wait for deployment and capture URL
      - task: AzureCLI@2
        displayName: 'Get App URL'
        name: 'DeployStep'
        inputs:
          azureSubscription: 'azure-qa-connection'
          scriptType: 'bash'
          scriptLocation: 'inlineScript'
          inlineScript: |
            APP_NAME="${{ parameters.containerName }}-$(Build.BuildId)"
    
            for i in {1..12}; do
              FQDN=$(az containerapp show \
                -n "$APP_NAME" \
                -g $(RESOURCE_GROUP) \
                --query properties.configuration.ingress.fqdn -o tsv)
    
              if [ -n "$FQDN" ] && [ "$FQDN" != "null" ]; then
                echo "App URL: https://$FQDN"
                echo "##vso[task.setvariable variable=QA_URL;isOutput=true]https://$FQDN"
                exit 0
              fi
              echo "Waiting for app to be ready... ($i/12)"
              sleep 10
            done
            echo "Timeout waiting for app"
            exit 1
    
      # 4. Run Integration Tests
      - script: |
          echo "Running tests against $(DeployStep.QA_URL)"
          export TARGET_URL="$(DeployStep.QA_URL)"
          npm test -- __tests__/integration.test.js
        displayName: 'Run Integration Tests'
    
      # 5. Vanish - Always cleanup, even if tests fail
      - task: AzureCLI@2
        displayName: 'Vanish - Delete App'
        condition: always()
        inputs:
          azureSubscription: 'azure-qa-connection'
          scriptType: 'bash'
          scriptLocation: 'inlineScript'
          inlineScript: |
            APP_NAME="${{ parameters.containerName }}-$(Build.BuildId)"
            echo "Deleting $APP_NAME..."
            az containerapp delete \
              --name "$APP_NAME" \
              --resource-group $(RESOURCE_GROUP) \
              --yes
            echo "Cleanup complete"

    Summary

    This pipeline automates the entire lifecycle of ephemeral QA environments:

  • 1.Build - Creates a Docker image in ACR
  • 2.Deploy - Spins up a temporary Container App
  • 3.Test - Runs integration tests against the live environment
  • 4.Vanish - Automatically cleans up all resources (even if tests fail)
  • You only pay for the few minutes the app is running. No forgotten resources, no wasted costs.

    Tip: The condition: always() on the cleanup step ensures the app is deleted even when tests fail.

    AJ

    Aziz Jarrar

    Full Stack Engineer

    Share this article