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 — zero waste, zero forgotten environments sitting around running up your bill at the end of the month.

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's 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 mark it as 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. The key design decision here is that the cleanup step runs with condition: always() — meaning even if the tests fail, the environment gets deleted. No exceptions.

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

        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 surprise costs at the end of the billing cycle.

Tip: The condition: always() on the cleanup step is the critical piece. Without it, a failed test run leaves the environment running indefinitely.

AJ

Aziz Jarrar

Full Stack Engineer

Share this article