Helm Provider 与应用发布:值注入与回滚

用 Terraform 管理 Helm Release:Provider 配置与认证、Chart 来源与版本锁定、values 注入的类型陷阱、Release 生命周期与状态同步、与 Kubernetes Provider 的分工,以及升级回滚与敏感值处理。

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 与 OperatorHelm Provider + kubectl_manifestCRD 时序问题

一句话: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 + chartversion 字段
OCIoci://...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原因
NamespaceKubernetes便于统一打标签
Secret / ConfigMapKubernetes避免写入 values 泄漏
Chart 主应用Helm复用模板
CRD 实例Kubernetes(或 kubectl)Helm 对 CRD 支持有限
NetworkPolicyKubernetes集群级策略
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 在真实生产环境中的完整轮廓。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「terraform」更多文章

  1. 模块注册表与分发:版本、文档与测试
  2. DNS 与证书编排:托管区域与自动验证
  3. IAM 策略建模:条件键、边界与跨账号