Learn Helm Chart - Helm SDK & Programmatic Usage
Episode 23 of 30

Learn Helm Chart - Helm SDK & Programmatic Usage

Using Helm as a Go library, not just a CLI: initializing an action client, installing and upgrading releases programmatically, listing releases, and rendering templates — the foundation for building operators, platform engineering tools, and self-service portals, plus an overview of language bindings.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

After episode 22, where we covered Helm plugins — extending the Helm CLI with custom commands — in this episode we take a deeper step: using Helm as a Go library inside your own programs. So far we've called Helm from the outside — via the terminal, scripts, or pipelines. But Helm 3 was built as a real Go library, and its CLI (helm) is just one "client" of that library. When you run helm upgrade, you're actually executing Go functions from the helm.sh/helm/v3/pkg/action package.

Why does this matter? Because there's a class of problems that can't be solved by calling the CLI:

  1. Kubernetes operators — a controller running continuously in the cluster, watching user desires, and deploying/upgrading charts automatically when desires change. Calling the CLI from within an operator isn't practical — you need fine-grained programmatic control, state, and error handling.
  2. Platform engineering tools — an internal developer portal (IDP) where developers press a "deploy this service to my namespace" button without needing to know Helm commands.
  3. CI/CD integrations — pipelines that need to validate, render, or deploy charts with business logic (for example, "only deploy if 80% of the cluster is healthy").
  4. Self-service portals — UIs showing a list of releases, versions, and a rollback button for teams that don't want to learn helm at all.

Think of the Helm CLI as a car engine, and the Helm SDK as the same engine without the body and dashboard — you can mount it into any vehicle you build yourself. All the features you've learned over 22 episodes — install, upgrade, rollback, template, values precedence — are available as Go functions you can call from code.

Important: this episode is intentionally high-level and practical, not a deep tour into Helm's source code. The goal is for you to know what can be done, why to do it programmatically, and how to start — not to understand every line of Helm's implementation.

Main Discussion

Understanding the Helm SDK Structure

The Helm Go package is divided into several parts, each with its own responsibility. The ones you need to know as an SDK user:

  • pkg/action — the high-level API for user-facing operations: action.NewInstall, action.NewUpgrade, action.NewUninstall, action.NewList. This is the most-used, because it's the programmatic version of the CLI commands.
  • pkg/cli — helpers for building a standard environment: reading the kubeconfig, namespace, and cluster connection configuration. This is the equivalent of "how Helm knows which cluster to talk to."
  • pkg/chart and pkg/chart/loader — the chart data structures and how to load them (from a directory, tarball, or registry).
  • pkg/action + pkg/release — concrete actions and release representations (status, manifest, values).
  • pkg/engine — the Go template rendering engine.

The mentality to build: the Helm SDK isn't "running helm commands from Go"; rather, it's "building an action object, configuring it, then executing it against the cluster." The difference is subtle but important — with the SDK you control the entire lifecycle directly, not just spawn a subprocess.

Initializing Configuration and Installing Releases

The first step of any program using the Helm SDK is building an action.Configuration — the object connecting actions with the cluster, release storage, and logger. The standard pattern:

main.go - installing a release with the Helm SDK
package main
 
import (
	"context"
	"fmt"
	"log"
 
	"helm.sh/helm/v3/pkg/action"
	"helm.sh/helm/v3/pkg/chart/loader"
	"helm.sh/helm/v3/pkg/cli"
	"k8s.io/cli-runtime/pkg/genericclioptions"
)
 
func main() {
	settings := cli.New()
 
	// 1. Build an action configuration connected to the cluster
	config := new(action.Configuration)
	if err := config.Init(settings.RESTClientGetter(), "myapp", "secrets", log.Printf); err != nil {
		log.Fatalf("failed to initialize configuration: %v", err)
	}
 
	// 2. Define the install action and its configuration
	install := action.NewInstall(config)
	install.Namespace = "myapp"
	install.ReleaseName = "myapp"
	install.Wait = true
	install.Timeout = 10 * 60 * 1e9
 
	// 3. Load the chart from a directory
	chart, err := loader.Load("./charts/myapp")
	if err != nil {
		log.Fatalf("failed to load chart: %v", err)
	}
 
	// 4. Execute the install against the cluster
	rel, err := install.Run(chart, map[string]interface{}{
		"replicaCount": 3,
		"image": map[string]interface{}{
			"tag": "1.24.0",
		},
	})
	if err != nil {
		log.Fatalf("install failed: %v", err)
	}
	fmt.Printf("Release %q installed (status: %s)\n", rel.Name, rel.Info.Status)
}

