Deploy Helm Charts to Kubernetes

The kubernetes:helm.sh/v3:Release resource, part of the Pulumi Kubernetes provider, manages Helm chart installations as Kubernetes releases. It embeds Helm as a library to orchestrate chart resources, providing the full spectrum of Helm features natively. This guide focuses on four capabilities: local and remote chart deployment, value configuration and overrides, namespace targeting, and resource dependency management.

Releases require a configured Kubernetes cluster and may reference Helm repositories or local chart directories. The examples are intentionally small. Combine them with your own cluster configuration, namespaces, and application-specific values.

Deploy a chart from a local directory

Teams developing custom charts or working with charts in version control often deploy directly from local filesystem paths.

import * as k8s from "@pulumi/kubernetes";

const nginxIngress = new k8s.helm.v3.Release("nginx-ingress", {
    chart: "./nginx-ingress",
});
from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs

nginx_ingress = Release(
    "nginx-ingress",
    ReleaseArgs(
        chart="./nginx-ingress",
    ),
)
package main

import (
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helm.NewRelease(ctx, "nginx-ingress", &helm.ReleaseArgs{
			Chart: pulumi.String("./nginx-ingress"),
		})
		if err != nil {
			return err
		}

		return nil
	})
}
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Helm.V3;
using Pulumi.Kubernetes.Helm.V3;

class HelmStack : Stack
{
    public HelmStack()
    {
        var nginx = new Release("nginx-ingress", new ReleaseArgs
        {
            Chart = "./nginx-ingress",
        });

    }
}

The chart property accepts a local directory path. Pulumi reads the chart from disk and installs it into your cluster. This approach works well for development workflows where charts are stored alongside infrastructure code.

Install a chart from a remote repository

Most production deployments pull charts from Helm repositories, allowing teams to version and distribute packaged applications.

import * as k8s from "@pulumi/kubernetes";

const nginxIngress = new k8s.helm.v3.Release("nginx-ingress", {
    chart: "nginx-ingress",
    version: "1.24.4",
    repositoryOpts: {
        repo: "https://charts.helm.sh/stable",
    },
});
from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs, RepositoryOptsArgs

nginx_ingress = Release(
    "nginx-ingress",
    ReleaseArgs(
        chart="nginx-ingress",
        version="1.24.4",
        repository_opts=RepositoryOptsArgs(
            repo="https://charts.helm.sh/stable",
        ),
    ),
)
package main

import (
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helm.NewRelease(ctx, "nginx-ingress", &helm.ReleaseArgs{
			Chart:   pulumi.String("nginx-ingress"),
			Version: pulumi.String("1.24.4"),
			RepositoryOpts: helm.RepositoryOptsArgs{
				Repo: pulumi.String("https://charts.helm.sh/stable"),
			},
		})
		if err != nil {
			return err
		}

		return nil
	})
}
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Helm.V3;
using Pulumi.Kubernetes.Helm.V3;

class HelmStack : Stack
{
    public HelmStack()
    {
        var nginx = new Release("nginx-ingress", new ReleaseArgs
        {
            Chart = "nginx-ingress",
            Version = "1.24.4",
            RepositoryOpts = new RepositoryOptsArgs
            {
                Repo = "https://charts.helm.sh/stable"
            }
        });

    }
}

The repositoryOpts property specifies the Helm repository URL. The version property pins the chart to a specific release. Together, these properties ensure reproducible deployments by fetching the exact chart version from the repository.

Override chart defaults with custom values

Charts expose configuration through values that control features like metrics, resource limits, and service types.

import * as k8s from "@pulumi/kubernetes";

const nginxIngress = new k8s.helm.v3.Release("nginx-ingress", {
    chart: "nginx-ingress",
    version: "1.24.4",
    repositoryOpts: {
        repo: "https://charts.helm.sh/stable",
    },
    values: {
        controller: {
            metrics: {
                enabled: true,
            }
        }
    },
});
from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs, RepositoryOptsArgs

nginx_ingress = Release(
    "nginx-ingress",
    ReleaseArgs(
        chart="nginx-ingress",
        version="1.24.4",
        repository_opts=RepositoryOptsArgs(
            repo="https://charts.helm.sh/stable",
        ),
        values={
            "controller": {
                "metrics": {
                    "enabled": True,
                },
            },
        },
    ),
)
package main

import (
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helm.NewRelease(ctx, "nginx-ingress", &helm.ReleaseArgs{
			Chart:   pulumi.String("nginx-ingress"),
			Version: pulumi.String("1.24.4"),
			RepositoryOpts: helm.RepositoryOptsArgs{
				Repo: pulumi.String("https://charts.helm.sh/stable"),
			},
			Values: pulumi.Map{
				"controller": pulumi.Map{
					"metrics": pulumi.Map{
						"enabled": pulumi.Bool(true),
					},
				},
			},
		})
		if err != nil {
			return err
		}

		return nil
	})
}
using System.Collections.Generic;
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Helm.V3;
using Pulumi.Kubernetes.Helm.V3;

