Jsonnet: The Good, the Bad, and the Meh
Each solution is the root of the next problem – Gerald M. Weinberg
I’ve been using Jsonnet for several years, and I think it would be good to summarize my experiences so far.
The good⌗
The best thing about Jsonnet is that it’s a superset of JSON. As a data templating language for generating JSON, it provides many features of programming languages (variables, functions, arithmetic operations, conditionals). Since it can generate JSON, it can also generate YAML, which is why I use it with tanka to create Kubernetes manifests.
In my opinion, the biggest drawback of Helm is its simple Go/template-based templating. I believe the following template work exemplifies the problems that arise in templating languages that fundamentally don’t understand their output:
metadata:
labels:
{{- include "ingress-nginx.labels" . | nindent 4 }}
{{- with .Values.controller.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
annotations: {{ toYaml .Values.controller.annotations | nindent 4 }}
...
If written in Jsonnet, it would look like:
metadata: {
local labels = import "ingress-nginx-labels.jsonnet",
labels: labels + $.values.controller.labels,
annotations: $.values.controller.annotations,
}
Above, $ refers to the top-level object at the time of evaluation. In other words, it points to the outermost object when the Jsonnet file is evaluated. In contrast,
self
refers to the object currently being defined.
Helm treats templates as strings without understanding the target. Jsonnet templates JSON by adding various language features.
{
a: 1,
b: self.a + 1, // self.a references 1, result is 2
nested: {
x: 10,
y: self.x + 5, // self.x references 10, result is 15
z: $.a + 5 // $.a references the top-level object's a(1), result is 6
}
}
Helm’s “oil and water strategy” is similar to inserting Smalltalk syntax into C like in Objective-C, and is widely used when templating HTML in web servers.
However, this strategy feels like wiring your brain in two layers. Having a template language that understands what it’s templating is (in my personal opinion) a good approach, not a bad one - especially when the target is defined in YAML or JSON, like with Kubernetes.
The bad⌗
Jsonnet’s Merging
feature allows you to modify an object from multiple files.
local baseConfig = {
name: "app",
resources: {
memory: "1Gi",
cpu: "100m"
}
};
local overrides = {
resources: {
memory: "2Gi"
},
replicas: 3
};
// Result will have merged fields
baseConfig + overrides
The result is:
{
"name": "app",
"resources": {
"memory": "2Gi"
},
"replicas": 3
}
The interesting point here is that the contents of resources
have been replaced with the contents from overrides
. If you want to merge the contents of resources
from baseConfig
with those from overrides
, you would do:
local overrides = {
resources+: {
memory: "2Gi"
},
replicas: 3
};
By using resources+:
, the contents of resources are merged rather than replaced. This feature effectively makes all objects open.
Helm is a combination of templates + values. While this makes it difficult to use values in ways that deviate from the intent of the templates, it also has the advantage of formalizing usage to reduce structural complexity.
kube-prometheus/jsonnet/kube-prometheus adopts this structural characteristic (advantage) of helm
with its components
and values
structure.
However, having all objects always open is both an advantage and a disadvantage. The kube-prometheus structure is just a kind of pattern that cannot be enforced. Things can always become complex.
The meh⌗
In my personal experience, debugging Jsonnet is always difficult. You can output intermediate objects with std.trace()
, but that’s not enough. Although this is a problem derived from Jsonnet’s power, I think there should be more linguistic constraints to prevent users from misusing Jsonnet. This thought leads me to consider CUE. For large and complex manifests, CUE could provide more stability. But that might mean losing the advantages that come with Jsonnet’s power. (In some ways, the advantage is also the disadvantage.)
Conclusion⌗
While Jsonnet’s charm is good, its advantages seem to fade in complex structures. Since Kubernetes manifests are essentially requests to the Kubernetes API, I sometimes wonder if the answer might be to generate them using regular programming languages, like bwplotka/mimic: mimic: Define your Deployments, Infrastructure and Configuration as a Go Code 🚀.