Azure
intermediate
35-45 minutes

CI/CD Pipelines

Automate deployments with Azure DevOps and GitHub Actions for continuous integration and delivery.

What you'll accomplish

Automated deployment pipeline triggered on every code push

Prerequisites

  • Application deployed to Azure
  • Code in GitHub or Azure Repos
  • Azure CLI configured

CI/CD Options for Azure

Azure supports multiple CI/CD approaches:

PlatformBest ForIntegration
GitHub ActionsGitHub repos, simple workflowsNative
Azure DevOpsEnterprise, complex pipelinesNative
Azure CLIScripts, simple automationDirect

GitHub Actions for Azure

Overview

GitHub Actions automate workflows directly in your repository:

Push to GitHub → GitHub Actions → Deploy to Azure
                      │
                      ├── Build
                      ├── Test
                      └── Deploy

Step 1: Create Service Principal

bash
# Create service principal for GitHub Actions
az ad sp create-for-rbac \
  --name "github-actions-deploy" \
  --role contributor \
  --scopes /subscriptions/SUBSCRIPTION_ID \
  --sdk-auth

# Save the output - you'll need it for GitHub secrets

Output:

json
{
  "clientId": "xxx",
  "clientSecret": "xxx",
  "subscriptionId": "xxx",
  "tenantId": "xxx"
}

Step 2: Add GitHub Secrets

  1. Go to your GitHub repository
  2. Settings → Secrets and variables → Actions
  3. Add secrets:
    • AZURE_CREDENTIALS - Entire JSON output
    • AZURE_SUBSCRIPTION_ID - Your subscription ID

Step 3: Create Workflow File

Create .github/workflows/azure-deploy.yml:

yaml
name: Deploy to Azure App Service

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  AZURE_WEBAPP_NAME: myapp-unique-name
  NODE_VERSION: '20.x'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build application
        run: npm run build

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: app
          path: .

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: app

      - name: Login to Azure
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy to App Service
        uses: azure/webapps-deploy@v2
        with:
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          package: .

Deploy to Container Apps

yaml
name: Deploy to Container Apps

on:
  push:
    branches: [main]

env:
  REGISTRY: myregistry.azurecr.io
  IMAGE_NAME: myapp
  CONTAINER_APP_NAME: myapp
  RESOURCE_GROUP: myapp-rg

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Login to Azure
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Login to ACR
        run: az acr login --name myregistry

      - name: Build and push image
        run: |
          docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} .
          docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

      - name: Deploy to Container Apps
        run: |
          az containerapp update \
            --name ${{ env.CONTAINER_APP_NAME }} \
            --resource-group ${{ env.RESOURCE_GROUP }} \
            --image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

Deploy with Publish Profile (Alternative)

yaml
name: Deploy with Publish Profile

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci
      - run: npm run build

      - name: Deploy to Azure
        uses: azure/webapps-deploy@v2
        with:
          app-name: myapp-unique-name
          publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
          package: .

Get publish profile:

bash
az webapp deployment list-publishing-profiles \
  --name myapp-unique-name \
  --resource-group myapp-rg \
  --xml > publishprofile.xml

Azure DevOps Pipelines

Overview

Azure DevOps provides enterprise-grade CI/CD:

Azure Repos/GitHub → Azure Pipelines → Azure Services
                          │
                          ├── Build Pipeline
                          ├── Release Pipeline
                          └── Environments

Step 1: Create Azure DevOps Project

  1. Go to dev.azure.com
  2. Create new organization (or use existing)
  3. Create new project

Step 2: Create Service Connection

  1. Project Settings → Service connections
  2. New service connection → Azure Resource Manager
  3. Choose authentication method:
    • Workload Identity federation (recommended)
    • Service principal (manual)
  4. Select subscription and resource group

Step 3: Create Pipeline

Basic Pipeline (azure-pipelines.yml)

yaml
trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

variables:
  webAppName: 'myapp-unique-name'
  azureSubscription: 'MyAzureServiceConnection'

