Building and publishing .NET Core Micro-services to Kubernetes using Azure DevOps

Published 11/15/2019 8:24:21 AM
Filed under Web development

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:

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:

  1. Create a new ASP.NET Core web application
  2. Create a Dockerfile for the application
  3. 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:

dotnet new sln -n MyMicroservice
dotnet new web -o src/MyMicroservice
dotnet sln add src/MyMicroservice
Steps required to create a new web application

The following actions are executed:

  1. First, we create a new solution file.
  2. Next, we create a new web application in src/MyMicroservice.
  3. 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:

FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS build

WORKDIR /repo

COPY MyMicroservice.sln .
COPY src/MyMicroservice/MyMicroservice.csproj ./src/MyMicroservice/MyMicroservice.csproj

RUN dotnet restore

COPY . ./

RUN dotnet publish ./src/MyMicroservice/MyMicroservice.csproj -c Release -o /app/

FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS runtime

COPY --from=build /app/ /app/
WORKDIR /app

ENTRYPOINT ["dotnet", "MyMicroservice.dll"]
Multi-stage Dockerfile

Let's go over the contents of the Dockerfile, step-by-step.

  1. 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.
  2. 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.
  3. 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.
  4. Then, we create a second stage. This stage is based on the ASP.NET Core runtime image. We give it the alias runtime.
  5. 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.
  6. 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:

docker build -t mymicroservice:1 .
Command to build the docker image

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:

docker run -p -d 80:8080 mymicroservice:1
Command to run a container using our docker image

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:

  1. Create a new azure container registry instance
  2. Register the azure container registry instance in Azure DevOps
  3. Create a pipeline definition to build and push a docker image
  4. 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:

az group create -n MyMicroServices -l westeurope
az acr create -n mymicroserviceregistry -g MyMicroServices --sku basic
Commands to create a new registry

This code performs the following steps:

  1. First, we create a new resource group in west-europe with the name MyMicroServices
  2. 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.

Navigating to the project settings

Next, select Service Connections under the Pipelines section. This will open up the service connection settings.

Service connection settings

Now, add a new service connection of type Docker registry. This will open up the docker registry settings panel.

Add docker registry service connection settings

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:

  1. First, we define the trigger for the build. Whenever something is pushed to master, a new build is started.
  2. Next, we'll specify that the build should run on a hosted Linux agent.
  3. 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:

Azure DevOps - Empty repo

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:

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.

Build artifacts

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.

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.

Created release pipeline

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.

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.

Release triggers

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