{{announcement.body}}
{{announcement.title}}

CI/CD as a Code for .NET Core Application and Kubernetes

DZone 's Guide to

CI/CD as a Code for .NET Core Application and Kubernetes

The process of building a simple CI/CD pipeline for existing .net core application to moving it to Azure Kubernetes Services using Azure DevOps.

· Microservices Zone ·
Free Resource

Building CI/CD process for the .NET Core application could be complicated especially when you are dealing with Kubernetes and Docker and if you need to include code style analysis, unit test, and code coverage report.

In this article, I am going to explain the process of building a simple CI/CD pipeline for existing .net core application to moving it to Azure Kubernetes Services using Azure DevOps. The final Pipeline will be easy to understand and reusable.

Deployment Architecture

The deployment process includes the following steps: fetching the code from Git Repository, building application, restoring NuGet packages, running unit test, building unit test and code coverage reports, pushing docker image to the registry and deploying to AKS cluster.

Azure Kubernetes Cluster Setup

I have used a simple AKS cluster, with 3 nodes architecture that contains an Ingress-Nginx load balancer. You can also use Traefik.io, istio.io even Standard Azure Load Balancer.

internet traffic

A detailed description of how to set up the AKS cluster including Ingress setup and deployment scripts can be found here.

After deploying AKS, the main resource group it will look like in the image below.

AKS deployment

Setup Service Connections

Before starting to configure the main pipeline steps the connection between Azure Container Registry(ACR) and Azure Kubernetes service needs to be granted by granting access of AKS service principal to ACR. RBAC service principal for Azure DevOps is created and everything is ready to push and pull Docker images withing pipelines. Alternatively, you can do it in Azure DevOps Service Connection which I will explain in the next session.

PowerShell
 




xxxxxxxxxx
1
16


 
1
$AksResourceGroup = '<rg-name>'
2
$AksClusterName = '<aks-cluster-name>'
3
$AcrName = '<acr-name>' 
4
$AcrResourceGroup = '<acr-rg-name>' 
5
 
          
6
# Get the id of the AKS service principal
7
ClientID=$(az aks show --resource-group $AksResourceGroup --name $AksClusterName --query "servicePrincipalProfile.clientId" --output tsv)
8
 
          
9
# Get the ACR registry resource id
10
AcrId=$(az acr show --name $AcrName --resource-group $AcrResourceGroup --query "id" --output tsv)
11
 
          
12
# Create role assignment
13
az role assignment create --assignee $ClientID --role acrpull --scope $AcrId
14
 
          
15
# Create a specific Service Principal for our Azure DevOps pipelines to be able to push and pull images and charts of our ACR
16
$ registryPassword=$(az ad sp create-for-rbac -n $acr-push --scopes $AcrId --role acrpush --query password -o tsv)



Here you can find the detailed description of how to configure a connection between ACR and AKS. 

Azure DevOps Service Connection With Azure Kubernetes Services and Azure Container Registry

By Using Service Connection you can connect Azure DevOps to your, already deployed AKS cluster, Azure Container Registry, Docker Registry (Docker Hub), and many other services.

docker hub

The creation of connection to ACR is quite easy, you just need to specify a connection name, a subscription, and a registry name and that’s it. 

devicemanagerreg

You can connect (and authenticate) your AKS cluster using Kubeconfig, Service Account, and Azure Subscription. In my project, I used Kubeconfig, as one of the last options, because you need just find kubeconfig JSON, copy it, and choose your Cluster context. 

aks-connect

You can find KubeConfig it in the following directory (In the Windows): C:\Users\your_user_name\.kube\config. Here is documentation on how to find and work with KubeConfig also for Linux and Mac. 

Pipeline Steps

Finlay I’m moving to pipeline steps and the very first step is to use a script for restoring all NuGet dependencies/packages, building dot net core application, Running Unit Tests, building code coverage report. Also please notice that I use system variable $(Build.BuildNumber) as a tag for the coverage report generation. At the end test results will be published as artifacts and Azure DevOps can build visualization analytics charts.

YAML
 




xxxxxxxxxx
1


 
1
- script: |
2
    dotnet restore
3
    dotnet build ./src/DeviceManager.Api/ --configuration $(buildConfiguration)
4
    dotnet test ./test/DeviceManager.Api.UnitTests/ --configuration $(buildConfiguration) --filter Category!=Integration --logger "trx;LogFileName=testresults.trx"
5
    dotnet test ./test/DeviceManager.Api.UnitTests/ --configuration $(buildConfiguration) --filter Category!=Integration /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(System.DefaultWorkingDirectory)/TestResults/Coverage/
6
    cd ./test/DeviceManager.Api.UnitTests/
