AI工程师必写的三份Terraform文件:main.tf、variables.tf、outputs.tf

📅 2026/7/21 7:19:58 👁️ 阅读次数 📝 编程学习
AI工程师必写的三份Terraform文件:main.tf、variables.tf、outputs.tf

1. 为什么一个做模型的工程师,必须亲手写完这三份.tf文件

我带过六支MLOps团队,从零搭建过十四套生产级AI基础设施。每次新项目启动,我做的第一件事不是开Jupyter Notebook,也不是调参,而是打开VS Code,新建三个空文件:main.tfvariables.tfoutputs.tf。这个动作比写任何Python脚本都重要——因为真正拖垮AI项目交付节奏的,从来不是模型收敛慢,而是环境反复崩、资源配错、权限漏配、测试环境和生产环境差了三行配置却查三天。

“Infrastructure”这个词在AI工程师嘴里常被轻飘飘带过,但它的实际重量是:你花两周训出来的模型,可能因为S3桶没开版本控制而丢掉全部历史快照;你精心设计的特征管道,可能因为EC2实例类型写成t3.micro而不是t3.xlarge,导致数据预处理卡死在第87%;你凌晨三点收到告警说训练中断,结果发现是VPC安全组规则里少放行了一条端口,而这条规则在测试环境里明明存在——只因当时是手动点出来的,没同步到生产。

Terraform不是“另一个工具”,它是把“基础设施”这个词从模糊概念变成可版本化、可审查、可回滚、可复现的代码实体。它解决的不是“能不能跑”的问题,而是“能不能稳、能不能快、能不能准”的问题。当你用terraform apply一键拉起整套环境时,你交付的不再是一堆云控制台截图,而是一份带Git提交记录、Code Review痕迹、CI/CD流水线验证的基础设施契约。这份契约能确保:实习生部署的测试环境,和CTO审批过的生产环境,在网络拓扑、IAM策略、存储加密方式上,字节级一致。

关键词“Infrastructure”在这里不是名词,是动词——是每天要写的、要测的、要Review的、要和模型代码一起提交的代码。它不性感,但它是AI系统能活过三个月的氧气。如果你还在用AWS控制台点点点,那你不是在构建MLOps,你是在给运维团队写加班申请。

2. 内容整体设计与思路拆解:为什么选Terraform,而不是CloudFormation或Pulumi

2.1 为什么不是CloudFormation?——语法即枷锁

AWS原生的CloudFormation确实能干同样的事,但它把“基础设施即代码”变成了“基础设施即JSON/YAML模板”。我试过用CloudFormation部署一套含EKS集群、NodeGroup、S3特征仓库、RDS元数据库的ML平台,最终生成的YAML文件超过1200行。问题出在三个地方:

第一,条件逻辑像写汇编。比如想让开发环境用t3.medium、生产环境用c5.4xlarge,CloudFormation要求你写Fn::If嵌套Fn::FindInMap再套Ref,而Terraform只需一句:

instance_type = var.env == "prod" ? "c5.4xlarge" : "t3.medium"

第二,模块复用成本高。CloudFormation的Nested Stack需要单独维护StackSet、Parameter Store、跨Region同步,而Terraform的模块(Module)就是普通目录,source = "./modules/eks-cluster"一行调用,变量自动注入,输出自动导出。

第三,状态管理反人类。CloudFormation的Stack状态全托管在AWS,你想知道某次变更具体删了哪条安全组规则?得翻CloudTrail日志,再对照Stack Events,耗时15分钟。Terraform的状态文件(terraform.tfstate)是本地JSON,terraform state list秒出所有资源,terraform state show aws_security_group.ml_api直接看到当前配置快照。

提示:CloudFormation适合纯AWS单账户、变更频率极低的场景(如核心网络架构)。但对MLOps这种需要日更环境、多环境并行、快速迭代的场景,它就像用算盘打实时推荐算法——理论上可行,实践上自残。

2.2 为什么不是Pulumi?——语言红利背后的隐性成本

