Jenkins, Kubernetes, and Hashicorp Vault
A big movement at Hootsuite is to have everything on Kubernetes and this includes our CI/CD pipelines. So given Jenkins, Kubernetes, and Vault our goal was to create a CI/CD system that was secure, portable, and scalable.
With this blog post I’ll be covering some of the steps we took to achieve this.
Figuring out how Jenkins and Kubernetes work togetherSo going into this, we knew two things: First we wanted Jenkins to be our CI/CD tool and second we wanted to take advantage of Kubernetes to schedule our jobs. To link the two together we decided to use the jenkins-kubernetes-plugin. This plugin allows the Jenkins Masters to make Kubernetes API calls to schedule builds inside their own Kubernetes Pods.
This provides the following benefits:
- Isolation: Builds being in their own Pod means it can’t affect other builds. Each Pod as per the Kubernetes documentation is a logical-host.
- Ephemeral: Pods do a fantastic job of cleaning up after themselves. Pods by its nature are ephemeral. So unless we explicitly want to keep changes the Pod makes in its lifetime, everything will be erased. No more conflicting workspaces!
Containerized Jenkins Master and AgentsNow that we have an idea of how Jenkins and Kubernetes will work together, we had to move our Jenkins Master and Agent into modern times. This meant defining two rock solid containers.
For the Jenkins Master we started off by creating an image that would contain all the plugins that it would require. The base of this plugin image was this one. The reason we separated the plugins out was to allow us to update plugins independently from the Jenkins Master. Our plugin base image ended up looking something like this:
From there we could then build our Jenkins Master image on top of it. One of the nice things about the official Jenkins image is that it offers a lot of flexibility. We took advantage of this by copying in configuration overrides, initial groovy startup scripts, and other files to ensure our Jenkins Master would start up configured and ready to go.
Files that we copy over to our Jenkins MasterAs for our Jenkins Agent, we used this image as the base. The benefit here being that the Kubernetes plugin that we chose was developed and tested with this image in mind. As such the communication between our Master and Agents is well supported.
Once we’ve decided how the images would look, we set up a Jenkins job that would regularly build and push our Master and Agent images to our registry. This way we would always be up to date and avoid having outdated versions of Jenkins and its plugins.
Using Vault to handle our CI/CD secretsWith our images set up, the next step was figuring out if we could #BuildABetterWay to manage our secrets in Jenkins. For this we turned to Vault. *
This choice was made for the following reasons:
- Provides a single source of truth; previously our secrets would get sprawled across our Jenkins Masters and became difficult to manage
- We are already using Vault extensively at Hootsuite, so we have lots of support and knowledge
- Vault supports a Kubernetes Authentication method (More on this below)
Previously we were using AppRole Authentication and while it worked well, it meant we had a secret_id and role_id that we had to manage. Ideally what we wanted was a way for our Pods to tell Vault that it belonged to a certain Kubernetes cluster and should be granted certain access. This is where Kubernetes Authentication comes in.
I’ve outlined the steps for our Kubernetes Cluster to authenticate with Vault:
- Before anything happens, we setup the Vault and Kubernetes relationship by giving Vault some information about our cluster:
- The cluster’s CA cert
- The host of our Kubernetes cluster
- A Vault policy
- A Vault role that is mapped to our Kubernetes namespace/serviceaccount
- When we define the Jenkins Master Pod, we add a field that attaches a ServiceAccount to that Pod. This ServiceAccount is referenced when the Pod starts up and is used to retrieve the account’s JWT.
- Once the JWT is retrieved it is sent over to Vault which then forwards it to Kubernetes.
- Vault will then receive a response from Kubernetes that says the JWT came from the correct namespace and is actually the Service Account it claims to be.
- Once Vault gets confirmation it knows that the Pod has the right ServiceAccount which means it is mapped to a Vault role, and so Vault gives back a VAULT_TOKEN that the Pod can then use.
The Five steps converted as a pictureWhat’s great is that there is no secret that has to be managed and the Pod only needs to use the Kubernetes API. So from the Pod’s perspective in the startup script of a container it would do something like:
And with the Vault token inside VAULT_LOGIN_RESULT it can use that for subsequent calls to Vault.
At this point you might be wondering how we can go from Vault Secrets to Jenkins Credentials. This is where the initial groovy startup scripts come in. On start up our entrypoint script reads in Jenkins related secrets from Vault and writes these values as JSON objects into a temporary file. This temporary file gets read by the startup script and converts those values into Jenkins Credentials.
So at a Vault path containing Jenkins Credentials, we would keep something like
Which on Jenkins Master gets converted to
A Hashicorp Vault Secret converted to a Jenkins CredentialFor information on how to programmatically add credentials check here.
With all that done we now have a way to securely retrieve CI/CD secrets from Vault and a way to convert them to Jenkins Credentials if needed.
* For those wondering why we didn’t use something like the Jenkins Vault Plugin, it was lacking in a few areas:
- It did not support Kubernetes Authentication
- The secrets could not be used to make Jenkins Credentials which other plugins can use
- It would mean adding initial setup scripts to our Jenkinsfiles
SummaryThe steps in this post hopefully described how you can improve your Jenkins CI/CD pipeline with Docker, Kubernetes, and Vault. Revisiting our goals let’s make sure we’ve made things: secure, portable, and scalable. We added security by letting Vault handle our CI/CD secrets. With Docker we ended up with a self contained Jenkins Master and Agent containers. Then, with Kubernetes orchestrating these containers we are able to handle dynamic workloads.
About the AuthorDavid Jung is a co-op Student on the Production, Operations, and Delivery team. He is currently studying Computer Engineering at The University of British Columbia. Connect with him on LinkedIn.