As we continue writing this serie , we come to the part were we are going to deploy our apps to our Azure Kubernetes service using Azure Devops
This article is a part of a series:
- Part 1 : How to setup nginx reverse proxy for aspnet core apps with and without Docker compose
- Part 2 :How to setup nginx reverse proxy && load balancer for aspnet core apps with Docker and azure kubernetes service
- Part 3 : How to configure an ingress controller using TLS/SSL for the Azure Kubernetes Service (AKS)
- Part 4 : switch to Azure Container Registry from Docker Hub
- Part 5-A: Using Azure DevOps, Automate Your CI/CD Pipeline and Your Deployments (setup)
- Part 5-B : Using Azure DevOps, Automate Your CI/CD Pipeline and Your Deployments (terraform)
- Part 5-C : Using Azure DevOps, Automate Your CI/CD Pipeline and Your Deployments (deployments)
- Part 6 : Using Github, Automate Your CI/CD Pipeline and Your Deployments
- Possible methods to reduce your costs .
Introduction :
Auto deploying deployment using Azure DevOps and AKS (Azure Kubernetes Service) is a powerful way to streamline the deployment process for containerized applications. With Azure DevOps, you can create automated deployment pipelines that trigger a release whenever new code is pushed to a repository. AKS provides a fully managed Kubernetes service that simplifies the deployment and operation of containerized applications, ensuring high availability, scalability, and security for production deployments. By integrating Azure DevOps with AKS, you can deploy applications to the cluster with a single click, and easily roll back deployments to a previous version if needed. This approach enables teams to deliver updates and new features to production more quickly and reliably, while minimizing the risk of errors or downtime.In different blog post we have seen how to deploy our infrastructure , right now , one more step , is auto deploying to aks .
Prerequisites
– Created service connection for azure container registry .
– Create a service connection for azure Kubernetes service .
1-Creating our Pipeline
Our pipeline will have different steps and we will explain each part in this part:
Full pipline
name: $(BuildDefinitionName)_$(date:yyyyMMdd)$(rev:.r)
trigger:
branches:
include:
- dev
- master
#our agent
pool:
name: demo-privateAgentDevOps
demands:
- Agent.Name -equals DevopsAg01
variables:
imageRepository: 'mydotnetappdemo'
containerRegistry: 'crdvsaksdevfc01.azurecr.io'
containerRegistryspn: 'acr-aks-001'
dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
hostnamesManifest: '$(Pipeline.Workspace)/manifests/01-backend-acr-deployment.yml'
frontendManifest: '$(Pipeline.Workspace)/manifests/01-nginx-frontend.yml'
ingressManifest: '$(Pipeline.Workspace)/manifests/ingress.yml'
imagePullSecret: 'context-auth'
kubernetesServiceConnection: 'aksspn'
hostnameImageRepo: 'mydotnetappdemo'
tag: '$(Build.BuildId)'
stages:
- stage: Build
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
displayName: Build and push Docker image to ACR
jobs:
- job: Build
displayName: Restore project
steps:
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: Docker@2
displayName: 'Build Project'
inputs:
containerRegistry: $(containerRegistryspn)
repository: '$(imageRepository)'
command: 'build'
Dockerfile: '**/Dockerfile'
tags: |
$(tag)
latest
- task: Docker@2
displayName: 'Push Docker image'
inputs:
containerRegistry: $(containerRegistryspn)
repository: $(imageRepository)
command: 'push'
tags: |
$(tag)
latest
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Pipeline.Workspace)/s/manifests'
artifact: 'manifests'
publishLocation: 'pipeline'
- stage: DEV
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
displayName: DEV - Deploy API to AKS
dependsOn: Build
jobs:
- deployment: Deploy
displayName: Deploy DEV
environment: 'development'
strategy:
runOnce:
deploy:
steps:
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'manifests'
targetPath: '$(Pipeline.Workspace)/manifests'
- task: replacetokens@3
displayName: Replace Build tag & environment values
inputs:
rootDirectory: '$(Pipeline.Workspace)/manifests/'
targetFiles: '*.yml'
encoding: 'utf-8'
writeBOM: true
actionOnMissing: 'warn'
keepToken: false
tokenPrefix: '#{'
tokenSuffix: '}#'
useLegacyPattern: false
enableTransforms: false
enableTelemetry: false
- task: KubernetesManifest@1
inputs:
action: 'createSecret'
connectionType: 'azureResourceManager'
azureSubscriptionConnection: 'terrafromspn'
azureResourceGroup: 'azure-loves-terraform-2023'
kubernetesCluster: 'achrafdoingaks'
secretType: 'dockerRegistry'
secretName: $(imagePullSecret)
dockerRegistryEndpoint: 'acr-aks-001'
- task: KubernetesManifest@0
displayName: Deploy to Kubernetes cluster
inputs:
action: deploy
kubernetesServiceConnection: $(kubernetesServiceConnection)
namespace: 'ingress'
manifests: |
$(hostnamesManifest)
$(ingressManifest)
imagePullSecrets: |
$(imagePullSecret)
containers: |
$(containerRegistry)/$(hostnameImageRepo):$(tag)
- task: PublishBuildArtifacts@1
displayName: Publish Swaggers
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'swagger-dev'
publishLocation: 'Container'
What will the pipeline look like :