class HelmStack : Stack
{
    public HelmStack()
    {
        var values = new Dictionary<string, object>
        {
            ["controller"] = new Dictionary<string, object>
            {
                ["metrics"] = new Dictionary<string, object>
                {
                    ["enabled"] = true
                }
            },
        };

        var nginx = new Release("nginx-ingress", new ReleaseArgs
        {
            Chart = "nginx-ingress",
            Version = "1.24.4",
            RepositoryOpts = new RepositoryOptsArgs
            {
                Repo = "https://charts.helm.sh/stable"
            },
            Values = values,
        });

    }
}

The values property accepts nested configuration that overrides chart defaults. Here, metrics are enabled in the controller. Values follow the chart’s schema; consult the chart’s documentation to understand available options.

Deploy a chart into a specific namespace

Kubernetes namespaces isolate workloads and resources. Charts can be deployed into specific namespaces for multi-tenant or environment separation.

import * as k8s from "@pulumi/kubernetes";

const nginxIngress = new k8s.helm.v3.Release("nginx-ingress", {
    chart: "nginx-ingress",
    version: "1.24.4",
    namespace: "test-namespace",
    repositoryOpts: {
        repo: "https://charts.helm.sh/stable",
    },
});
from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs, RepositoryOptsArgs

nginx_ingress = Release(
    "nginx-ingress",
    ReleaseArgs(
        chart="nginx-ingress",
        version="1.24.4",
        namespace="test-namespace",
        repository_opts=RepositoryOptsArgs(
            repo="https://charts.helm.sh/stable",
        ),
    ),
)
package main

import (
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helm.NewRelease(ctx, "nginx-ingress", &helm.ReleaseArgs{
			Chart:     pulumi.String("nginx-ingress"),
			Version:   pulumi.String("1.24.4"),
			Namespace: pulumi.String("test-namespace"),
			RepositoryOpts: helm.RepositoryOptsArgs{
				Repo: pulumi.String("https://charts.helm.sh/stable"),
			},
		})
		if err != nil {
			return err
		}

		return nil
	})
}
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Helm.V3;
using Pulumi.Kubernetes.Helm.V3;

class HelmStack : Stack
{
    public HelmStack()
    {
        var nginx = new Release("nginx-ingress", new ReleaseArgs
        {
            Chart = "nginx-ingress",
            Version = "1.24.4",
            Namespace = "test-namespace",
            RepositoryOpts = new RepositoryOptsArgs
            {
                Repo = "https://charts.helm.sh/stable"
            },
        });

    }
}

The namespace property targets a specific namespace for all chart resources. If the namespace doesn’t exist, set createNamespace to true to create it automatically. Without this property, charts deploy to the default namespace.

Combine values from files and inline configuration

Complex deployments often split configuration between reusable YAML files and environment-specific inline values.

import * as pulumi from "@pulumi/pulumi";
import * as k8s from "@pulumi/kubernetes";
import {FileAsset} from "@pulumi/pulumi/asset";

const release = new k8s.helm.v3.Release("redis", {
    chart: "redis",
    repositoryOpts: {
        repo: "https://raw.githubusercontent.com/bitnami/charts/eb5f9a9513d987b519f0ecd732e7031241c50328/bitnami",
    },
    valueYamlFiles: [new FileAsset("./metrics.yml")],
    values: {
        cluster: {
            enabled: true,
        },
        rbac: {
            create: true,
        }
    },
});

// -- Contents of metrics.yml --
// metrics:
//     enabled: true
import pulumi
from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs, RepositoryOptsArgs

nginx_ingress = Release(
    "redis",
    ReleaseArgs(
        chart="redis",
        repository_opts=RepositoryOptsArgs(
            repo="https://raw.githubusercontent.com/bitnami/charts/eb5f9a9513d987b519f0ecd732e7031241c50328/bitnami",
        ),
        value_yaml_files=[pulumi.FileAsset("./metrics.yml")],
        values={
            cluster: {
                enabled: true,
            },
            rbac: {
                create: true,
            }
        },
    ),
)

# -- Contents of metrics.yml --
# metrics:
#     enabled: true
package main