7
    dotnet reportgenerator "-reports:$(System.DefaultWorkingDirectory)/TestResults/Coverage/coverage.cobertura.xml" "-targetdir:$(System.DefaultWorkingDirectory)/TestResults/Coverage/Reports" "-reportTypes:htmlInline" "-tag:$(Build.BuildNumber)"
8
    cd ../../
9
    dotnet publish ./src/DeviceManager.Api/ --configuration $(buildConfiguration) --output $BUILD_ARTIFACTSTAGINGDIRECTORY



Below you can see Tests blade with statistic data and Test Results in the Tests section of the Azure DevOps.

azure devops

VS Test run

Step 2 — Code Coverage Results

The next step is to publish code coverage results to DefaultWorkingDirectory. The whole process is based on Cubertura Report Generator, a .Net core library.

YAML
 




xxxxxxxxxx
1


 
1
- task: PublishCodeCoverageResults@1
2
  inputs:
3
    codeCoverageTool: cobertura
4
    summaryFileLocation: $(System.DefaultWorkingDirectory)/TestResults/Coverage/**/*.xml
5
    reportDirectory: $(System.DefaultWorkingDirectory)/TestResults/Coverage/Reports
6
    failIfCoverageEmpty: false



The first statistic results are already available: 

PublishCodeCoverageResults

The detailed file by file report you can find on the Code Coverage tab. This report is based on generated xml reports. 

XML reports

Step 3 and 4 — Building Container and Pushing it to the ACR

After the previous steps have been completed the project is to be put into a container. For this, I am going to use version 1 Docker step. You need to specify a path to a Docker file as I did it here, provide image name, for example, boriszn/devicemanagerapi and tag — 1.102.1., and the last step is to specify a container registry, for example, devicemanagerreg.azurecr.io.

For a tag creation, I used semantic versioning and hard-coded some version numbers to simplify the Pipeline, however you can also use different approaches to build the a tag, for example using variables that are required to run a pipeline or take then from git version.

YAML
 




xxxxxxxxxx
1


1
- task: Docker@1
2
  displayName: 'Containerize the application'
3
  inputs:
4
    azureSubscriptionEndpoint: $(serviceConnection)
5
    azureContainerRegistry: $(containerRegistry)
6
    dockerFile: './src/DeviceManager.Api/Dockerfile'
7
    imageName: '$(fullImageName)'
8
    includeLatestTag: true



The next step is to push our dockerized app to the Azure Container Registry. Here I specified command and image to push. You should take into account that I pushed 2 images with the first image tag 1.58338.4 and the second one is the tag: latest

YAML
 




xxxxxxxxxx
1
15


1
- task: Docker@1
2
  displayName: 'Push image'
3
  inputs:
4
    azureSubscriptionEndpoint: $(serviceConnection)
5
    azureContainerRegistry: $(containerRegistry)
6
    command: 'Push an image'
7
    imageName: '$(fullImageName)'
8
 
          
9
- task: Docker@1
10
  displayName: 'Push latest image'
11
  inputs:
12
    azureSubscriptionEndpoint: $(serviceConnection)
13
    azureContainerRegistry: $(containerRegistry)
14
    command: 'Push an image'
15
    imageName: '$(imageName):latest'



push image

device manager

AKS Deployment Steps

For Azure Kubernetes Deployment you need to replace a build number in AKS deployment YAML file and display it in Azure DevOps after successful execution.

YAML
 




xxxxxxxxxx
1
10


 
1
- task: PowerShell@2
2
  displayName: 'Replace version number in AKS deployment yaml'
3
  inputs:
4
    targetType: inline
5
    script: |
6
        # Replace image tag in aks YAML
7
        ((Get-Content -path $(aksKubeDeploymentYaml) -Raw) -replace '##BUILD_ID##','$(imageTag)') | 
8
        Set-Content -Path $(aksKubeDeploymentYaml)
9
        # Get content
10
        Get-Content -path  $(aksKubeDeploymentYaml)



After that run the “Apply” command for the AKS cluster.

Two important parameters here are a path to my cluster deployment YAML script (./deployment/aks-deployment.yaml) and a cluster name (device-manager-API-aks). Other parameters were explained in the previous sections.

YAML
 




xxxxxxxxxx
1


 
1
- task: Kubernetes@1
2
  displayName: 'kubectl apply'
3
  inputs:
4
    kubernetesServiceEndpoint: $(kubernetesServiceEndpoint)
5
    azureSubscriptionEndpoint: $(serviceConnection)
6
    azureResourceGroup: $(azureResourceGroupName)
7
    kubernetesCluster: $(aksClusterName)
8
    arguments: '-f $(aksKubeDeploymentYaml)'
9
    command: 'apply'



kubectl apply

That’s it! Below I’ve listed a complete YAML pipeline that we configured in the previous few sections and that is easy to import to your Azure DevOps projects. If you have any questions reach out in the comments! 