Let's dissect it step by step. Step 1 builds an action.Configuration — note settings.RESTClientGetter(), which produces a connection from the same kubeconfig kubectl uses. This means your program automatically honors the context, namespace, and cluster credentials already configured. The second parameter ("myapp") is the namespace for release state storage, and "secrets" selects the Helm 3 release storage backend.

Step 2 is action.NewInstall(config) — the install object is the programmatic version of the helm install flags: Wait, Timeout, Atomic, DryRun, all struct fields. Step 3 loads the chart from a directory via loader.Load. Step 4 is the heart: install.Run(chart, values) — the second parameter is a Go map equivalent to the --set values and -f files you know. Note the values can be nested map[string]interface{} — this is the programmatic way to deliver values.

There are two details in the example above worth noting because they distinguish SDK usage from the CLI. First, install.Timeout is written as 10 * 60 * 1e9 — Go uses nanoseconds for durations, so that expression is 10 minutes in nanoseconds; in real practice most people write 10 * time.Minute with a time import for readability. Second, the error from Run carries richer information than CLI output: besides the message, you can access rel.Info.Description or the status for programmatic logic — for example, deciding whether a failure warrants a retry, a rollback, or a stop. This is where the SDK wins: those decisions can be encoded, not left to a human reading logs.

Note

Values precedence is the same as the CLI. The values you pass directly to Run override the chart's built-in values, and to override with files you can still use loader/values.Options — for example, values.Options{ValueFiles: []string{"deploy/values-prod.yaml"}} which then gets merged. All the priority rules from episode 6 still apply; only the medium changes: from files and flags to Go objects.

Upgrade, List, and Other Lifecycle Operations

The same pattern applies to all operations — create an action object, configure it, run it. For upgrade:

Upgrading an existing release
upgrade := action.NewUpgrade(config)
upgrade.Namespace = "myapp"
upgrade.Wait = true
upgrade.Timeout = 10 * 60 * 1e9
 
chart, _ := loader.Load("./charts/myapp")
rel, err := upgrade.Run("myapp", chart, map[string]interface{}{
	"replicaCount": 6,
})
if err != nil {
	log.Fatalf("upgrade failed: %v", err)
}
fmt.Printf("Release %q upgraded to revision %d\n", rel.Name, rel.Version)

The key difference from install: Run accepts an existing release name, and Helm performs the upgrade with a three-way merge as we learned in episode 5 — it compares the old manifest, the new manifest, and the actual state in the cluster. The revision increments each time, and history is preserved.

For listing all releases in the cluster:

Listing all releases
list := action.NewList(config)
list.AllNamespaces = true
list.StateMask = action.ListDeployed | action.ListFailed
 
releases, err := list.Run()
if err != nil {
	log.Fatalf("failed to list releases: %v", err)
}
for _, rel := range releases {
	fmt.Printf("%s\t%s\t%s\t%d\n", rel.Name, rel.Namespace, rel.Info.Status, rel.Version)
}

And for rendering templates without a cluster — the equivalent of helm template — you don't need a cluster configuration at all. Just use action.Install with DryRun=true and ClientOnly=true, or use the pkg/engine package directly:

Rendering templates without a cluster (helm template)
i := action.NewInstall(config)
i.DryRun = true
i.ClientOnly = true
i.ReleaseName = "myapp"
 
rel, err := i.Run(chart, values)
if err != nil {
	log.Fatalf("render failed: %v", err)
}
fmt.Println(rel.Manifest)

rel.Manifest holds all the rendered YAML — exactly the same as helm template --debug output. This pattern is very useful for tools that need to validate or inspect manifests before actually deploying.

Loading Charts from Repositories and Registries

So far the examples use loader.Load from a local directory. In production, tools often need to load charts from an HTTP repository (episode 17) or an OCI registry (episode 18) — and the SDK provides the paths. For classic repositories, use repo.FindChartInRepoURL, which takes a URL, chart name, and version, then downloads the tarball:

Loading a chart from an HTTP repository
import "helm.sh/helm/v3/pkg/getter"
import "helm.sh/helm/v3/pkg/repo"
 
chartURL, err := repo.FindChartInRepoURL(
	"https://charts.bitnami.com/bitnami", "nginx", "15.11.2",
	getter.All(settings),
)
if err != nil {
	log.Fatalf("chart not found: %v", err)
}
 
