文章

Terraform 深度指南

Terraform 深度指南

概述

本文是对 Terraform 基础设施即代码Terraform 生产级实践 的深度补充。前两篇覆盖了基础语法、State 管理、Module、CI/CD 集成和 Terragrunt。本文聚焦 HCL 高级语法、资源生命周期、Provider 架构、函数全集、模块开发、原生测试、性能调优、调试技巧 等深度主题——这些是从”会用 Terraform”到”精通 Terraform”的分水岭。

一句话:Terraform 的 80% 时间花在 State 和 Module 上(已有笔记覆盖),剩下 20% 的深度知识决定了你能不能写出优雅、健壮、可维护的 IaC 代码。


一、HCL 高级语法

1.1 表达式与运算符

# 三元条件表达式
locals {
  instance_type = var.environment == "prod" ? "n1-standard-8" : "n1-standard-2"
  enable_backup = var.environment != "dev" ? true : false
}

# for 表达式 —— 列表转换
locals {
  # [for item in var.list : upper(item)]           # 列表 → 列表
  # [for k, v in var.map : "${k}=${v}"]             # map → 列表
  # {for k, v in var.map : k => upper(v)}           # map → map
  # [for s in var.list : upper(s) if length(s) > 3] # 带 filter

  availability_zones = [for n in range(3) : "us-east1-${chr(97 + n)}"]
  # → ["us-east1-a", "us-east1-b", "us-east1-c"]

  node_labels = {
    for k, v in var.node_pools : k => merge(v, { pool = k })
  }
}

# for_each 遍历 map(推荐,比 count 更适合多资源)
resource "google_compute_instance" "vm" {
  for_each = var.instances  # map(string) → key 是实例名

  name         = each.key
  machine_type = each.value.machine_type
  zone         = each.value.zone
}

# count —— 基于条件的 0/1 创建
resource "google_compute_instance" "bastion" {
  count = var.enable_bastion ? 1 : 0
  name  = "bastion-${count.index}"
}
# 引用: google_compute_instance.bastion[0]
# 注意:count 改变会导致后续资源 index 偏移,for_each 更安全

1.2 Dynamic Blocks —— 动态生成嵌套块

# 传统写法:每个规则手写一个 block(重复、不可扩展)
resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port = 80
    to_port   = 80
    protocol  = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port = 443
    to_port   = 443
    protocol  = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Dynamic 写法:从变量动态生成
variable "ingress_rules" {
  type = list(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
  default = [
    { port = 80,  protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], description = "HTTP" },
    { port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], description = "HTTPS" },
    { port = 22,  protocol = "tcp", cidr_blocks = ["10.0.0.0/8"], description = "SSH from internal" },
  ]
}

resource "aws_security_group" "web" {
  name = "web-sg"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }

  dynamic "egress" {
    for_each = var.egress_rules
    iterator = rule  # 自定义迭代器名称(默认是 block 名)
    content {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      cidr_blocks = rule.value.cidr_blocks
    }
  }
}

1.3 类型系统与自定义校验

# 类型约束
variable "ports" {
  type = list(string)                    # 简单类型
  default = ["80", "443"]
}

variable "instance_config" {
  type = object({                        # 对象类型
    name          = string
    machine_type  = string
    zone          = string
    tags          = list(string)
    enable_gpu    = optional(bool, false)    # optional + 默认值(>= 1.3)
    gpu_count     = optional(number, 0)
  })
}

variable "labels" {
  type = map(string)                     # map 类型
  default = {}
}

# 可空类型(nullable)
variable "ami_id" {
  type     = string
  nullable = false                       # 必须提供非 null 值
}

# 自定义校验 validation(>= 1.2)
variable "cluster_name" {
  type    = string
  default = "prod-cluster"

  validation {
    condition     = length(var.cluster_name) <= 40 && can(regex("^[a-z0-9-]+$", var.cluster_name))
    error_message = "集群名必须 1-40 字符,只含小写字母、数字、连字符。"
  }
}

variable "node_count" {
  type    = number
  default = 3

  validation {
    condition     = var.node_count >= 1 && var.node_count <= 100
    error_message = "节点数必须在 1-100 之间。"
  }
}

variable "cidr_block" {
  type    = string
  default = "10.0.0.0/16"

  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "必须是有效的 CIDR 格式,如 10.0.0.0/16。"
  }
}

1.4 moved blocks —— 安全重命名资源(>= 1.1)

# 问题:重命名 resource 会导致 destroy + create,生产环境灾难
# 解决:moved block 告诉 Terraform "旧地址 → 新地址",不重建

# 旧代码:
# resource "google_compute_instance" "web" {
#   name = "web-server"
# }

# 新代码(重命名):
moved {
  from = google_compute_instance.web
  to   = google_compute_instance.web_server
}

resource "google_compute_instance" "web_server" {
  name         = "web-server"
  machine_type = "n1-standard-4"
}

# terraform plan 输出:
# # google_compute_instance.web has moved to google_compute_instance.web_server
#   resource "google_compute_instance" "web_server" {
#       ...
#   }
# Plan: 0 to add, 0 to change, 0 to destroy.

最佳实践:重命名后,下一个 plan 会显示 “has moved to”,确认无误后 apply。之后 moved block 可以删除(它已完成使命)。

1.5 import block —— 声明式导入(>= 1.5)

# 传统方式:terraform import 命令行逐个导入(见生产级实践笔记)
# 新方式:在代码中声明 import block,plan 时预览,apply 时执行

import {
  to = aws_instance.example
  id = "i-1234567890abcdef0"
}

resource "aws_instance" "example" {
  # 写入与云上资源匹配的配置
  ami           = "ami-12345678"
  instance_type = "t3.micro"
}

# 批量导入:配合 for_each
import {
  for_each = {
    sg_web  = "sg-aaa"
    sg_db   = "sg-bbb"
    sg_ssh  = "sg-ccc"
  }
  to = aws_security_group.imported[each.key]
  id = each.value
}

resource "aws_security_group" "imported" {
  for_each = toset(["sg_web", "sg_db", "sg_ssh"])
  name     = each.key
}

# 生成配置文件(不写 resource,让 Terraform 帮你生成)
# terraform plan -generate-config-out=generated.tf
# → 自动生成 resource 块,你只需调整参数

1.6 Locals —— 局部变量

locals {
  # 复杂逻辑集中管理,避免在 resource 中重复
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
    CreatedAt   = formatdate("YYYY-MM-DD", timestamp())
  }

  # 计算子网 CIDR
  subnets = cidrsubnets(var.vpc_cidr, 4, 4, 4, 2)
  # 10.0.0.0/16 → ["10.0.0.0/20", "10.0.16.0/20", "10.0.32.0/20", "10.0.48.0/18"]

  # 条件拼接
  enable_monitoring = var.environment == "prod" || var.environment == "staging"
  log_retention_days = var.environment == "prod" ? 90 : 30
}