stages:
  - stage: Build
    displayName: 'Build Stage'
    jobs:
      - job: Build
        steps:
          - task: NodeTool@0
            inputs:
              versionSpec: '20.x'
            displayName: 'Install Node.js'

          - script: |
              npm ci
              npm run build
            displayName: 'Install and build'

          - script: npm test
            displayName: 'Run tests'

          - task: ArchiveFiles@2
            inputs:
              rootFolderOrFile: '$(Build.SourcesDirectory)'
              includeRootFolder: false
              archiveType: 'zip'
              archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'

          - task: PublishBuildArtifacts@1
            inputs:
              pathtoPublish: '$(Build.ArtifactStagingDirectory)'
              artifactName: 'drop'

  - stage: Deploy
    displayName: 'Deploy Stage'
    dependsOn: Build
    condition: succeeded()
    jobs:
      - deployment: Deploy
        environment: 'production'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: '$(azureSubscription)'
                    appType: 'webAppLinux'
                    appName: '$(webAppName)'
                    package: '$(Pipeline.Workspace)/drop/*.zip'

Multi-Stage Pipeline

yaml
trigger:
  - main
  - develop

pool:
  vmImage: 'ubuntu-latest'

stages:
  - stage: Build
    jobs:
      - job: Build
        steps:
          - task: NodeTool@0
            inputs:
              versionSpec: '20.x'

          - script: npm ci && npm run build
            displayName: 'Build'

          - script: npm test
            displayName: 'Test'

          - publish: $(Build.SourcesDirectory)
            artifact: app

  - stage: DeployStaging
    displayName: 'Deploy to Staging'
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
    jobs:
      - deployment: DeployStaging
        environment: 'staging'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: 'MyAzureServiceConnection'
                    appName: 'myapp-staging'
                    package: '$(Pipeline.Workspace)/app'

  - stage: DeployProduction
    displayName: 'Deploy to Production'
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployProduction
        environment: 'production'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: 'MyAzureServiceConnection'
                    appName: 'myapp-production'
                    package: '$(Pipeline.Workspace)/app'

Step 4: Configure Environments

  1. Pipelines → Environments
  2. Create staging and production
  3. Add approval checks for production:
    • Environment → Approvals and checks
    • Add approval with required approvers

Container Pipeline

yaml
trigger:
  - main

resources:
  - repo: self

variables:
  dockerRegistryServiceConnection: 'myACRConnection'
  imageRepository: 'myapp'
  containerRegistry: 'myregistry.azurecr.io'
  dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
  tag: '$(Build.BuildId)'

stages:
  - stage: Build
    jobs:
      - job: Build
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: Docker@2
            displayName: 'Build and push'
            inputs:
              command: buildAndPush
              repository: $(imageRepository)
              dockerfile: $(dockerfilePath)
              containerRegistry: $(dockerRegistryServiceConnection)
              tags: |
                $(tag)
                latest

  - stage: Deploy
    jobs:
      - deployment: Deploy
        environment: 'production'
        pool:
          vmImage: 'ubuntu-latest'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureCLI@2
                  inputs:
                    azureSubscription: 'MyAzureServiceConnection'
                    scriptType: 'bash'
                    scriptLocation: 'inlineScript'
                    inlineScript: |
                      az containerapp update \
                        --name myapp \
                        --resource-group myapp-rg \
                        --image $(containerRegistry)/$(imageRepository):$(tag)

Azure CLI Deployment Scripts

Basic Deployment Script

bash
#!/bin/bash
set -e

# Configuration
RESOURCE_GROUP="myapp-rg"
APP_NAME="myapp-unique-name"

# Build
npm ci
npm run build

# Deploy
az webapp deployment source config-zip \
  --resource-group $RESOURCE_GROUP \
  --name $APP_NAME \
  --src $(npm pack | tail -1)

echo "Deployment complete!"

Blue-Green Deployment

bash
#!/bin/bash
set -e

RESOURCE_GROUP="myapp-rg"
APP_NAME="myapp-unique-name"
SLOT_NAME="staging"

# Deploy to staging slot
az webapp deployment source config-zip \
  --resource-group $RESOURCE_GROUP \
  --name $APP_NAME \
  --slot $SLOT_NAME \
  --src app.zip

# Wait for deployment
sleep 30

# Test staging
STAGING_URL=$(az webapp show \
  --resource-group $RESOURCE_GROUP \
  --name $APP_NAME \
  --slot $SLOT_NAME \
  --query defaultHostName -o tsv)

