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 FREEFrequently Asked Questions
Chart Installation & Sources
chart property to a local path, such as ./nginx-ingress.chart name, version, and configure repositoryOpts with the repository URL in the repo field.Values & Configuration
values property for inline configuration or valueYamlFiles for YAML files. When both are specified, inline values take precedence over file contents.values take precedence over valueYamlFiles. File contents are read and merged first, then values override any conflicts.Upgrade & Lifecycle Behavior
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.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.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
skipAwait to false (the default) on the Release, then use dependsOn in dependent resources. This ensures all chart resources are ready before creating dependents.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.waitForJobs is ignored if skipAwait is enabled. To wait for Jobs to complete, ensure skipAwait is false.status.namespace and status.name output properties to construct resource identifiers, then use Kubernetes resource get methods to retrieve them.Namespaces & CRDs
namespace property to your target namespace. Use createNamespace: true to automatically create the namespace if it doesn’t exist.skipCrds to true. By default, CRDs are installed if not already present in the cluster.