# 在任意位置引用
resource "google_compute_instance" "app" {
  name         = "app-${var.environment}"
  machine_type = local.machine_type
  tags         = [local.common_tags.Project, local.common_tags.Environment]
}

1.7 字符串与模板

# Heredoc —— 多行字符串
locals {
  user_data = <<-EOT
    #!/bin/bash
    apt-get update -y
    apt-get install -y nvidia-driver-535
    nvidia-smi
    echo "Setup complete on $(hostname)"
  EOT

  # 模板插值
  cluster_fqdn = "${var.cluster_name}.${var.domain}"

  # 条件模板
  config = templatefile("${path.module}/templates/config.yaml.tpl", {
    environment = var.environment
    replicas    = var.environment == "prod" ? 3 : 1
    features    = var.features
  })

  # %{ if } / %{ for } 模板指令
  nginx_config = <<-EOT
    server {
      listen 80;
      %{ for port in var.ports }
      location /port${port} {
        proxy_pass http://127.0.0.1:${port};
      }
      %{ endfor }
      %{ if var.enable_ssl }
      listen 443 ssl;
      ssl_certificate /etc/ssl/cert.pem;
      %{ endif }
    }
  EOT
}

二、资源生命周期管理

2.1 Lifecycle Meta-Arguments

resource "aws_rds_instance" "main" {
  name                 = "prod-db"
  instance_class       = "db.r6g.xlarge"
  allocated_storage    = 200

  lifecycle {
    # 1. create_before_destroy —— 先建新再删旧(零停机替换)
    # 适用于:不可变资源(如带特定 AMI 的 EC2、需要替换的安全组规则)
    create_before_destroy = true

    # 2. prevent_destroy —— 防止误删(生产保护)
    # terraform destroy 会报错,必须手动移除此行才能销毁
    prevent_destroy = true

    # 3. ignore_changes —— 忽略某些属性的漂移检测
    # 适用于:其他工具管理的属性(如 tags 由外部系统管理)
    ignore_changes = [
      tags["LastModified"],
      tags["ModifiedBy"],
      user_data,               # 忽略 user_data 变更(由 Packer 管理)
    ]

    # 4. replace_triggered_by —— 当依赖资源变更时触发替换
    replace_triggered_by = [
      aws_ami.app.id,           # AMI 更新 → 替换 EC2
      random_id.config_hash.hex,
    ]

    # 5. precondition(>= 1.2)—— 资源创建/更新前的断言
    precondition {
      condition     = data.aws_ec2_instance_type.instance.supported_architectures[0] == "x86_64"
      error_message = "必须选择 x86_64 架构的实例类型。"
    }

    # 6. postcondition(>= 1.2)—— 资源创建/更新后的断言
    postcondition {
      condition     = self.status == "available"
      error_message = "RDS 实例创建后状态不是 available。"
    }
  }
}

2.2 Timeout 超时控制

resource "aws_ec2_fleet" "gpu" {
  # 默认超时由 Provider 决定,可自定义
  timeouts {
    create = "30m"   # 创建超时 30 分钟(GPU 实例可能需要较长时间)
    update = "20m"
    delete = "15m"
  }
}

resource "aws_db_instance" "main" {
  timeouts {
    create = "60m"   # RDS 创建可能需要很长时间
    delete = "30m"
  }
}

2.3 depends_on —— 显式依赖

# 通常 Terraform 自动推断依赖(通过引用 resource.id 等)
# 但有时依赖是隐式的,需要显式声明

resource "aws_instance" "app" {
  ami           = data.aws_ami.app.id
  instance_type = "t3.large"

  # 显式依赖:EC2 必须在 IAM Profile 创建之后才能启动
  depends_on = [
    aws_iam_instance_profile.app,
    aws_cloudwatch_log_group.app,   # 启动脚本中会写日志到这个 group
  ]
}

# Module 之间的依赖
module "eks" {
  source = "./modules/eks"
  # ...
  depends_on = [module.vpc]   # EKS 集群必须在 VPC 就绪后创建
}

# depends_on 的陷阱:
# 1. 只影响创建/销毁顺序,不传递数据
# 2. 过度使用会导致依赖图变复杂,plan 变慢
# 3. 优先用 data 引用替代(如 data.aws_vpc.selected.id)

2.4 Provisioners —— 最后手段

# ⚠️ HashiCorp 官方建议:Provisioners 是最后手段
# 优先用:cloud-init/user_data、Packer 预装、Ansible 配置

resource "aws_instance" "app" {
  ami           = data.aws_ami.app.id
  instance_type = "t3.large"

  # local-exec —— 在运行 Terraform 的机器上执行
  provisioner "local-exec" {
    command = "echo ${self.private_ip} >> inventory.ini"
    interpreter = ["/bin/bash", "-c"]
    environment = {
      KUBECONFIG = "~/.kube/config"
    }
  }

  # remote-exec —— 在目标资源上执行(需要连接信息)
  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx",
      "sudo systemctl enable nginx",
    ]

    connection {
      type        = "ssh"
      host        = self.public_ip
      user        = "ubuntu"
      private_key = file("~/.ssh/id_rsa")
    }

    # on_failure 控制行为
    on_failure = continue   # 或 fail(默认)
  }

  # 销毁时执行(清理操作)
  provisioner "local-exec" {
    when    = destroy
    command = "aws s3 rm s3://${self.bucket_name} --recursive"
  }
}

2.5 null_resource 与 triggers

# null_resource:不创建实际资源,只用于触发 provisioner
resource "null_resource" "cluster_setup" {
  # triggers 变化 → 重新执行 provisioner
  triggers = {
    cluster_version = google_container_cluster.main.id
    config_hash     = sha256(local.kubeconfig)
    build_id        = var.build_id
  }

  provisioner "local-exec" {
    command = <<-EOT
      kubectl apply -f manifests/
      helm upgrade --install argocd argo/argo-cd
    EOT
  }
}

# terraform_data(>= 1.4)—— null_resource 的替代品
resource "terraform_data" "deployment" {
  input = {
    version = var.app_version
    replicas = var.replicas
  }

  provisioner "local-exec" {
    command = "kubectl set image deployment/app app=${var.app_image}"
  }
}
# terraform_data 的优势:input 属性参与 plan 输出,更透明

三、Provider 高级用法

3.1 Provider 别名 —— 多区域/多账号

# 默认 Provider
provider "aws" {
  region = "us-east-1"
}

# 别名 Provider:另一个区域
provider "aws" {
  alias  = "west"
  region = "us-west-2"
}

# 别名 Provider:另一个 AWS 账号(跨账号管理)
provider "aws" {
  alias  = "shared_services"
  region = "us-east-1"

  assume_role {
    role_arn = "arn:aws:iam::999999999999:role/TerraformCrossAccount"
  }
}

