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:
| Platform | Best For | Integration |
|---|---|---|
| GitHub Actions | GitHub repos, simple workflows | Native |
| Azure DevOps | Enterprise, complex pipelines | Native |
| Azure CLI | Scripts, simple automation | Direct |
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
- Go to your GitHub repository
- Settings → Secrets and variables → Actions
- Add secrets:
AZURE_CREDENTIALS- Entire JSON outputAZURE_SUBSCRIPTION_ID- Your subscription ID
Step 3: Create Workflow File
Create .github/workflows/azure-deploy.yml:
yamlname: 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
yamlname: 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)
yamlname: 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:
bashaz 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
- Go to dev.azure.com
- Create new organization (or use existing)
- Create new project
Step 2: Create Service Connection
- Project Settings → Service connections
- New service connection → Azure Resource Manager
- Choose authentication method:
- Workload Identity federation (recommended)
- Service principal (manual)
- Select subscription and resource group
Step 3: Create Pipeline
Basic Pipeline (azure-pipelines.yml)
yamltrigger: - 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
yamltrigger: - 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
- Pipelines → Environments
- Create
stagingandproduction - Add approval checks for production:
- Environment → Approvals and checks
- Add approval with required approvers
Container Pipeline
yamltrigger: - 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:
- Pipelines → Library → Variable groups
- Create group with variables
- Reference in pipeline:
yamlvariables: - group: 'my-variable-group'
Azure Key Vault Integration
yamlsteps: - 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.