Now let’s understand this pipeline by task :
The first stage of our pipeline will consist of building our project (dotnet ) , building the docker image and pushing it to our azure container registry .
The second part , will run only if the first stage is done with success .
replacetokens@3
Replace Tokens is an extension that can be used in Azure DevOps, which provides the possibility to replace tokens in the code files with variables values (which can be configured in the Pipelines Library) during the execution of the CI/CD process.
In our case , in each time we build our solution we want to update our deployment the build tag of the new image that we have created .
You may say why not using latest ?
When deploying Docker images, it is generally recommended to avoid using the “latest” tag and instead use a specific version or build tag. The reason for this is that the “latest” tag is a floating tag that always points to the most recent build of an image. This can lead to unpredictability and make it difficult to manage and track the versions of deployed images.
By using a specific version or build tag, you can ensure that the same version of an image is deployed consistently across different environments, such as development, testing, and production. This can also make it easier to troubleshoot issues and roll back to a previous version if necessary.
When using build tags, it is common to use a versioning scheme that includes the version number and the Git commit hash or build ID. This enables you to track the version of the code that was used to build the image and ensure consistency across different environments.
In summary, it is generally better to use a specific version or build tag when deploying Docker images, rather than relying on the “latest” tag, to ensure consistency and predictability in your deployment process.
- task: replacetokens@3
displayName: Replace Build tag & environment values
inputs:
rootDirectory: '$(Pipeline.Workspace)/manifests/'
targetFiles: '*.yml'
encoding: 'utf-8'
writeBOM: true
actionOnMissing: 'warn'
keepToken: false
tokenPrefix: '#{'
tokenSuffix: '}#'
useLegacyPattern: false
enableTransforms: false
enableTelemetry: false
this task will replace the code inside the #{Build.BuildId}# below inside our deployement file with the build id of our pipline
containers:
- name: backend-restapp
image: crdvsaksdevfc01.azurecr.io/mydotnetappdemo:#{Build.BuildId}#
KubernetesManifest@1
The Create imagePullSecret parameter in the KubernetesManifest@1 task in Azure DevOps is used to create a Docker registry credential secret in the target Kubernetes cluster.
When you deploy a container to a Kubernetes cluster, the container image is typically stored in a Docker registry, such as Docker Hub or Azure Container Registry. In order to pull the image from the registry, the Kubernetes cluster needs to be authenticated with the registry using credentials, such as a username and password or a token.
To avoid storing these credentials in plaintext in the Kubernetes manifest, you can create an image pull secret that contains the credentials and reference it in the manifest file. The KubernetesManifest@1 task in Azure DevOps can create this image pull secret for you by specifying the Docker registry server, username, and password as task inputs.
When you enable the Create imagePullSecret parameter in the KubernetesManifest@1 task, Azure DevOps creates a secret in the target Kubernetes cluster that contains the Docker registry credentials. The secret is named “acr-credentials” and can be referenced in the manifest file using the “imagePullSecrets” field.
By creating the image pull secret with Azure DevOps, you can securely authenticate your Kubernetes cluster with your Docker registry and ensure that your container images can be pulled successfully during deployment.
- task: KubernetesManifest@1
inputs:
action: 'createSecret'
connectionType: 'azureResourceManager'
azureSubscriptionConnection: 'terrafromspn'
azureResourceGroup: 'azure-loves-terraform-2023'
kubernetesCluster: 'achrafdoingaks'
secretType: 'dockerRegistry'
secretName: $(imagePullSecret)
dockerRegistryEndpoint: 'acr-aks-001'
Now time to deploy :
- task: KubernetesManifest@0
displayName: Deploy to Kubernetes cluster
inputs:
action: deploy
kubernetesServiceConnection: $(kubernetesServiceConnection)
namespace: 'ingress'
manifests: |
$(hostnamesManifest)
$(ingressManifest)
imagePullSecrets: |
$(imagePullSecret)
containers: |
$(containerRegistry)/$(hostnameImageRepo):$(tag)
The “action: deploy” in the “KubernetesManifest@0” task in Azure DevOps specifies that the task should deploy the Kubernetes manifests to the target cluster.
The “KubernetesManifest@0” task is used to deploy Kubernetes manifests, which are YAML or JSON files that define Kubernetes resources, such as pods, services, and deployments. The task allows you to specify the manifest files, the Kubernetes cluster to deploy to, and various other parameters that control the deployment process.
When the “action” parameter is set to “deploy”, the task will create or update the resources in the target Kubernetes cluster based on the manifest files provided. If a resource does not exist, it will be created. If a resource already exists, it will be updated to match the configuration in the manifest file.
The “KubernetesManifest@0” task can also be configured to perform other actions, such as deleting resources or running custom kubectl commands. However, when the “action” parameter is set to “deploy”, the task will only deploy the manifests to the target cluster.
Overall, the “KubernetesManifest@0” task with “action: deploy” is a powerful tool for automating the deployment of Kubernetes resources in Azure DevOps, allowing you to quickly and easily deploy your applications and services to a Kubernetes cluster.
2-Creating Our Manifest Files
In our pipeline we are deploying an Ingress and a deployment.
Deployment file :
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-restapp
namespace: ingress
labels:
app: backend-restapp
tier: backend
spec:
replicas: 2
selector:
matchLabels:
app: backend-restapp
template:
metadata:
labels:
app: backend-restapp
tier: backend
spec:
containers:
- name: backend-restapp
image: crdvsaksdevfc01.azurecr.io/mydotnetappdemo:#{Build.BuildId}#
ports:
- containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
name: my-backend-service ## VERY VERY IMPORTANT - NGINX PROXYPASS needs this name
labels:
app: backend-restapp
tier: backend
spec:
selector:
app: backend-restapp
ports:
- name: http
port: 5000 # ClusterIP Service Port
targetPort: 5000 # Container Port
type: ClusterIP
Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webapp
namespace: ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /\
spec:
ingressClassName: nginx
rules:
- http:
paths:
- backend:
service:
name: my-backend-service
port:
number: 5000
path: /
pathType: Prefix
- backend:
service:
name: my-backend-service
port:
number: 5000
path: /acr(/|$)(.*)
pathType: Prefix
- host: himhelloworld.com
http:
paths:
- backend:
service:
name: my-backend-service
port:
number: 5000
path: /
pathType: Prefix
- backend:
service:
name: my-backend-service
port:
number: 5000
path: /acr
pathType: Prefix
