# 使用别名 Provider
resource "aws_s3_bucket" "east_bucket" {
  bucket = "my-east-bucket"
  # 使用默认 provider (us-east-1)
}

resource "aws_s3_bucket" "west_bucket" {
  bucket = "my-west-bucket"
  provider = aws.west    # ← 指定别名
}

resource "aws_s3_bucket" "shared_bucket" {
  bucket   = "shared-services-bucket"
  provider = aws.shared_services
}

# GCP 多项目示例
provider "google" {
  project = "main-project"
  region  = "asia-southeast1"
}

provider "google" {
  alias   = "data_project"
  project = "data-analytics-project"
  region  = "us-central1"
}

3.2 Data Sources —— 读取已有资源

# 查询 AMI(每次 plan 动态查询最新)
data "aws_ami" "latest" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# 查询可用区
data "aws_availability_zones" "available" {
  state = "available"
  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}

# 查询 VPC(由另一个 Terraform 项目管理)
data "aws_vpc" "selected" {
  filter {
    name   = "tag:Name"
    values = ["prod-vpc"]
  }
}

# 查询子网(配合 for_each 动态创建资源)
data "aws_subnets" "private" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.selected.id]
  }
  filter {
    name   = "tag:Tier"
    values = ["private"]
  }
}

# 在每个私有子网中创建一个副本
resource "aws_db_subnet_group" "main" {
  name       = "main"
  subnet_ids = data.aws_subnets.private.ids
}

# 查询 IAM 策略文档
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "app" {
  name               = "app-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

# 查询外部数据(如调用 API 获取最新配置)
data "external" "instance_info" {
  program = ["python3", "${path.module}/scripts/get_instance_info.py"]
  query = {
    cluster_name = var.cluster_name
  }
}
# 返回 JSON,通过 data.external.instance_info.result.xxx 引用

3.3 Provider 函数(>= 1.8)

# 部分 Provider 提供自定义函数(provider-defined functions)
# 这些函数在 plan 和 apply 阶段都可用

# AWS Provider 函数
data "aws_region" "current" {}  # 传统 data source

# >= 1.8 可以直接用 provider 函数(无需 data source)
# provider::aws::arn_parse(arn) → 解析 ARN
# provider::aws::arn_build(...) → 构建 ARN

# 注意:provider 函数支持尚在早期,具体可用函数请查看 Provider 文档

3.4 Provider 版本约束

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      # 版本约束语法:
      # >= 5.0          至少 5.0
      # ~> 5.0          >= 5.0, < 6.0(推荐:锁定大版本)
      # ~> 5.30         >= 5.30, < 6.0
      # >= 5.0, < 6.0   显式范围
      # = 5.31.0        精确版本(不推荐,缺乏补丁更新)
      version = "~> 5.30"
    }
  }
}

# .terraform.lock.hcl —— 依赖锁定文件
# 类似 package-lock.json,记录实际使用的精确版本
# 应该提交到 Git,确保团队使用相同版本

四、函数全集速查

4.1 集合函数

函数说明示例
concat合并列表concat(["a","b"], ["c"])["a","b","c"]
distinct去重distinct(["a","a","b"])["a","b"]
flatten展平嵌套列表flatten([["a"],["b",["c"]]])["a","b","c"]
merge合并 mapmerge({a=1}, {b=2}){a=1, b=2}
contains判断元素存在contains(["a","b"], "a")true
element按索引取元素(循环)element(["a","b","c"], 5)"c"
lookupmap 取值带默认lookup({a=1}, "b", 0)0
keys / valuesmap 的键/值列表keys({a=1, b=2})["a","b"]
zipmap两个列表组合成 mapzipmap(["a","b"], [1,2]){a=1, b=2}
setunion / setintersection集合运算setunion(["a"],["b"])["a","b"]
chunklist分块chunklist(["a","b","c","d"], 2)[["a","b"],["c","d"]]
coalesce返回第一个非空值coalesce("", "", "default")"default"
coalescelist列表版 coalescecoalescelist([], ["a"])["a"]

4.2 字符串函数

# 格式化
format("v%s-%s", "1.0", "prod")          # → "v1.0-prod"
formatlist("instance-%s", ["web","db"])  # → ["instance-web", "instance-db"]

# 大小写
upper("hello")    # → "HELLO"
lower("HELLO")    # → "hello"
title("hello world")  # → "Hello World"

# 分割与拼接
split(",", "a,b,c")         # → ["a", "b", "c"]
join("-", ["a", "b", "c"])  # → "a-b-c"

# 替换
replace("hello world", "world", "terraform")  # → "hello terraform"
regex("[a-z]+", "abc123")    # → "abc"
regexall("[a-z]+", "abc123XYZdef")  # → ["abc", "def"]

# 前缀/后缀
trimprefix("v1.0.0", "v")   # → "1.0.0"
trimsuffix("file.txt", ".txt")  # → "file"
trim("  hello  ", " ")       # → "hello"

# 缩进处理
indent(4, "line1\nline2")    # 每行缩进 4 空格

# Base64
base64encode("hello")        # → "aGVsbG8="
base64decode("aGVsbG8=")     # → "hello"
base64gzip("large text")     # → gzip 压缩后 base64

# 文件
file("${path.module}/script.sh")        # 读取文件内容
fileexists("${path.module}/config.yaml") # 文件是否存在
fileset("${path.module}", "*.tf")        # 匹配文件列表
filebase64("${path.module}/cert.pem")    # 文件内容 base64
filebase64sha256("${path.module}/app.zip")  # 文件 SHA256 的 base64

# 模板
templatefile("${path.module}/templates/user_data.sh.tpl", {
  region   = var.region
  cluster  = var.cluster_name
  packages = var.packages
})

4.3 网络/编码函数

# CIDR 计算
cidrhost("10.0.0.0/16", 4)      # → "10.0.0.4"(取第 N 个 IP)
cidrnetmask("10.0.0.0/16")      # → "255.255.0.0"
cidrsubnet("10.0.0.0/16", 4, 0) # → "10.0.0.0/20"(子网计算)
cidrsubnets("10.0.0.0/16", 4, 4, 8)  # → ["10.0.0.0/20", "10.0.16.0/20", "10.0.32.0/24"]

# URL 解析
urlescape("hello world")    # → "hello%20world"
urldecode("hello%20world")  # → "hello world"

# JSON / YAML
jsonencode({name = "test", port = 80})  # → '{"name":"test","port":80}'
jsondecode('{"name":"test"}')           # → {"name" = "test"}
yamlencode({a = 1, b = [1, 2]})         # → "a: 1\nb:\n- 1\n- 2\n"
yamldecode(file("config.yaml"))         # 解析 YAML 文件

# 编码/解码
urlencode("https://example.com?a=b c")  # URL 编码
textencodebase64("hello", "UTF-8")
textdecodebase64("aGVsbG8=", "UTF-8")
csvdecode(file("users.csv"))            # CSV → list of objects

