Continuous Integration and Deployment on Kubernetes with Azure Devops

Tim Park
7 min readMay 1, 2018

--

Continuous integration and deployment is well accepted in the industry as a best practice. It enables you to learn immediately when there is an unexpected build break, be it because of a code defect or an unforeseen integration break between two changes. It also relieves us from manual and tedious deployment processes: instead we build an automated process once and have it run with each check-in to source control.

I’m going to walkthrough how you can put these principles in practice with Visual Studio Team Services (VSTS) working against a Kubernetes cluster. To keep it simple, I am going to use a super simple node.js application that does nothing more than display a secret passphrase back to you.

Clone this application from Github, create a new project in VSTS, push this application to your newly created project in VSTS, and then click “Build and Release” in the top bar.

Press “+ New Definition” and then use the repository that you just pushed up to VSTS:

Next you’ll be given the option of starting with a process template for your build. None of these are useful for our purposes, so start with an empty process.

We next need to name our build process and choose a build agent type (the type of machine our build runs on). Name it and switch the build agent type to Hosted Linux Preview since we are building a node.js application and using Kubernetes ecosystem tooling:

Next, click the “Phase 1” build placeholder to the left. Rename it “Test and Deploy” and leave the rest of the settings the same.

On the now “Test and Deploy” bar to the left, press the + to add a task to the build phase.

This will pop up a number of preconfigured tasks. In the search box, enter ‘npm’ and select the top ‘npm’ one. This task enables us to work with node’s package manager npm. Click on the npm install bar to the left:

By default, this task installs the dependencies specified in your package.json, which is actually the first task we want, so just leave it alone in the editor to the right. Click the ‘+’ again and then add another instance of npm:

This one I’ve renamed to ‘npm test’, adjusted the command to “custom” and specified the custom command as test. One of the conventions in node.js is to specify a test runner as a custom npm command using package.json:

{  "name": "simple-server",  "main": "server.js",  "scripts": {    "start": "node server.js",    "test": "cross-env PORT=5000 mocha --exit"  },  ...
}

These tests are implemented in test/ in the project and what this build step will do is execute them, and if any of the tests fail, stop the build process.

Our server application relies on a secret passed into the application to decide what to pass back to the user on a request. Following twelve factor application principles we pass this in through an environmental variable such that the application is decoupled from its configuration. This is a pretty standard pattern for applications and is typically used for sensitive things like connection strings etc.

Since our application depends on this environmental variable, our tests do too, so we need to configure a test value during our test runs. We can do this in the Variables tab to the top, adding SECRET_PASSPHRASE to the environment that our tests run under:

Press “Save & Queue” and our first build will be kicked off that will clone the source code repository, fetch npm dependencies, and then run the tests:

With this, we have set up the continuous integration portion of the build for our simple application. Let’s now look at how we go about continuously deploying it on our Kubernetes cluster with VSTS.

The first thing we need to do is build a container with our application code.

Our project contains a Dockerfile that specifies how to build a container with our application code:

FROM node:carbonWORKDIR /appCOPY . .
RUN npm install
EXPOSE 80CMD [ "devops/start-service" ]

To build this, add a ‘Docker’ build step to the build:

And then configure it to build the Dockerfile that is part of our project:

Authorize with your subscription, select your container registry, confirm you have selected the “Build an image” action, and have checked off “Include Source Tags” and “Include Latest Tag”.

Add another Docker build step and this time configure it to push our image to Azure Container Registry:

We now have all of the build steps in place to build and push a container to Azure Container Registry. Press “Save & queue” to run a build with these two new steps, confirm that the build is successful, and that the container is pushed to your registry.

Let’s take a pause and set up continuous integration for new changes from source control. This is simple when you are hosting your project in VSTS. Just click “Triggers” in your build project, and then check off “Enable continuous integration”

With this, any new changes to our application will trigger our build task and a full build, test, and deploy of our application.

Our final step is to deploy our application on a Kubernetes cluster using helm.

Our repo contains a helm chart in ./devops that we’ll use to deploy the application. It contains resources for a Kubernetes deployment and a service that will frontend the pods running our application that use the container we just built.

Let’s deploy this chart using VSTS. First, add the Helm install as a build step to install the helm toolset on the agents and just select the defaults:

Add a second helm build step and configure it to do an install via upgrade of our chart. You’ll need the ‘config’ file for your Kubernetes deployment (or use AKS in Azure), Kubernetes master endpoint (which you can get with ‘kubectl cluster-info’). We’ll then pass in the image name for the container we built in the previous step and ask helm to use that the container to deploy:

Before we click “Save and Queue”, we also need to create a secret for our SECRET_PASSPHRASE in our Kubernetes cluster. These secrets are typically housed in a separate ops specific repo that manages them, not our main source code base for an application.

$ kubectl create secret generic secret-passphrase --from-literal=SECRET_PASSPHRASE="Open Sesame"

If you look at devops/templates/deployment.yaml you’ll see that this secret is referenced there to fill an environmental variable that is passed to our application at run time:

...
containers:
- name: {{ .Values.serviceName }}
image: {{ .Values.image }}
imagePullPolicy: {{ .Values.imagePullPolicy }}
env:
- name: SECRET_PASSPHRASE
valueFrom:
secretKeyRef:
name: secret-passphrase
key: SECRET_PASSPHRASE
resources:
requests:
cpu: {{ .Values.cpuRequest }}
memory: {{ .Values.memoryRequest}}
...

With this, we should be able to test, build, and deploy our application using VSTS. This cycle will happen each time someone pushes to the repository, alerting us whenever there is a build break and only deploying to our cluster when everything is green:

If you liked this, you should follow me on Twitter.

You might also check out bedrock, an open source project I maintain to make cloud native devops easier.

--

--