if curl -sf "https://$STAGING_URL/health" > /dev/null; then
  echo "Staging healthy, swapping..."

  # Swap staging to production
  az webapp deployment slot swap \
    --resource-group $RESOURCE_GROUP \
    --name $APP_NAME \
    --slot $SLOT_NAME \
    --target-slot production

  echo "Swap complete!"
else
  echo "Staging health check failed!"
  exit 1
fi

Container App Deployment

bash
#!/bin/bash
set -e

RESOURCE_GROUP="myapp-rg"
REGISTRY="myregistry"
APP_NAME="myapp"
IMAGE_TAG=$(git rev-parse --short HEAD)

# Build and push image
az acr build \
  --registry $REGISTRY \
  --image $APP_NAME:$IMAGE_TAG \
  --image $APP_NAME:latest \
  .

# Update container app
az containerapp update \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --image $REGISTRY.azurecr.io/$APP_NAME:$IMAGE_TAG

echo "Deployed $IMAGE_TAG"

Deployment Strategies

Rolling Deployment

Default for App Service - gradual replacement:

yaml
- task: AzureWebApp@1
  inputs:
    azureSubscription: 'connection'
    appName: 'myapp'
    package: '$(Pipeline.Workspace)/app'

Slot Swap (Zero Downtime)

yaml
- task: AzureAppServiceManage@0
  inputs:
    azureSubscription: 'connection'
    action: 'Swap Slots'
    webAppName: 'myapp'
    sourceSlot: 'staging'
    targetSlot: 'production'

Canary Deployment

Route percentage of traffic to new version:

bash
# Deploy new version to staging
az webapp deployment source config-zip \
  --name myapp --slot staging --src app.zip

# Route 10% traffic to staging
az webapp traffic-routing set \
  --name myapp \
  --resource-group myapp-rg \
  --distribution staging=10

# If successful, increase
az webapp traffic-routing set \
  --distribution staging=50

# Complete rollout
az webapp deployment slot swap \
  --name myapp --slot staging

Environment Management

Multiple Environments

yaml
# azure-pipelines.yml
parameters:
  - name: environment
    type: string
    default: 'staging'
    values:
      - staging
      - production

variables:
  - ${{ if eq(parameters.environment, 'staging') }}:
      - name: appName
        value: 'myapp-staging'
  - ${{ if eq(parameters.environment, 'production') }}:
      - name: appName
        value: 'myapp-production'

Variable Groups

Store shared variables:

  1. Pipelines → Library → Variable groups
  2. Create group with variables
  3. Reference in pipeline:
yaml
variables:
  - group: 'my-variable-group'

Azure Key Vault Integration

yaml
steps:
  - task: AzureKeyVault@2
    inputs:
      azureSubscription: 'connection'
      KeyVaultName: 'myapp-keyvault'
      SecretsFilter: '*'

  - script: |
      echo "Using secret: $(MY-SECRET-NAME)"

Monitoring Deployments

Deployment Status

bash
# List deployments
az webapp deployment list \
  --name myapp \
  --resource-group myapp-rg \
  --output table

# Get deployment log
az webapp log deployment show \
  --name myapp \
  --resource-group myapp-rg \
  --deployment-id DEPLOYMENT_ID

Rollback

bash
# List deployment history
az webapp deployment list \
  --name myapp \
  --resource-group myapp-rg

# Redeploy previous version
az webapp deployment source config-zip \
  --name myapp \
  --resource-group myapp-rg \
  --src previous-version.zip

Best Practices

1. Use Branch Protection

  • Require PR reviews before merging to main
  • Require status checks to pass

2. Test Before Deploy

yaml
- script: npm test
  displayName: 'Run tests'
  continueOnError: false

3. Use Staging Environments

Test in staging before production.

4. Implement Health Checks

yaml
- script: |
    for i in {1..10}; do
      if curl -sf https://$APP_URL/health; then
        echo "Healthy!"
        exit 0
      fi
      sleep 10
    done
    exit 1
  displayName: 'Health check'

5. Notify on Failure

yaml
- task: SendEmail@1
  condition: failed()
  inputs:
    To: '[email protected]'
    Subject: 'Deployment Failed'
    Body: 'Pipeline $(Build.DefinitionName) failed.'
Note

Checkpoint: Create a CI/CD pipeline that automatically deploys your application when you push to the main branch.