# 哈希
md5("hello")         # → "5d41402abc4b2a76b9719d911017c592"
sha1("hello")
sha256("hello")
sha512("hello")
uuid()               # 生成随机 UUID
uuidv5("dns", "example.com")  # 基于命名空间的确定性 UUID

4.4 时间/日期函数

timestamp()           # → "2026-08-03T10:30:00Z"(当前 UTC 时间 RFC3339)
formatdate("YYYY-MM-DD", timestamp())           # → "2026-08-03"
formatdate("YYYY-MM-DD hh:mm:ss", timestamp())  # → "2026-08-03 10:30:00"
formatdate("DD MMM YYYY hh:mm ZZZ", timestamp())  # → "03 Aug 2026 10:30 UTC"

timeadd(timestamp(), "24h")    # 当前时间 + 24 小时
timeadd(timestamp(), "-2h30m") # 当前时间 - 2 小时 30 分

# 注意:timestamp() 每次 plan 都会变化,不要直接用作资源属性
# 正确做法:用变量固定时间
variable "deployment_date" {
  type    = string
  default = "2026-08-03"
}

4.5 类型转换与判断

# 类型转换
toint("42")       # → 42
tonumber("3.14")  # → 3.14
tostring(42)      # → "42"
tobool("true")    # → true
tolist({a=1})     # → 错误(map 不能转 list)
toset(["a","a"])  # → toset(["a"])(去重)

# 类型判断
can(tostring(var.x))          # 是否可以转换
can(aws_instance.app.id)      # 资源是否已存在(用于条件逻辑)
try(aws_instance.app.id, "unknown")  # 尝试取值,失败返回默认值

# try 在数据处理中非常有用
locals {
  # 安全地从可能缺失的字段中取值
  zone = try(data.external.info.result.zone, var.default_zone)
  tags = try(var.extra_tags, {})
}

4.6 加密函数

# RSA 加解密(>= 1.6)
rsadecrypt(encrypted_data, private_key_pem)

# 密码生成
# 使用 random provider(需要单独声明 required_providers)
resource "random_password" "db" {
  length           = 32
  special          = true
  override_special = "!#$%&*()-_=+[]{}<>:?"
  min_upper        = 2
  min_lower        = 2
  min_numeric      = 2
  min_special      = 2
}

resource "random_id" "build" {
  byte_length = 8
  # 生成如 "aB3xK9mN" 的随机 ID
}

resource "random_shuffle" "azs" {
  input        = data.aws_availability_zones.available.names
  result_count = 3
  # 随机打乱可用区列表
}

五、模块开发最佳实践

5.1 模块结构

terraform-modules/
├── modules/
│   └── vpc/
│       ├── main.tf              # resource 定义
│       ├── variables.tf         # 输入变量(含 validation)
│       ├── outputs.tf           # 输出值
│       ├── versions.tf          # provider/version 约束
│       ├── README.md            # 必须有!
│       ├── examples/            # 使用示例(可被 Terratest 测试)
│       │   └── basic/
│       │       └── main.tf
│       └── tests/               # 单元测试(可选)
│           └── vpc_test.go
├── modules/
│   └── eks-gpu/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       └── modules/             # 子模块(内部使用,不发布)
└── README.md

5.2 模块版本约束

# versions.tf —— 在模块内部声明依赖
terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0"
    }
  }
}

# 调用模块时锁定版本
module "vpc" {
  source  = "git::ssh://git@example.com/terraform-modules.git//vpc?ref=v2.1.0"
  # ref=v2.1.0 锁定到 Git tag

  # 或从 Terraform Registry
  # source  = "terraform-aws-modules/vpc/aws"
  # version = "5.1.0"
}

5.3 变量设计原则

# variables.tf —— 模块的"API"

# 1. 每个变量都要有 description
variable "name" {
  type        = string
  description = "VPC 名称前缀,将用于所有资源命名"
}

# 2. 合理的默认值(可选变量才有默认值)
variable "cidr" {
  type        = string
  description = "VPC CIDR 块"
  default     = "10.0.0.0/16"

  validation {
    condition     = can(cidrhost(var.cidr, 0))
    error_message = "必须是有效的 IPv4 CIDR 格式。"
  }
}

# 3. 复杂对象用 object 类型 + optional 字段
variable "subnets" {
  type = list(object({
    name              = string
    cidr              = string
    availability_zone = string
    type              = optional(string, "private")  # private | public | database
    tags              = optional(map(string), {})
  }))
  description = "子网配置列表"
  default     = []
}

# 4. sensitive 变量不会显示在 plan 输出中
variable "db_password" {
  type        = string
  description = "数据库主密码"
  sensitive   = true
}

# 5. nullable = false 防止传 null
variable "environment" {
  type     = string
  nullable = false
}

5.4 输出设计原则

# outputs.tf

# 1. 输出关键资源 ID 和名称
output "vpc_id" {
  value       = aws_vpc.main.id
  description = "创建的 VPC ID"
}

# 2. 输出子网 ID 列表(给下游模块用)
output "private_subnet_ids" {
  value       = aws_subnet.private[*].id
  description = "私有子网 ID 列表"
}

output "public_subnet_ids" {
  value       = aws_subnet.public[*].id
  description = "公有子网 ID 列表"
}

# 3. sensitive 输出不会显示在 plan 中,但会写入 State
output "db_endpoint" {
  value       = aws_db_instance.main.endpoint
  description = "RDS 端点地址"
}

output "db_password" {
  value       = random_password.db.result
  sensitive   = true
  description = "数据库密码(存储在 State 中,使用方需注意安全)"
}

# 4. depends_on —— 当输出被其他模块引用时确保依赖正确
output "vpc" {
  value = {
    id              = aws_vpc.main.id
    cidr_block      = aws_vpc.main.cidr_block
    private_subnets = aws_subnet.private[*].id
    public_subnets  = aws_subnet.public[*].id
  }
  description = "VPC 完整信息"
  depends_on  = [aws_route_table_association.private, aws_route_table_association.public]
}

# 5. precondition —— 输出前的校验
output "subnet_count" {
  value       = length(aws_subnet.private)
  description = "私有子网数量"

  precondition {
    condition     = length(aws_subnet.private) >= 2
    error_message = "至少需要 2 个私有子网以支持高可用。"
  }
}

5.5 模块测试

# tests/basic.tftest.hcl —— Terraform 原生测试(>= 1.6)
# 文件放在模块目录下的 tests/ 中

# 定义测试变量
variables {
  name = "test-vpc"
  cidr = "10.1.0.0/16"
  subnets = [
    { name = "private-a", cidr = "10.1.0.0/24", availability_zone = "us-east-1a" },
    { name = "private-b", cidr = "10.1.1.0/24", availability_zone = "us-east-1b" },
  ]
}

