Terraform

Terraform Helm Provider in Practice: Managing Charts with Terraform

2026-04-19
NicheeLab Editorial Team

Running Helm by hand or through ad-hoc CI scripts leaves gaps in value provenance, diff visibility, and rollback consistency. With Terraform's Helm Provider, you can manage Chart releases declaratively as part of your IaC.

This article focuses on stable features documented by HashiCorp, summarizing the design points you cannot skip in production operations and the angles certification exams tend to test.

Helm Provider Basics and the Big Picture

Terraform's Helm Provider integrates Helm CLI-equivalent operations into the Terraform plan/apply workflow and manages state through the helm_release resource. This makes diffs visible in plan and automates upgrades on apply.

Charts can be fetched from a repository URL or a local path. The provider uses Kubernetes API connection information (kubeconfig or explicit credentials) to run Helm install and upgrade operations.

Exam questions tend to target the core helm_release attributes (chart, version, namespace, values, set, atomic, wait, timeout, create_namespace, and so on) and how each one influences plan, apply, and rollback behavior.

  • helm_release represents a single Helm release. Destroying it runs the equivalent of helm uninstall.
  • plan detects differences in values/set. set_sensitive lets you keep secrets hidden while still participating in diff management.
  • atomic=true auto-rolls back on upgrade failure. wait=true and timeout control the wait-for-stabilization behavior.
ComponentRoleTypical settings / examples
helm_releaseDeclares a Chart releasechart, version, namespace, values, set
Helm ProviderBackend for Helm operationskubernetes connection info, registry/repository auth
Terraform StateHolds the desired state of the releaseDiff and history management via plan/apply

Terraform x Helm x Kubernetes flow

plan / applyHelm Upgrade / Installfetch chartTerraform CLItfstate / diffHelm ProviderChart RepoOCI / HTTP / FSKubernetes APITerraform CLI fetches the Chart via the Helm Provider and installs it on Kubernetes. tfstate handles diff management.

Minimal helm_release example

provider "helm" {
  kubernetes {
    config_path    = var.kubeconfig_path
    config_context = var.kube_context
  }
}

resource "helm_release" "nginx" {
  name             = "nginx"
  repository       = "https://charts.bitnami.com/bitnami"
  chart            = "nginx"
  version          = "15.0.0"
  namespace        = "web"
  create_namespace = true
  values           = [file("values-prod.yaml")]
  wait             = true
  timeout          = 600
  atomic           = true
}

Provider Configuration and Kubernetes Auth Options

The Helm Provider needs a Kubernetes connection under the hood. Typically you either reference a kubeconfig or explicitly specify host, token, and CA. In multi-cluster operations, the standard play is to set up provider aliases and pass them into modules.

In CI, prefer short-lived tokens (e.g. obtained via OIDC) passed through environment variables and kept out of state. A two-tier setup — kubeconfig locally, explicit credentials in CI — is also common in practice.

  • When reading kubeconfig, use kubernetes { load_config_file=true } or specify config_path/context.
  • Explicit specification is built on the trio of host/token/cluster_ca_certificate. Grant least-privilege RBAC.
  • For multi-cluster setups, use aliases and inject them into modules via providers.
Auth methodProsCaveats
kubeconfig referenceEasy for local development; plays well with existing toolsRequires managing file distribution in CI
Explicit specification (host+token+CA)No file required; short-lived tokens raise the security barAssumes variables are kept secret and rotated
In-cluster (future option)Connects naturally from inside a PodRequires careful environment constraints and RBAC design

Typical connection patterns (default and alias)

# ローカル: kubeconfig を参照
provider "helm" {
  kubernetes {
    config_path    = var.kubeconfig_path
    config_context = var.kube_context
  }
}

# CI: 明示的に接続(別クラスタに alias で接続)
provider "helm" {
  alias = "eks"
  kubernetes {
    host                   = var.eks_api_endpoint
    token                  = var.eks_bearer_token
    cluster_ca_certificate = var.eks_cluster_ca
  }
}

