Abstractions have a cost and a value. Programming languages are more expressive than writing out binary opcodes by hand, but don't really remove any features from the underlying machine. If your machine can do something, your C program can probably do something. So we use C. (It even ADDS the value that you can describe a procedure without knowledge of the underlying hardware. It may not be optimal on every machine, but at least you have a starting point for optimization!)

Helm, like any abstraction, has costs and values. For example, it's very valuable to be able to encode something like "my app consists of a frontend and a backend that run in the same Pod and must each use the same version of the code":

apiVersion: v1
type: Pod
metadata:
name: foo
spec:
containers:
- name: frontend
image: my.registry.io/foo-frontend:{{ .tag }}
- name: backend
image: my.registry.io/foo-backend:{{ .tag }}

Now your frontend and backend containers can't get out of sync, and it saves someone from having to manually ensure that they are sync'd. There's no way to do it wrong! You supply {{ tag }} and it remembers the constraint that you required.

The problem with this abstraction is that there's no escape hatch. If you wanted to run different versions of foo-frontend and foo-backend, there is no way to say "but no, really, this time I'm violating the rules for a good reason". You've reduced the features available... the only way forward is to start over with nothing.

The result of this is that every individual Helm chart has to account for every possibility that the manifests could possibly encode, and invent their own programming language that is identical in expressiveness to the underlying manifests. And they do! Differently every time! For example, if I wanted to allow people to override the container images, I'd have to make my template look like:

containers:
- name: frontend
{{ if .tag }}
image: my.registry.io/foo-frontned:{{ .tag }}
{{ else }}
image: {{ .frontend_image }}
{{ end }}
- ...

Now it's possible, but not in a way that anyone could search for on the Internet. You will have to read the code or hope I wrote documentation for my ad-hoc Kubernetes extension.

I think we can all agree that didn't save anyone any time or effort.

This example conveniently flows into my other complaint with Helm. The "yaml files" that declare templates aren't actually valid YAML. You can't use something like `prettier` to autoformat them. You can't use the YAML language server to provide code advice as you type. You can't use `kubeval` to validate them. You are throwing all of that away to Build Your Own Thing. It is actually very insidious and for that reason I consider Helm to be more harmful than helpful. It isn't an abstraction, it's just a macro that might be good for one person the instant they happened to type something in.

The other problem is that Helm charts have no upgrade path. They are only designed for "please explode this project into my cluster, I promise to clean it up In The Future". It never gets cleaned up and brings a little piece of un-updated Windows 95 nostalgia right into your cluster.

Helm is actively harmful. And people love it, because it saves them a tiny bit of time one day at the cost of a lot of time in the future.