# 测试步骤 1:plan
run "validate_plan" {
  command = plan

  # 断言 plan 输出
  assert {
    condition     = length(plan_values.aws_subnet.private) == 2
    error_message = "应该创建 2 个私有子网"
  }
}

# 测试步骤 2:apply(需要真实的云凭证)
run "create_vpc" {
  command = apply

  assert {
    condition     = output.vpc_id != ""
    error_message = "VPC ID 不应为空"
  }

  assert {
    condition     = length(output.private_subnet_ids) == 2
    error_message = "应该输出 2 个私有子网 ID"
  }
}

# 运行测试
# terraform test
# terraform test -filter="tests/basic.tftest.hcl"
// tests/vpc_test.go —— Terratest 集成测试
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestVpcModule(t *testing.T) {
    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../examples/basic",
        Vars: map[string]interface{}{
            "name": "terratest-vpc",
            "cidr": "10.2.0.0/16",
        },
    })

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    vpcID := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcID)

    subnets := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
    assert.Equal(t, 2, len(subnets))
}

六、Terraform Cloud / Enterprise

6.1 核心特性对比

特性OSSCloud (Free)Cloud (Plus)Enterprise
远程 State + 锁手动配置
VCS 集成
远程 Plan/Apply
Private Module Registry
Sentinel Policy
Team 管理
SSO / SAML
自托管N/A

6.2 Terraform Cloud 工作流

Developer → Push to GitHub
  → TFC webhook 触发
  → TFC 自动 plan(在 TFC 的 Runner 上)
  → plan 输出显示在 PR 评论中
  → Reviewer 审批
  → 点击 "Confirm & Apply" 或自动 apply
  → State 自动保存在 TFC

6.3 Sentinel Policy as Code

# Sentinel 策略:禁止创建未加密的 S3 bucket
import "tfplan/v2" as tfplan

# 规则 1:所有 S3 bucket 必须启用加密
no_unencrypted_s3 = rule {
    all tfplan.resources.aws_s3_bucket as _, buckets {
        all buckets as _, bucket {
            bucket.applied.server_side_encryption_configuration is not null
        }
    }
}

# 规则 2:所有 EC2 实例必须有 Name 标签
ec2_has_name_tag = rule {
    all tfplan.resources.aws_instance as _, instances {
        all instances as _, inst {
            "Name" in keys(inst.applied.tags)
        }
    }
}

# 规则 3:生产环境禁止创建 0.0.0.0/0 的入站规则
no_world_open_ingress = rule when tfplan.terraform_version is not "" {
    all tfplan.resources.aws_security_group_rule as _, rules {
        all rules as _, r {
            r.applied.cidr_blocks not contains "0.0.0.0/0" or
            r.applied.from_port != 22
        }
    }
}

main = rule {
    no_unencrypted_s3 and
    ec2_has_name_tag and
    no_world_open_ingress
}

6.4 替代方案:OPA / Conftest

# OPA Rego 策略(开源,不依赖 Terraform Cloud)
package terraform

# 禁止在生产环境创建小型 EC2 实例
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_instance"
    resource.change.after.instance_type == "t3.micro"
    msg := sprintf("禁止创建 t3.micro 实例: %s", [resource.address])
}

# 禁止未加密的 EBS 卷
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_ebs_volume"
    not resource.change.after.encrypted
    msg := sprintf("EBS 卷必须加密: %s", [resource.address])
}

# 必须有成本标签
deny[msg] {
    resource := input.resource_changes[_]
    resource.change.after.tags != null
    not "CostCenter" in keys(resource.change.after.tags)
    msg := sprintf("资源必须标记 CostCenter: %s", [resource.address])
}

# 使用:conftest test terraform_plan.json
# 或在 CI 中:terraform plan -out=tfplan && terraform show -json tfplan | conftest test -

七、性能调优

7.1 plan 慢的常见原因与解决

# 1. State 太大 → 拆分 State(见生产级实践笔记)

# 2. refresh 慢(每个资源都调云 API 检查实际状态)
# 解决:-refresh=false 跳过 refresh(需要确认 State 是最新的)
terraform plan -refresh=false

# 3. 大量 data source 查询
# 解决:减少 data source,改用变量传入

# 4. Provider 并行度不足
# 默认并行度 10,大型基础设施可提高
terraform plan -parallelism=30
terraform apply -parallelism=30

# 5. 网络延迟(跨区域 Provider 调用)
# 解决:在离云区域近的机器上运行 Terraform

7.2 Targeted Plan —— 只操作部分资源

# 只 plan 指定资源(开发调试时非常有用)
terraform plan -target=aws_instance.app
terraform plan -target=module.vpc
terraform plan -target=aws_instance.app -target=aws_security_group.web

# 注意:-target 不应作为日常操作方式
# 它会导致 State 不完整,下次完整 plan 可能出现意外变更
# 仅用于:调试、紧急修复、逐步迁移

7.3 State 性能优化

# 1. 定期清理 State 中的孤儿资源
terraform state list | grep -v "module\." | xargs -I{} terraform state show {}

# 2. 使用 state push 替代 apply(仅更新 State,不操作云资源)
# 适用场景:手动在云上修改了资源,同步到 State
terraform state push updated.tfstate

# 3. State 文件压缩(大型 State 文件)
# 远程后端会自动处理,本地 State 需手动管理

# 4. 避免在 State 中存储大对象
# 错误:把整个 JSON 配置存到 State
# 正确:存引用(如 S3 路径),运行时读取

7.4 配置优化技巧

# 1. 减少 data source 使用(每次 plan 都会查询云 API)
# 错误:每次 plan 查询最新 AMI
data "aws_ami" "latest" {
  most_recent = true
  # ...
}

# 正确:用变量固定 AMI ID(由 CI/CD 或 Packer 流水线更新)
variable "ami_id" {
  type    = string
  default = "ami-0123456789abcdef0"
}

# 2. 避免在 for_each 中使用不确定的值
# 错误:for_each 遍历 data source 输出(每次可能变化)
# 正确:for_each 遍历变量(确定的输入)

# 3. 使用 locals 减少重复计算
locals {
  # 计算一次,多处引用
  azs = slice(data.aws_availability_zones.available.names, 0, 3)
  common_tags = merge(var.default_tags, { ManagedBy = "terraform" })
}

八、调试技巧

8.1 日志级别

# TF_LOG 环境变量控制日志级别
export TF_LOG=TRACE    # 最详细(含 HTTP 请求/响应)
export TF_LOG=DEBUG    # 调试信息
export TF_LOG=INFO     # 一般信息
export TF_LOG=WARN     # 警告
export TF_LOG=ERROR    # 仅错误
export TF_LOG=OFF      # 关闭(默认)

# 日志写入文件(不污染终端)
export TF_LOG_PATH=/tmp/terraform.log

# Provider 级别日志
export TF_LOG_PROVIDER=DEBUG   # 只看 Provider 日志

