Pitfalls of Helm – Insights from 3 years with the leading K8s package manager
glasskube.eu
glasskube.eu
I worked at an org where an admin went a little rogue and updated the config without using the macro file. Another admin didn't realize. Well, didn't realize until it was too late.
I mean, as soon as you template it and need to do `{{ something | indent 4 }}` or some shit to make the template work you know you're on a bad track.
Failing that if you actually need/want a procedural/non-pure language then I think Kotlin or Ruby take the cake. Both have extremely strong support for DSLs which IMO is key to reaching a modicum of usability.
In that case I preferred to describe infrastructure rather than program it.
Plus, well, there's a disturbingly high number of people who think that semantic whitespace is a positive.
These optimizations for some perceived ergonomic win almost always make terrible tradeoffs versus using a good well established data format. And especially systems which favor human consumption but create extreme difficulties for machine handling, those are the worst!
Yaml being such a non-Context Free Grammar is a huge pain. There's so much state in the parser. It only gets worse from there. Yaml has all kinds of wild crazy capabilities. References, a variety of inline content blocks, and weird ways to invoke stuff?? GitHub yesterday did a code review of Frigate, an enormously popular surveillance video analysis tool that's heavily downloaded, and found, oh yes, a huge glaring yaml bug allowing remote execution, because executing arbitrary code is just built right in to yaml amid 3000 other crazy hacks & who would have known to go look for & disable that capability?! https://news.ycombinator.com/item?id=38630295 https://github.blog/2023-12-13-securing-our-home-labs-frigat...
Typing is not the problem (even though I see so many people just terrible beyond words at navigating project structures or the command line... Improve! Some day!).
(Of course the reality is that braces are how you write scoping information and indentation is how you read it, and CQRS is actually a good thing.)
You can prefer them be coupled for taste reasons but they are fundamentally different. Code minification is a practical example where you can save many characters by omitting whitespace.
This is like tabs vs spaces. The white spaces crowd prefer the trade offs, and the tab crowd don't. But the white space crowd can't argue the perks of tabs - they exist.
disclaimer: Am braces/whitespace camp.
Now that terrible text editors won, I have to watch a 2 Vs 4 space war.
If only we could have a character that could represent indentation and people could set the rendering so they can visualise it in their own preferred way.
And folks who want to remove braces should be pointed to the Apple certificate snafu.
> Now that terrible text editors won
yes. but not all hope is lost, maybe AGI helps/wipes us out!
However I would be completely on board if we could agree on a formatting configuration file that people could check into a repo for IDEs to pick up.
For language-specific formatting, you can run formatters in pre-commit-hooks and CI. Treefmt[2] can help with that if you want to cover a lot of languages in your repo.
It looks like IntelliJ supports it, so that could cut down on the amount of work required to get it in a project.
I see it, I understand the argument for coupling them. But the argument makes an assumption I am not comfortable with - it says they are there for the same exact same reason, which is not actually true. It is often incidentally true in practice depending on the needs of the language - but it is not universally true across all needs in all languages.
It's the same thing with semi-colons. Having a statement separator provides practical benefits in several languages. In many of these languages, they are also optional.
If you were to say that all languages should parse both styles to get the best of both worlds, that wouldn't be completely unreasonable. But it makes the parsing more complex than necessary to support both, so only one is often supported - which is fair.
I mean, you can argue for having both for a myriad of reasons. It's redundant and no big deal, a lot of languages do just that. And some languages do fine with only indentation and no braces. But there is no language that does braces without indentation, or at least stylewise the code is always indented.
https://github.com/con2/emrichen
If JSON had comments I might lean toward json.
Also, there's the old dirt hack
{"item": "this is a comment",
"item": "//so is this but more obvious",
"item": "because in 99.99% of implementations, last value wins:"
"item": 42}String concatenation may have been a mistake. The concept of a string may have been a mistake. Every sequence of bytes has some structure, and in order to mash two "strings" together they need to be serialized in the correct way. Even when building error messages it would be nice if you can reliably identify the "chrome" from the "content".
Also look at terminal escape sequences. When we print text to a terminal we should probably be replacing non-printable characters with some sort of encoding so that the reader can understand that 1. these are from the content not the application and 2. Not do stuff like delete the output line to trick the reader.
Every time you put two strings together you should think about how you need to properly encode one into the other. Output unaware text templating almost always fails this because it relies on the user to do this for every single interpolation, and that is doomed to fail.
I wrote an even longer rant about this on my blog a while ago: https://kevincox.ca/2022/02/08/escape-everything/
This more shows that one shouldn’t be using string templating to create data structures in the first place.
No comments and having to escape.... well, practically everything you'd commonly be entering makes JSON suck at this sort of task.
The main issue is that it make it more difficult to mix hardcoded and inserted values.
labels:
- mylabel
- {{ extraLabels | indent 4 }} # toJson doesn't work here.
Also the small technical concern that YAML isn't actually a superset of JSON. (But you are far less likely to hit these cases than other escaping bugs). labels: {{ append .extraLabels "myLabel" "myOtherLabel" | toJson }}
your commentary talks about json but your code snippet is in yaml, so it's possible one or both of us are solving the wrong problem thing:
- item1
- {{ foo | toJson }} # <-- is not going to do what you expect
unless you quite literally wanted thing:
- alpha
- - alpha1
- alpha2 labels:
- mylabel
- {{ extraLabels | indent 4 }} # toJson doesn't work here.
Which has an extra `-` and as you pointed out would produce a nested list.But I meant this which would merge the lists.
labels:
- mylabel
{{ extraLabels | indent 4 }} # toJson doesn't work here.We decided to go with Starlark as the base language for our DSL, and we've had a consistently great experience. Users report that it's very approachable, and the starlark-go library is very pleaeant to deal with.
Configuration languages like yaml, HCL, etc. are more reasonable alternatives.
yaml is a tree serialization format with some human-targeted ergonomic features like comments and multiline strings built in.
it'll work for simple configuration files just as well as format-less .ini files will. for complex configurations, even xml is better, and that's saying a lot.
in practice for object graphs of any sort of complexity, like cloudformation or k8s configs, you want a programming language which can reduce the kolmogorov complexity of your configuration, because that dominates ops in the limit. or, IOW, configuration is code is configuration is code is ...
as a human i want to be able to both read and write, easiliy, by knowing wtf is going on for the input(s), and to understand clearly within the config-at-hand, what does what ect (without 5x paragraphs per 1 key-value tuple).
then insert more templates to generate more templates, and then it is a fun spiral
k8s reminds me of those 20min long commercials combined with the energizer bunny, "but wait there's more" .. "and there's more" .. "and there's more" - as with orcrastrating infrastructure as microservices, there's always more and more complexity to add to the monster. More layers, secutity, networking, ect, ect..
yes, the actual descriptors are atrociously hideous, but it's okay, it's low-level, evolving pretty quickly, and there are nice high-level representations -- https://cdk8s.io/docs/latest/plus/
so, yes, of course, compared to FTP copying PHP files into cgi-bin k8s is more complex, but the feature set is also different.
of course, not everyone needs declarative gitops-based blue-green deployment with pristine dev/demo/staging/UAT envs on each new PR. and usually when people think they do it's mostly just FAANG envy :)
but, speaking from experience, setting up a k3s cluster is easy, cheap, and deploying things on it with a "kubectl apply" is also easy. setting up CronJobs to do backups to some S3-compatible thing is also quite doable, and so on. and you end up with a big bag of YAML. is it better than snapshotting a VM? who knows!
Which, to your point, isn't to say that YAML is "good", but I think there was at least an advancement through minimalism.
My feeling is we will probably all look back at the over-use of text based DSLs and configuration languages as a giant mistake. Not just with respect to K8s, but IaC, CI/CD config and the rest of the DevOps YAML/config language mess. In retrospect, it has been a case of simplistic instead of simple. "Declarative" config languages are hello world optimized, and what looked great at the beginning of the S-curve is starting to look pretty damn bad.
For internal applications, it's in an awkward place of being both too complex and too simple, and in a lot of cases what you really want to do is just write your own operator for the complex cases and use kustomize for the simple cases.
Most of the problems with updating and installing helm charts go away if you manage it with something like argocd to automatically keep everything up to date.
Probably even better is to ship a controller and a CRD for the config.
Doing it that means you ship a schema for the parameters of the config, and that you have code that can handle complexities of upgrades/migrations that tools like kustomize and helm struggle or fail at altogether.
The challenge has historically been that controllers are a lot harder to write, but I think that story has improved over the years
and all the customizations just end up being yaml merges from a configmap string or CRD if you're lucky
I love how in Helm I can just look at the templates and figure out what values I need to change to get what I want, and I love each environment only needing a single values file to see all the customizations for it.
People complain about it being a template language, but that is exactly what you need!
Isn't the "weird syntax" just either Yaml files or just JSON Patches, which is a pretty easy standard?
>having to look at a bunch of different files to see what is going on
I consider that a feature, not a bug. prod/larger-memory-request.yaml makes it much easier for me to see what goes into deploying the prod environment instead of for example the test environment.
A template is much easier to read. I had zero experience with go templating, but was able to figure out what it all meant just by looking at the templates... they still looked like kubernetes resources
As for looking at a bunch of different files, if you like having a "larger-memory-request" file, you can still do that with helm... you can use as many values files as you want, just include them in precedence order. You can have your "larger-memory-request" values file.
Keep your customizations flat and compile them to yaml+grep to find out what’s getting overridden and where.
Helm's problem is that `values.yml` is basically the API and every helm chart provides its own (often incomplete) interface with poorly documented defaults. Some of those can spread over 3k+ lines and it's utterly overwhelming to figure out what to do with that.
templates are only good if your templates can remain simple and do not need to expose most of the output fields. my experience developing a chart for wide distribution has been very much that your templates will not remain simple (and will turn into an incomprehensible mess, since you'll need them to handle tasks templates are fundamentally poorly suited for) and that there is always someone, somewhere, that needs some particular resource field exposed in values.yaml. the
> As a result, the number of possibilities for configuration is often unreasonably large and complicated, mimicking the actual resources they want to create, but without any schema validation!
bit from the op is incredibly true. values.yaml grows, over time, to have every field in the objects it generates, just organized differently, without validation, and with extra complicated relationships with other settings
kustomize allowing you to provide a base set of resources that users can apply their own patches to avoids that config surface bloat problem entirely
Maybe it's just us, but our operations team puts pretty hard restrictions on how we're allowed to talk to the K8s API directly. We can turn a regular Deployment around as fast as we can write it, but if we needed a controller and CRD update it'd take us like three days minimum. (Which, I even sort of understand because I see the absolute garbage code in some of the operators the other teams are asking them to deploy...)
If a product team sees itself needing to deploy an operator, it's likely the platform is subpar and should be improved, or the product team is overengineering something and could do with rethinking their approach.
As in most cases, a conversation with your platform/ops/devops/sre/infra team should help clarify things.
But how do you package the controller + CRD? The two leading choices are `kubectl apply -f` on a url or Helm and as soon as you need any customization to the controller itself you end up needing a tool like helm.
Internal applications don't have this problem, so you can easily keep your chart interface simple and scoped to the different ways you need to deploy your own stack.
With Kustomize, you just publish the base manifests and users can override whatever they want. Not that Kustomize doesn't have its own set of problems.
https://github.com/bjw-s/helm-charts/tree/main/charts/librar...
Full disclosure: our startup is trying to build a tool where you don't have to pick, so trying to test the hypothesis
Most of the problems associated with Helm go away if you stop using Kubernetes.
So yeah we do this. Our product runs in 3 public clouds (working on 5), single VM, etc. and our customers install it themselves. We're helm plus Replicated. AMA.
Also curious what issues you've seen replicated prevent.
Replicated saved work by handling a configuration gui for the end user, licensing/ entitlements, support bundle collection, private image proxy, things like that we didn't want to deal with.
What I found is that when doing such review, you realize 99 of the template variables are not relevant for you and the one place you need to template is missing a value. Just extracting the rendered manifests and modify them by hand from there becomes more maintainable. Like you say, there is a very limited set of common denominators.
For smaller orgs, just running a single container and increasing the Node size takes you a very long way. That doesn’t need helm.
So Linux is a mess? And we should rethink how rpm and deb work?
Or all Deb issues go away if you stop using Linux?
People forget that Helm is a package manager first and foremost (the only one for Kubernetes). It also happens to include a templating mechanism. The templating part has its issues, but until we find another package manager, I don't see Helm going anywhere.
Linux is indeed a mess, yes. RPM and Deb are both awful formats stuck in the 90s, with even worse package managers on the top. Even with the legacy of those, installing a package does not involve templating a whitespace-sensitive language with a mediocre template language.
> Or all Deb issues go away if you stop using Linux?
Never had any issues with debs on FreeBSD. Or NixOS if one likes the Linux kernel.
The idea that helm is even a package manager is fanciful at best, in any case.
The front page of helm.sh literally says "The package manager for Kubernetes". If it was advertised as "the best templating engine for K8s" or something similar I would agree with you.
People try to abuse Helm.sh as a fancy templating engine. And the testament to that is all the articles "Helm vs Kustomize vs JSonnet vs ..."
I take your image as the FROM for my own Dockerfile, tweak a few settings, maybe alter the CMD, and then run my image instead of trying to do some sort of ad absurdum variation on a Twelve Factor App.
I would say, helm is at it's best when you need to _publicly distribute_ a complex application, AND the majority of the users on the receiving end don't care about the complexity.
You don't need Helm if your manifests are simple. But when your manifests become complex, helm will make it even more complex by turning each field in the plain k8s manifests into a toggle in a values.yaml file.
yes, it's nice to do env-var-substitution, and --set is not that dumb.
There's a ton of pitfalls and limits. It's still a genuinely useful tool and provides value. But it's mostly useful if you use it in the simplest possible ways with small charts.
I just wish the "operation engine" were decoupled from the "generation engine", and pluggable. I like how it watches the deployment, has atomic upgrades, can do rollbacks. But if you want a complex deployment, you currently have to DIY it.
This is not endorsement. This is to point out that it makes hardly any sense! Use a proper programming language and serialize the object tree/network to whatever format is necessary.
It's not really that. Jinja is python, Helm is written in Go and uses one of the Go template languages, which has a passing similarity to Jinja.
The rest of your comment is spot on, of course.
… this is just an odd complaint. Naturally, there isn't a schema — there inherently cannot be one. Values are the options for the app at hand; they're naturally dependent on the app.
> but without any schema validation!
I have seen people supply JSON schemas for values with the chart. I appreciate that.
Of all the pitfalls … the clunky stringly-typed "manipulate YAML with unsafe string templating" is the biggest pitfall, to me…
They want Helm to recognize that the cpuLimit value is used as a CPU limit for a Pod and throw errors for any cpuLimit that isn't a valid CPU limit.
Agreed that the user will have to write their own schema for CLI arguments.
Additionally, our applications consume our cloud configuration (eg something that launches pods on heterogenous GPUs needs to know which clusters support which GPUs, our colo cluster has H100s but our Google cluster has A100s etc.) Writing in the same language in the same monorepo makes it very easy to share that state.
Anyways, I want to take the opportunity to ask other DevOps in the room something: What workflow do you use for performing CD for a bunch of Helm charts? I mean, I guess you version a bunch of values.yaml files in a repo but how do you manage the installation of the repository from the CD runner and so on? I'm curious because from the top of my head, Helm implies the runner (the actor who calls Helm to install a chart) to install a repository and then install the package from that repository passing to it the path to the values.yaml file, but on a CD pipeline, this actor is usually a disposable container. Do you install during one of the steps of the CD pipeline the repositories from an in-house created script perhaps every time the pipeline runs? Are you hopefully using a GitOps approach instead?
Thanks in advance for any answer.
several commenters have mentioned Cue/Jsonnet/friends as great alternatives, others find them limiting / prefer pulumi with a general purpose language
our solution at kurtosis is another, and tilt.dev took the same route we did...adopt starlark as a balanced middle-ground between general-purpose languages and static configs. you do get the lovely experience of writing in something pythonic, but without the "oops this k8s deployment is not runnable/reproducible in other clusters because I had non-deterministic evaluation / relied on external, non-portable devices"
I use kapp from Project Carvel and the "helm template" subcommand to work around Helm's inability to control desired state. I've found that kapp does a pretty good job of converging whatever resources Helm installed.
What I do to remediate this sadness is use Helm from Tanka. There is still sadness but now it's wrapped in a nice Jsonnet wrapper and I can easily mutate the output using Jsonnet features without having to mess with nasty Go templating.
I've said it a million times before but it's always worth saying again:
Don't use string templating for structured data.
We don't have any CRD, but the approach would extend to those, plus you get auto complete. The k8s jsonachema isn't super easy to work directly with, though.
I’ll never understand why we don’t just use a language. I started writing all my k8s config in python and it’s great.
I have be working on a new project and I split the IaC stuff into two layers, essentially using Pulumi (w/Kotlin) to spin up the k8s cluster and dependencies for Config Connector (on GCP). From there I'm just generating and applying manifests with fabric8 (more Kotlin).
It's not quite as good as Jsonnet in some cases (because of lazy vs non-lazy mostly and always-supported deep-merge etc) but Kotlin is immensely powerful and has things like the `lazy` helper to help here.
Having the entire repo defined in Kotlin though is very nice. Build system is Gradle w/Kotlin script, frontend is htmx only generated by Kotlin DSL, IaC all Kotlin as described.
We did similar at $CURRENT_JOB with Typescript but Kotlin is miles better IMO.
I'm slowly chipping away at that problem by implementing some tooling. For example I recently added "traceback" functionality in https://github.com/kubecfg/kubecfg
Another thing that I noticed is that most people who end up writing template libraries for jsonnet are using too many functions and not leveraging the strengths of jsonnet, namely object extension.
I opensourced a library I'm using internally at $work. It's far from perfect and sorely lacking docs and examples but if you want to give jsonnet another go I'd recommend you try kubecfg + https://github.com/kubecfg/k8s-libsonnet
Something Jsonnet-esque but with more typing help, better debugging (especially error messages instead of `blah thunk, thunk thunk` would go a long way.
I love the idea of using a tool bundled with kubectl for zero dependencies, but their examples and tutorials are horrible. I can't figure out how to use it correctly to have 1 copy of YAML that would deploy to 5 different environments. It seems I would need multiple copies of kustomization.yaml in multiple folders, if I have multiple namespaces/pods/etc...
It wasn't bad once I got through the docs / examples. They just assume so much existing knowledge I didn't have.
The development story is much better (breakpoints! WHAT!?, loops and control flow!?), you can catch common issues quicker by adding tests, there's one "serialise" step so you don't have to deal with YAML's quirks and you can version/diff your generated manifests.
It's dumb, and stupid, but it works and it's far less cognitive load.
Now: handling mildly dynamic content outside of those generated manifests... that's a massive pain, releasing a new version of a container and avoiding to touch the generated manifests: not working for me.
I appreciate that TF has loops and dynamic blocks, etc etc, but sometimes it's just a lot easier to look at a Jinja2 template and run a script to generate the TF.
But people (including me) were unhappy at yaml reading; I also grew to hate it with a passion because it's neither go nor yaml, and super difficult to read in general. We are a typescript company, and https://cdk8s.io/ has been great for us. We can unit test parts of charts without rendering the whole thing, distribute canonical pod/deployment/service definitions, etc.
In all of the cases, we combined this with config outputted by terraform, for env specific overrides, etc.
Because you effectively CAN'T dynamically configure subcharts with templating that's done in your main chart, see eg https://github.com/helm/helm/pull/6876 here comes the hack.
We run helm in helm. The top chart runs post-install and post-upgrade hook job which runs helm in a pod with a lot of permissions. The outer helm creates values override yaml for the subchart into a ConfigMap, using liberal templating, which gets mounted in the helm runner pod. Then helm runs in there with the custom values and does its own thing.
Not proud but it lets us do a lot of dynamic things straight helm can't.
I personally like cuelang's philosophy but it could become a little messy when you have to iterate and handle user inputs in large codebases.
https://github.com/helm/helm/issues/2298
https://github.com/helm/helm/pull/10309
https://github.com/helm/community/pull/301
I can understand moving cautiously, but it's at a point where it almost feels like allowing users to understand what Helm is doing seems not to be a priority for Helm's developers.
I would rather use Terraform's Kubernetes or Kubectl module for this. Are there any pros or cons I should consider?
I think one of the key things I like about it is that Terraform will show me what it plans to change whereas Helm doesn't (last time I checked)
that's why I strongly suggest that if anyone is a "whole TF shop," they go the operator route, because trying any lower level management is the road to ruin
Helm diff will show you a similar diff to terraform. Running Helmfile in CD isn't a bad move, its really simple, and its a pattern that is easy to grok by any engineer. I think this is still a valid approach in a simple setup, its what some people call "CD OPS". It's a push model instead of pull, and there are downsides, but its not the end of the world.
Ultimately, at scale, i think gitops tooling like Flux and ArgoCD are some of the nicest patterns. Especially Flux's support for OCI artifacts as a source of truth. However then you will venture into the realm of kustomize, and much more complex tooling and concepts, which is not always worth doing.
The diffs are very noisy to read because they show every possible attribute, even those you didn’t write.
The ready-made resources can some times be a bit behind on versions but you also have a raw manifest resource as an escape hatch if you depend on bleeding edge
Pros: The templating capabilities are fantastic, because it leverages terraform. Bye bye string templates. This also makes it easy to use values from other data sources.
In conclusion, it’s good but not great.
You can also save yourself a step by just spitting out JSON, which is valid YAML.
Kinda sad because it is trivial to do in Docker. And still possible to do in k8s with some configmaps.
This is what values and templates are for: no need to guess where you are deployed, I'll tell you that via values, template will make sense and adjustments of how resources will look like.
What Helm can't do currently is handle Ingress renames, and they do not allow loading files that are outside the chart.
the crd shit is borderline untenable. i learned about it during an absolutely cursed calico upgrade. oops.
since kustomize integrates tightly with kubectl these days though, i just use that for new things.
i want fewer, simpler tools.
Basically the idea starts from a world without templates where you would distribute the k8s YAML in a form that is ready to be directly applied, with whatever sensible defaults you want directly present in the YAML
The the user would then just change the values in their copy of the file to suit their needs and apply that.
We all recoil in horror to such a thought, but let's stop a moment to think about why we do:
The user effectively "forked" the YAML by placing their values there and what a nightmare would that be once the user would get a new version of the upstream file, potentially completely overhauled .
If the changes are very small, a simple three way merge like you'd do with git would suffice to handle that. But what about larger changes?
Most of the conflicts in the simple cases stem from the fact that text based diff/merge tools are oblivious to the structure of the YAML file and can only so a so-so job with many of the changes. Unfortunately most people are familiar only with text based merge tools and so they have been primed the hard way to assume that the merges only rarely work.
Structural merges otoh so work much much better. But still if the upstream refractors the application in a significant way (e.g. changes a deployment into a stateful set or moves pieces of config from a configmap into a secret!) not even a structural merge can save you.
My idea was to bring the manifest author into play and make them "annotate" the pieces of the manifest forest that contain configuration that has a high level meaning to the application and that would be moved around in the YAML forest as it gets reshaped.
Another realization was that often such configuration snippets are deeply embedded in other internal "languages" wrapped inside string fields, subject to escaping and encodings (e.g. base64). E.g. a JSON snippet inside a TOML string value inside # base64 encoded annotation value (if you haven't seen these abominations I'm so happy for you you innocent child)
So I implemented a tool that uses neated bidirectional parsers ("lenses") that can perform in-place editing of structured files. The edits preserve formatting, comments, quoting styles, etc.
Even steing fields that are normally thought of as just strings are actually better though if as nested "formats". For example the OCI image references are composed of multiple parts. If you want to just copy images to your private registry and "rebase" all you image references to the new base, you can do it with an update that understands the format of the OCI image references instead of just doing substring replacement.
Knot8 is an opinionated tool meant to help manifest authors and users manage setting/diffing/pulling annotated YAML k8s manifest packages
https://github.com/mkmik/knot8
I didn't have the time to evangelize this approach much so it didn't get any traction (and perhaps it would because it doesn't have enough merits). But I encourage to give it a go. It might inspire you
I also pulled out the "lens" mechanism in a separate binary in case it could be useful to edit general purpose files:
The job of these tools is ultimately to generate a bunch of data structures compatible with Kubernetes API schemas. That's it. Take some input, go brrr, and spit out some serialized data structure.
If we replace "YAML" with "JSON", this all seems a bit absurd.
Helm is a tool where you write JSON templates and JSON templating helper functions, then users can provide a values.json file which specifies inputs for the templates and maybe even can contain templated values themselves. But it's easy to get the contents of the values.json slightly wrong in ways that silently fail, so the JSON templates spit out something but it might still be semantically valid Kubernetes JSON. So you can include JSON schema files alongside your JSON templates package to specify the schema of the values.json file, which the tool will check. Then sometimes the values.json file is too verbose to write by hand, so the user chooses another tool (or god forbid writes a small script in a general purpose programming language) to generate their values.json file.
Absurd.
Kustomize is a tool where you give it a bunch of JSON files, or maybe tell it to use one or more of the hellish Helm charts, which is itself a glorified JSON templater. Then you write a JSON configuration file which looks declarative but is secretly somewhat imperative. In this JSON file you specify which transformations you'd like to apply, maybe even specify some inline patches which use the JSON path patch syntax or a strategic merge where you write some more JSON. Some bits of the tool allow you to generate more JSON from files on your disk, like .env files or data files. If the feature set doesn't work you can then use KRM functions as generators or transformers, which allow you to escape from JSON hell and use a general purpose programming language to emit or transform JSON, but the configuration to this KRM function is itself provided as JSON.
Absurd.
These YAML-obsessed tools like Helm and Kustomize purport to be easier because "it's just YAML!" but what you're really doing is writing all of the arguments for a computer program which you must understand, but in YAML. Kustomize is slightly less bad because it doesn't allow for much control flow, but in Helm templates you have control flow, loops, and helper functions. So it's really just a program anyway with a YAML syntax, but you're confined to a stringly-typed templating language.
This exists with tools like Kyverno, too: "oh, the policies are just YAML!" but they introduce a special DSL for writing conditionals, so you're still writing a program anyway, but in a YAML inner platform. If you think of it as "just JSON" then the YAML smokescreen and cargo cult falls away. It then becomes much more tempting to write something specific to your needs in Python, TypeScript, Ruby, Haskell, OCaml, or another general purpose programming language, where you can do whatever it is you need to do without this leaky YAML purism. Kubewarden's scope overlaps a bit with Kyverno, but Kubewarden chooses WASM as the common language instead of YAML.
However, there's an argument to be made about the security model of Helm and Kustomize. Assume that I trust that the authors of those tools are not going to smuggle my data to a malicious third party or install spyware on my computer and that they do not have critical security bugs. Then I can confidently run those tools on my computer with inputs (e.g. charts and kustomizations) that I do not trust. To say nothing of whether the tool will generate a deployment which compromises my cluster when applied.
I think what I'd like to see from the community is tooling that targets Deno or WASM as the common ground. Then I can sufficiently sandbox "packages" like helm charts or kustomizations without worrying that they have general access to my computer or CI/CD runner as if I'd imported a random Python package. I've already started writing something in Deno that tries to emulate Grafana's Tanka in TypeScript. My next job is to write a tool which runs WASM-compiled KRM functions.
But I might need a configuration language for that tool, so maybe I'll choose YAML...