问题背景
很多公司过去几年里一步步从单云走向多云,原因各有不同:
- 业务出海,需要在多个地域部署。
- 监管要求数据在境内保存,但海外用户多,需要海外资源。
- 单一云厂商出过故障,迫使做灾备。
- 不同业务用不同云服务(AWS 的 Lambda、阿里云的 ACK、Azure 的 AAD),整合管理。
- 公司收购,IT 资产在不同云上。
但多云管理是个大坑:
- 阿里云 ECS、AWS EC2、Azure VM 各有各的 API,各有各的 SDK。
- IAM 体系不同,权限模型不同。
- 资源状态没有统一视图,盘点困难。
- 不同云的同名概念含义不同(VPC 在 AWS 和 Azure 里范围不一样)。
- 手工创建的资源没人记得,改了哪个、删了哪个都对不上账。
- 出了安全事件需要排查所有云上的资源,散落在多个控制台。
Terraform 是目前最主流的多云基础设施即代码(IaC)工具。阿里云、AWS、Azure、GCP、腾讯云、华为云都提供官方或社区的 Terraform Provider。这篇文章讲清楚怎么用 Terraform 统一管理多云资源,避开常见陷阱,建立可维护的 IaC 项目。
适用读者
- 负责多云架构的运维工程师、SRE、DevOps。
- 想从手工控制台 / 自研脚本迁移到 IaC 的同学。
- 维护 Terraform 项目、解决漂移 / 状态冲突 / 漂移检测的工程师。
- 准备把 Terraform 集成到 CI/CD 流水线的同学。
适用场景
- 中大规模基础设施(10~10000 个云资源)。
- 多云或混合云(AWS + 阿里云 + 私有 K8s)。
- 应用、数据库、网络、监控的代码化管理。
- 与 Jenkins、GitLab CI、GitHub Actions、Argo CD 配合的 GitOps 流水线。
核心知识点
Terraform 是 HashiCorp 公司开发的基础设施即代码工具,用声明式 HCL(HashiCorp Configuration Language)描述"我要什么资源",然后由 Terraform 协调云厂商 API 把现实拉到这个状态。
关键概念:
- Provider:对接云厂商 / 服务商的插件,例如
aws、alicloud、azurerm、google、tencentcloud。
- Resource:要管理的资源,例如
aws_instance、alicloud_vpc。
- Data Source:查询已有资源,例如
aws_ami、alicloud_zones。
- State:Terraform 维护的"实际状态"文件,对比期望状态和实际状态。
- Plan:根据期望和状态算出要做的操作(create / read / update / delete)。
- Apply:执行 plan,把实际状态推到期望状态。
- Module:可复用的 Terraform 代码单元。
1. 写代码(HCL) → *.tf 文件
2. terraform init → 下载 provider / module
3. terraform plan → 算出 diff
4. terraform apply → 推送到云
5. terraform destroy → 销毁(慎用)
6. terraform state → 操作 state 文件
7. terraform import → 把已有资源纳入管理
安装
# macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Ubuntu / Debian
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common
wget -O- https://apt.releases.hashicorp.com/gpg | \
gpg --dearmor | \
sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install terraform
# 验证
terraform version
多云场景的核心挑战
- Provider 差异:每个云有自己的资源模型,跨云抽象困难。
- 身份认证:每个云的 AK/SK、IAM Role 都不一样。
- State 同步:多云资源在同一个 state 里,要考虑一致性。
- 网络打通:跨云网络(VPC Peering、VPN、SD-WAN)需要规划。
- 统一监控:跨云指标、日志、告警需要统一平台。
- 合规:不同云合规要求不同,数据驻留、加密、审计。
- 成本管理:多云账单合并分析。
Terraform 解决的是"用统一的语言描述资源",但具体落地仍然要懂每个云。
项目结构
terraform-multi-cloud/
├── main.tf # 入口
├── versions.tf # provider / terraform 版本
├── variables.tf # 输入变量
├── outputs.tf # 输出
├── terraform.tfvars # 变量值(不进版本控制)
├── backend.tf # state 后端
├── providers/
│ ├── aws.tf
│ ├── alicloud.tf
│ └── azurerm.tf
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── ecs/
│ └── rds/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── production/
└── .gitignore
versions.tf
# versions.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
alicloud = {
source = "aliyun/alicloud"
version = "~> 1.200"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
tencentcloud = {
source = "tencentcloudstack/terraform-provider-tencentcloud"
version = "~> 1.80"
}
}
}
provider 配置
# providers/aws.tf
provider "aws" {
region = "ap-southeast-1"
default_tags {
Environment = "production"
ManagedBy = "terraform"
Project = "myapp"
}
}
# providers/alicloud.tf
provider "alicloud" {
region = "cn-hangzhou"
profile = "default"
}
# providers/azurerm.tf
provider "azurerm" {
features {}
subscription_id = var.azure_subscription_id
}
# providers/google.tf
provider "google" {
project = var.gcp_project_id
region = "asia-southeast1"
}
风险提示:每个 provider 都需要认证信息,不要把 AK/SK 写进代码。建议用环境变量、CI secret、Vault。
后端配置
# backend.tf
terraform {
backend "s3" {
bucket = "myorg-terraform-state"
key = "multi-cloud/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
kms_key_id = "arn:aws:kms:ap-southeast-1:111122223333:key/abcd-1234"
dynamodb_table = "terraform-lock"
}
}
不同云的后端:
# 阿里云 OSS 后端
terraform {
backend "oss" {
bucket = "myorg-terraform-state"
key = "multi-cloud/terraform.tfstate"
region = "cn-hangzhou"
prefix = "terraform/state"
encrypt = true
}
}
# 腾讯云 COS 后端
terraform {
backend "cos" {
secret_id = var.tencent_secret_id
secret_key = var.tencent_secret_key
region = "ap-shanghai"
bucket = "myorg-terraform-state-1234567890"
prefix = "terraform/state"
key = "multi-cloud/terraform.tfstate"
}
}
变量
# variables.tf
variable "environment" {
description = "环境名"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "环境必须是 dev、staging 或 production。"
}
}
variable "aws_region" {
description = "AWS 区域"
type = string
default = "ap-southeast-1"
}
variable "alicloud_region" {
description = "阿里云区域"
type = string
default = "cn-hangzhou"
}
variable "instance_type" {
description = "ECS/EC2 实例规格"
type = string
default = "ecs.t5.large"
}
variable "vpc_cidr" {
description = "VPC CIDR"
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrnetmask(var.vpc_cidr))
error_message = "VPC CIDR 必须是合法的 CIDR。"
}
}
变量值文件(每个环境一个):
# environments/production/terraform.tfvars
environment = "production"
aws_region = "ap-southeast-1"
alicloud_region = "cn-hangzhou"
instance_type = "ecs.g6.xlarge"
vpc_cidr = "10.0.0.0/16"
风险提示:tfvars 文件可能含敏感信息(数据库密码、IP 白名单),建议不进 Git,用 CI secret 注入。
创建 VPC(多云)
# modules/vpc/main.tf
variable "vpc_cidr" {
type = string
description = "VPC CIDR"
}
variable "vpc_name" {
type = string
description = "VPC 名称"
}
variable "environment" {
type = string
description = "环境"
}
# AWS VPC
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
tags = {
Name = "${var.vpc_name}-aws"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
count = 3
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.vpc_name}-public-${count.index + 1}"
}
}
data "aws_availability_zones" "available" {
state = "available"
}
# 阿里云 VPC
resource "alicloud_vpc" "this" {
vpc_name = "${var.vpc_name}-aliyun"
cidr_block = var.vpc_cidr
}
resource "alicloud_vswitch" "public" {
count = 3
vpc_id = alicloud_vpc.this.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
zone_id = data.alicloud_zones.available.zones[count.index].id
vswitch_name = "${var.vpc_name}-public-${count.index + 1}"
}
data "alicloud_zones" "available" {
available_resource_creation = "VSwitch"
}
output "aws_vpc_id" {
value = aws_vpc.this.id
}
output "alicloud_vpc_id" {
value = alicloud_vpc.this.id
}
创建 ECS / EC2
# modules/ecs/main.tf
variable "instance_type" {
type = string
}
variable "vpc_id" {
type = string
}
variable "subnet_ids" {
type = list(string)
}
variable "image_id" {
type = string
}
variable "count" {
type = number
default = 2
}
# AWS EC2
resource "aws_instance" "this" {
count = var.count
ami = var.image_id
instance_type = var.instance_type
subnet_id = var.subnet_ids[count.index % length(var.subnet_ids)]
vpc_security_group_ids = [aws_security_group.this.id]
tags = {
Name = "web-${count.index + 1}"
}
}
resource "aws_security_group" "this" {
name = "web-sg"
description = "Allow HTTP and SSH"
vpc_id = var.vpc_id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
编排入口
# environments/production/main.tf
module "vpc" {
source = "../../modules/vpc"
vpc_cidr = var.vpc_cidr
vpc_name = "prod"
environment = var.environment
}
module "ecs" {
source = "../../modules/ecs"
instance_type = var.instance_type
vpc_id = module.vpc.aws_vpc_id
subnet_ids = module.vpc.aws_subnet_ids
image_id = data.aws_ami.ubuntu.id
count = 5
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
output "ec2_public_ips" {
value = module.ecs.public_ips
}
跑起来
cd environments/production
terraform init
terraform plan -out=tfplan
# 检查 plan 输出,确认要创建/改/删的资源符合预期
terraform apply tfplan
风险提示:第一次跑前必须用 terraform plan 看清要做的操作。apply 前再加一次 --dry-run 或者让同事帮忙 review plan 输出。
实战二:State 管理
State 是什么
State 是 Terraform 记录"上次 apply 后实际状态"的文件。每次 apply 之后,Terraform 会更新 state 与云上资源保持一致。
State 存在哪里、谁能访问、是分布式还是本地,关系到协作能力和安全。
远程后端选择
| 后端 |
适合 |
特点 |
| Local |
个人开发 |
简单,团队不能用 |
| S3 |
AWS 生态 |
标配,DynamoDB 加锁 |
| OSS |
阿里云 |
国产云首选 |
| COS |
腾讯云 |
国产云 |
| GCS |
GCP |
谷歌云 |
| Azure Storage |
Azure |
微软云 |
| Terraform Cloud |
跨云 |
HashiCorp 官方,付费 |
| Consul |
自建 |
老方案,HashiCorp 维护 |
| etcd v3 |
自建 |
复杂,不推荐 |
| HTTP |
简单 |
自建服务器存 |
S3 + DynamoDB 后端
# 创建 S3 bucket 和 DynamoDB 表
resource "aws_s3_bucket" "terraform_state" {
bucket = "myorg-terraform-state"
lifecycle {
prevent_destroy = true # 防止误删
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.terraform_state.arn
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
resource "aws_kms_key" "terraform_state" {
description = "KMS key for Terraform state"
deletion_window_in_days = 30
}
State 命令
# 列出 state 中的资源
terraform state list
# 查看某个资源
terraform state show aws_instance.web[0]
# 移动资源(在 state 中重命名)
terraform state mv aws_instance.old aws_instance.new
# 移除资源(不销毁云上资源)
terraform state rm aws_instance.web
# 导入已有资源
terraform import aws_instance.web i-1234567890abcdef0
State 锁
并发 apply 同一个 state 会冲突。S3 后端用 DynamoDB 表做锁,OSS 后端用 OSS 自带锁。开启后,同一时间只能有一个 apply 在跑。
如果 apply 中途失败(比如网络断),锁可能没释放,会阻塞后续操作:
# 查看锁
terraform force-unlock <LOCK_ID>
风险提示:force-unlock 是高风险操作,确认没有其他人正在 apply 才能执行。
实战三:模块化设计
模块基础
模块是 Terraform 的复用单元。source 可以是本地路径、Git 地址、Terraform Registry。
module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
vpc_name = "prod"
}
module "consul" {
source = "hashicorp/consul/aws"
version = "~> 0.3"
servers = 3
}
module "github_repo" {
source = "git@github.com:myorg/terraform-modules.git//vpc?ref=v1.0.0"
}
模块版本约束
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0" # >= 5.0, < 6.0
}
version 是强烈建议。ref 锁定 Git tag 能保证可复现。
模块输入输出
# modules/vpc/variables.tf
variable "vpc_cidr" {
type = string
description = "VPC CIDR"
}
variable "vpc_name" {
type = string
description = "VPC 名称"
}
# modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.this.id
description = "VPC ID"
}
output "subnet_ids" {
value = aws_subnet.public.id
description = "Public subnet IDs"
}
模块组合
module "vpc" {
source = "./modules/vpc"
...
}
module "web" {
source = "./modules/web"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.subnet_ids
...
}
多云资源在一个 module 里
# modules/web/main.tf
resource "aws_lb" "aws_web" {
name = "web-alb"
internal = false
load_balancer_type = "application"
security_groups = [var.aws_security_group_id]
subnets = var.aws_subnet_ids
}
resource "alicloud_slb" "aliyun_web" {
name = "web-slb"
address_type = "internet"
internet = true
vswitch_id = var.alicloud_vswitch_id
bandwidth = 5
}
但要谨慎:多云资源耦合在一起会让 state 变大、apply 变慢。建议多云资源分模块,按云或按业务域拆分。
实战四:多环境管理
方案 A:目录分层
environments/
├── dev/
│ ├── main.tf
│ └── terraform.tfvars
├── staging/
│ ├── main.tf
│ └── terraform.tfvars
└── production/
├── main.tf
└── terraform.tfvars
每个环境独立 state、独立 plan、独立 apply。
cd environments/production
terraform init
terraform apply
优点:环境间完全隔离,state 独立。缺点:环境多了要切目录、init 多次。
方案 B:Workspace
terraform workspace new dev
terraform workspace new staging
terraform workspace new production
terraform workspace select production
terraform apply
Workspace 把 state 按目录存到同一个后端,前缀是 env:/<workspace_name>。
优点:不用切目录。缺点:环境间差异不直观、容易误操作、不适合大差异(不同 region、不同云)。
方案 C:Terragrunt
Terragrunt 是 Terraform 的 wrapper,提供更复杂的多环境管理(DRY 配置、依赖管理、远程 state 自动化)。
# terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../modules//vpc"
}
inputs = {
vpc_cidr = "10.0.0.0/16"
vpc_name = "prod"
}
terragrunt init
terragrunt plan
terragrunt apply
优势:
- 多模块共享配置(remote state、provider)。
- 自动管理依赖(module 间依赖)。
terragrunt run-all plan 一键跑多个模块。
学习曲线略陡,但大项目必备。
实战五:流水线集成
典型流程
Git Push → CI 触发 → terraform init → terraform plan → 输出 plan 给 review → merge → terraform apply
GitHub Actions
# .github/workflows/terraform.yml
name: Terraform
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
terraform:
name: Terraform
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.6
- name: Terraform Init
run: terraform init
working-directory: environments/production
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
working-directory: environments/production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Show Plan
if: github.event_name == 'pull_request'
run: terraform show -no-color tfplan
working-directory: environments/production
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
working-directory: environments/production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
风险提示:CI 上 apply 是高风险操作,建议加手动审批(environment: production + when: manual)。
GitLab CI
# .gitlab-ci.yml
stages:
- validate
- plan
- apply
terraform:validate:
stage: validate
image: hashicorp/terraform:1.6.6
script:
- cd environments/production
- terraform init -backend=false
- terraform validate
- terraform fmt -check
terraform:plan:
stage: plan
image: hashicorp/terraform:1.6.6
script:
- cd environments/production
- terraform init
- terraform plan -out=tfplan
- terraform show -no-color tfplan > plan.txt
artifacts:
paths:
- environments/production/tfplan
expire_in: 1 day
environment:
name: production
rules:
- if: $CI_MERGE_REQUEST_ID
terraform:apply:
stage: apply
image: hashicorp/terraform:1.6.6
script:
- cd environments/production
- terraform init
- terraform apply -auto-approve tfplan
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
Atlantis
Atlantis 是 Terraform 专用 CI 工具,通过 PR 评论触发 plan/apply:
# atlantis.yaml
version: 3
projects:
- name: production
dir: environments/production
workspace: production
terraform_version: 1.6.6
apply_requirements:
- approved
- mergeable
workflow: terraform-workflow
workflows:
terraform-workflow:
plan:
steps:
- init
- plan
apply:
steps:
- apply
在 PR 上评论:
atlantis plan
atlantis apply
Atlantis 会自动评论 plan 输出,需要 maintainer 加 atlantis approve 才能 apply。
Spacelift / Env0
商业化 Terraform 平台,提供 SaaS / Private 部署。功能齐全:policy as code、drift detection、cost estimation、并行执行。
实战六:漂移检测
什么是漂移
"漂移(Drift)"是 Terraform 期望状态和云上实际状态不一致。可能因为:
- 有人在控制台手工改了资源。
- 自动化脚本(非 Terraform)改资源。
- 资源被云厂商主动调整(比如磁盘扩容)。
- 资源被外部删除。
漂移会导致 Terraform 状态不准确,下次 plan 会显示 reconcile 操作。
漂移检测方法
方法 1:定时 plan
# 加到 cron,每天跑一次 plan
0 2 * * * cd /path/to/project && terraform plan -detailed-exitcode -out=tfplan || (echo "drift detected"; terraform show -no-color tfplan | mail -s "drift" ops@example.com)
-detailed-exitcode:
- 0:无 diff
- 1:执行出错
- 2:有 diff,需要 apply
方法 2:Driftctl
Driftctl 是专门做漂移检测的开源工具,比 terraform plan 更专业,能并行检测大量资源:
# 安装
curl -L "$(curl -s https://api.github.com/repos/snyk/driftctl/releases/latest | grep -o 'https://.*driftctl_linux_amd64' | head -1)" -o driftctl
chmod +x driftctl
sudo mv driftctl /usr/local/bin/
# 扫描
driftctl scan --to aws+tf
# 输出 JSON
driftctl scan --to aws+tf --output json://drift.json
# 上传到 S3 / 发 Slack 通知
Driftctl 还能识别"已删除"和"未管理"的资源。
方法 3:Spacelift / Env0
商业化平台自带漂移检测。
方法 4:refresh-only plan
Terraform 1.5+ 提供 refresh-only plan:
terraform plan -refresh-only
只刷新 state,不做 create/update/delete。用来识别"什么资源实际状态变了"。
处理漂移
发现漂移后,处理方式:
- 真漂移(应该恢复到 Terraform 期望):
terraform apply。
- 云上调整(云厂商主动扩容):更新 Terraform 代码反映新状态。
- 真删除(云上资源应该被删):从 state 中移除。
- 非预期修改:人工 review 决定。
实战七:常见陷阱
陷阱 1:把密码写进代码
resource "aws_db_instance" "this" {
password = "MyPassword123" # 错!会被 Git 记录
}
正确做法:
resource "aws_db_instance" "this" {
password = var.db_password # 从变量读
}
# variables.tf
variable "db_password" {
type = string
sensitive = true
}
# 用环境变量
export TF_VAR_db_password="xxx"
terraform apply
或者用 data "aws_ssm_parameter" 读 AWS Parameter Store / Secrets Manager / 阿里云 KMS。
陷阱 2:循环依赖
resource "aws_security_group" "web" {
ingress {
cidr_blocks = [aws_instance.bastion.private_ip] # 反向依赖
}
}
resource "aws_instance" "bastion" {
vpc_security_group_ids = [aws_security_group.web.id] # 正向依赖
}
正确做法:解耦,安全组规则用 CIDR 块而不是 IP。
陷阱 3:State 锁死
现象:Error: Error acquiring the state lock。
原因:上一次 apply 没正常结束,锁没释放。
处理:
# 确认没有其他人正在 apply
terraform force-unlock <LOCK_ID>
风险提示:force-unlock 是高危操作,必须先确认。
陷阱 4:误删资源
terraform destroy
会删除所有管理的资源。生产环境必须用 -target 限定范围或 -var 切换环境。
风险提示:永远不要在生产跑 terraform destroy。如果要下线环境,先在测试环境试一遍,限制 IAM 权限加 MFA。
陷阱 5:漂移导致 apply 失败
# 现象
Error: Provider produced inconsistent final plan
资源在 apply 中被外部改了。解决:
# 重新 plan
terraform plan
# 看 diff,refresh state,再 apply
terraform apply
陷阱 6:Provider 版本不兼容
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
升级 provider 后资源字段可能变。先在测试环境升级,看 plan 输出。
陷阱 7:模块地狱
module 套 module 套 module,每个 module 有大量 input,最后没人能改。
经验法则:
- 最多 3 层嵌套。
- module inputs ≤ 10 个。
- module outputs 越少越好。
- 业务模块和基础设施模块分开。
陷阱 8:多云网络误配置
# 错:把两个云的资源放在同一 module 里,没考虑网络隔离
resource "aws_instance" "this" {
vpc_security_group_ids = [aws_security_group.merged.id]
}
resource "alicloud_instance" "this" {
security_groups = [alicloud_security_group.merged.id]
}
跨云网络是独立的,强行合并配置会增加耦合。
陷阱 9:IAM 权限过宽
# 错:给 Terraform 用 root 账号
provider "aws" {
access_key = "AKIA..." # root user
secret_key = "..."
}
正确做法:给 Terraform 创建专用 IAM 用户 / 角色,权限最小化。
陷阱 10:日志泄露敏感信息
# 错:CI 输出日志
terraform apply
# 看到 plan 输出里包含明文密码
正确做法:
variable "db_password" {
type = string
sensitive = true # 标记为敏感
}
sensitive = true 后,Terraform 默认不在 plan/apply 输出中显示这个变量值。
# 把已有 AWS EC2 导入
terraform import aws_instance.web i-1234567890abcdef0
# 阿里云 ECS
terraform import alicloud_instance.web i-bp1234567890abcdef0
导入后,Terraform 接管该资源。下次 plan 时会显示资源的当前状态(不是 Terraform 代码写的状态)。
风险提示:导入只能导入已有资源,要让 Terraform 真的不重新创建,需要把资源的当前属性写到 Terraform 代码里。
Terraformer 是 Google 出的工具,能自动从云上扫描资源并生成 Terraform 代码:
# 安装
brew install terraformer
# 扫描 AWS
terraformer import aws --resources=vpc,subnet,ec2 --regions=ap-southeast-1
# 扫描阿里云
terraformer import alicloud --resources=vpc,vswitch,ecs --regions=cn-hangzhou
生成的代码放在 generated/ 目录。
方法 3:手动写 + moved 块
Terraform 1.1+ 提供 moved 块,资源重命名时不需要 destroy/import:
# 老资源地址
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}
实战九:跨云灾备
案例:阿里云主 + AWS 备
# 主集群在阿里云
module "primary_aliyun" {
source = "./modules/web"
cloud = "alicloud"
...
}
# 备集群在 AWS
module "dr_aws" {
source = "./modules/web"
cloud = "aws"
...
}
# 跨云数据同步
resource "alicloud_oss_bucket" "dr_source" {
bucket = "myorg-dr-source"
}
resource "aws_s3_bucket" "dr_target" {
bucket = "myorg-dr-target"
}
resource "aws_s3_bucket_replication_configuration" "dr" {
bucket = aws_s3_bucket.dr_target.id
role = aws_iam_role.replication.arn
rule {
id = "dr-rule"
status = "Enabled"
destination {
bucket = aws_s3_bucket.dr_target.arn
storage_class = "STANDARD_IA"
}
}
}
跨云灾备的关键:
- 数据同步(OSS → S3、PolarDB → RDS、Redis AOF)。
- 健康检查 + DNS 切换(Route 53 / 阿里云 DNS)。
- 演练(季度 / 半年一次)。
实战十:Policy as Code(Sentinel / OPA)
商业版 Terraform Cloud / Enterprise 提供 Sentinel 策略引擎。开源方案是 OPA + Conftest。
# policy/s3_public.rego
package terraform.s3
deny[msg] {
resource := input.resource.aws_s3_bucket[name]
resource.acl == "public-read"
msg := sprintf("S3 bucket '%s' 不能是 public-read", [name])
}
deny[msg] {
resource := input.resource.aws_s3_bucket[name]
resource.acl == "public-read-write"
msg := sprintf("S3 bucket '%s' 不能是 public-read-write", [name])
}
# 用 conftest 验证
conftest test plan.json --policy policy/
CI 流水线里加这一步骤,违规就阻断。
实战十一:成本管理
成本估算
Terraform Cloud / Infracost 提供 cost estimation:
# 安装 Infracost
curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/packages/cli/docker/install.sh | sh
# 生成 cost report
infracost breakdown --path=environments/production --format=json --out-file=infracost.json
infracost output --path=infracost.json --format=table
CI 集成 Infracost,每次 PR 显示 cost 变化:
# GitHub Actions
- name: Infracost
uses: infracost/actions/setup@v2
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Generate cost estimate
run: |
infracost breakdown --path=environments/production --format=json --out-file=/tmp/infracost.json
infracost comment gitlab --path=/tmp/infracost.json --repo=$GITHUB_REPOSITORY --pull-request=$PR_NUMBER --behavior=update --token=$GITHUB_TOKEN
标签规范
locals {
common_tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = "myapp"
CostCenter = "engineering"
Owner = "ops-team"
}
}
# AWS
provider "aws" {
default_tags {
tags = local.common_tags
}
}
# 阿里云
resource "alicloud_vpc" "this" {
vpc_name = "prod"
tags = local.common_tags
}
标签让账单分析、权限管理、自动化都更容易。
实战十二:实战案例:完整的多云项目
背景
某 SaaS 公司:AWS 主力(数据库 + 核心服务)、阿里云辅助(中国用户)、Cloudflare 全球 CDN。
项目结构
infra/
├── modules/
│ ├── vpc/ # 通用 VPC
│ ├── ecs/ # 通用 VM
│ ├── rds/ # 通用数据库
│ ├── eks/ # AWS K8s
│ ├── ack/ # 阿里云 K8s
│ ├── s3/ # AWS S3 存储
│ ├── oss/ # 阿里云 OSS
│ └── cloudfront/ # CDN
├── environments/
│ ├── aws-production/
│ ├── aws-staging/
│ ├── alicloud-production/
│ ├── alicloud-staging/
│ └── shared-services/ # 跨环境共享
├── policies/ # OPA 策略
├── scripts/ # 辅助脚本
└── .github/workflows/
跨云共享:CDN
# modules/cloudfront/main.tf
resource "aws_cloudfront_distribution" "this" {
enabled = true
comment = "Global CDN"
origin {
domain_name = var.alicloud_oss_bucket_host # 阿里云 OSS 作为源
origin_id = "alibaba-oss"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
default_cache_behavior {
target_origin_id = "alibaba-oss"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = var.certificate_arn
ssl_support_method = "sni-only"
}
}
实战十三:常见监控和告警
CloudWatch 告警
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
alarm_name = "high-cpu-${var.environment}"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "80"
alarm_description = "EC2 CPU > 80% for 4 minutes"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.web.name
}
}
阿里云云监控
resource "alicloud_cloud_monitor_service_alarm" "cpu_alarm" {
service = "ecs"
resources = join(",", alicloud_instance.web.id)
metric_name = "CPUUtilization"
threshold = 80
statistics = "Average"
comparison_operator = ">"
evaluation_count = 3
contact_groups = [alicloud_cloud_monitor_contact_group.default.id]
period = 300
}
实战十四:故障案例复盘
案例 1:state 锁死导致 apply 卡 6 小时
现象:CI pipeline terraform apply 卡在 acquiring state lock 6 小时。
根因:上一次 CI job OOM 退出,state 锁未释放。
修复:
# 1. 确认没人正在 apply
# 2. 查看 DynamoDB 表中 LockID
aws dynamodb scan --table-name terraform-lock
# 3. 强制解锁
terraform force-unlock <LOCK_ID>
复盘:CI 加超时机制,apply 超时后清理 lock。Terraform 1.6+ 锁有自动过期。
案例 2:误删生产 RDS
现象:测试环境跑 terraform destroy 时忘记切环境变量,误删了生产 RDS 实例。
根因:terraform destroy 不带 -target 会销毁所有 state 里的资源,工程师切错 profile。
修复:
- 强制使用
prevent_destroy 标记关键资源:
resource "aws_db_instance" "production" {
...
lifecycle {
prevent_destroy = true
}
}
- IAM 权限隔离:测试 IAM 不能动生产资源。
terraform destroy 加二次确认。
- 数据库开启自动备份 + Point-in-Time Recovery。
案例 3:Provider 升级导致资源重建
现象:升级 hashicorp/aws 5.0 → 5.50,plan 显示要重建 200 个 EC2 实例。
根因:新版本对某些字段默认值做了调整,触发资源属性变更。
修复:
- 升级前在测试环境 plan,diff 确认。
- 强制锁定资源不重建:
resource "aws_instance" "this" {
lifecycle {
prevent_destroy = true # 防止误删
# 还能用 ignore_changes 忽略某些字段
ignore_changes = [ami, user_data]
}
}
- Provider 升级拆小步,不要一次跳多个 minor。
案例 4:跨 region 资源错配
现象:plan 在 ap-southeast-1,但实际创建在 us-east-1。
根因:CI 环境变量没传,Terraform 用了默认 region。
修复:CI 强制注入 AWS_REGION、ALICLOUD_REGION 等变量,并在 plan 输出中校验。
风险提示清单
- 生产
terraform destroy:必须加审批,必须用 -target 限制范围。
force-unlock:确认无人在 apply。
- provider 升级:测试环境先 plan,prod 环境不变更字段。
- 资源 attribute 变化:用
lifecycle { ignore_changes = [...] } 屏蔽。
- 敏感信息:用 Vault / KMS / Parameter Store / 环境变量,不要写代码。
- IAM 权限:Terraform 用专用账号,最小权限。
- 状态文件加密:S3 / OSS 开启 SSE-KMS。
- 跨云网络:VPC Peering / VPN / SD-WAN 配置要单独管理。
- CI 流水线泄露 plan:plan 输出可能含敏感信息,加
sensitive = true 标记。
- destroy protection:关键资源加
lifecycle { prevent_destroy = true }。
- State 备份:S3 开启 versioning,定期备份到异地。
- 多云账户:用 AWS Organizations / 阿里云资源目录 / GCP Folders 统一管理。
最佳实践 Checklist
- [ ] 项目分层:
modules/、environments/、providers/ 分离。
- [ ] 状态后端用远程(不用 local),加锁、加密。
- [ ] 变量用
variable 块定义,敏感变量标记 sensitive。
- [ ] 资源命名规范:
{project}-{env}-{role}-{instance}。
- [ ] 统一标签:
Environment、ManagedBy、Project、CostCenter、Owner。
- [ ] 模块化:业务模块和基础设施模块分开。
- [ ] 版本约束:
required_version、required_providers、module.version。
- [ ]
terraform fmt 统一格式。
- [ ]
terraform validate 静态检查。
- [ ]
tflint 静态检查(社区 linter)。
- [ ]
checkov / tfsec 安全扫描。
- [ ] 关键资源加
prevent_destroy。
- [ ] 漂移检测:定时 plan / driftctl。
- [ ] CI 集成:plan → review → apply 流程。
- [ ] 敏感信息用 Vault / KMS,不进代码。
- [ ] Policy as Code:OPA / Conftest 阻断违规。
- [ ] 成本监控:Infracost / 账单分析。
- [ ] 文档:每个 module 有 README,说明 inputs / outputs / 用途。
常见问题 FAQ
Q1:Terraform 1.5+ 跟以前有什么不一样?
A:1.1+ 引入 moved 块做资源重命名,1.2+ 引入 removed 块,1.5+ 引入 import block、refresh-only plan、check block,1.6+ 加 state lock 自动过期。生产建议 1.6+。
Q2:Terraform 1.x 之后还有 Terraform 0.x 吗?
A:没有了。Terraform 0.13 之后改为 1.0。当前最新是 1.7+(以实际版本为准)。
Q3:OpenTofu 是什么?
A:HashiCorp 把 Terraform 改 BSL 协议后,社区 fork 出 OpenTofu(基于 Terraform 1.5)。OpenTofu 是 MPL 协议开源,兼容 Terraform 1.5 API。
Q4:Terraform 和 Pulumi 选哪个?
A:Terraform 是声明式 HCL,生态最大。Pulumi 用通用语言(Python、Go、TypeScript)写 IaC,对程序员更友好,但生态略小。多数团队用 Terraform。
Q5:Terraform 怎么加密敏感变量?
A:三种方式:(1) sensitive = true 标记,CI 输出不显示;(2) 后端用 SSE-KMS;(3) 敏感值从 Vault / KMS / Parameter Store 读,不写在 tfvars 里。
Q6:Terraform 的 state 文件能共享吗?
A:可以,但要避免多人同时改。用远程后端 + 锁,每个环境一个 state,按目录或 workspace 隔离。
Q7:怎么从云控制台手工创建的资源迁移到 Terraform?
A:用 terraform import(单资源)或 terraformer(批量)。导入后把资源属性写进 Terraform 代码。
Q8:Terraform 怎么管理已经存在但不在 state 里的资源?
A:terraform import 把资源加入 state,然后写 Terraform 代码描述它。
Q9:Terraform 跑在 K8s 里?
A:可以,常见的做法是 terraform-operator / Spacelift Agent / Atlantis。生产里用 Kubernetes 跑 Atlantis Pod 比较常见。
Q10:怎么减少 apply 时间?
A:(1) 用 -target 限定范围;(2) 用 -parallelism 提高并发;(3) 把大 state 拆成小 state;(4) 用 -refresh=false 跳过 refresh(不推荐,可能漏掉漂移)。
Q11:Terraform Cloud / Enterprise 值得用吗?
A:团队规模小(< 5 人)、项目少(< 10 个),用 GitHub Actions + S3 后端足够。规模大了上 Terraform Cloud / Spacelift / Env0,节省状态管理、policy、审计成本。
Q12:如何处理跨云资源依赖?
A:避免强耦合。把多云资源分到不同 module / state,跨云信息通过 output + 远程 state 数据源传递(terraform_remote_state)。
Q13:Terraform drift 怎么处理?
A:定期 terraform plan -detailed-exitcode 或用 driftctl。漂移原因通常是人手工改了云上资源。处理:(1) 重新 apply 让 state 同步;(2) 把真实状态写进 Terraform 代码;(3) 严禁手工改云上资源(这是 IaC 的核心约定)。
Q14:怎么测试 Terraform 代码?
A:商业版有 Sentinel / OPA。开源自建:单元测试用 terraform plan 验证资源数 / 属性,集成测试用 terratest(Go)。
总结
Terraform 解决的是"用统一语言描述多云资源"的问题。但实际落地需要:
- 理解每个云的资源模型和差异。
- 严格管理 state(远程、加密、加锁)。
- 模块化设计(避免一个 state 太大)。
- 流水线集成(plan → review → apply)。
- 漂移检测(保证期望和实际一致)。
- Policy as Code(安全合规)。
- 成本监控(多云账单分析)。
核心心法:
- 所有资源用代码描述,禁手工改云上。
- 状态用远程后端,加锁加密。
- 流水线审批后才 apply。
- 模块化拆分,避免大 state。
- 漂移检测,定时 plan。
- 敏感信息走 Vault / KMS。
- 关键资源
prevent_destroy。
把这套流程走完,多云管理能上一个台阶。
如果你还想与更多同行交流多云运维的实战经验,不妨到云栈社区看看,那里常有关于 IaC 与 CI/CD 的深度讨论。
附录:常用命令速查
# 初始化
terraform init
terraform init -upgrade # 升级 provider
# 格式化
terraform fmt
terraform fmt -check # CI 检查用
# 验证
terraform validate
# Plan / Apply
terraform plan
terraform plan -out=tfplan
terraform plan -detailed-exitcode # CI 用
terraform plan -refresh-only # 1.5+ 刷新 state 不 apply
terraform apply
terraform apply tfplan
terraform apply -auto-approve
terraform apply -target=module.vpc # 限定范围
terraform destroy
terraform destroy -target=...
# State
terraform state list
terraform state show <addr>
terraform state mv <from> <to>
terraform state rm <addr>
terraform state pull # 下载 state
terraform state push # 上传 state(高危)
# Import
terraform import <addr> <id>
terraform import aws_instance.web i-1234567890abcdef0
# Workspace
terraform workspace new dev
terraform workspace list
terraform workspace select production
terraform workspace delete dev
# 调试
TF_LOG=DEBUG terraform apply # 详细日志
TF_LOG_PATH=/tmp/tf.log terraform apply
附录:HCL 语法速查
# 变量
variable "name" {
type = string
default = "default"
description = "描述"
sensitive = false
validation {
condition = length(var.name) > 0
error_message = "name 不能为空。"
}
}
# 输出
output "name" {
value = aws_instance.web.id
description = "Web 实例 ID"
sensitive = false
}
# 资源
resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t3.micro"
tags = {
Name = "web"
}
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [ami]
}
depends_on = [aws_vpc.main]
}
# 数据源
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# 本地值
locals {
common_tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
# Module
module "vpc" {
source = "./modules/vpc"
version = "1.0.0"
vpc_cidr = "10.0.0.0/16"
}
# Provider
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-southeast-1"
}
# 远程 state 数据源
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "myorg-terraform-state"
key = "vpc/terraform.tfstate"
region = "ap-southeast-1"
}
}
附录:资源属性与字段示例
AWS EC2
resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t3.medium"
subnet_id = "subnet-12345"
vpc_security_group_ids = ["sg-12345"]
key_name = "my-key"
user_data = filebase64("userdata.sh")
monitoring = true
ebs_optimized = true
root_block_device {
volume_size = 20
volume_type = "gp3"
encrypted = true
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
}
tags = {
Name = "web"
}
}
阿里云 ECS
resource "alicloud_instance" "web" {
image_id = "ubuntu_22_04_x64"
instance_type = "ecs.g6.large"
security_groups = [alicloud_security_group.web.id]
vswitch_id = alicloud_vswitch.public.id
internet_max_bandwidth_out = 5
internet_charge_type = "PayByTraffic"
instance_name = "web"
key_name = "my-key"
password = var.instance_password
system_disk_category = "cloud_essd"
system_disk_size = 40
tags = {
Name = "web"
}
}
腾讯云 CVM
resource "tencentcloud_instance" "web" {
instance_name = "web"
availability_zone = "ap-shanghai-2"
image_id = "img-12345"
instance_type = "S5.MEDIUM4"
vpc_id = tencentcloud_vpc.main.id
subnet_id = tencentcloud_subnet.public.id
allocate_public_ip = true
internet_max_bandwidth_out = 5
security_groups = [tencentcloud_security_group.web.id]
tags = {
Name = "web"
}
}
附录:常见资源 type 速查
| 云 |
资源 type |
| AWS |
aws_vpc, aws_subnet, aws_instance, aws_db_instance, aws_s3_bucket, aws_lb, aws_eks_cluster |
| 阿里云 |
alicloud_vpc, alicloud_vswitch, alicloud_instance, alicloud_db_instance, alicloud_oss_bucket, alicloud_slb, alicloud_cs_kubernetes |
| 腾讯云 |
tencentcloud_vpc, tencentcloud_subnet, tencentcloud_instance, tencentcloud_cdb_instance, tencentcloud_cos_bucket, tencentcloud_clb |
| Azure |
azurerm_virtual_network, azurerm_subnet, azurerm_virtual_machine, azurerm_postgresql_server, azurerm_storage_account |
| GCP |
google_compute_network, google_compute_subnetwork, google_compute_instance, google_sql_database_instance, google_storage_bucket |
不同 provider 版本字段可能略有差异,以实际版本为准。
附录:参考资源