# 常见调试场景:
# 1. "Provider 产生了不一致的 plan"
#    → TF_LOG=DEBUG 查看每个 API 调用的请求和响应
# 2. "State 锁无法释放"
#    → TF_LOG=DEBUG 查看 DynamoDB 锁操作
# 3. "Module 下载失败"
#    → TF_LOG=TRACE 查看 Git clone 过程

8.2 Console —— 交互式表达式求值

# 启动交互式控制台
terraform console

# 在控制台中求值
> var.environment
"prod"
> aws_vpc.main.cidr_block
"10.0.0.0/16"
> [for s in data.aws_subnets.private.ids : cidrhost(data.aws_subnet.selected[s].cidr_block, 0)]
> merge({a=1}, {b=2})
{
  "a" = 1
  "b" = 2
}
> exit

# 非交互式求值
echo 'length(var.subnets)' | terraform console
echo 'jsonencode({environment = var.environment})' | terraform console

8.3 Plan 输出分析

# JSON 格式 plan(便于程序分析)
terraform plan -out=tfplan
terraform show -json tfplan > plan.json

# 用 jq 分析 plan
# 统计将创建的资源数
cat plan.json | jq '.resource_changes | length'

# 查看所有将创建的资源
cat plan.json | jq '.resource_changes[] | select(.change.actions[] | contains("create")) | .address'

# 查看所有将删除的资源
cat plan.json | jq '.resource_changes[] | select(.change.actions[] | contains("delete")) | .address'

# 查看特定资源的变更详情
cat plan.json | jq '.resource_changes[] | select(.address == "aws_instance.app") | .change'

8.4 Crash 日志

# Terraform 崩溃时会生成 crash.log
ls -la crash.log

# crash.log 包含:
# - 崩溃时的 goroutine 堆栈
# - 当时的配置文件内容
# - State 文件内容(可能含敏感信息!)

# 提交 HashiCorp 支持工单时附上 crash.log(注意脱敏)

8.5 常见错误诊断

# 1. "Error: Failed to query available provider packages"
# → 检查网络代理(HTTP_PROXY/HTTPS_PROXY)
# → 检查 required_providers 中的 source 路径

# 2. "Error: Provider produced inconsistent result after apply"
# → Provider bug 或 API 返回异常
# → TF_LOG=DEBUG 查看 API 响应
# → 临时解决:terraform state rm + 重新 import

# 3. "Error: no suitable image found"
# → 检查可用区是否支持该实例类型
# → 检查 AMI 是否在当前区域可用

# 4. "Error: acquiring state lock"
# → 其他人正在 apply,等待或检查锁是否过期
# → 确认无人操作后:terraform force-unlock <lock-id>

# 5. "Error: Cycle detected"
# → 资源之间有循环依赖
# → 检查 depends_on 和 data source 引用链
# → 使用 -target 隔离问题资源

# 6. "Error: Value for unconfigurable attribute"
# → 尝试设置 Provider 不支持修改的属性
# → 添加 lifecycle { ignore_changes = [attr] }

九、密钥管理

9.1 Secret 管理方案对比

方案安全性复杂度适用场景
tfvars + git-crypt小团队
AWS Secrets ManagerAWS 环境
HashiCorp Vault大型企业
Terraform Cloud VariablesTFC 用户
External Data Source自定义方案

9.2 AWS Secrets Manager 集成

# 在 Secrets Manager 中存储密钥(通过 AWS CLI 或控制台)
# aws secretsmanager create-secret --name prod/db --secret-string '{"user":"admin","password":"xxx"}'

# Terraform 中读取
data "aws_secretsmanager_secret" "db" {
  name = "prod/db"
}

data "aws_secretsmanager_secret_version" "db" {
  secret_id = data.aws_secretsmanager_secret.db.id
}

locals {
  db_credentials = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}

resource "aws_db_instance" "main" {
  username = local.db_credentials.user
  password = local.db_credentials.password
  # password 不会出现在 plan 输出中(但会写入 State!)
}

# ⚠️ 注意:即使 data source 读取的 secret,也会被写入 State 文件
# 确保 State backend 启用了加密(S3 SSE 或 GCS CSEK)

9.3 HashiCorp Vault 集成

# Vault Provider
provider "vault" {
  address = "https://vault.example.com:8200"
  token   = var.vault_token  # 从环境变量注入
}

# 从 Vault 读取密钥
data "vault_generic_secret" "db" {
  path = "secret/data/prod/database"
}

resource "aws_db_instance" "main" {
  username = data.vault_generic_secret.db.data["username"]
  password = data.vault_generic_secret.db.data["password"]
}

# Vault 动态密钥(按需生成,自动过期)
data "vault_generic_secret" "dynamic_db" {
  path = "database/creds/readonly"
  # 每次读取生成新的临时凭证
}

9.4 敏感数据在 State 中的保护

# 1. 标记 sensitive
variable "api_key" {
  type      = string
  sensitive = true   # plan 输出中不显示
}

output "connection_string" {
  value     = "postgres://${var.db_user}:${var.db_password}@${aws_db_instance.main.endpoint}"
  sensitive = true
}

# ⚠️ 但 sensitive 只影响 plan/apply 输出,密钥仍然明文存储在 State 文件中!

# 2. State 加密
# S3: 开启 SSE-KMS
terraform {
  backend "s3" {
    bucket            = "my-tfstate"
    key               = "prod/terraform.tfstate"
    encrypt           = true           # 必须开启
    kms_key_id        = "arn:aws:kms:..."  # 使用 KMS 加密
    dynamodb_table    = "terraform-locks"
  }
}

# 3. 完全避免密钥进入 State 的策略
# 方案 A:在 user_data 中从 Secrets Manager 拉取(Terraform 只存引用)
resource "aws_instance" "app" {
  user_data = templatefile("${path.module}/user_data.sh.tpl", {
    secret_arn = aws_secretsmanager_secret.db.arn  # 只传 ARN,不传值
  })
}
# user_data.sh.tpl:
# #!/bin/bash
# aws secretsmanager get-secret-value --secret-id ${secret_arn} | jq -r '.SecretString' > /app/config.json

# 方案 B:使用 External Secrets Operator(K8s 集群内拉取)
# Terraform 只创建 SecretStore 和 ExternalSecret CRD,不存实际密钥

十、Terraform 原生测试(>= 1.6)

10.1 测试文件结构

modules/vpc/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
    ├── basic.tftest.hcl       # 基础测试
    ├── multi_az.tftest.hcl    # 多可用区测试
    └── validation.tftest.hcl  # 变量校验测试

10.2 完整测试示例

# tests/basic.tftest.hcl

# 定义测试用变量
variables {
  name = "test-vpc"
  cidr = "10.10.0.0/16"
  subnets = [
    {
      name              = "private-a"
      cidr              = "10.10.0.0/24"
      availability_zone = "us-east-1a"
      type              = "private"
    },
    {
      name              = "private-b"
      cidr              = "10.10.1.0/24"
      availability_zone = "us-east-1b"
      type              = "private"
    },
  ]
}