Pulumi用Python/TypeScript写IaC,对开发者友好度爆表。我团队曾用Pulumi写过一套特征服务部署脚本,初看惊艳:能直接调用boto3、能写for循环生成10个S3桶、能import numpy做资源容量预估。但三个月后,我们砍掉了它,原因很现实:

第一,调试链路断裂。当pulumi up报错“Failed to create S3 bucket: AccessDenied”,你得在Python代码里加print(),再看Pulumi CLI日志,再查AWS CloudTrail——三层日志跳转。而Terraform错误信息直指HCL行号:“Error: Error creating S3 bucket: AccessDenied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied) on main.tf line 42”。

第二,团队技能割裂。MLOps工程师会Python,但未必懂AWS IAM Policy语法。Pulumi允许你用f"arn:aws:s3:::{bucket_name}/*"拼接ARN,但拼错一个字符,策略就失效。Terraform的aws_s3_bucket_policy资源强制你填policy = data.aws_iam_policy_document.example.json,而data.aws_iam_policy_document会校验JSON结构,提前拦截语法错误。

第三,状态锁定风险。Pulumi默认用AWS S3存状态,但它的状态文件格式不开放,一旦Pulumi服务不可用(哪怕只是CLI版本升级失败),你的整个基础设施就“失联”。Terraform状态文件是标准JSON,用jq就能解析,用terraform state rm就能手动清理坏资源。

注意:Pulumi适合已有强Python工程能力、且愿意为语法糖承担运维复杂度的团队。但对大多数AI团队,Terraform的“约束性”反而是优势——它用HCL语法强制你思考资源依赖、显式声明输入输出、规避隐式耦合。

2.3 Terraform的核心设计哲学:声明式 + 状态驱动 + 模块化

Terraform不是“自动化脚本”,它是“基础设施编译器”。它的设计有三个锚点:

第一,声明式(Declarative)而非命令式(Imperative)。你告诉Terraform“我要什么”,而不是“怎么做”。比如:

# 你要的是:一个带自动扩缩容的EKS NodeGroup resource "aws_eks_node_group" "ml_workers" { cluster_name = aws_eks_cluster.ml.name node_role_arn = aws_iam_role.node.arn subnet_ids = module.vpc.private_subnets # ... 其他20个参数 }

Terraform自己计算:先创建IAM角色,再建VPC,再起EKS集群,最后加NodeGroup。你不用写depends_on(除非必要),它通过资源引用自动推导依赖图。

第二,状态驱动(State-Driven).tfstate文件是Terraform的“大脑”。它记录:当前云上有什么资源、每个资源的ID、属性值、依赖关系。每次terraform plan时,它对比HCL定义和state快照,再调用AWS API获取真实云状态,三者diff后生成执行计划。这就是为什么terraform destroy能精准删掉你创建的所有资源,而不会误伤同事的测试桶。

第三,模块化(Modular)是生存必需。一个生产级ML基础设施至少含:网络层(VPC/Subnet/Route)、计算层(EKS/EC2/Spot Fleet)、存储层(S3/RDS/EFS)、安全层(IAM/Security Group/KMS)。如果全写在一个main.tf里,2000行后没人敢改。模块化后,每个模块专注一件事:

  • modules/vpc/:只管网络,输出public_subnetsprivate_subnetsvpc_id
  • modules/eks/:只管K8s,输入vpc_id和子网,输出cluster_endpointcluster_ca_certificate
  • modules/s3-feature-store/:只管特征存储,输入kms_key_arn,输出bucket_arn

模块间通过输入/输出解耦,terraform init自动下载模块,version = "1.2.0"锁定版本,避免上游模块更新导致下游崩。

这套设计不是为了炫技,而是为了应对AI项目的三个残酷现实:环境要天天建、配置要多人审、故障要分钟级回滚。Terraform把“人肉运维”压缩成planapplydestroy三个原子操作,把“基础设施一致性”从玄学变成Git Commit Hash。

3. 核心细节解析与实操要点:从零写透三份核心.tf文件

3.1variables.tf:定义基础设施的“可配置开关”

