Even if you're brand new to Kubernetes, you may have heard of a tool called Helm. However, you may not realize until you try deploying your own software with it, or editing a chart, that it's not the easiest tool to write stuff for. In the words of someone in a Matrix chat that I don't remember, and slightly paraphrased:

Imagine writing yaml, with jinja templates. Including stuff like `{{ something | indent 10000 }}` because yaml indentation is important.

It's not the easiest to understand a Helm chart just by looking at it. Have a small excerpt from the bitnami/nginx chart:

spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels: {{- include "common.labels.matchLabels" . | nindent 6 }}
  template:
    metadata:
      labels: {{- include "common.labels.standard" . | nindent 8 }}
        {{- if .Values.podLabels }}
        {{- include "common.tplvalues.render" (dict "value" .Values.podLabels "context" $) | nindent 8 }}
        {{- end }}

Even in that small snippet, it may be hard to see what the resulting yaml may look like.

So, you may be thinking: Is there a better way to handle this?

... Of course there is. Why would I be writing this if there wasn't?

Introducing: kubecfg! Kubecfg is, in the author's (bitnami's) words, a tool for "managing complex enterprise Kubernetes environments as code." Or, as I would say: It's a tool for helping make creating Kubernetes resources more sane. It helps reduce the repetition involved in the process, and allows you to do things like reference parts of other resources from another resource.

Kubecfg also gets around the indentation issue by using something called jsonnet. jsonnet is a language (and a tool for the language) that is pretty much json on steroids. Honestly, their website can do a better job at explaining it - go take a look at it! If you want to see some examples in the context of Kubernetes, read the rest of this post and the git repo linked at the end.

Here's a simple example, showing a simple deployment definition, and a service that references it:

local kube = import "kube-libsonnet/kube.libsonnet";

{
    deployment: kube.Deployment("hello-world") {
        // `+` at the end of a key means we're extending something else
        spec+: {
            replicas: 1,
            template+: {
                spec+: {
                    containers_+: {
                        helloWorld: kube.Container("hello-world") {
                            image: "tutum/hello-world:latest", // first google result for "docker http hello world", tbh
                            ports_+: { http: { containerPort: 80 } },
                            readinessProbe: {
                                httpGet: {
                                    path: "/",
                                    port: "http"
                                },
                                initialDelaySeconds: 5,
                                periodSeconds: 5
                            }
                        }
                    }
                }
            },
        }
    },
    service: kube.Service("hello-world") {
        target_pod: $.deployment.spec.template
    },
}

Lets see what that looks like:

lumi@ubuntu:~/workspace$ kubectl show test.jsonnet
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations: {}
  labels:
    name: hello-world
  name: hello-world
spec:
  minReadySeconds: 30
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      name: hello-world
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      annotations: {}
      labels:
        name: hello-world
    spec:
      containers:
      - args: []
        env: []
        image: tutum/hello-world:latest
        imagePullPolicy: Always
        name: hello-world
        ports:
        - containerPort: 80
          name: http
        readinessProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        stdin: false
        tty: false
        volumeMounts: []
      imagePullSecrets: []
      initContainers: []
      terminationGracePeriodSeconds: 30
      volumes: []
---
apiVersion: v1
kind: Service
metadata:
  annotations: {}
  labels:
    name: hello-world
  name: hello-world
spec:
  ports:
  - name: http
    port: 80
    targetPort: 80
  selector:
    name: hello-world
  type: ClusterIP

That 31 lines of jsonnet code generated over twice as many lines of Kubernetes resource definitions for us! (although, I will admit that much of it isn't needed, as it's default values). However, let me point some things out that are nice:

    service: kube.Service("hello-world") {
        target_pod: $.deployment.spec.template
    }

Notice how here, all we had to do to define the service was reference the pod it targets. That's it. We didn't need to hand-write a selector, grab ports manually, or anything. Which brings me to the next thing: at any point, we can reference other objects in what we are working on.

You can define a deployment, grab the info from it needed to create a service, then grab the info from that service to create an ingress definition. You can make a secret, reference it from the env vars of a deployment, and have changes in one place apply everywhere. You don't need to repeat yourself anymore, which helps reduce mistakes massively. It even warns you in many cases of things like missing properties on definitions.

If you want a larger example of how you can use jsonnet, I'm publishing the code I used to deploy Ghost for this blog here. And before you ask, no, that's not the real database password. I generated a fake one there, and changed it before deploying this 😉.