Automating Ephemeral QA Environments in Azure Container Apps
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:
rg-qa-environments).Step 1: Create the Service Connection
Create a connection that allows Azure DevOps to manage resources in your Azure subscription.
azure-qa-connection)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.
acr-credentialsACR_PASSWORDStep 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:
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.
Aziz Jarrar
Full Stack Engineer