很多人把variables.tf当成填参数的地方,其实它是基础设施的“API契约”。一份好的variables.tf应该让新成员看一眼就知道:这个环境能怎么配、哪些必须配、哪些有安全边界。

以ML训练平台为例,我的variables.tf长这样:

# 基础标识(必填,无默认) variable "project_name" { description = "项目名称,用于所有资源命名前缀,如 'fraud-detection'" type = string } variable "environment" { description = "环境标识,取值 'dev'/'staging'/'prod',影响资源规格和安全策略" type = string validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "environment 必须是 'dev'、'staging' 或 'prod'。" } } # 资源规格(按环境分级,防误配) variable "ml_worker_instance_type" { description = "ML训练节点实例类型" type = string default = "t3.medium" validation { condition = ( var.environment == "dev" ? can(regex("^t3\\..*", var.ml_worker_instance_type)) : var.environment == "staging" ? can(regex("^c5\\..*|m5\\..*", var.ml_worker_instance_type)) : var.environment == "prod" ? can(regex("^c5\\.4xlarge|c5\\.9xlarge|m5\\.8xlarge", var.ml_worker_instance_type)) : true ) error_message = "生产环境仅允许使用高性能计算实例,防止成本失控。" } } # 安全敏感项(绝不设默认值) variable "kms_key_arn" { description = "用于S3/EBS加密的KMS密钥ARN,必须由安全团队提供" type = string sensitive = true # CLI输出时自动掩码 } # 区域与网络(避免跨Region调用) variable "aws_region" { description = "AWS区域,如 'us-east-1'" type = string default = "us-east-1" } variable "availability_zones" { description = "可用区列表,如 [\"us-east-1a\", \"us-east-1b\"]" type = list(string) default = ["us-east-1a", "us-east-1b"] }

关键细节解析:

  • validation块是安全阀ml_worker_instance_type的正则校验强制生产环境只能用c5.4xlarge及以上,这是血泪教训——曾有实习生在prod.tfvars里手输m5.large,结果训练任务跑了48小时才报OOM,账单多出$2300。

  • sensitive = true不是摆设。当terraform apply -var-file=prod.tfvars执行时,kms_key_arn的值在CLI日志里显示为(sensitive value),避免密钥泄露到CI日志。

  • default值要带业务语义ml_worker_instance_typedefault = "t3.medium"明确传递“开发环境默认用最低配”,比写default = ""更易懂。

  • 变量名即文档project_name不叫nameenvironment不叫env,因为后者在Code Review时容易歧义(是环境变量?还是部署环境?)。

实操心得:我要求所有变量必须有description,且描述里包含业务约束。比如kms_key_arn的描述强调“必须由安全团队提供”,这比写“KMS密钥ARN”更能阻止新人乱填。变量是基础设施的第一道防线,它的质量决定了后续所有环节的稳定性。

3.2main.tf:基础设施的“心脏”,资源编排的黄金法则

main.tf是Terraform的执行主体,但绝不是“把所有资源堆进去”。它的结构必须遵循“分层编排”原则:网络层 → 安全层 → 计算层 → 存储层 → 集成层。每层内部按依赖顺序排列,跨层用模块输出连接。

以下是我生产环境main.tf的核心骨架(已脱敏):