# 测试 1:验证 plan 正确
run "plan_validation" {
  command = plan

  assert {
    condition     = length(plan_values.aws_subnet.private) == 2
    error_message = "应该创建 2 个私有子网"
  }

  assert {
    condition     = plan_values.aws_vpc.main[0].cidr_block == "10.10.0.0/16"
    error_message = "VPC CIDR 应为 10.10.0.0/16"
  }
}

# 测试 2:验证 apply 成功
run "apply_test" {
  command = apply

  assert {
    condition     = output.vpc_id != ""
    error_message = "VPC ID 不应为空"
  }

  assert {
    condition     = length(output.private_subnet_ids) == 2
    error_message = "应该输出 2 个子网 ID"
  }
}

# 测试 3:验证变量校验
run "invalid_cidr_rejected" {
  command = plan

  variables {
    cidr = "invalid-cidr"
  }

  # 预期失败
  expect_failures = [
    var.cidr,
  ]
}

# 测试 4:使用不同的 provider 配置
run "multi_region" {
  command = plan

  variables {
    region = "us-west-2"
  }

  provider "aws" {
    region = "us-west-2"
  }

  assert {
    condition     = plan_values.aws_subnet.private[0].availability_zone == "us-west-2a"
    error_message = "子网应该在 us-west-2 区域"
  }
}
# 运行所有测试
terraform test

# 运行特定测试文件
terraform test -filter=tests/basic.tftest.hcl

# 详细输出
terraform test -verbose

# 在 CI 中使用(不需要真实云资源时用 mock)
terraform test -mock=tests/mock.tftest.hcl

10.3 Mock 测试(>= 1.7)

# tests/mock.tftest.hcl —— 不调用真实云 API 的测试

mock_provider "aws" {
  mock_resource "aws_vpc" {
    defaults = {
      id           = "vpc-mock12345"
      cidr_block   = var.cidr
      instance_tenancy = "default"
    }
  }

  mock_resource "aws_subnet" {
    defaults = {
      id     = "subnet-mock-${count.index}"
      vpc_id = "vpc-mock12345"
    }
  }

  mock_data "aws_availability_zones" {
    defaults = {
      names = ["us-east-1a", "us-east-1b", "us-east-1c"]
    }
  }
}

run "mock_test" {
  command = plan

  assert {
    condition     = length(plan_values.aws_subnet.private) == 2
    error_message = "Mock 测试:应该创建 2 个子网"
  }
}

十一、多环境管理策略对比

11.1 三种方案对比

方案优点缺点适用场景
terraform workspace内置、简单State 隔离不彻底、容易混淆环境仅 dev/staging(非生产)
目录分离 + tfvars彻底隔离、清晰文件重复中小团队
TerragruntDRY、自动依赖额外工具学习成本大型多环境

11.2 Workspace 模式(不推荐用于生产)

# Workspace 使用同一套 .tf 代码,不同 State
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

terraform workspace select prod
terraform apply -var-file="prod.tfvars"

# 在代码中区分
# ⚠️ 这种 if/else 嵌套是反模式
locals {
  env = terraform.workspace
  instance_count = local.env == "prod" ? 5 : 1
}

11.3 目录分离模式(推荐)

infrastructure/
├── modules/                    # 可复用模块
│   ├── vpc/
│   ├── eks/
│   └── rds/
├── environments/               # 环境配置
│   ├── dev/
│   │   ├── backend.tf
│   │   ├── main.tf             # 引用 modules/
│   │   └── terraform.tfvars
│   ├── staging/
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── backend.tf
│       ├── main.tf
│       └── terraform.tfvars
# environments/prod/main.tf
module "vpc" {
  source = "../../modules/vpc"
  # ...
}

module "eks" {
  source = "../../modules/eks"
  # ...
}

# environments/prod/backend.tf
terraform {
  backend "s3" {
    bucket = "my-tfstate-prod"
    key    = "prod/terraform.tfstate"
    # ...
  }
}

11.4 Terragrunt 模式(大型环境推荐)

详见 Terraform 生产级实践 中的 Terragrunt 章节

infrastructure-live/
├── terragrunt.hcl              # 全局配置(backend + provider)
├── prod/
│   ├── env.hcl                 # 环境变量
│   ├── vpc/
│   │   └── terragrunt.hcl      # 仅 source + inputs(极简)
│   └── eks/
│       └── terragrunt.hcl
└── staging/
    └── ...

十二、Terraform 与 K8s 协作边界

12.1 分界原则

┌─────────────────────────────────────────┐
│           Terraform 管理范围              │
│  VPC / 子网 / 安全组 / 路由表             │
│  K8s 集群本身(EKS/GKE/AKS)              │
│  节点池 / 负载均衡器 / IAM 角色           │
│  数据库 / 缓存 / 消息队列(RDS/ElastiCache)│
│  DNS 记录 / CDN / WAF                    │
│  K8s 中的 "基础设施级" 资源(Namespace、  │
│  StorageClass、CRD 安装如 ArgoCD)        │
├─────────────────────────────────────────┤
│           ArgoCD/Flux 管理范围            │
│  Deployment / Service / Ingress          │
│  ConfigMap / Secret(业务配置)           │
│  HPA / PDB / NetworkPolicy               │
│  Helm Release / Kustomize                │
│  业务应用的一切                           │
└─────────────────────────────────────────┘

12.2 冲突避免

# ❌ 错误:Terraform 和 ArgoCD 同时管理同一个 Deployment
# ArgoCD selfHeal 会把 Terraform 的变更覆盖回来

# ✅ 正确:Terraform 只管 "安装 ArgoCD",不管 ArgoCD 管理的应用
resource "helm_release" "argocd" {
  name       = "argocd"
  repository = "https://argoproj.github.io/argo-helm"
  chart      = "argo-cd"
  version    = "7.3.0"
  namespace  = "argocd"
}

# ✅ Terraform 管理的 K8s 资源应该打标签
resource "kubernetes_namespace" "system" {
  for_each = toset(["argocd", "monitoring", "ingress-nginx"])
  metadata {
    name = each.key
    labels = {
      "managed-by"     = "terraform"
      "app.kubernetes.io/managed-by" = "terraform"
    }
  }
}

# ✅ 使用 ArgoCD 的 IgnoreDifferences 避免与 Terraform 冲突
# argocd 应用配置中:
# ignoreDifferences:
#   - group: ""
#     kind: Namespace
#     jsonPointers:
#       - /metadata/labels/managed-by

十三、故障排查实战

13.1 资源 Drift 检测

# 手动检测 drift(实际状态 vs State 记录)
terraform plan -detailed-exitcode
# exit code 0: 无变更
# exit code 1: 错误
# exit code 2: 有变更(存在 drift)

