YAML Templating Solutions: Helm & Kustomize

Writing config files by hand is like coding with Notepad instead of an IDE. Let's find a better way, and take an overview of the popular solutions Helm & Kustomize.

The modern term Kubernetes engineer derives from an ancient Greek idiom that translates roughly to... YAML craftsperson. Sorry, bad joke.

Jokes aside, let's approach this from a developer experience perspective: The fundamental API of Kubernetes, to define your infrastructure, is declarative YAML. So soon, you will have lots and lots of YAML.

That is hard to manage.

But on the bright side: If your services look similar, common patterns will emerge.

The main tools, in 2020, to help you to organize YAML and factor out common patterns for reuse, are Helm and Kustomize.

How Do They Work?

Let’s start with a simple use case.

You want anyone on your team to be able to add a new service to your application. Maybe it’s a new REST API, or a new data-source for GraphQL, whatever you need. You want to set conventions for how engineers define these services.

That’s where YAML templating comes in! Templating lets you define common patterns and standards for your team, but you don’t end up with that situation where everyone needs to be closely familiar with that YAML.

The main concept is that one person sets up the template, and everyone else can reuse it, just changing some key values. Due to the magic of templating, everything works out.

Let’s start with Helm.

Helm

This is what a very simple Helm chart looks like:

$ tree .
.
├── Chart.yaml
├── templates
│   ├── deployment.yaml
│   └── service.yaml
└── values.yaml

1 directory, 4 files
$

There are 3 things to pay attention to:

Let’s look into templates:

$ cat templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.name }}
  labels:
    app: {{ .Values.name }}
spec:
  selector:
    matchLabels:
      app: {{ .Values.name }}
  template:
    metadata:
      labels:
        app: {{ .Values.name }}
        tier: web
    spec:
      containers:
      - name: {{ .Values.name }}
        image: {{ .Values.name }}
        ports:
        - containerPort: 8080
$

This looks like a Kubernetes deployment. But if you tried to kubectl apply it, it’d fail. That’s because there’s a mess of curly braces in the way.

If you’re a Go developer, you know what this is. These are just Go templates. And they have lots of the same abstractions as most templating languages—variables, loops, helper functions, etc.

So these braces, they’re the template variables. When Helm generates YAML, it replaces those braces with the values of the variables.

And this way, we can ensure consistent patterns to our definitions—like how this template ensures every deployment has an app label.

Also, in this file, you can see it evaluating Values.name everywhere. Where does that come from?

Let’s look at our values file:

$ cat values.yaml
# Default values for yaml.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

name: default
$

The chart’s values file defines the arguments a user can pass to the template, with reasonable defaults.

And we can use Helm with the --set flag to pass parameters. Let’s try using it, and create a dexdotdev deployment.

$ helm template . --set name=dexdotdev
---
# Source: yaml/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: dexdotdev
  labels:
    app: dexdotdev
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: dexdotdev
---
# Source: yaml/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dexdotdev
  labels:
    app: dexdotdev
spec:
  selector:
    matchLabels:
      app: dexdotdev
  template:
    metadata:
      labels:
        app: dexdotdev
        tier: web
    spec:
      containers:
      - name: dexdotdev
        image: dexdotdev
        ports:
        - containerPort: 8080
$

As you can see, now all those curly braces we had before were replaced by the variable we specified.

There’s much more to Helm that helps manage deployments! Linting, plugins, testing, rollbacks, upgrades, history.

And here's the link to the GitHub repo if you'd like to follow along at home.

But let’s pause here to talk about the other YAML management tool: Kustomize.

Kustomize

Kustomize looks very different. Helm organizes things into charts, then lets you pass parameters to these charts—either on the command-line, or with values.yaml files.

With Kustomize, you start with a working YAML file, not a template. In the example, we’ve created a base YAML directory. Let’s take a look:

$ cat base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
spec:
  selector:
    matchLabels:
        app: name
  template:
    metadata:
        labels:
            app: name
    spec:
        containers:
        - name: name
            image: replaceme
            ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: service
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: name
$

The base contains a working Deployment. No curly braces!

Then Kustomize gives us a bunch of tools and idioms for layering changes on top of the base YAML file.

Let’s take a service called glitch. We’ll generate the YAML for it by using that base file, and layering changes on top of it.

We do that with the kustomization file:

$ cat glitch/kustomization.yaml
resources:
- ../base

images:
- name: replaceme
  newName: glitch
namePrefix: glitch-
commonLabels:
    app: glitch
patchesStrategicMerge:
- patch.yaml
$

Let’s step line by line through this example. We can see that we are:

We can see the results of applying this layering by running kustomize build.

Here's the new, generated YAML. Compare it with the base YAML above!

$ kustomize build
apiVersion: v1
kind: Service
metadata:
  labels:
    app: glitch
  name: glitch-service
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: glitch
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: glitch
  name: glitch-deployment
spec:
  selector:
    matchLabels:
      app: glitch
  template:
    metadata:
      labels:
        app: glitch
    spec:
      containers:
      - image: glitch
        name: name
        ports:
        - containerPort: 8080
$

In this example, each time a developer needs to add a new service, they add a new kustomization config that layers that service’s overrides on top of the base file.

$ tree .
.
├── base
│   ├── deployment.yaml
│   └── kustomization.yaml
├── glitch
│   ├── kustomization.yaml
│   └── patch.yaml
├── muxer
│   ├── kustomization.yaml
│   └── patch.yaml
├── rectangler
│   ├── kustomization.yaml
│   └── patch.yaml
├── red
│   ├── kustomization.yaml
│   └── patch.yaml
└── storage
    ├── kustomization.yaml
    └── patch.yaml

6 directories, 12 files
$

Here's the link to the GitHub repo set up for Kustomize.

What's the real difference?

We’ve talked about the ways you can use Helm & Kustomize to define new services and set conventions across services internally.

Which brings me to an important topic: Kustomize is a simpler tool. It has less bells and whistles. That can be a good thing or a bad thing, depending on what you need.

So let’s talk about the extras that Helm brings:

Developers on your team have probably heard about Helm. And they probably learned about it thanks to Helm repos.

The #1 way people find out about Helm is they will find a cool Helm chart for say a server that identifies iguanas in photos. And they will want to add it to their dev environment.

More common examples include: the Postgres database chart, or the Airflow chart, or the Grafana chart.

The reason Helm charts are so popular is:

Helm lets you set arguments for a template. Understandable arguments. And then lets you share the template!

So if you want to use an open-source service without having to read all its YAML, those arguments give you an easier way to get a handle on how to use it. That’s why if you’re a team that wants to leverage open-source services, you will most likely use Helm at some point.

And because those templates are easy to share, Helm lets you easily publish approachable, structured APIs for making your services work in a dev environment.

Using a remote Helm chart has similar ergonomics as using a local Helm chart. From the perspective of devs that opens up many possibilities.

Which Should You Choose?

The answer to Helm or Kustomize? depends on two questions:

Who is going to be using these configs? How much do they need to know?

If you’re writing all the configs yourself, use Kustomize. Kustomize is like regular expressions. You can express a ton with just a little. But to use it well, you need in-depth knowledge of how the YAML fits together, and how the layers and patches are going to interact.

But maybe you don’t want devs to have to dig through YAML or even understand how things work. But maybe you want to empower devs to add that new iguana service as safely and as easily as possible. For that, you should put in the extra effort and set up a Helm chart.

For further study into these tools, try figuring out how templating works in these two fairly advanced, real-world projects: Airflow, for Helm; and Cluster API, for Kustomize.

Hopefully this article gave you a basic understanding of how YAML templating works, and clarified how to choose your go-to templating solution.

For more content like this, please sign up to our newsletter!