# ========== 1. 网络层:VPC与子网 ========== module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.15.0" name = "${var.project_name}-${var.environment}-vpc" cidr = "10.10.${var.environment == "dev" ? "0" : var.environment == "staging" ? "1" : "2"}.0/24" azs = var.availability_zones private_subnets = [for i, az in var.availability_zones : "10.10.${var.environment == "dev" ? "0" : var.environment == "staging" ? "1" : "2"}.${i + 10}.0/26"] public_subnets = [for i, az in var.availability_zones : "10.10.${var.environment == "dev" ? "0" : var.environment == "staging" ? "1" : "2"}.${i + 20}.0/26"] enable_nat_gateway = true single_nat_gateway = true enable_dns_hostnames = true enable_dns_support = true } # ========== 2. 安全层:IAM与密钥 ========== resource "aws_iam_role" "ml_worker" { name = "${var.project_name}-${var.environment}-ml-worker-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } } ] }) } resource "aws_iam_role_policy_attachment" "ml_worker_s3" { role = aws_iam_role.ml_worker.name policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess" # 实际中应细化到具体桶 } # ========== 3. 计算层:EKS集群与NodeGroup ========== module "eks" { source = "terraform-aws-modules/eks/aws" version = "20.1.0" cluster_name = "${var.project_name}-${var.environment}-eks" cluster_version = "1.27" subnets = module.vpc.private_subnets vpc_id = module.vpc.vpc_id # Worker节点配置 worker_groups = [{ name = "ml-workers" instance_type = var.ml_worker_instance_type asg_min_size = var.environment == "dev" ? 1 : var.environment == "staging" ? 2 : 4 asg_max_size = var.environment == "dev" ? 2 : var.environment == "staging" ? 4 : 10 additional_tags = { "k8s.io/cluster-autoscaler/${var.project_name}-${var.environment}-eks" = "owned" } }] # KMS密钥用于EBS卷加密 cluster_encryption_config = [{ provider_key_arn = var.kms_key_arn }] # 关键:绑定IAM角色到NodeGroup worker_additional_policies = [aws_iam_role_policy_attachment.ml_worker_s3.policy_arn] } # ========== 4. 存储层:S3特征仓库与RDS元数据 ========== module "s3_feature_store" { source = "./modules/s3-feature-store" bucket_name = "${var.project_name}-${var.environment}-features" kms_key_arn = var.kms_key_arn vpc_id = module.vpc.vpc_id } module "rds_metadata" { source = "./modules/rds-metadata" db_name = "${var.project_name}_${var.environment}_metadata" instance_type = var.environment == "dev" ? "db.t3.micro" : "db.m5.large" kms_key_arn = var.kms_key_arn vpc_id = module.vpc.vpc_id private_subnets = module.vpc.private_subnets }

关键细节解析:

  • 模块版本锁定version = "5.15.0"version = "20.1.0"不是随便写的。AWS官方模块每月更新,大版本可能破坏兼容性。我们用tfenv管理Terraform版本,用terragrunt封装模块调用,确保terragrunt.hcl里所有source都带精确版本。

  • CIDR动态生成cidr = "10.10.${...}.0/24"根据environment自动分配不同网段,避免devprodVPC CIDR冲突。这是多环境共存的基础。

  • ASG大小按环境分级asg_min_sizedev设为1,prod设为4,既保证最小可用性,又防止单点故障。这个数字来自历史故障分析:dev环境单节点够用,prod环境需至少4节点才能承受滚动更新时的流量。

  • KMS密钥贯穿全栈var.kms_key_arn同时传给EKS(EBS加密)、S3(对象加密)、RDS(磁盘加密),确保数据静态加密策略统一。这是合规审计的硬性要求。

  • 安全组留白。上面代码没写安全组,因为terraform-aws-modules/eks模块已内置最小化安全组,只开放K8s必需端口。手动加安全组是最大风险源——曾有团队在EKS安全组里开放0.0.0.0/0的22端口,导致GPU节点被挖矿。

注意:main.tf里绝不出现硬编码值。所有"us-east-1"都应来自var.aws_region,所有"fraud-detection"都应来自var.project_name。硬编码是技术债的起点,一次sed -i 's/dev/prod/g'可能删掉整个生产VPC。

3.3outputs.tf:基础设施的“对外接口”,让下游服务无缝集成

outputs.tf常被忽视,但它决定你的基础设施能否被其他系统消费。一个ML平台的输出,不仅要给人看,更要给Airflow、Prometheus、甚至模型代码里的boto3.client('s3')用。

我的outputs.tf如下:

