Jenkins X is the self described CI/CD solution for modern cloud applications on Kubernetes.
Jenkins X uses Helm to manage Kubernetes deployments. This short post is about leveraging the dynamism of Helm charts to allow different environment variables per environment and even per pull request.
Prerequisite #
You have a cluster running that is managed by Jenkins X. There are plenty of getting started tutorials on the Jenkins X website and since this post is more about how to use Helm with Jenkins X, we won't be covering installation.
Jenkins X #
Jenkins X subscribes to the GitOps methodology, which means the Git is used as a single source of truth for continuous delivery. With Jenkins X, Helm charts are kept in your code repository, builds are triggered by PRs or branch merges, and Jenkins X handles deploying to either a preview or an environment based upon your configuration.
It's likely that you would want different environment variables based upon which environment your application is deployed. I'm going to show you how to do this.
Builds #
For our contrived use of environment variables, we're going to use a slightly modified version of the Jenkins X Python quickstart application. You can find the finished product on GitHub.
PRs and Staging #
For pull requests, we want our app to show the Jenkins branch name as noted in the built-in BRANCH_NAME
environment variable. If we merge to master and deploy to staging, we'll just set the branch as master
.
For building previews of pull requests, Jenkins X uses a simple Makefile to inject values into a few of the Helm YAML files. We're going to follow this approach, take the BRANCH_NAME
variable from Jenkins and use that in our deployment definition.
We're going to add a branchName
key under the preview
key in the charts/preview/values.yaml file. preview
will look like this:
preview:
image:
repository:
tag:
pullPolicy: IfNotPresent
branchName:
Now, in charts/preview/Makefile, we'll grab the BRANCH_NAME
environment variable from Jenkins. After creating the two branchName
lines, the Makefile will add our new environment variable to our preview values.
OS := $(shell uname)
preview:
ifeq ($(OS),Darwin)
sed -i "" -e "s/version:.*/version: $(PREVIEW_VERSION)/" Chart.yaml
sed -i "" -e "s/version:.*/version: $(PREVIEW_VERSION)/" ../*/Chart.yaml
sed -i "" -e "s/tag:.*/tag: $(PREVIEW_VERSION)/" values.yaml
# This is a new line.
sed -i "" -e "s/branchName:.*/branchName: $(BRANCH_NAME)/" values.yaml
else ifeq ($(OS),Linux)
sed -i -e "s/version:.*/version: $(PREVIEW_VERSION)/" Chart.yaml
sed -i -e "s/version:.*/version: $(PREVIEW_VERSION)/" ../*/Chart.yaml
sed -i -e "s|repository:.*|repository: $(DOCKER_REGISTRY)\/duffn\/jx-environment-variables|" values.yaml
sed -i -e "s/tag:.*/tag: $(PREVIEW_VERSION)/" values.yaml
# This is a new line.
sed -i -e "s/branchName:.*/branchName: $(BRANCH_NAME)/" values.yaml
else
echo "platfrom $(OS) not supported to release from"
exit -1
endif
echo " version: $(PREVIEW_VERSION)" >> requirements.yaml
jx step helm build
Now, we're going to add an env
section at the bottom of charts/jx-environment-variables/values.yaml.
env:
- name: BRANCH_NAME
value: "master"
Finally, in charts/jx-environment-variables/templates/deployment.yaml, we'll utilize the variables that we setup. If our values.yaml contains branchName
, we'll display that in our app, otherwise we'll get the env
section that we created above.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: {{ template "fullname" . }}
labels:
draft: {{ default "draft-app" .Values.draft }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
spec:
replicas: {{ .Values.replicaCount }}
template:
metadata:
labels:
draft: {{ default "draft-app" .Values.draft }}
app: {{ template "fullname" . }}
{{- if .Values.podAnnotations }}
annotations:
{{ toYaml .Values.podAnnotations | indent 8 }}
{{- end }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.internalPort }}
{{/*
Here's the section we added.
*/}}
{{- if .Values.env }}
env:
{{- if .Values.branchName }}
- name: BRANCH_NAME
value: {{ .Values.branchName | quote }}
{{- else }}
{{ toYaml .Values.env | indent 10 }}
{{- end }}
{{- end }}
resources:
{{ toYaml .Values.resources | indent 12 }}
When we build a PR, we can see that we'll have PR-XX
output by our app and when we build staging, we'll see master
. It works!
Production #
Okay, but now we want to display something different when we promote our staging image to production. How do we do that since it's going to use exactly the same image and the values.yaml file as well?
Thankfully, you can override values per environment using the production repository that Jenkins X creates for us when we initialize our cluster. Mine happens to be called environment-razorfortune-production. This is the repository that Jenkins X will use when we promote an image to our production environment.
In the env/values.yaml file in this repository, let's add our production variable.
jx-environment-variables:
env:
- name: BRANCH_NAME
value: "i am in production!"
Note: You need to add any overriding values underneath a key that is the exact name of your application. Don't let this trip you up!
And now when we promote our image to production, we'll get i am in production!
.
Conclusion #
This example was a bit contrived, but now you can see some of the power in utilizing Jenkins X for continuous delivery in Kubernetes. Now you have environment specific variables (or anything that you can put in values.yaml for that matter) in Jenkins X. Get to it!