# モジュール側では providers で注入
module "payments_release" {
  source    = "./modules/release"
  providers = { helm = helm.eks }
  # ... module inputs ...
}

Choosing Between values, set, and set_sensitive

Helm values are provided through values (an array of YAML file strings) and set (individual key=value overrides). In Terraform, diffs of these surface in plan and are propagated to upgrades on apply. For secrets, set_sensitive keeps the value out of plan output.

A maintainable design uses templated values files for the bulk of the configuration and keeps per-environment differences minimal with set. When you need to specify arrays explicitly, set_list is also an option.

  • Use values for the overall shape, set for fine adjustments, and set_sensitive for secrets.
  • values merges last-wins. Watch out for surprising behavior when overriding arrays.
  • If you must keep secrets out of plan output, always use set_sensitive.
MechanismPrimary useProsCaveats
valuesLarge configs and shared values across environmentsReadable and reusableRequires thought about array merge behavior and file split policy
setSmall diffs; tweaking numbers or booleansMinimal, explicit diffsA misspelled key shows up as a diff
set_sensitiveSecrets such as passwords and tokensKeeps values out of plan outputOperational discipline is needed to avoid leaking via references or logs
set_listOverriding or specifying arraysLets you pass arrays safelyRequires managing value types and ordering

Practical example: combining values and set_sensitive

locals {
  values_file = templatefile("${path.module}/values-${var.env}.yaml", {
    image_tag = var.image_tag
  })
}

resource "helm_release" "app" {
  name       = "app"
  repository = var.chart_repository
  chart      = var.chart_name
  version    = var.chart_version
  namespace  = var.namespace

  values = [
    local.values_file
  ]

  set {
    name  = "replicaCount"
    value = tostring(var.replicas)
  }

  set_sensitive {
    name  = "secrets.DB_PASSWORD"
    value = var.db_password
  }

  wait    = true
  timeout = 900
  atomic  = true
}

Lifecycle and Drift Mitigation

Terraform keeps the desired state of helm_release in state and detects drift via plan. Running helm upgrade by hand causes drift that the next plan will surface. In operations, enforcing the principle that all changes go through Terraform minimizes drift.

Upgrade stability hinges on configuring wait=true and timeout appropriately. Enabling atomic=true triggers Helm's rollback on failure, avoiding half-applied states.

When taking an existing Helm release under Terraform management, use import. After importing, verify with plan that the resource declaration and the live release are aligned.

  • Unify operational changes through Terraform and reserve manual helm for emergencies only.
  • Balance stability and speed with the right combination of wait/timeout/atomic.
  • After import, always verify that chart/version/values are consistent with the live release.
SettingPurposeField guidance / commentary
waitWait for resources to stabilizetrue is the default for production. Watch out for job/hook behavior.
timeoutUpper bound on wait timeTune to roughly a 10-20 minute upper bound in production
atomicAuto-rollback on failureRecommended true. Combine with logs when debugging.

Flow for importing an existing release

# 1) 先にリソース宣言(最小)
resource "helm_release" "postgres" {
  name       = "postgres"
  namespace  = "data"
  repository = "https://charts.bitnami.com/bitnami"
  chart      = "postgresql"
}

# 2) 既存の Helm リリースを import(namespace/name 形式の ID を利用可能)
# terraform import helm_release.postgres data/postgres

# 3) chart/version/values を合わせ、plan で差分を確認して整合を取る

Modularization and CI/CD Integration

Confine per-environment differences to module inputs and values templates, and always pin Chart versions. That ensures reproducible plans and stable CI/CD applies.

For multi-cluster or multi-namespace setups, split modules per project and switch target clusters via provider aliases. Even when using workspaces, keep readability with an explicit var env.

  • Pin Chart versions and review diffs via PR when bumping them.
  • Pass environment names as explicit variables and absorb them in template rendering.
  • In CI, persist plan as an artifact and review it before apply.