YAML
 




xxxxxxxxxx
1
86


1
trigger:
2
- develop
3
 
          
4
pool:
5
  vmImage: 'ubuntu-latest'
6
 
          
7
variables:
8
  imageName: 'boriszn/devicemanagerapi'
9
  buildConfiguration: 'Release'
10
  fullImageName: '$(imageName):$(imageTag)'
11
  containerRegistry: devicemanagerreg.azurecr.io
12
  imageTag: '1.$(build.buildId).4'
13
  serviceConnection: 'az-connect'
14
  azureResourceGroupName: 'boriszn-rg-aks-devicemanager-api-we'
15
  aksClusterName: 'device-manager-api-aks'
16
  aksKubeDeploymentYaml: './deployment/aks-deployment.yaml'
17
  kubernetesServiceEndpoint: 'device-managerapi-aks-connect'
18
 
          
19
steps:
20
- script: |
21
    dotnet restore
22
    dotnet build ./src/DeviceManager.Api/ --configuration $(buildConfiguration)
23
    dotnet test ./test/DeviceManager.Api.UnitTests/ --configuration $(buildConfiguration) --filter Category!=Integration --logger "trx;LogFileName=testresults.trx"
24
    dotnet test ./test/DeviceManager.Api.UnitTests/ --configuration $(buildConfiguration) --filter Category!=Integration /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(System.DefaultWorkingDirectory)/TestResults/Coverage/
25
    cd ./test/DeviceManager.Api.UnitTests/
26
    dotnet reportgenerator "-reports:$(System.DefaultWorkingDirectory)/TestResults/Coverage/coverage.cobertura.xml" "-targetdir:$(System.DefaultWorkingDirectory)/TestResults/Coverage/Reports" "-reportTypes:htmlInline" "-tag:$(Build.BuildNumber)"
27
    cd ../../
28
    dotnet publish ./src/DeviceManager.Api/ --configuration $(buildConfiguration) --output $BUILD_ARTIFACTSTAGINGDIRECTORY
29
- task: PublishTestResults@2
30
  inputs:
31
    testRunner: VSTest
32
    testResultsFiles: '**/*.trx'
33
 
          
34
- task: PublishCodeCoverageResults@1
35
  inputs:
36
    codeCoverageTool: cobertura
37
    summaryFileLocation: $(System.DefaultWorkingDirectory)/TestResults/Coverage/**/*.xml
38
    reportDirectory: $(System.DefaultWorkingDirectory)/TestResults/Coverage/Reports
39
    failIfCoverageEmpty: false
40
 
          
41
- task: PublishBuildArtifacts@1
42
 
          
43
- task: Docker@1
44
  displayName: 'Containerize the application'
45
  inputs:
46
    azureSubscriptionEndpoint: $(serviceConnection)
47
    azureContainerRegistry: $(containerRegistry)
48
    dockerFile: './src/DeviceManager.Api/Dockerfile'
49
    imageName: '$(fullImageName)'
50
    includeLatestTag: true
51
 
          
52
- task: Docker@1
53
  displayName: 'Push image'
54
  inputs:
55
    azureSubscriptionEndpoint: $(serviceConnection)
56
    azureContainerRegistry: $(containerRegistry)
57
    command: 'Push an image'
58
    imageName: '$(fullImageName)'
59
 
          
60
- task: Docker@1
61
  displayName: 'Push latest image'
62
  inputs:
63
    azureSubscriptionEndpoint: $(serviceConnection)
64
    azureContainerRegistry: $(containerRegistry)
65
    command: 'Push an image'
66
    imageName: '$(imageName):latest' 
67
 
          
68
- task: PowerShell@2
69
  displayName: 'Replace version number in AKS deployment yaml'
70
  inputs:
71
    targetType: inline
72
    script: |
73
        # Replace image tag in aks YAML
74
        ((Get-Content -path $(aksKubeDeploymentYaml) -Raw) -replace '##BUILD_ID##','$(imageTag)') | 
75
        Set-Content -Path $(aksKubeDeploymentYaml)
76
        # Get content
77
        Get-Content -path  $(aksKubeDeploymentYaml)
78
- task: Kubernetes@1
79
  displayName: 'kubectl apply'
80
  inputs:
81
    kubernetesServiceEndpoint: $(kubernetesServiceEndpoint)
82
    azureSubscriptionEndpoint: $(serviceConnection)
83
    azureResourceGroup: $(azureResourceGroupName)
84
    kubernetesCluster: $(aksClusterName)
85
    arguments: '-f $(aksKubeDeploymentYaml)'
86
    command: 'apply'


Topics:
.net core ,aks ,azure devops ,cicd pipeline ,continious deployment ,kubernates ,microservice ,yaml

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}