import (
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/yaml"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helm.NewRelease(ctx, "redis", &helm.ReleaseArgs{
			Chart:   pulumi.String("redis"),
			RepositoryOpts: helm.RepositoryOptsArgs{
				Repo: pulumi.String("https://charts.helm.sh/stable"),
			},
			ValueYamlFiles: pulumi.AssetOrArchiveArray{
				pulumi.NewFileAsset("./metrics.yml"),
			},
			Values: pulumi.Map{
				"cluster": pulumi.Map{
					"enabled": pulumi.Bool(true),
				},
				"rbac": pulumi.Map{
					"create": pulumi.Bool(true),
				},
			},
		})
		if err != nil {
			return err
		}

		return nil
	})
}

// -- Contents of metrics.yml --
// metrics:
//     enabled: true
using System.Collections.Generic;
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Helm.V3;
using Pulumi.Kubernetes.Helm.V3;

class HelmStack : Stack
{
    public HelmStack()
    {
        var nginx = new Release("redis", new ReleaseArgs
        {
            Chart = "redis",
            RepositoryOpts = new RepositoryOptsArgs
            {
                Repo = "https://raw.githubusercontent.com/bitnami/charts/eb5f9a9513d987b519f0ecd732e7031241c50328/bitnami"
            },
            ValueYamlFiles = new FileAsset("./metrics.yml");
            Values = new InputMap<object>
            {
                ["cluster"] = new Dictionary<string,object>
                {
                    ["enabled"] = true,
                },
                ["rbac"] = new Dictionary<string,object>
                {
                    ["create"] = true,
                }
            },
        });
    }
}

// -- Contents of metrics.yml --
// metrics:
//     enabled: true

The valueYamlFiles property accepts FileAsset references to external YAML files. Inline values take precedence over file-based values, allowing you to override specific settings while keeping shared configuration in files. This pattern supports environment-specific customization without duplicating entire value sets.

Create resources that depend on chart readiness

Downstream resources often need to wait for chart resources to be fully deployed and ready before they can be created.

import * as k8s from "@pulumi/kubernetes";

const nginxIngress = new k8s.helm.v3.Release("nginx-ingress", {
    chart: "nginx-ingress",
    version: "1.24.4",
    namespace: "test-namespace",
    repositoryOpts: {
        repo: "https://charts.helm.sh/stable",
    },
    skipAwait: false,
});

// Create a ConfigMap depending on the Chart. The ConfigMap will not be created until after all of the Chart
// resources are ready. Notice skipAwait is set to false above. This is the default and will cause Helm
// to await the underlying resources to be available. Setting it to true will make the ConfigMap available right away.
new k8s.core.v1.ConfigMap("foo", {
    metadata: {namespace: namespaceName},
    data: {foo: "bar"}
}, {dependsOn: nginxIngress})
import pulumi
from pulumi_kubernetes.core.v1 import ConfigMap, ConfigMapInitArgs
from pulumi_kubernetes.helm.v3 import Release, ReleaseArgs, RepositoryOptsArgs

nginx_ingress = Release(
    "nginx-ingress",
    ReleaseArgs(
        chart="nginx-ingress",
        version="1.24.4",
        namespace="test-namespace",
        repository_opts=RepositoryOptsArgs(
            repo="https://charts.helm.sh/stable",
        ),
        skip_await=False,
    ),
)

# Create a ConfigMap depending on the Chart. The ConfigMap will not be created until after all of the Chart
# resources are ready. Notice skip_await is set to false above. This is the default and will cause Helm
# to await the underlying resources to be available. Setting it to true will make the ConfigMap available right away.
ConfigMap("foo", ConfigMapInitArgs(data={"foo": "bar"}), opts=pulumi.ResourceOptions(depends_on=nginx_ingress))
package main

import (
	corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		release, err := helm.NewRelease(ctx, "nginx-ingress", helm.ReleaseArgs{
			Chart:     pulumi.String("nginx-ingress"),
			Version:   pulumi.String("1.24.4"),
			Namespace: pulumi.String("test-namespace"),
			RepositoryOpts: helm.RepositoryOptsArgs{
				Repo: pulumi.String("https://charts.helm.sh/stable"),
			},
			SkipAwait: pulumi.Bool(false),
		})
		if err != nil {
			return err
		}

		// Create a ConfigMap depending on the Chart. The ConfigMap will not be created until after all of the Chart
		// resources are ready. Notice SkipAwait is set to false above. This is the default and will cause Helm
		// to await the underlying resources to be available. Setting it to true will make the ConfigMap available right away.
		_, err = corev1.NewConfigMap(ctx, "cm", &corev1.ConfigMapArgs{
			Data: pulumi.StringMap{
				"foo": pulumi.String("bar"),
			},
		}, pulumi.DependsOnInputs(release))
		if err != nil {
			return err
		}

		return nil
	})
}
using System.Threading.Tasks;
using Pulumi;
using Pulumi.Kubernetes.Core.V1;
using Pulumi.Kubernetes.Types.Inputs.Helm.V3;
using Pulumi.Kubernetes.Helm.V3;

