Terraform Helm Provider: Deploy Helm Charts to Kubernetes with OpenTofu
Manage Helm releases as code #
Helm is the package manager of Kubernetes. The Terraform and OpenTofu helm provider installs and upgrades charts as helm_release resources, so the cluster and its base components (ingress controller, certificate manager, monitoring) are created in the same pipeline. Compare with managing plain manifests in Kubernetes with Terraform and with GitOps.
Providers #
The Helm provider needs access to the cluster. This example uses an EKS cluster created in the same configuration:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.17"
}
}
}
data "aws_eks_cluster" "this" {
name = var.cluster_name
}
data "aws_eks_cluster_auth" "this" {
name = var.cluster_name
}
provider "helm" {
kubernetes {
host = data.aws_eks_cluster.this.endpoint
cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data)
token = data.aws_eks_cluster_auth.this.token
}
}For a local cluster (K3s), use config_path = "~/.kube/config" inside the kubernetes block. Version 3 of the provider changes this block syntax to an attribute, so check the documentation of the version you install.
Install a chart #
resource "helm_release" "ingress_nginx" {
name = "ingress-nginx"
repository = "https://kubernetes.github.io/ingress-nginx"
chart = "ingress-nginx"
version = "4.11.3"
namespace = "ingress-nginx"
create_namespace = true
set {
name = "controller.replicaCount"
value = "2"
}
set {
name = "controller.service.annotations.service\\.beta\\.kubernetes\\.io/aws-load-balancer-type"
value = "nlb"
}
}Always pin version. The set block overrides single values, and the dots in annotation names must be escaped.
Use a values file #
For many settings, a values file is clearer. Use templatefile to inject Terraform values:
resource "helm_release" "cert_manager" {
name = "cert-manager"
repository = "https://charts.jetstack.io"
chart = "cert-manager"
version = "v1.16.1"
namespace = "cert-manager"
create_namespace = true
values = [
templatefile("${path.module}/values/cert-manager.yaml.tftpl", {
replicas = var.environment == "pro" ? 2 : 1
})
]
}crds:
enabled: true
replicaCount: ${replicas}Sensitive values #
resource "helm_release" "app" {
name = "app"
chart = "./charts/app"
namespace = "default"
set_sensitive {
name = "database.password"
value = var.db_password
}
}set_sensitive hides the value in the plan, but it is still stored in the state. See secrets management: prefer secrets that the application reads from Secrets Manager or an operator.
Dependencies and CRDs #
Charts that define custom resources (cert-manager issuers, Prometheus rules) cannot be planned until their CRDs exist. Install them in separate steps with depends_on, or in separate configurations, and use kubernetes_manifest or a second Helm chart for the custom resources.
resource "helm_release" "app" {
# ...
depends_on = [helm_release.cert_manager, helm_release.ingress_nginx]
}Upgrades and rollbacks #
- Changing
versionorvaluesand applying runs ahelm upgrade. atomic = truerolls back automatically when the release fails, andtimeoutsets the wait time.helm_releasecan leave a release in a failed state. Fix it withhelm rollbackortofu apply -replace=helm_release.app.
When to use it #
Use the Helm provider for the platform components that you install once per cluster. For application deployments, a GitOps tool such as Argo CD offers continuous reconciliation and visibility that Terraform runs do not. Related: Terraform with Kubernetes and Helm installation.