Skip to main content

CI/CD Pipelines

This guide outlines the key principles and practices for managing an Azure DevOps pipeline that implements conditional builds, multi-service Docker images, Azure Container Registry (ACR) push, and Azure Container Apps deployment via CLI, on a cloud-native infrastructure.

Modular & Conditional Execution

Best Practices:

  • Use parameters for conditional builds (e.g., buildFrontend, buildBackend, buildWorker).
  • Enables pipeline reuse with minimal changes and full control over what runs in each execution.
  • Parameter names should be clear and scoped per service.

Docker Build & Push Strategy

Best Practices:

  • Organize Dockerfile paths and context as pipeline variables.
  • Separating build, tag, and push improves readability and debugging.
  • Use a unique build tag per run ($(Build.BuildId)) for traceability.
  • Use DockerInstaller task only if the native Docker runtime is missing (e.g., on Microsoft-hosted agents).

Image Tagging & Registry Publishing

Best Practices:

  • Push to Azure Container Registry only after a successful build.
  • Use separate repositories per service role.
  • Avoid latest tags — use versioned or build ID–based tags for reproducibility.

Deployment to Azure Container Apps

Best Practices:

  • Use AzureContainerApps@1 task for simple image updates.
  • For full environment control, use AzureCLI@2 with az containerapp update --set-env-vars.
  • Do not include credentials in YAML — prefer using Key Vault references or pipeline secrets.

Environment Variables (Env Vars)

Best Practices:

  • Separate by role (frontend/backend/worker).
  • Avoid hardcoded secrets (e.g., SECRET_KEY, DB_PASSWORD). Instead, use:
    • Azure Key Vault references
    • Pipeline secrets
    • YAML templates with environment-specific overrides
  • Critical info such as tokens, credentials, and URLs should be isolated in variable groups or external secret stores.

Stage Structure & Flow

Best Practices:

  • Clear separation between Build and Deploy stages.
  • Deployments should depend on the successful completion of the Build stage (dependsOn: Build).
  • Use an agent pool with a clear label and consistent configuration (e.g., VMSSAgents).

Secrets & Security

Best Practices:

  • All sensitive values must be passed via pipeline secrets.
  • Avoid hardcoded credentials inline — not even for testing.
  • Where CLI updates occur, use authenticated identities, not exposed secrets.

Documentation & Maintainability

Best Practices:

  • Each section should have comments explaining its purpose (e.g., # Build Frontend, # Deploy Celery Beat).
  • Parameters should include a displayName, not just internal name, to improve UI usability.
  • Maintain a README or internal wiki with usage instructions and input options for the pipeline.

Example Azure DevOps Pipeline – Containerized App Deployment (Dummy Example)

trigger: none # Only manual runs

parameters:
- name: buildFrontend
type: boolean
default: false
- name: buildBackend
type: boolean
default: false
- name: buildWorker
type: boolean
default: false

variables:
tag: "$(Build.BuildId)"
dockerRegistryServiceConnection: "myContainerRegistryConnection"
containerRegistry: "myregistry.azurecr.io"

frontendImage: "frontend-app"
backendImage: "backend-api"
workerImage: "background-worker"

dockerfileFrontend: "$(Build.SourcesDirectory)/frontend/Dockerfile"
dockerfileBackend: "$(Build.SourcesDirectory)/backend/Dockerfile"
dockerfileWorker: "$(Build.SourcesDirectory)/worker/Dockerfile"

contextFrontend: "$(Build.SourcesDirectory)/frontend"
contextBackend: "$(Build.SourcesDirectory)/backend"
contextWorker: "$(Build.SourcesDirectory)/worker"

azureSubscription: "My-Azure-Sub"
resourceGroup: "my-prod-rg"
containerAppFrontend: "frontend-app"
containerAppBackend: "backend-api"
containerAppWorker: "worker-job"

stages:
- stage: BuildAndPush
displayName: "Build and Push Images"
jobs:
- job: Build
pool:
vmImage: "ubuntu-latest"
steps:
- task: DockerInstaller@0
inputs:
dockerVersion: "20.10.7"

- task: Docker@2
displayName: "Login to ACR"
inputs:
command: login
containerRegistry: "$(dockerRegistryServiceConnection)"

# Build Frontend
- ${{ if eq(parameters.buildFrontend, true) }}:
- script: |
docker build -t $(frontendImage):$(tag) -f $(dockerfileFrontend) $(contextFrontend)
docker tag $(frontendImage):$(tag) $(containerRegistry)/$(frontendImage):$(tag)
docker push $(containerRegistry)/$(frontendImage):$(tag)
displayName: "Build & Push Frontend"

# Build Backend
- ${{ if eq(parameters.buildBackend, true) }}:
- script: |
docker build -t $(backendImage):$(tag) -f $(dockerfileBackend) $(contextBackend)
docker tag $(backendImage):$(tag) $(containerRegistry)/$(backendImage):$(tag)
docker push $(containerRegistry)/$(backendImage):$(tag)
displayName: "Build & Push Backend"

# Build Worker
- ${{ if eq(parameters.buildWorker, true) }}:
- script: |
docker build -t $(workerImage):$(tag) -f $(dockerfileWorker) $(contextWorker)
docker tag $(workerImage):$(tag) $(containerRegistry)/$(workerImage):$(tag)
docker push $(containerRegistry)/$(workerImage):$(tag)
displayName: "Build & Push Worker"

- stage: Deploy
displayName: "Deploy Container Apps"
dependsOn: BuildAndPush
jobs:
- job: Deploy
pool:
vmImage: "ubuntu-latest"
steps:
# Deploy Frontend
- ${{ if eq(parameters.buildFrontend, true) }}:
- task: AzureContainerApps@1
inputs:
azureSubscription: "$(azureSubscription)"
resourceGroup: "$(resourceGroup)"
containerAppName: "$(containerAppFrontend)"
imageToDeploy: "$(containerRegistry)/$(frontendImage):$(tag)"

# Deploy Backend
- ${{ if eq(parameters.buildBackend, true) }}:
- task: AzureContainerApps@1
inputs:
azureSubscription: "$(azureSubscription)"
resourceGroup: "$(resourceGroup)"
containerAppName: "$(containerAppBackend)"
imageToDeploy: "$(containerRegistry)/$(backendImage):$(tag)"

# Deploy Worker
- ${{ if eq(parameters.buildWorker, true) }}:
- task: AzureContainerApps@1
inputs:
azureSubscription: "$(azureSubscription)"
resourceGroup: "$(resourceGroup)"
containerAppName: "$(containerAppWorker)"
imageToDeploy: "$(containerRegistry)/$(workerImage):$(tag)"

We'd love your feedback
Was this helpful?