CI/CD with Azure DevOps
Introduction
Continuous Integration and Continuous Deployment (CI/CD) is a modern software development approach that enables teams to deliver code changes more frequently and reliably. Azure DevOps is Microsoft's powerful platform that provides a complete set of tools to implement CI/CD workflows efficiently.
In this tutorial, we'll explore how to set up CI/CD pipelines using Azure DevOps, specifically designed for beginners who want to automate their build, test, and deployment processes.
What is CI/CD?
Before diving into Azure DevOps, let's understand the fundamental concepts:
- Continuous Integration (CI): The practice of frequently merging code changes into a central repository, followed by automated builds and tests.
- Continuous Deployment (CD): The automated process of deploying applications to various environments (development, testing, production) after passing the CI stage.
Here's a visual representation of a typical CI/CD workflow:
Getting Started with Azure DevOps
Step 1: Create an Azure DevOps Account and Project
- Visit Azure DevOps and sign up for a free account
- After signing in, create a new organization or use an existing one
- Create a new project by clicking the "New Project" button
- Give your project a name, select visibility (public or private), and click "Create"
Step 2: Set Up a Code Repository
Azure DevOps provides Git repositories for source control. Let's set one up:
- Navigate to "Repos" in the left sidebar
- Initialize a new repository or import an existing one
- If it's a new repository, you can clone it to your local machine:
# Clone the repository to your local machine
git clone https://dev.azure.com/{your-organization}/{your-project}/_git/{your-repo}
# Navigate to the repository
cd {your-repo}
# Create a sample application (for demonstration)
mkdir sample-app
cd sample-app
# Add your application files
# Commit and push your changes
git add .
git commit -m "Initial commit"
git push origin main
Building Your First CI Pipeline
Azure DevOps uses YAML files to define pipelines. Let's create a simple CI pipeline for a Node.js application:
Step 3: Create azure-pipelines.yml
In the root of your repository, create a file named azure-pipelines.yml
:
# Node.js CI Pipeline
trigger:
- main # Trigger the pipeline on changes to the main branch
pool:
vmImage: 'ubuntu-latest' # Use the latest Ubuntu agent
steps:
- task: NodeTool@0
inputs:
versionSpec: '16.x' # Use Node.js 16.x
displayName: 'Install Node.js'
- script: |
npm install
npm run build
displayName: 'npm install and build'
- script: |
npm test
displayName: 'run tests'
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/test-results.xml'
mergeTestResults: true
testRunTitle: 'Node.js Tests'
condition: succeededOrFailed() # Run this step even if previous steps failed
Step 4: Create and Run the Pipeline
- In Azure DevOps, navigate to "Pipelines" in the left sidebar
- Click "Create Pipeline"
- Select your repository location (Azure Repos Git)
- Choose your repository
- Select "Existing Azure Pipelines YAML file"
- Select the
azure-pipelines.yml
file you created - Click "Run" to start the pipeline
After running, you'll see the pipeline execution with all the defined steps. Each step shows its status (succeeded, failed) and logs.
Implementing Continuous Deployment
Now that we have CI in place, let's extend it to include CD:
Step 5: Modify the Pipeline for Deployment
Update your azure-pipelines.yml
to include deployment stages:
# Node.js CI/CD Pipeline
trigger:
- main
variables:
# Build configuration
buildConfiguration: 'Release'
# Azure Web App name
webAppName: 'your-app-name'
stages:
- stage: Build
displayName: 'Build stage'
jobs:
- job: Build
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '16.x'
displayName: 'Install Node.js'
- script: |
npm install
npm run build
displayName: 'npm install and build'
- script: |
npm test
displayName: 'run tests'
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
replaceExistingArchive: true
displayName: 'Archive files'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
displayName: 'Publish artifacts'
- stage: Deploy
displayName: 'Deploy stage'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: Deploy
pool:
vmImage: 'ubuntu-latest'
environment: 'development'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'Your-Azure-Subscription'
appName: '$(webAppName)'
package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'
displayName: 'Deploy to Azure Web App'
Step 6: Configure Deployment Environment
- In Azure DevOps, navigate to "Environments" under "Pipelines"
- Create a new environment named "development"
- Configure approval and check requirements if needed
Step 7: Configure Azure Service Connection
To deploy to Azure, you need to set up a service connection:
- Navigate to "Project settings" (bottom left)
- Select "Service connections"
- Click "New service connection"
- Choose "Azure Resource Manager"
- Follow the prompts to authenticate with your Azure account
- Name your connection 'Your-Azure-Subscription' (match the name in your YAML)
Working with Variable Groups and Secrets
For sensitive information like connection strings or API keys, use variable groups:
Step 8: Create a Variable Group
- Navigate to "Library" under "Pipelines"
- Click "+ Variable group"
- Name your group (e.g., "Development-Variables")
- Add variables with their values
- Toggle the lock icon for sensitive variables to make them secret
- Save the variable group
Step 9: Reference Variable Group in Pipeline
Update your pipeline to use the variable group:
trigger:
- main
variables:
- group: Development-Variables # Reference the variable group
stages:
# Your stages here
You can reference variables using $(variableName)
syntax in your pipeline.
CI/CD for Different Application Types
Web Applications
For web applications, you typically:
- Build the application
- Run tests
- Package the application
- Deploy to a web service (App Service, VM, etc.)
API Services
For APIs, you might want to:
- Build and test the API
- Run integration tests
- Deploy to the hosting environment
- Run API tests to verify deployment
Here's a sample pipeline for an ASP.NET Core Web API:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
steps:
- task: UseDotNet@2
inputs:
version: '6.0.x'
displayName: 'Install .NET 6'
- script: dotnet restore
displayName: 'Restore NuGet packages'
- script: dotnet build --configuration $(buildConfiguration)
displayName: 'Build the project'
- script: dotnet test
displayName: 'Run tests'
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
displayName: 'Publish the project'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'api-drop'
displayName: 'Publish artifacts'
Real-World Example: Full-Stack Application Deployment
Let's walk through a practical example of deploying a full-stack application with a React frontend and Node.js backend:
Frontend Pipeline (React)
trigger:
branches:
include:
- main
paths:
include:
- 'frontend/*'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '16.x'
displayName: 'Install Node.js'
- script: |
cd frontend
npm install
npm run build
displayName: 'Build React frontend'
- task: CopyFiles@2
inputs:
SourceFolder: 'frontend/build'
Contents: '**'
TargetFolder: '$(Build.ArtifactStagingDirectory)/frontend'
displayName: 'Copy frontend build files'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/frontend'
ArtifactName: 'frontend'
displayName: 'Publish frontend artifact'
Backend Pipeline (Node.js)
trigger:
branches:
include:
- main
paths:
include:
- 'backend/*'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '16.x'
displayName: 'Install Node.js'
- script: |
cd backend
npm install
displayName: 'Install backend dependencies'
- script: |
cd backend
npm test
displayName: 'Run backend tests'
- task: CopyFiles@2
inputs:
SourceFolder: 'backend'
Contents: |
**
!node_modules/**
TargetFolder: '$(Build.ArtifactStagingDirectory)/backend'
displayName: 'Copy backend files'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/backend'
ArtifactName: 'backend'
displayName: 'Publish backend artifact'
Deployment Pipeline
Create a separate deployment pipeline that consumes the artifacts produced by the frontend and backend pipelines:
trigger: none # Manual trigger or trigger from other pipelines
resources:
pipelines:
- pipeline: frontendBuild
source: 'Frontend-CI' # Name of your frontend pipeline
- pipeline: backendBuild
source: 'Backend-CI' # Name of your backend pipeline
stages:
- stage: Deploy
jobs:
- job: DeployFrontend
pool:
vmImage: 'ubuntu-latest'
steps:
- download: frontendBuild
artifact: 'frontend'
- task: AzureStaticWebApp@0
inputs:
azure_static_web_apps_api_token: $(StaticWebAppToken)
app_location: '$(Pipeline.Workspace)/frontendBuild/frontend'
displayName: 'Deploy frontend to Static Web App'
- job: DeployBackend
pool:
vmImage: 'ubuntu-latest'
steps:
- download: backendBuild
artifact: 'backend'
- task: AzureWebApp@1
inputs:
azureSubscription: 'Your-Azure-Subscription'
appType: 'webAppLinux'
appName: '$(BackendAppName)'
package: '$(Pipeline.Workspace)/backendBuild/backend'
startUpCommand: 'npm start'
displayName: 'Deploy backend to App Service'
Advanced Azure DevOps Features
Gates and Approvals
To add approval gates:
- Navigate to your environment in the "Environments" section
- Click "Add check" under "Checks"
- Select "Approvals"
- Add the required approvers and configure timeout settings
Release Gates
Release gates validate your deployments against external criteria before or after deployment:
environments:
- name: Production
deployment:
gates:
- task: AzureMonitor@0
inputs:
connectedServiceNameARM: 'Your-Azure-Subscription'
resourceGroupName: 'your-resource-group'
resourceType: 'Microsoft.Insights/components'
resourceName: 'your-application-insights'
alertRule: 'Failed Requests'
Branch Policies
To enforce code quality, configure branch policies:
- Navigate to "Repos" > "Branches"
- Find your main branch, click "..." and select "Branch policies"
- Configure required reviewers, build validation, and other policies
Tips for Effective CI/CD Implementation
- Start Small: Begin with a simple CI pipeline, then gradually add CD stages
- Use Branch Policies: Ensure code quality with required reviews and build validation
- Parallelize Tasks: Speed up pipelines by running independent tasks in parallel
- Cache Dependencies: Cache npm, NuGet packages to reduce build time
- Monitor Pipeline Metrics: Track build times, failure rates, and deployment frequencies
- Implement Infrastructure as Code: Use ARM templates or Terraform for infrastructure deployments
- Secure Your Secrets: Never hardcode credentials; use variable groups and Azure Key Vault
Common Issues and Troubleshooting
Failed Builds
- Check that all required tools and dependencies are installed in your pipeline
- Verify your code builds successfully in a local environment
- Review pipeline logs for specific error messages
Failed Deployments
- Check service connection permissions
- Verify that your deployment configuration is correct
- Test deployment scripts locally before running them in the pipeline
Permission Issues
- Review Azure DevOps permissions for your team
- Ensure your service principal has proper permissions in Azure
Summary
In this tutorial, we've explored how to set up CI/CD pipelines using Azure DevOps, from basic integration to advanced deployment strategies. We've covered:
- Creating Azure DevOps projects and repositories
- Building CI pipelines for different application types
- Implementing CD pipelines for automated deployments
- Working with variable groups and secrets
- Setting up approval gates and branch policies
- Best practices for effective CI/CD implementation
By implementing CI/CD with Azure DevOps, you can dramatically improve your development workflow, reduce manual errors, and deliver high-quality software more frequently.
Additional Resources
Exercises
- Create a simple web application and set up a basic CI pipeline
- Extend your pipeline to include automated testing
- Configure a CD pipeline to deploy your application to Azure App Service
- Implement branch policies and pull request validation
- Add approval gates to your production deployment stage
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)