chart, err := loader.Load(chartURL)

Note getter.All(settings) — this creates a set of download clients honoring the same repository configuration (credentials, proxy, TLS) as the CLI. The result is a path to a tarball already downloaded into the cache, and loader.Load then reads it just like a local directory.

For an OCI registry, the flow is a bit different: you load directly from an oci:// reference using action.NewRegistryClient for authentication, then registryClient.Fetch / chart.Chart gets resolved. The principle is the same — create objects, configure, run — only the retrieval medium differs. This matters for tools that must manage releases from published charts, not from local chart sources.

Real Use Cases: Why Programmatic Is More Powerful

To keep it concrete, let's look at three real use cases that are almost always the reason teams use the SDK:

Custom operator. A Kubernetes operator built with controller-runtime (Kubebuilder/Operator SDK). Inside its reconcile loop, the operator calls the Helm SDK to manage application releases: when a MyApp CustomResource is created, the operator installs the chart; when its spec changes, the operator renders and upgrades; when the resource is deleted, the operator uninstalls. This is the foundation of the pattern many internal "app-of-apps" use. The main advantage: the operator can react to changes without human or pipeline intervention — something impossible with the CLI.

Self-service portal. A platform team builds an internal UI where developers pick a service name, namespace, and resource sizing — then the Go backend calls the Helm SDK with those choices. Developers never touch helm; the risk of misconfiguration drops drastically because the UI only offers validated options, not free-form strings.

CI/CD integrations. Pipelines needing more logic than just helm upgrade: validating that changes only affect a specific release, gating based on smoke test results, or automatic updates to several clusters from one commit. With the SDK, this logic lives in tested code, not in fragile shell command chains.

Other Language Bindings and REST Alternatives

The official Helm SDK is Go, but the ecosystem already provides paths from other languages:

Python (helm-py) — the official binding using cgo to bridge the Helm library into Python. It lets Python tools (for example, Airflow tasks, data tooling) manage releases without writing Go. The drawback: it needs a C build toolchain and a compatible Helm version in the target environment.

JavaScript/TypeScript — there's no official binding equivalent to helm-py. Common choices: wrapping the Helm CLI in a subprocess (for example, via child_process), or using a library like @helm/kind for local use. For production, many TypeScript teams instead call a Go-built service behind it.

REST API alternatives — for architectures that can't use a library at all, there are several paths: ChartMuseum provides an API for chart management (upload, download, metadata) that we covered in episode 17; some projects wrap Helm in an HTTP service (for example, helm-repo-service or internal platform solutions); and for GitOps, ArgoCD and Flux expose APIs to manage Helm-based applications (we'll cover them in detail in episode 25). The REST approach is useful when the consumers are frontends or unsupported languages.

Warning

Beware of CLI subprocesses as an "SDK". Replacing the helm command in a pipeline with exec("helm", "upgrade", ...) looks easy but is fragile: no type checking, text-based error parsing, state must be maintained via temporary files, and parallel upgrades can trample each other. If you only need one or two commands, the CLI is enough. If you start building logic (list → filter → upgrade → verify), that's the sign you should move to the SDK.

Conclusion

In this episode 23 we dissected the Go Helm SDK: the package structure (action, cli, chart/loader, engine), initializing an action.Configuration connected to the cluster, installing with action.NewInstall, upgrading with action.NewUpgrade, listing with action.NewList, and rendering templates without a cluster — closed with real use cases (operators, self-service portals, CI/CD) and an overview of other languages (helm-py, TypeScript, REST paths).

The core takeaways:

  • The Helm SDK is Helm itself — the CLI is just one of its users.
  • The pattern is always the same: initialize config → create an action object → configure fields → Run.
  • action.NewInstall + loader.Load + Run(chart, values) is the trio that solves 90% of needs.
  • DryRun + ClientOnly gives you helm template inside code.
  • Move from CLI to SDK when your need shifts from "running commands" to "building logic around releases."

In the next episode, episode 24, we cover CI/CD pipeline integration: combining everything you've learned — linting, testing, packaging, pushing charts to a registry, and deploying to a cluster — into automated pipelines with GitHub Actions, GitLab CI/CD, Jenkins, and Tekton. See you in episode 24!

Learn Helm Chart - Helm SDK & Programmatic Usage | Learn Helm Chart