class HelmStack : Stack
{
    public HelmStack()
    {
        var nginx = new Release("nginx-ingress", new ReleaseArgs
        {
            Chart = "nginx-ingress",
            Version = "1.24.4",
            Namespace = "test-namespace",
            RepositoryOpts = new RepositoryOptsArgs
            {
                Repo = "https://charts.helm.sh/stable"
            },
            SkipAwait = false,
        });

        // Create a ConfigMap depending on the Chart. The ConfigMap will not be created until after all of the Chart
        // resources are ready. Notice SkipAwait is set to false above. This is the default and will cause Helm
        // to await the underlying resources to be available. Setting it to true will make the ConfigMap available right away.
        new ConfigMap("foo", new Pulumi.Kubernetes.Types.Inputs.Core.V1.ConfigMapArgs
        {
            Data = new InputMap<string>
            {
                {"foo", "bar"}
            },
        }, new CustomResourceOptions
        {
            DependsOn = nginx,
        });

    }
}

The skipAwait property controls whether Pulumi waits for chart resources to reach a ready state. Setting it to false (the default) ensures all chart resources are available before dependent resources are created. The dependsOn option creates an explicit dependency, preventing the ConfigMap from being created until the release is complete.

Beyond these examples

These snippets focus on specific Release features: local and remote chart deployment, value configuration (inline and file-based), and namespace targeting and resource dependencies. They’re intentionally minimal rather than full application deployments.

The examples assume pre-existing infrastructure such as a Kubernetes cluster with configured kubeconfig, Helm chart repositories for remote charts, and local chart directories for filesystem-based charts. They focus on configuring the release rather than provisioning the cluster or managing chart repositories.

To keep things focused, common release patterns are omitted, including:

  • Atomic deployments and rollback (atomic, cleanupOnFail)
  • CRD management (skipCrds, disableCRDHooks)
  • Chart verification and signing (verify, keyring)
  • Upgrade behavior (forceUpdate, resetValues, reuseValues)
  • Resource lifecycle controls (timeout, waitForJobs, maxHistory)

These omissions are intentional: the goal is to illustrate how each release feature is wired, not provide drop-in deployment modules. See the Helm Release resource reference for all available configuration options.

Let's deploy Helm Charts to Kubernetes

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Chart Installation & Sources
How do I install a Helm chart from a local directory?
Set the chart property to a local path, such as ./nginx-ingress.
How do I install a chart from a remote Helm repository?
Specify the chart name, version, and configure repositoryOpts with the repository URL in the repo field.
What happens if I don't specify a chart version?
The latest version of the chart is installed automatically.
Values & Configuration
How do I configure custom values for a Helm chart?
Use the values property for inline configuration or valueYamlFiles for YAML files. When both are specified, inline values take precedence over file contents.
What's the precedence when I use both values and valueYamlFiles?
Inline values take precedence over valueYamlFiles. File contents are read and merged first, then values override any conflicts.
Upgrade & Lifecycle Behavior
What's the difference between resetValues and reuseValues?
When upgrading, reuseValues merges the last release’s values with any new overrides, while resetValues discards previous values and uses only the chart’s built-in defaults. If both are specified, resetValues takes precedence and reuseValues is ignored.
What does the atomic property do?
Setting atomic to true purges the chart if installation fails. It also automatically disables skipAwait, forcing the provider to wait for all resources to be ready.
Why is the replace property unsafe in production?
The replace property re-uses a release name even if it’s already in use, which can cause conflicts and unexpected behavior in production environments.
Resource Management & Dependencies
How do I make other resources wait for a Helm release to be ready?
Set skipAwait to false (the default) on the Release, then use dependsOn in dependent resources. This ensures all chart resources are ready before creating dependents.
What does skipAwait do by default?
By default, skipAwait is false, meaning the provider waits until all resources are in a ready state before marking the release as successful. Setting it to true skips this wait logic.
Does waitForJobs work with skipAwait enabled?
No, waitForJobs is ignored if skipAwait is enabled. To wait for Jobs to complete, ensure skipAwait is false.
How do I query Kubernetes resources created by a Helm release?
Use the release’s status.namespace and status.name output properties to construct resource identifiers, then use Kubernetes resource get methods to retrieve them.
Namespaces & CRDs
How do I deploy a chart to a specific namespace?
Set the namespace property to your target namespace. Use createNamespace: true to automatically create the namespace if it doesn’t exist.
How do I prevent CRDs from being installed?
Set skipCrds to true. By default, CRDs are installed if not already present in the cluster.