The last week I spend quite a bit of time figuring out the best way to deploy micro-services on Kubernetes with Azure DevOps. At first it felt quite hard, because there are so many tools involved. But with the right tools I got it working within an hour, which as a pleasant surprise!
In this post I'll talk you through the steps that I took to build and publish a .NET Core micro service to Azure Kubernetes Service from an Azure DevOps pipeline. We'll cover the following topics:
Before we dive in, let's take a short look at the technical requirements for this post.
Technical requirements
For the purpose of this post, I'm going to assume that you have the following tools on your machine:
- Docker Desktop
- .NET Core SDK 3.0/2.2
- Visual Studio Code or a similar editor
- Azure CLI 2.0
- GIT
In addition to this you'll need to have access to the following resources:
- An active Azure Subscription (This can be a trial)
- An active Azure DevOps project
I'm assuming that you are somewhat familiar with the different concepts of Kubernetes. In case you want a good introduction to Kubernetes, check out this blogpost by Bruno Krebs: https://auth0.com/blog/kubernetes-tutorial-step-by-step-introduction-to-basic-concepts/.
Let's start with a simple micro-service and package it.
Packaging a .NET Core micro-service
When you have a ASP.NET Core web application that you want to deploy it to kubernetes as a micro-service, you'll need to package it as a container image first.
Let's create a sample application and package it as a docker container image. We need to perform the following steps to do this:
- Create a new ASP.NET Core web application
- Create a Dockerfile for the application
- Build the image locally to test it
Let's get started.
Create a new ASP.NET Core web application
First, we're going to create a new ASP.NET Core web application. I'll be using ASP.NET Core 3.0, but the steps we're going to take here will also work for older versions of ASP.NET Core.
Execute the following commands in a terminal inside an empty folder:
The following actions are executed:
- First, we create a new solution file.
- Next, we create a new web application in src/MyMicroservice.
- Finally, we add the web project to the solution file.
After creating the project it's a good practice to commit your changes to GIT.
Create a new file .gitignore
in the root of your project, and add the following contents to it:
bin/
obj/
.vs/
.idea/
.ionide/
After configuring the ignore rules for GIT, use the following commands to convert the working folder to a GIT repo:
git init
git add .
git commit -m "Initial commit"
Now that we have the web application set up, let's take a look at packaging it as a docker container image.
Create a Dockerfile for the application
There are many ways in which you can approach the process of containerizing a .NET Core application.
I personally like to build a multi-stage docker build. In a multi-stage docker build you define multiple images in one docker file. The first stage, will build the app and produce the deliverables inside an image. The second stage copies the deliverables from the first stage into a new image.
Using multiple stages allows me to import secrets into the first stage without having to publish them to my production environment. We're only publishing the second stage, which doesn't contain any unwanted files, such as secrets, from the first stage.
To create a multi-stage Dockerfile for our micro-service we need to add a new Dockerfile to the root of the solution, in the same folder as the MyMicroservice.sln
file.
Add the following contents to this file:
Let's go over the contents of the Dockerfile, step-by-step.
- First, we define a new image that is based on the .NET Core SDK image and give it the alias
build
. This alias we'll use later to copy files from this new image. - Next, we copy the solution file and project files. We run
dotnet restore
after copying these files to get the dependencies. Doing so, allows us to cache the nuget packages we need for later builds. - After that, we copy the rest of the project files and run
dotnet publish
on the web project to produce the artifacts we need for production. - Then, we create a second stage. This stage is based on the ASP.NET Core runtime image. We give it the alias runtime.
- Next, we copy the deliverables produced in the build stage to the runtime stage. Notice that it doesn't include any source files that may contain nuget secrets and other stuff that we might need.
- Finally, we set the working folder and the entrypoint so that we can run the web application from the container.
With the Dockerfile in place we can build the image locally to test whether it will build correctly.
Build the image locally to test it
To build a docker image on your local machine, use the following command:
This will build the stages in the order as they appear in the docker file. The final stage is then tagged as mymicroservice
and ready to run.
You can run the docker image as a container using the following command:
Now that we have the application package, let's set up a build pipeline to execute the build commands automatically when we push sources to Azure DevOps.
Setting up a build pipeline
To build your .NET Micro-service on Azure DevOps, you'll need a DevOps organization and project on https://dev.azure.com. So if you haven't got one, go there and create yourself a new DevOps project.
I'm also going to assume you have access to an Azure subscription. You'll need it to setup Azure Container Registry and
Note: Azure DevOps is free to use if you're working on a public project or only have a few people working on your project! So there's really no reason not to give it a shot!
We're going to perform the following steps to push the code to Azure DevOps and run an automated build:
- Create a new azure container registry instance
- Register the azure container registry instance in Azure DevOps
- Create a pipeline definition to build and push a docker image
- Add the Azure DevOps GIT repo as a remote and push the code
Let's start with creating a new azure container registry instance.
Create a new azure container registry instance
If you want to publish container images from your build you're going to need to have push access to a docker registry. Typically, you'll want a private registry in your organization when you're working for a customer.
To create a new azure container registry instance, execute the following command in PowerShell or a similar terminal:
This code performs the following steps:
- First, we create a new resource group in west-europe with the name
MyMicroServices
- Next, we create a new container registry in the resource group that we just created.
It will take a few minutes to complete the steps. After you've completed the steps, we're ready to register the container instance in Azure DevOps.
Register the azure container registry instance in Azure DevOps
To register a container registry in Azure DevOps, go to the project settings of the project you want to register the container registry in.
Next, select Service Connections
under the Pipelines
section. This will open up the service connection settings.
Now, add a new service connection of type Docker registry
. This will open up the docker registry settings panel.
Choose Azure Container Registry
as the type of registry to connect to. Next, give the registry connection a memorable name. Then, select your subscription and the registry you just created. Finally, click OK to register it.
Note: Azure DevOps may ask you to login to your Azure Subscription before it can find the container registry. Follow the steps on the screen to do so.
Now that we have the container registry, let's build a pipeline that will build the docker image and publish it to the container registry.
Create a pipeline definition to build and push a docker image
When you've used Azure DevOps before, back when it was called Visual Studio Team Services, you're probably familiar with the graphical editor for the build pipeline. You no longer need to do this. You can create Azure DevOps pipelines inside your project as a YAML file.
Add a new file called azure-pipelines.yml
to the root of your project, and add the following contents to this file:
trigger:
- master
pool:
vmImage: "ubuntu-16.04"
variables:
registryConnection: myRegistryConnection
imageName: mymicroservice
steps:
- task: Docker@2
displayName: "Build image"
inputs:
repository: $(imageName)
containerRegistry: $(registryConnection)
command: buildAndPush
Dockerfile: Dockerfile
tags: $(Build.BuildNumber)
It performs the following steps:
- First, we define the trigger for the build. Whenever something is pushed to master, a new build is started.
- Next, we'll specify that the build should run on a hosted Linux agent.
- Then, we define a build step specifying that we want to build a docker image and push it to a container registry.
The build will use the variables from the variables section to specify the name of the image to build and the container registry connection to push to. Make sure you modify those to the values you've used earlier to create the image and registry.
Make sure to commit your changes to your local repository. Use the following commands to do so:
git add .
git commit -m "Add build pipeline definition"
Now that you've set up the build pipeline definition, let's push it to Azure DevOps.
Add the Azure DevOps GIT repo as a remote and push the code
To push the code to Azure DevOps, you'll need to know the URL of the repository. You can find this URL by navigating to the repos section of your DevOps project. You'll get to see the following page when the repo is still empty:
Copy the Clone URL at the top of the page and use it in the following command to add the repo as the remote for the GIT repository on your machine:
git remote add origin <url>
Replace the <url>
token with the URL you copied from your DevOps project.
Now, push the code to the new remote using the following command:
git push -u origin master
This command will not only push the code to the DevOps project. It will also make sure that, it will pushed there automatically, next time you invoke git push
.
Take a moment to enjoy the results. After you've done that for a minute, navigate to the pipelines section in Azure DevOps and notice how it automatically picked up your build!
Now that you have a running build, let's take a look at setting up a release pipeline for it.
Setting up a release pipeline
In the previous steps we've created a new micro-service, defined a docker image for it, and pushed it to a registry from our automated build. In this section we're going to take a look at building a release pipeline for our micro-service.
The goal of our release pipeline is to release the micro-service that we've created to a kubernetes cluster. We're going to have to complete the following steps to do so:
- Create a new kubernetes cluster
- Register the kubernetes cluster with Azure DevOps
- Create kubernetes manifests for the micro-service
- Modify the build to publish the kubernetes manifests
- Create a release pipeline
Let's start by creating a new kubernetes cluster in Azure.
Create a new kubernetes cluster
Before we can publish our micro-service to production we need a spot for it to run. This will be a kubernetes cluster in Azure.
To create a new kubernetes cluster, we first need to enable the AKS preview extension on the Azure CLI. Use the following command to do so:
az extension add --name aks-preview
Once this command is finished, create a new cluster using the following command:
az aks create -g MyMicroServices -n microservicedevcluster --node-count 3 --generate-ssh-keys --attach-acr mymicroserviceregistry
This command creates a new cluster in the MyMicroServices
group. It will automatically attach the container registry to the cluster so you can pull images from this registry into the kubernetes cluster.
Note: The command will take a long time to complete, depending on how many nodes you've selected to use. Typically it will take up to 20 minutes to complete.
Once you've setup the cluster, let's register it with Azure DevOps.
Register the kubernetes cluster with Azure DevOps
To deploy to the new kubernetes cluster, we need to register it in Azure DevOps. Navigate to the project settings of your Azure DevOps project and add a new Service Connection for the cluster.
First, select the Azure Subscription as the authentication. Next, give the connection a memorable name. Then, choose the subscription to connect to. After that, select the cluster from the list of available clusters. Finally, click OK to register the cluster.
Now that you've got the cluster registered, let's setup the kubernetes manifests for the service.
Create kubernetes manifests for the micro-service
To deploy our micro-service to kubernetes we're going to define two manifests. One for the deployment of the service and one for the service definition so the service is accessible.
The deployment manifests creates/updates a scalable set of pods for your application which will run the container image that you've created.
Create a new folder in your project with the name manifests
and add a new file to it called deployment.yml
. Add the following contents to this file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mymicroservice
labels:
app: mymicroservice
spec:
replicas: 1
selector:
matchLabels:
app: mymicroservice
template:
metadata:
labels:
app: mymicroservice
spec:
containers:
- name: search
image: mymicroserviceregistry.azurecr.io/mymicroservice
ports:
- containerPort: 80
name: http
imagePullPolicy: Always
This deployment manifest file our micro-service to the cluster. We're asking Kubernetes to deploy just 1 copy of the micro-service using the image that we've pushed to the private docker registry.
Notice, that we haven't specified the version of the image to deploy. We'll let Azure DevOps figure that out for us.
After we've created the deployment manifest, we need to define the service manifest.
The service manifest is used to expose your application within the cluster to other micro-services. It routes requests to a well-known endpoint and port to one of the pods that are deployed for your micro-service.
Create a new file service.yml
in the manifests
folder and add the following contents to it:
apiVersion: v1
kind: Service
metadata:
name: mymicroservice
spec:
selector:
app: mymicroservice
ports:
- port: 80
targetPort: 80
This service manifests, routes all requests going to the mymicroservice
based on the selector that we've specified in the service definition.
Now that we have the manifests, we can move on to publishing from our build.
Modify the build to publish the kubernetes manifests
In the previous section, Setting up a build pipeline, we've set up an initial build definition for our micro-service. This build definition doesn't include a way to publish the Kubernetes manifests. So we need to change it.
Open up the azure-pipelines.yml
file in the root of the project and add the following lines to the file:
- task: PublishBuildArtifacts@1
displayName: "Publish artifacts"
inputs:
pathtoPublish: manifests
artifactName: "manifests"
publishLocation: "Container"
This task takes care of copying the contents of the manifests folder and publishing them as an artifact of the build.
Commit the changes and push them to Azure DevOps using the following commands:
git commit -am "Add publish artifacts step"
git push
A new build is automatically started for you. As soon as it's finished you will find a new artifact on the build status page, called manifests.
After we've published the manifests in the build, we need to setup a release pipeline to deploy the micro-service in Kubernetes.
Create a release pipeline
In the previous section we modified the build pipeline to produce an artifact containing the manifests. In this section, we're going to use the manifests from the build to deploy the micro-service to Kubernetes.
First, we need to create a new release pipeline. Navigate to the Pipelines > Releases section in Azure DevOps and click the New button to create a new release pipeline.
You'll get a list of templates for the new release pipeline. Scroll down to the bottom and click Apply next to the Empty job template.
Once the template is applied, you will see the following screen layout.
On the left, you'll find the artifacts. These artifacts can be released to production. Next to the artifacts area, you can find the first stage in the release pipeline. You can rename this stage in the panel to the right.
Click on the Add an artifact button and select the build you want to publish from. Give the new artifact a name and click Add to add the artifact to the pipeline.
Next, click the link below the name of the stage in the stage symbol on screen. This will open up the job view.
In this screen we can set up the steps needed to deploy to Kubernetes. Add a new task to the Agent job by clicking on the + button.
Search for kubernetes in the search box and select the Deploy to kubernetes task. Click the Add button to add it to the list of tasks.
Then, click on the task in the release pipeline to edit its properties. You'll need to configure the following properties:
- Kubernetes service connection: Select the connection you registered earlier
- Namespace: default
- Manifests: Browse to the deployment.yml file in the manifests artifact
- Containers: Enter the name of the image you want to deploy. In our case:
mymicroserviceregistry.azurecr.io/mymicroservice
Repeat the same steps for the service.yml
file to deploy the service definition to Kubernetes as well.
Now that you have the pipeline set up, let's configure it so a new release is started when the build finishes.
Navigate to the Pipeline tab and click the lightning symbol next to the artifact of the release pipeline.
Click the slider at the top of the properties panel so the continuous deployment trigger is enabled. You can, optionally, specify for which branch the deployment should trigger.
Save the pipeline and queue a new release to deploy your micro-service. You can view the results by executing the following commands:
az aks get-credentials -n <cluster-name> -g <cluster-group>
kubectl get pods
kubectl get services
The first line retrieves the credentials for kubectl to access the AKS cluster. You will need to modify this command to point to the right cluster in the right resource group.
The next lines retrieve the pods and services. The result should be a list of pods and services that were deployed to the cluster from the release pipeline.
Summary
And that's what it takes to deploy a micro-service to kubernetes from Azure DevOps.
First, we looked at how to package .NET micro-services using a multi-stage docker file.
Next, we looked at building docker images from a build pipeline. Thanks to the Docker task it's a breeze to build a docker image.
Finally, we used the Kubernetes tasks to deploy the micro-services. The Kubernetes task will be of great help, since you don't need to think about version numbers. The version is automatically picked up by the release pipeline and injected where needed.
I hope you liked reading this tutorial. Let me know what you think on twitter! @willem_meints