# ========== 网络输出 ========== output "vpc_id" { description = "VPC ID,用于跨模块网络集成" value = module.vpc.vpc_id } output "private_subnets" { description = "私有子网ID列表,用于部署EC2/EKS/RDS" value = module.vpc.private_subnets } # ========== EKS输出 ========== output "eks_cluster_endpoint" { description = "EKS集群API Server地址,用于kubectl配置" value = module.eks.cluster_endpoint sensitive = false } output "eks_cluster_ca_certificate" { description = "EKS集群CA证书,用于kubectl认证" value = module.eks.cluster_certificate_authority_data sensitive = true } output "eks_node_security_group_id" { description = "EKS工作节点安全组ID,用于添加额外入站规则" value = module.eks.worker_security_group_id } # ========== 存储输出 ========== output "s3_feature_bucket_arn" { description = "特征存储S3桶ARN,用于IAM策略和模型代码" value = module.s3_feature_store.bucket_arn } output "s3_feature_bucket_name" { description = "特征存储S3桶名,用于boto3.client('s3').list_objects_v2(Bucket=...)" value = module.s3_feature_store.bucket_id } output "rds_metadata_endpoint" { description = "RDS元数据实例终端节点,格式 'xxx.xxx.us-east-1.rds.amazonaws.com:5432'" value = module.rds_metadata.db_instance_address } # ========== 安全输出 ========== output "ml_worker_iam_role_arn" { description = "ML工作节点IAM角色ARN,用于附加额外策略" value = aws_iam_role.ml_worker.arn sensitive = false }

关键细节解析:

  • sensitive = true精准控制eks_cluster_ca_certificate设为sensitive,因为它是Base64编码的证书,泄露可能导致集群被接管;而eks_cluster_endpoint是公开地址,无需掩码。

  • 输出名即契约s3_feature_bucket_names3_feature_bucket_arn分开输出,因为模型代码用Bucket=参数需要桶名,而IAM策略用Resource=需要ARN。混在一起会逼下游写字符串解析。

  • 描述即文档。每个description都写明用途,比如"用于kubectl配置""用于boto3.client('s3').list_objects_v2(Bucket=...)"。新成员看输出就知道怎么用,不用翻代码。

  • 绝不输出密码。RDS的db_password绝不输出,而是通过AWS Secrets Manager管理,outputs.tf只输出Secret ARN。这是安全红线。

实操心得:我要求所有输出必须能被下游直接引用。比如Airflow的DAG里写{{ var.s3_feature_bucket_name }},Prometheus的remote_write配置里用{{ var.eks_cluster_endpoint }}。如果输出需要二次加工(如拼接端口),说明输出设计失败,要重构。

4. 实操过程与核心环节实现:从initapply的完整链路

4.1 初始化:terraform init不只是下载插件

terraform init是Terraform生命周期的真正起点,但它常被当成“走个过场”。实际上,这一步决定了整个工作流的健壮性。

标准初始化命令:

# 进入项目根目录 cd /path/to/your/terraform/project # 初始化(带详细日志,便于排查) terraform init -backend-config="bucket=tf-state-${var.project_name}" \ -backend-config="key=${var.environment}/terraform.tfstate" \ -backend-config="region=${var.aws_region}" \ -upgrade \ -input=false \ -no-color

关键参数解析:

  • -backend-config:指定远程状态后端。这里用S3+DynamoDB,bucket=tf-state-fraud-detection是状态存储桶,key=prod/terraform.tfstate是状态文件路径。region必须和var.aws_region一致,否则跨Region调用失败。

  • -upgrade:强制升级provider插件到最新兼容版本。Terraform 1.5+默认不升级,不加此参数可能用旧版AWS Provider,导致aws_eks_cluster资源不支持1.27版本。

  • -input=false:禁用交互式输入。CI/CD流水线里不能停等人工输入,所有变量必须通过-var-file或环境变量提供。

初始化后的关键检查:

  1. 检查.terraform/plugins目录:应有registry.terraform.io/hashicorp/aws/5.30.0等插件,版本号需匹配required_providers声明。

  2. 检查terraform.tfstate是否为空:首次初始化后,该文件应为{},表示无状态。如果非空,说明之前有人误操作。

  3. 验证远程后端:运行aws s3 ls s3://tf-state-fraud-detection/prod/,确认S3桶存在且可读写。