# 自动化 drift 检测脚本
#!/bin/bash
set -e

WORKSPACE=$1
terraform workspace select $WORKSPACE
terraform plan -detailed-exitcode -out=/dev/null 2>&1 | tee plan-output.txt
EXIT_CODE=$?

if [ $EXIT_CODE -eq 2 ]; then
  echo "⚠️ Drift detected in $WORKSPACE"
  # 发送告警
  curl -X POST "$ALERT_WEBHOOK" -d "{\"text\": \"Terraform drift in $WORKSPACE: $(cat plan-output.txt | head -50)\"}"
elif [ $EXIT_CODE -eq 0 ]; then
  echo "✅ No drift in $WORKSPACE"
else
  echo "❌ Plan failed in $WORKSPACE"
  exit 1
fi

13.2 State 修复实战

# 场景:手动修改了安全组规则,导致 plan 显示要删除该规则

# 方案 1:接受手动修改,更新 State
terraform plan -refresh=false   # 先看 State 中的记录
terraform apply -refresh        # refresh 更新 State

# 方案 2:用代码覆盖手动修改
terraform apply                 # 按 HCL 配置重新应用

# 方案 3:忽略该属性的漂移
# 在 HCL 中添加 lifecycle { ignore_changes = [ingress] }

# 场景:State 中引用了已删除的资源
# 症状:plan 报错 "resource not found"
terraform state rm aws_instance.old   # 从 State 中移除
# 然后添加新的 resource 块或 import

# 场景:资源地址错误(Module 重构后)
terraform state mv module.old_module.aws_instance.app module.new_module.aws_instance.app
# 移动 State 中的地址,不操作实际资源

13.3 Provider 版本升级

# 场景:aws provider 从 4.x 升级到 5.x(破坏性变更)

# 1. 查看 upgrade guide
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade

# 2. 更新版本约束
# version = "~> 5.0"

# 3. 升级 Provider 插件
terraform init -upgrade

# 4. 运行 plan 查看变更
terraform plan

# 5. 处理 deprecated 资源
# 某些资源可能被重命名或废弃
# 查看迁移指南逐个处理

# 6. 使用 moved blocks 处理重命名的资源
moved {
  from = aws_alb.main
  to   = aws_lb.main   # aws_alb → aws_lb
}

十四、常用命令速查

14.1 日常命令

# === 生命周期 ===
terraform init                    # 初始化(下载 Provider、初始化 backend)
terraform init -upgrade           # 初始化并升级 Provider 版本
terraform init -reconfigure       # 重新配置 backend(切换 backend 时用)
terraform plan                    # 预览变更
terraform plan -out=tfplan        # 预览并保存计划
terraform apply                   # 应用变更(会提示确认)
terraform apply tfplan            # 应用已保存的计划
terraform apply -auto-approve     # 自动确认(CI/CD 中使用)
terraform destroy                 # 销毁所有资源
terraform destroy -target=...     # 销毁指定资源

# === 格式化与校验 ===
terraform fmt                     # 格式化当前目录
terraform fmt -recursive          # 递归格式化所有子目录
terraform fmt -check              # 只检查不修改(CI 用)
terraform validate                # 验证配置语法
terraform validate -json          # JSON 格式输出

# === State 操作 ===
terraform state list              # 列出所有资源
terraform state show <address>    # 显示资源详情
terraform state mv <old> <new>    # 移动资源地址
terraform state rm <address>      # 从 State 移除(不删实际资源)
terraform state pull              # 输出 State 到 stdout
terraform state push <file>       # 从文件更新 State
terraform import <addr> <id>      # 导入已有资源
terraform force-unlock <lock-id>  # 强制解锁 State

# === 工作区 ===
terraform workspace list          # 列出工作区
terraform workspace new <name>    # 创建工作区
terraform workspace select <name> # 切换工作区
terraform workspace delete <name> # 删除工作区
terraform workspace show          # 当前工作区名

# === 调试 ===
terraform console                 # 交互式表达式求值
terraform output                  # 列出所有输出
terraform output <name>           # 查看特定输出
terraform output -json            # JSON 格式输出
terraform graph                   # 生成依赖图(DOT 格式)
terraform show tfplan             # 显示已保存的计划
terraform show -json tfplan       # JSON 格式计划

# === 模块 ===
terraform get                     # 下载/更新模块
terraform get -update             # 强制更新模块

# === 测试 ===
terraform test                    # 运行 .tftest.hcl 测试
terraform test -filter=<file>     # 运行特定测试
terraform test -verbose           # 详细输出

# === 其他 ===
terraform version                 # 版本信息
terraform providers               # 列出使用的 Provider
terraform login                   # 登录 Terraform Cloud
terraform logout                  # 退出 Terraform Cloud
terraform metadata                # 元数据信息

14.2 命令选项速查

# -var / -var-file —— 传入变量
terraform plan -var="region=us-west-2"
terraform plan -var-file="prod.tfvars"

# -target —— 只操作特定资源
terraform plan -target=module.vpc

# -refresh=false —— 跳过 refresh(快速 plan)
terraform plan -refresh=false

# -parallelism=N —— 并行度
terraform apply -parallelism=20

# -lock=false —— 不获取锁(危险!仅在调试时用)
terraform plan -lock=false

# -input=false —— 禁止交互式输入(CI/CD 中使用)
terraform apply -input=false -auto-approve

# -no-color —— 禁用彩色输出(日志文件用)
terraform plan -no-color

# -compact-warnings —— 紧凑警告
terraform plan -compact-warnings

# -json —— JSON 格式输出
terraform plan -json   # 流式 JSON 输出(可被程序消费)

# -generate-config-out —— 生成导入的资源配置
terraform plan -generate-config-out=generated.tf

# -migrate-state —— 迁移 State(切换 backend 时)
terraform init -migrate-state

# -reconfigure —— 重新配置 backend(不迁移 State)
terraform init -reconfigure

关联知识

参考资源

学习时间

阶段时间备注
HCL 高级语法2026-08-03dynamic blocks, for/for_each, validation, moved, import blocks
资源生命周期2026-08-03lifecycle meta-args, timeouts, provisioners, null_resource
Provider 高级用法2026-08-03alias 多区域/多账号, data sources, provider 函数
函数全集2026-08-03集合/字符串/网络/编码/时间/类型/加密
模块开发2026-08-03结构设计, 版本约束, 变量/输出原则, 模块测试
原生测试2026-08-03tftest.hcl, mock provider, Terratest
性能调优2026-08-03plan 慢排查, parallelism, targeted plan, State 优化
调试技巧2026-08-03TF_LOG, console, JSON plan 分析, crash log
密钥管理2026-08-03Secrets Manager, Vault, sensitive, State 加密
多环境管理2026-08-03workspace vs 目录分离 vs Terragrunt

状态: ✅ 完成 下次复习日期: 2026-08-10