Skip to content
Go back

Modern Kubernetes: Can we replace Helm?

Published:  at  12:00 AM
David Desmarais-Michaud

Table of Contents

Open Table of Contents

Introduction

For a long time, Kubernetes resource management has been synonymous with Helm.

There have been plenty of attempts to replace Helm and its templating miasma known as Charts. But those attempts never seem to stick, sometimes because they’re not different enough, or more often because the size and mass of the Helm ecosystem creates an inertia that’s hard to overcome.

This post explores how Yoke is trying to do the impossible: introducing Flights, a complete alternative to Helm Charts, while bringing Helm along for the ride.

Charts and Flights – What’s the Difference?

At the end of the day, both Helm Charts and Yoke Flights are ways to dynamically package Kubernetes resources.

At an algebraic level, both can be viewed as functions:

y = f(x)

Where the function f is the chart or flight, x represents the inputs, and y is the final set of resources we want to deploy as a single release.

# With Helm
resources = helm.chart(values)

# With Yoke
resources = yoke.flight(stdin)

From that perspective, working with Helm or Yoke is about transforming inputs into outputs. The difference lies in how we express that transformation function.

The Helm transformation function is the Go template engine. We write a number of YAML files, organized as best we see fit, and define one or more resources per file.

We then use the template engine to express logic: conditionals, loops, and data manipulation via pipelines and Sprig functions to work with strings, maps, and slices.

In my opinion, this feels like the most straightforward approach when you think about Kubernetes as a collection of YAML documents.
It’s also very practical when your configuration needs are minimal.

But the cracks start to show almost immediately.

So although we think of Helm as an almost no-code solution, I can’t help but feel it’s actually a poor-code solution.

Reaching for a Better Language

What most people reach for at this point is the idea of a better configuration language.

We assume YAML is the problem — that our struggles stem from YAML being bad at expressing configuration.

So we reach for Jsonnet, CUE, or maybe even Apple’s new PKL.

And while I do think these tools generally improve the situation — offering benefits like reduced whitespace sensitivity or better-integrated typing — I still believe they miss the mark.

That’s because, in my view, the problem with Helm Charts isn’t that the target (YAML) is a poor configuration language. The real issue is that what we actually want is a good way to express a transformation function. Inputs must lead to outputs.

As a software developer, I can’t help but think that the best way to model a transformation from one type of data to another is… well, a function.

Just a plain, old, imperative, boring-looking function or program. If this, then do that. Our bread and butter.

You might disagree on which language or paradigm is best for expressing this kind of transformation.

Maybe a functional language like Haskell is ideal — especially for mapping one type to another, from input to output, from standard input to standard output.

Or maybe Rust with its blazing speed and memory safety?

Or Go with its tight integration within the Kubernetes ecosystem?

And that’s okay.

The larger point is this: the best tools we have for handling structured data and producing structured output are programming languages.

That’s the position Yoke takes.

Of course, it wouldn’t be feasible to support just any source code, nor would it be safe to execute arbitrary binaries.

That’s why, as luck would have it, Yoke supports WebAssembly as a shared target for code execution.

It runs in a safe, sandboxed, and predictable environment.

As long as your programming ecosystem can target WebAssembly, you get first-class support in Yoke.

A Flight is program that reads inputs over stdin, and writes Kubernetes resources over stdout.

A Tale of Two Ecosystems

So now that I’ve convinced a small percentage of readers that maybe what they really want is to develop their transformation functions in a type-safe, powerful development ecosystem — and are ready to make the switch — we have to ask the next hard question:

What exactly are we buying into? Where is the ecosystem?

What can I install, practically speaking? We can definitely build new “charts” as “flights”.

Some things already exist as Flights hosted by the Yoke project, like its “air traffic controller” or “yokecd”, a Yoke-extended version of ArgoCD.

That said, the Yoke ecosystem is still new. Adoption is, for now, just a dream on the horizon. The ecosystem still has to be built.

But what about the existing Helm ecosystem?

If I need Helm just to install redis, is switching to Yoke even worth it?

And what about all the internal Charts we use at our organizations?
Does everything need to be ported to code on day one in order to start using Yoke?