Split unitScopeBenefit
Per-chart modulePer app or middlewareSeparation of concerns and clear ownership
Per-namespace stackPermission and isolation boundaryAligns with RBAC design
Per-cluster projectFault tolerance / regionFailure domain isolation and reduced risk

Module composition and provider alias injection

# ルート側
provider "helm" {
  alias = "prod"
  kubernetes {
    host                   = var.prod_host
    token                  = var.prod_token
    cluster_ca_certificate = var.prod_ca
  }
}

module "orders" {
  source    = "./modules/release"
  providers = { helm = helm.prod }

  name            = "orders"
  namespace       = "app"
  chart_repository= "oci://registry.example.com/helm"
  chart_name      = "orders"
  chart_version   = "1.2.3"
  env             = "prod"
}

# modules/release/main.tf(例)
resource "helm_release" "this" {
  name       = var.name
  namespace  = var.namespace
  repository = var.chart_repository
  chart      = var.chart_name
  version    = var.chart_version
  values     = [templatefile("${path.module}/values-${var.env}.yaml", {})]
  wait       = true
  timeout    = 900
  atomic     = true
}

Security and Secret Handling

Handle secret values by combining Terraform's sensitive variables with helm_release's set_sensitive. That minimizes exposure in plan/apply output and in state. Keep kubeconfigs and tokens out of the repository — pass them from short-lived sources for safety.

Follow the principle of least privilege for RBAC and scope permissions to the target namespace. When repository authentication is required, pass repository_username / repository_password and related parameters via variables.

  • Combine var.sensitive=true with set_sensitive for secrets.
  • Encrypt state and protect the backend (remote state plus access control).
  • Use short-lived tokens in CI environment variables and suppress them in log output.
RiskRecommended mitigationTerraform / Helm feature
Plaintext exposureUse sensitive variables and set_sensitivevariable.sensitive, set_sensitive
Leakage to logsSuppress output and check during reviewMasking of sensitive values
Credential distributionShort-lived tokens and least privilegeExplicit provider auth and RBAC

Safely passing in secrets

variable "db_password" {
  type      = string
  sensitive = true
}

resource "helm_release" "db" {
  name       = "db"
  repository = "https://charts.bitnami.com/bitnami"
  chart      = "postgresql"
  version    = "12.6.0"
  namespace  = "data"

  set_sensitive {
    name  = "global.postgresql.auth.postgresPassword"
    value = var.db_password
  }

  wait    = true
  timeout = 1200
  atomic  = true
}

Check Your Understanding

Pro

問題 1

You are deploying a PostgreSQL Helm Chart from Terraform in production. You want to pass the database password safely, keep its value out of terraform plan output, and still maintain diff management. Which approach is most appropriate?

  1. Pass the password through a set_sensitive block on helm_release
  2. Store the plaintext value in a values file and manage it in Git
  3. Pass a base64-encoded value via the set block (and accept that it appears in logs)
  4. Hold the plaintext in locals and embed it into values

正解: A

set_sensitive masks secrets while still participating in diff management. Plaintext values or set entries carry a high exposure risk, and base64 encoding is not secrecy.

Frequently Asked Questions

Can I use a local Chart directory?

Yes. You can specify a local path (e.g. ./charts/app) for the chart argument of helm_release. The repository argument is not required in that case.

How do I migrate an existing Helm release under Terraform management?

First declare the matching helm_release resource, then run terraform import. The ID can be the release name (or namespace/name when a namespace is involved). After importing, align chart/version/values with the actual release and verify there is no drift via terraform plan.

How do I use Charts from authenticated repositories or registries?

Pass repository along with credentials such as repository_username / repository_password as variables. Use set_sensitive and sensitive variables for secrets to minimize exposure in plan output and state.

Check what you learned with practice questions

Practice with certification-focused question sets

Try free practice questions
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.