1. Helm Provider 的定位与配置
一句话总结: Helm Provider 让 Terraform 能安装 Chart,但它管理的是「Release 这个对象」而非集群内的每个资源,因此它的能力边界与 Kubernetes Provider 截然不同。
在 Terraform 中管理 Kubernetes 工作负载有两条路径:用 Kubernetes Provider 逐个描述 Deployment、Service、ConfigMap,或用 Helm Provider 安装一个已经打包好的 Chart。前者粒度细、可控性强;后者复用生态、上手快。
terraform {
required_providers {
helm = {
source = "hashicorp/helm"
version = "~> 2.14"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.31"
}
}
}
provider "helm" {
kubernetes {
config_path = var.kubeconfig_path
config_context = var.kube_context
}
}
两条路径的分工建议:
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 使用社区 Chart(Ingress、监控) | Helm Provider | 复用成熟模板 |
| 自定义应用、需精细控制 | Kubernetes Provider | 无模板黑盒 |
| Chart + 少量集群级资源 | 两者混用 | 各取所长 |
| 需要 CRD 与 Operator | Helm Provider + kubectl_manifest | CRD 时序问题 |
一句话:Helm Provider 管理的是 Release 的「声明」,而不是它创建出来的那些 Kubernetes 对象。
2. Chart 来源与版本锁定
一句话总结: Chart 版本必须显式锁定,来源可以是仓库、本地路径或 OCI;不锁版本等于把生产环境交给上游的发布节奏。
resource "helm_release" "ingress_nginx" {
name = "ingress-nginx"
namespace = "ingress-nginx"
repository = "https://kubernetes.github.io/ingress-nginx"
chart = "ingress-nginx"
version = "4.11.3"
create_namespace = true
timeout = 600
wait = true
}
三种来源形式:
# 本地 Chart 目录:适合自研 Chart,随代码一起评审
resource "helm_release" "app" {
name = "app"
chart = "${path.module}/charts/app"
}
# OCI 仓库:现代分发方式,无需 helm repo add
resource "helm_release" "app_oci" {
name = "app"
chart = "oci://registry.example.com/charts/app"
version = "1.4.0"
}
| 来源 | 语法 | 版本锁定方式 |
|---|---|---|
| HTTP 仓库 | repository + chart | version 字段 |
| OCI | oci://... | version 字段 |
| 本地目录 | ./charts/app | 随代码提交 |
| 打包 tgz | ./charts/app-1.0.0.tgz | 文件名含版本 |
create_namespace = true 让 Terraform 一并创建命名空间,但要注意它不管理命名空间的标签与配额——需要时仍应单独用 Kubernetes Provider 定义。
一句话:
version字段是 Helm Release 唯一的可复现性保障,省掉它等于放弃回滚能力。
3. values 注入与类型陷阱
一句话总结: Helm Provider 提供
values、set、set_list三类注入方式,其中set会把所有值转成字符串,是副本数变成"3"的根因。
resource "helm_release" "app" {
name = "app"
chart = "${path.module}/charts/app"
namespace = "app"
values = [
file("${path.module}/values/common.yaml"),
templatefile("${path.module}/values/${var.environment}.yaml.tpl", {
image_tag = var.image_tag
replicas = var.replicas
}),
]
}
三类注入方式的行为差异:
| 方式 | 类型处理 | 适用 |
|---|---|---|
values | 保留 YAML 原生类型 | 复杂嵌套结构 |
set | 全部转为字符串 | 简单标量覆盖 |
set_list | 字符串列表 | 数组类字段 |
# set 注入会把数字变成字符串,Chart 若做类型校验会失败
set {
name = "replicaCount"
value = var.replicas
}
# 需要数字时改用 YAML 合并,保留原生类型
values = [yamlencode({ replicaCount = var.replicas })]
values 列表是按顺序合并的,后面的覆盖前面的,因此「公共值在前、环境值在后」是约定俗成的顺序。
values = [
file("${path.module}/values/common.yaml"),
file("${path.module}/values/${var.environment}.yaml"),
yamlencode({ image = { tag = var.image_tag } }),
]
一句话:
set的隐式字符串转换是 Helm + Terraform 组合中最经典的「类型不匹配」故障源。
4. Release 生命周期与状态同步
一句话总结: Release 的创建、更新与销毁由 Terraform 驱动,但集群内的漂移不会被自动感知,
helm_release的状态与集群实际状态可能长期不一致。
resource "helm_release" "app" {
name = "app"
chart = "${path.module}/charts/app"
namespace = "app"
create_namespace = true
wait = true
wait_for_jobs = true
timeout = 600
atomic = true
cleanup_on_fail = true
}
atomic 与 cleanup_on_fail 是生产环境的必备组合:安装失败自动回滚,不留半成品 Release。
| 参数 | 作用 | 建议 |
|---|---|---|
wait | 等待资源就绪 | 开启 |
wait_for_jobs | 等待 Job 完成 | 有初始化任务时开启 |
atomic | 失败自动回滚 | 开启 |
cleanup_on_fail | 失败清理资源 | 开启 |
timeout | 等待上限 | 按 Chart 规模调整 |
漂移的典型来源是「有人用 kubectl edit 改了 Deployment」,此时 terraform plan 显示无变化,因为 Helm Provider 只对比 values 与 Chart 版本,不比对集群内对象。
# 检测集群内是否有人手工改动
kubectl diff -f <(helm get manifest app -n app) || true
# 强制 Terraform 重新对账
terraform apply -replace=helm_release.app
一句话:Helm Provider 的
plan是「配置对比」而不是「状态对比」,别指望它发现人工改动。
5. 与 Kubernetes Provider 的协作
一句话总结: 两者共用一份 kubeconfig 但职责不同:Helm 负责「装 Chart」,Kubernetes Provider 负责「补 Chart 不管理的周边资源」,关键是显式声明依赖顺序。
resource "helm_release" "app" {
name = "app"
chart = "${path.module}/charts/app"
namespace = kubernetes_namespace.app.metadata[0].name
depends_on = [kubernetes_secret.app_config]
}
resource "kubernetes_secret" "app_config" {
metadata {
name = "app-config"
namespace = kubernetes_namespace.app.metadata[0].name
}
data = {
DATABASE_URL = var.database_url
}
}
顺序很重要:Secret 必须先于 Release 存在,否则 Pod 会因找不到挂载对象而一直 CreateContainerConfigError。
| 资源 | 归属 Provider | 原因 |
|---|---|---|
| Namespace | Kubernetes | 便于统一打标签 |
| Secret / ConfigMap | Kubernetes | 避免写入 values 泄漏 |
| Chart 主应用 | Helm | 复用模板 |
| CRD 实例 | Kubernetes(或 kubectl) | Helm 对 CRD 支持有限 |
| NetworkPolicy | Kubernetes | 集群级策略 |
provider "kubernetes" {
config_path = var.kubeconfig_path
config_context = var.kube_context
}
两个 Provider 使用同一份 kubeconfig,但建议通过变量而非硬编码路径传入,便于在 CI 中切换上下文。
一句话:Chart 负责「应用怎么跑」,Kubernetes Provider 负责「应用能访问什么」——两者不要互相越界。
6. 升级与回滚策略
一句话总结: Helm 自带 revision 历史,但通过 Terraform 回滚的正确方式是「回退代码 + apply」,而不是手工执行
helm rollback,否则状态会立刻漂移。
# 查看发布历史
helm history app -n app
# 手工回滚会与 Terraform 状态脱节,仅用于紧急止损
helm rollback app 3 -n app
正确的回滚流程是把镜像标签或 Chart 版本退回上一个提交,再让 Terraform 执行:
variable "image_tag" {
type = string
description = "应用镜像标签,回滚时回退到上一个提交值"
}
resource "helm_release" "app" {
values = [
yamlencode({
image = {
repository = var.image_repository
tag = var.image_tag
}
}),
]
}
| 回滚方式 | 速度 | 状态一致性 | 适用 |
|---|---|---|---|
helm rollback | 秒级 | 破坏,需后续 apply 修复 | 紧急止损 |
回退代码 + apply | 分钟级 | 一致 | 常规流程 |
-replace 重建 | 分钟级 | 一致 | 状态已损坏 |
配合 helm_release 的 revision 属性可以观测当前版本:
terraform state show helm_release.app | grep revision
一句话:Helm 的 revision 历史是「集群侧的记忆」,Terraform 的 state 才是「期望状态的唯一真相」。
7. 多环境与敏感值处理
一句话总结: 多环境的差异应集中在 values 文件与少量变量上,敏感值一律走 Secret 而不是 values,否则会明文写进 Terraform state。
locals {
env_values = {
dev = "values/dev.yaml"
prod = "values/prod.yaml"
}
}
resource "helm_release" "app" {
name = "app-${var.environment}"
chart = "${path.module}/charts/app"
namespace = "app-${var.environment}"
values = [
file("${path.module}/${local.env_values[var.environment]}"),
yamlencode({
environment = var.environment
replicaCount = var.environment == "prod" ? 3 : 1
}),
]
}
敏感值的处理有一条铁律:凡是写进 values 的内容,都会以明文出现在 Terraform state 中。
# 错误做法:密码进入 values,进而进入 state
values = [yamlencode({ database = { password = var.db_password } })]
# 正确做法:由 Kubernetes Secret 承载,Chart 通过 existingSecret 引用
resource "kubernetes_secret" "db" {
metadata {
name = "app-db"
namespace = kubernetes_namespace.app.metadata[0].name
}
data = { password = var.db_password }
}
resource "helm_release" "app" {
values = [
yamlencode({ database = { existingSecret = "app-db" } }),
]
}
| 内容 | 存放位置 | 是否进 state |
|---|---|---|
| 副本数、镜像标签 | values | 是(非敏感) |
| 数据库密码 | Secret + existingSecret | 否 |
| TLS 证书 | Secret | 否 |
| 环境标识 | values | 是 |
一句话:判断一个值能不能写进 values,标准是「它出现在 state 里是否可接受」。
8. 总结
Helm Provider 的使用可以归纳为「锁、序、分离」三件事:
| 环节 | 要点 |
|---|---|
| 定位 | 管理 Release,不管理集群内每个对象 |
| 版本 | version 必须锁定,本地 Chart 随代码提交 |
| 注入 | 优先 values,set 会转成字符串 |
| 合并 | values 列表按顺序覆盖,公共在前 |
| 稳定性 | atomic + cleanup_on_fail + wait |
| 协作 | Secret 先于 Release,用 depends_on 显式声明 |
| 回滚 | 回退代码再 apply,不用 helm rollback |
| 敏感值 | 走 Secret + existingSecret,不进 values |
一句话收尾:Helm Provider 是 Terraform 与 Kubernetes 生态之间的一座桥,桥的两端各有各的状态模型:Helm 有 revision 历史,Terraform 有 state 与计划。理解「Terraform 管理的是声明、Helm 管理的是发布」这条边界,就能避开绝大多数「plan 无变化但集群已变」的困惑。至此,从 Serverless 到托管数据库,从 IAM 到 DNS 证书,从模块分发到应用发布,这一批六个方向共同勾勒出 Terraform 在真实生产环境中的完整轮廓。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。