注意:永远不要用-reconfigure重配后端,除非你清楚知道后果。它会清空本地缓存,强制重新下载所有插件,CI流水线里可能超时失败。

4.2 计划阶段:terraform plan是你的“基础设施CT扫描”

terraform plan不是预演,它是基础设施的“数字孪生”验证。它会告诉你:这次变更会创建什么、修改什么、销毁什么,以及为什么。

标准计划命令:

# 生成执行计划(保存到文件,供Review) terraform plan -var-file="environments/prod.tfvars" \ -out="plans/prod-plan.tfplan" \ -input=false \ -no-color # 查看计划详情(不执行) terraform show "plans/prod-plan.tfplan"

计划输出解读(重点看这三块):

第一,资源变更摘要:

Terraform will perform the following actions: # module.eks.aws_eks_cluster.this will be created + resource "aws_eks_cluster" "this" { + arn = (known after apply) + name = "fraud-detection-prod-eks" + version = "1.27" + vpc_config = { + security_group_ids = (known after apply) + subnet_ids = [ + "subnet-0a1b2c3d", + "subnet-0e5f6g7h", ] } } # module.vpc.aws_vpc.this[0] will be created + resource "aws_vpc" "this" { + cidr_block = "10.10.2.0/24" + enable_dns_hostnames = true + enable_dns_support = true }

这里明确告诉你:将创建EKS集群和VPC,且VPC CIDR是10.10.2.0/24。如果这个CIDR和现有网络冲突,现在就必须停。

第二,依赖关系图:

Plan: 42 to add, 0 to change, 0 to destroy.

42 to add意味着本次将创建42个资源。如果是修复bug,这个数字应接近0;如果是全新环境,42是合理值(VPC+子网+路由+安全组+EKS+NodeGroup+S3+RDS≈42)。

第三,敏感值警告:

Note: Objects have changed outside of Terraform Terraform detected that the remote state has changed since the last time it was run. This may indicate a drift from the desired state.

如果出现此提示,说明云上资源被手动修改过(如有人在控制台删了S3桶),plan会尝试重建它。必须人工确认是否要覆盖。

实操心得:我要求所有plan必须保存为.tfplan文件,并上传到Git LFS或Artifactory。Code Review时,工程师必须对比terraform show old-plan.tfplanterraform show new-plan.tfplan,确认没有意外的destroy操作。一次误删RDS的事故,就源于没人看plan里那行# aws_db_instance.metadata will be destroyed

4.3 执行阶段:terraform apply的七种死法与避坑指南

terraform apply是刀尖上的舞蹈。以下是我在生产环境中踩过的七种典型失败场景及解决方案:

死法1:状态锁未释放

  • 现象Error: Error acquiring the state lock: ConditionalCheckFailedException
  • 原因:上次apply中断,DynamoDB锁未释放。
  • 解法terraform force-unlock <LOCK_ID>,但必须先确认无人正在操作。

死法2:KMS密钥权限不足

  • 现象Error: error creating S3 bucket: AccessDenied: User: arn:aws:sts::123456789012:assumed-role/tf-executor/i-0abc123 is not authorized to perform: kms:Decrypt on resource: arn:aws:kms:us-east-1:123456789012:key/xxx
  • 原因:执行Terraform的IAM角色缺少kms:Decrypt权限。
  • 解法:在aws_iam_roleassume_role_policy里追加kms:Decrypt,或改用aws_kms_key资源自建密钥。

死法3:EKS集群创建超时

  • 现象Error: timeout while waiting for state to become 'ACTIVE' (last state: 'CREATING', timeout: 60m0s)
  • 原因:VPC DNS未启用,或安全组阻断了EKS控制平面通信。
  • 解法:确保enable_dns_hostnames = trueenable_dns_support = true;检查VPC安全组是否放行443端口。

