Terraform如何参数化多个Kubernetes清单做多环境部署
以下是三种生产环境常用的实现方式,可根据自身场景选择:
方案1:
templatefile模板渲染(优先推荐,无额外依赖,维护成本低)
这个方案基于Terraform原生的模板渲染能力,提前把YAML里需要差异化的字段替换成模板变量,部署时根据传入的环境参数动态渲染最终的YAML内容。- 先把
manifests目录下的YAML文件改成模板格式:需要根据环境变化的字段(比如副本数、镜像标签、资源配额、命名空间)替换为Terraform模板变量,格式为${变量名};YAML里原本K8s自身的${xxx}格式引用(比如Downward API、ConfigMap挂载引用)要转义成$${xxx},避免被Terraform误解析。
举个修改后的gdp-collector-deployment.yaml示例:
apiVersion: apps/v1 kind: Deployment metadata: name: gdp-collector namespace: gdp-${env} spec: replicas: ${replicas} selector: matchLabels: app: gdp-collector template: metadata: labels: app: gdp-collector spec: containers: - name: collector image: registry.internal/gdp/collector:${image_tag} resources: limits: cpu: ${cpu_limit} memory: ${memory_limit} # K8s原生变量引用加$转义 env: - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP- 定义环境差异化参数,可以单独拆成
dev.tfvars、prod.tfvars文件,也可以统一写在变量定义里:
# variables.tf variable "env" { type = string validation { condition = contains(["dev", "prod"], var.env) error_message = "环境仅支持dev/prod." } } variable "env_config" { type = map(any) default = { dev = { replicas = 1 image_tag = "dev-latest" cpu_limit = "500m" memory_limit = "512Mi" } prod = { replicas = 3 image_tag = "v1.2.0-stable" cpu_limit = "2000m" memory_limit = "4Gi" } } }- 修改
main.tf逻辑,替换原来直接读取YAML的写法,遍历模板文件逐个渲染后再提交给K8s:
locals { # 遍历manifests下所有yaml文件,传入环境参数渲染 rendered_manifests = { for yaml_file in fileset("${path.module}/manifests/", "*.yaml") : trimsuffix(yaml_file, ".yaml") => templatefile( "${path.module}/manifests/${yaml_file}", merge( { env = var.env }, var.env_config[var.env] ) ) } } resource "kubectl_manifest" "yaml-manifests" { for_each = local.rendered_manifests yaml_body = each.value }部署时指定对应环境的参数即可,比如开发环境执行
terraform apply -var="env=dev",生产环境指定-var="env=prod",如果用独立tfvars文件就加-var-file=prod.tfvars参数。- 先把
方案2:YAML结构化解码修改(适合不想改动原有YAML文件的场景)
如果不想把现有YAML改成模板格式,可以先把读取到的YAML解析为Terraform原生对象,针对需要修改的字段做定向合并修改,再编码回YAML格式应用。示例代码:data "kubectl_path_documents" "yaml-files" { pattern = "${path.module}/manifests/*.yaml" } locals { decoded_yamls = { for idx, raw_doc in data.kubectl_path_documents.yaml-files.documents : idx => yamldecode(raw_doc) } # 按资源类型定向修改字段 modified_yamls = { for k, resource in local.decoded_yamls : k => merge( resource, # 统一修改所有资源的命名空间 { metadata = merge(resource.metadata, { namespace = "gdp-${var.env}" }) }, # Deployment类型修改副本数、镜像、资源配置 resource.kind == "Deployment" ? { spec = merge(resource.spec, { replicas = var.env_config[var.env].replicas template = merge(resource.spec.template, { spec = merge(resource.spec.template.spec, { containers = [ for container in resource.spec.template.spec.containers : merge(container, { image = "${split(":", container.image)[0]}:${var.env_config[var.env].image_tag}" resources = { limits = { cpu = var.env_config[var.env].cpu_limit memory = var.env_config[var.env].memory_limit } } }) ] }) }) }) } : {} ) } final_manifests = { for k, v in local.modified_yamls : k => yamlencode(v) } } resource "kubectl_manifest" "yaml-manifests" { for_each = local.final_manifests yaml_body = each.value }这个方案的缺点是如果YAML层级深、改动字段多,合并逻辑写起来很繁琐,容易出问题,适合改动字段非常少的场景。
方案3:按环境拆分资源目录(适合环境差异极大的场景)
如果dev和prod环境的K8s资源结构差异非常大,不是改几个字段就能兼容的,可以直接把manifests目录按环境拆分:├── manifests │ ├── dev │ │ ├── gdp-configmap.yaml │ │ ├── gdp-agent-deamonset.yaml │ │ ├── gdp-collector-configmap.yaml │ │ ├── gdp-collector-deployment.yaml │ ├── prod │ │ ├── gdp-configmap.yaml │ │ ├── gdp-agent-deamonset.yaml │ │ ├── gdp-collector-configmap.yaml │ │ ├── gdp-collector-deployment.yaml │ │ ├── gdp-pdb.yaml # 生产环境额外的Pod中断预算资源之后读取文件时直接根据环境参数拼路径即可:
data "kubectl_path_documents" "yaml-files" { pattern = "${path.module}/manifests/${var.env}/*.yaml" } resource "kubectl_manifest" "yaml-manifests" { for_each = toset(data.kubectl_path_documents.yaml-files.documents) yaml_body = each.value }这个方案逻辑最简单,但如果两个环境的YAML大部分内容一致,会产生大量重复配置,后续更新需要同时改两份,维护成本高。
注意:使用
templatefile方案时一定要记得转义YAML里原有的${}格式引用,否则Terraform渲染阶段会报变量未定义的错误。
内容的提问来源于stack exchange,提问作者fledgling