Are we just so trapped by Helm’s gravitational pull that we can never escape its orbit?

It sure feels that way.

But that’s not the whole story.

Yoke recognizes that it has no path forward — not even a snowball’s chance in hell — without some degree of interoperability with Helm.

And fortunately, things just kind of worked out. Yoke executes code compiled to WebAssembly to transform inputs into outputs.

Helm is written in Go. Go can be compiled to WebAssembly. Yoke can use Helm.

Let’s be clear: Yoke doesn’t use Helm to do package management or deployment.
But users can build Flights that embed Helm Charts and execute them to get their desired resources.

This means users can extend, combine, and manipulate Charts however they like.

And — importantly — we can embed our existing Charts into Flights on day one, and progressively port the templating logic over to real code.
This provides a smooth migration path from Charts to Flights, rather than forcing a hard rewrite.

Chartered Flights

⚠️ Warning: The following section contains Go code. As of the time of this post, using Helm in code requires being in the Go ecosystem. However, I have plans to remove this limitation. For more information, see this issue.

This wouldn’t be a yoke blog without a little bit of a demonstration in code.

All we need to remember, is that a Flight is an runnable program that writes resources to stdout.

Our program is going to need to include and run the Chart. The following is an example written in Go of how to do exactly that. Let’s begin!

Remember, WebAssembly modules do not have access to the filesystem or network.
That means we need to embed the Chart into our program.

Thankfully, since Go 1.16, this is easy to do using Go’s embed package:

import "embed"

//go:embed all:chart
var chartFS embed.FS

Alternatively, we can embed the .tgz artifact downloaded via helm pull:

import _ "embed"

//go:embed chart.tgz
var archive []byte

Next, we import Yoke’s Helm wrapper and use it to create a chart instance that we can render:

import (
  "github.com/yokecd/yoke/pkg/flight"
  "github.com/yokecd/yoke/pkg/helm"
)

// ...

// Using the embedded chart file system
chart, err := helm.LoadChartFromFS(chartFS)
if err != nil {
  return fmt.Errorf("failed to load chart from embedded FS: %w", err)
}

// Or, if using the .tgz archive:
chart, err := helm.LoadChartFromZippedArchive(archive)
if err != nil {
  return fmt.Errorf("failed to load chart from zipped archive: %w", err)
}

We can then invoke the chart using a release name, namespace, and any values we want:

resources, err := chart.Render(
  flight.Release(),
  flight.Namespace(),
  // This value can be any type. It will be marshaled to JSON before being passed to Helm under the hood.
  map[string]any{},
)

The resources returned are of type:

k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured

This allows us to work with them in a generic, flexible way.

We can write the resources as JSON over stdout once we’ve finished manipulating them:

json.NewEncoder(os.Stdout).Encode(resources)

And with that final step we’ve created a (abridged) program that embeds and executes a Chart.

All that is left to do is compile it to WebAssembly, and use yoke to apply it.

# compile to WebAssembly.
GOOS=wasip1 GOARCH=wasm go build -o ./main.wasm ./main.go

# deploy via yoke
yoke apply release-name ./main.wasm

Conclusion

So what have we learned?

We’ve looked at Helm — for all its strengths — and seen how it starts to fall apart once you outgrow simple configuration.

We’ve talked about how Yoke offers a fresh approach: treating templating not as a special YAML problem, but as a real programming problem. Inputs in, outputs out. Transformation as code.

This shift in perspective gifts us strong typing, real tooling, actual debuggers, and the freedom to express our logic in the language of our choice (as long as it compiles to WebAssembly).

And we’ve addressed the elephant in the room: Helm’s massive gravitational pull.

But instead of pretending Helm doesn’t exist, Yoke embraces it.

You can embed Charts. You can render them. You can gradually migrate them.

It’s not all-or-nothing. It’s not rewrite-everything-on-day-one: Yoke is about opening the escape hatch, not slamming the door shut.

There’s still work to do. The yoke ecosystem is young.

But if you believe Kubernetes resource management is important enough to deserve a better programming environment than a text engine, maybe it’s time to give Yoke a try.


Suggest Changes

Next Post
Dynamic Airways -- Redefining Kubernetes Application Lifecycle as Code