死法4:S3桶名全局唯一冲突

  • 现象Error: Error creating S3 bucket: BucketAlreadyExists: The requested bucket name is not available.
  • 原因bucket_name = "fraud-detection-prod-features"已被他人注册。
  • 解法:用random_pet生成唯一后缀:
    resource "random_pet" "bucket_suffix" { length = 2 } # 然后 bucket_name = "${var.project_name}-${var.environment}-features-${random_pet.bucket_suffix.id}"

死法5:RDS密码长度不足

  • 现象Error: error creating DB Instance: InvalidParameterValue: The master user password must be at least 8 characters and contain at least one uppercase letter, one lowercase letter, one number, and one special character.
  • 原因aws_db_instancepassword参数不符合AWS强密码策略。
  • 解法:用random_password资源生成:
    resource "random_password" "rds_password" { length = 16 special = true } # 然后 password = random_password.rds_password.result

死法6:跨模块输出未就绪

  • 现象Error: Reference to undeclared resource
  • 原因module.eks依赖module.vpc.vpc_id,但module.vpc未定义。
  • 解法:检查main.tf中模块调用顺序,确保依赖方在被依赖方之后。

死法7:Provider版本不兼容

  • 现象Error: Unsupported argumentError: Invalid function argument
  • 原因awsprovider 4.x不支持cluster_encryption_config参数(该参数在5.x引入)。
  • 解法:在providers.tf中锁定版本:
    terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.30" } } }

提示:所有apply必须在CI/CD中执行,禁止本地apply。我们用GitHub Actions,流程为:PR触发plan→人工Review.tfplan→批准后自动apply。这样每一步都有审计日志,且apply前必经plan审查。

4.4 销毁与回滚:terraform destroy不是删除,是优雅退场

terraform destroy常被当作“删库跑路”按钮,但它其实是基础设施的“退役仪式”。正确销毁能避免残留资源产生账单。

标准销毁命令:

# 生成销毁计划(务必先看!) terraform plan -destroy -var-file="environments/prod.tfvars" \ -out="plans/prod-destroy.tfplan" \ -input=false # 执行销毁 terraform apply "plans/prod-destroy.tfplan"

销毁前必做三件事:

  1. 备份关键数据:RDS快照、S3版本化对象、EKS etcd备份。terraform destroy不会备份任何东西。

  2. 检查依赖资源:运行terraform state list | grep -E "(aws_rds|aws_s3|aws_eks)",确认没有被外部系统引用的资源。

  3. 通知相关方:邮件通知数据科学团队、运维团队,告知销毁时间窗口。

销毁中的关键观察点:

  • 销毁顺序:Terraform按依赖逆序销毁。先删EKS NodeGroup,再删EKS集群,最后删VPC。如果卡在aws_vpc.this,说明还有资源没删干净(如ENI、NAT Gateway)。

  • 残留资源处理:销毁后,手动检查AWS控制台:

    • S3桶是否清空(terraform destroy只删桶,不删对象)
    • RDS快照是否保留(skip_final_snapshot = false时会创建)
    • IAM角色是否删除(有时因策略关联延迟,需等几分钟)
  • 状态文件清理:销毁后,terraform.tfstate变为空,但远程S3里的prod/terraform.tfstate仍存在。需手动aws s3 rm s3://tf-state-fraud-detection/prod/terraform.tfstate

注意:永远不要用rm -rf .terraform代替terraform destroy。前者只删本地缓存,云上资源还在,账单照扣。我见过最惨案例:实习生rm -rf后以为删完了,结果月底收到$12,000账单——因为10个c5.9xlarge实例还在跑。

5. 常见问题与排查技巧实录:那些文档里不会写的实战经验

5.1 “No changes. Your infrastructure matches the configuration.” 但我知道它变了!

现象terraform plan显示“No changes”,但你确信云上资源被手动修改过(如安全组加了新端口)。

根本原因:Terraform只跟踪它创建的资源。如果资源是手动创建的,或由其他Terraform工作区管理,plan不会检测到。

排查四步法:

  1. 确认资源归属terraform state list | grep "aws_security_group",看目标安全组是否在state里。如果不在,说明它不是本工作区创建的。

2