.NET CI/CD Pipeline
Introduction
Continuous Integration and Continuous Deployment (CI/CD) pipelines have become essential components in modern software development. They automate the process of building, testing, and deploying applications, helping developers deliver high-quality code more efficiently and reliably.
In this tutorial, we'll explore how to set up CI/CD pipelines for .NET applications. We'll look at the core concepts, tools, and practical implementations that will help you automate your .NET application's development lifecycle.
What is a CI/CD Pipeline?
A CI/CD pipeline is an automated workflow that helps developers integrate code changes more frequently and reliably deploy applications to production environments.
- Continuous Integration (CI) involves automatically building and testing code changes whenever they're committed to version control.
- Continuous Deployment (CD) extends the CI process by automatically deploying successful builds to various environments (development, staging, production).
Benefits of CI/CD for .NET Applications
- Faster Development Cycles: Automate repetitive tasks like building, testing, and deployment
- Higher Code Quality: Detect bugs early through automated testing
- Consistent Deployments: Reduce human error in deployment processes
- Quicker Feedback: Get immediate feedback on code changes
- Reduced Risk: Deploy smaller changes more frequently
Essential Components of a .NET CI/CD Pipeline
A typical .NET CI/CD pipeline includes these key stages:
- Source Control: Code repository (GitHub, Azure DevOps, GitLab)
- Build: Compilation and assembly of code
- Test: Running unit tests, integration tests, etc.
- Package: Creating deployment packages (NuGet, Docker images)
- Deploy: Deploying to target environments
Setting Up CI/CD for .NET Applications
Let's explore three popular platforms for setting up CI/CD pipelines for .NET applications:
1. GitHub Actions
GitHub Actions allows you to create workflows directly in your GitHub repository. Here's how to create a basic CI/CD pipeline for a .NET application:
- Create a
.github/workflows
directory in your repository - Add a YAML file (e.g.,
dotnet.yml
) with your workflow configuration
name: .NET CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
- name: Publish
run: dotnet publish -c Release -o ./publish
- name: Deploy to Azure Web App
if: github.ref == 'refs/heads/main'
uses: azure/webapps-deploy@v2
with:
app-name: 'your-app-name'
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ./publish
This workflow will:
- Trigger when code is pushed to the main branch or when a pull request targets the main branch
- Set up .NET on the runner
- Restore dependencies, build, and test your application
- Publish the application if all previous steps succeed
- Deploy to an Azure Web App if the branch is main
2. Azure DevOps Pipelines
Azure DevOps offers robust CI/CD capabilities that integrate well with .NET applications. Here's a sample YAML pipeline:
trigger:
- main
pool:
vmImage: 'windows-latest'
variables:
buildConfiguration: 'Release'
dotnetSdkVersion: '7.0.x'
stages:
- stage: 'Build'
displayName: 'Build and Test'
jobs:
- job: 'BuildAndTest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '$(dotnetSdkVersion)'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'Test'
inputs:
command: 'test'
projects: '**/*Tests/*.csproj'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
- stage: 'Deploy'
displayName: 'Deploy to Azure'
dependsOn: 'Build'
condition: succeeded()
jobs:
- job: 'DeployToAzure'
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'drop'
downloadPath: '$(System.ArtifactsDirectory)'
- task: AzureRmWebAppDeployment@4
inputs:
ConnectionType: 'AzureRM'
azureSubscription: 'Your-Azure-Subscription'
appType: 'webApp'
WebAppName: 'your-app-name'
packageForLinux: '$(System.ArtifactsDirectory)/**/*.zip'
This pipeline organizes the workflow into stages for build and deploy, making it clear which steps belong to each phase of the process.
3. Jenkins Pipeline for .NET
If you're using Jenkins for CI/CD, you can define a pipeline in a Jenkinsfile
:
pipeline {
agent any
environment {
DOTNET_CLI_HOME = '/tmp/dotnet_cli_home'
DOTNET_CLI_TELEMETRY_OPTOUT = '1'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Restore') {
steps {
sh 'dotnet restore'
}
}
stage('Build') {
steps {
sh 'dotnet build --configuration Release --no-restore'
}
}
stage('Test') {
steps {
sh 'dotnet test --configuration Release --no-build'
}
}
stage('Publish') {
steps {
sh 'dotnet publish --configuration Release --no-build --output ./publish'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
// Example deployment command (adjust as needed)
sh 'echo "Deploying application..."'
// Add your deployment steps here
}
}
}
post {
always {
// Clean up workspace
cleanWs()
}
success {
echo 'CI/CD pipeline completed successfully!'
}
failure {
echo 'CI/CD pipeline failed!'
}
}
}
Real-World Example: Building a Complete CI/CD Pipeline for a .NET Core Web API
Let's build a practical example of a CI/CD pipeline for a .NET Core Web API that includes:
- Building and testing the application
- Creating a Docker image
- Deploying to Kubernetes
Step 1: Create a Basic .NET Core Web API
First, we'll need a simple Web API to work with:
// Controllers/WeatherForecastController.cs
using Microsoft.AspNetCore.Mvc;
namespace MyApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
}
Step 2: Create a Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["MyApi.csproj", "./"]
RUN dotnet restore "MyApi.csproj"
COPY . .
RUN dotnet build "MyApi.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
EXPOSE 80
ENTRYPOINT ["dotnet", "MyApi.dll"]
Step 3: Create a GitHub Actions Workflow for Complete CI/CD
name: .NET API CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-test-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore -c Release
- name: Test
run: dotnet test --no-build --verbosity normal -c Release
# Only proceed with Docker build and deployment on main branch
- name: Login to Container Registry
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Deploy to Kubernetes
- name: Set up kubectl
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: azure/setup-kubectl@v3
- name: Set Kubernetes context
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBECONFIG }}
- name: Deploy to Kubernetes
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
# Update the k8s deployment manifest with the new image
sed -i 's|IMAGE_TO_REPLACE|${{ steps.meta.outputs.tags }}|g' k8s/deployment.yaml
# Apply the Kubernetes manifests
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
Step 4: Create Kubernetes Manifests
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dotnet-api
labels:
app: dotnet-api
spec:
replicas: 3
selector:
matchLabels:
app: dotnet-api
template:
metadata:
labels:
app: dotnet-api
spec:
containers:
- name: dotnet-api
image: IMAGE_TO_REPLACE # This will be replaced during CI/CD
ports:
- containerPort: 80
resources:
limits:
cpu: "500m"
memory: "256Mi"
requests:
cpu: "200m"
memory: "128Mi"
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: dotnet-api-service
spec:
selector:
app: dotnet-api
ports:
- port: 80
targetPort: 80
type: ClusterIP
Advanced CI/CD Pipeline Techniques
1. Multi-Environment Deployments
For enterprise applications, you might need to deploy to multiple environments:
name: Multi-Environment Deployment
on:
push:
branches: [ main ]
jobs:
build:
# Build and test steps here
deploy-dev:
needs: build
environment: Development
runs-on: ubuntu-latest
steps:
# Deploy to development environment
deploy-staging:
needs: deploy-dev
environment: Staging
runs-on: ubuntu-latest
steps:
# Deploy to staging environment
deploy-production:
needs: deploy-staging
environment: Production
# Requires manual approval
runs-on: ubuntu-latest
steps:
# Deploy to production environment
2. Database Migrations
Including database migrations in your CI/CD pipeline:
- name: Apply Database Migrations
run: |
dotnet tool install --global dotnet-ef
dotnet ef database update --project src/MyApp.Data
env:
ConnectionStrings__DefaultConnection: ${{ secrets.DB_CONNECTION }}
3. Security Scanning
Adding security scanning to your pipeline:
- name: Security scan
uses: snyk/actions/dotnet@master
with:
args: --severity-threshold=high
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
Best Practices for .NET CI/CD Pipelines
- Keep builds fast: Optimize build scripts and use build caching
- Separate unit and integration tests: Run unit tests early and frequently
- Use environment variables for sensitive information
- Version your artifacts: Tag Docker images, NuGet packages with appropriate versions
- Implement proper branch policies: Protect important branches
- Use Infrastructure as Code (IaC): Define infrastructure with tools like Terraform or ARM templates
- Monitor your pipelines: Set up alerts for failed builds/deployments
- Include quality gates: Code coverage, static code analysis
Troubleshooting Common Issues
Here are some common issues you might encounter with .NET CI/CD pipelines and how to solve them:
Build Failures
- Issue: Build fails due to missing dependencies
- Solution: Ensure your pipeline has proper restore steps before build
Test Failures
- Issue: Tests pass locally but fail in CI
- Solution: Check for environment-specific dependencies or race conditions
Deployment Failures
- Issue: Deployment fails with permission errors
- Solution: Verify service principal or account permissions
Summary
In this tutorial, we've covered:
- The fundamentals of CI/CD pipelines for .NET applications
- Setting up CI/CD with GitHub Actions, Azure DevOps, and Jenkins
- A complete real-world example with Docker and Kubernetes deployment
- Advanced techniques and best practices for effective CI/CD
By implementing CI/CD pipelines, you can significantly improve your .NET application development workflow, making it more efficient, reliable, and maintainable.
Additional Resources
- Microsoft's official documentation on DevOps for .NET
- GitHub Actions for .NET
- Azure DevOps Documentation
Practice Exercises
- Set up a basic CI pipeline for a simple .NET console application
- Extend your pipeline to include unit testing with xUnit or NUnit
- Create a CD pipeline that deploys your application to Azure App Service
- Implement a multi-stage pipeline that deploys to staging first, then production
- Add code quality checks using SonarQube or other static analysis tools
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)