问题背景
很多公司过去几年里一步步从单云走向多云,原因各有不同:
业务出海,需要在多个地域部署。
监管要求数据在境内保存,但海外用户多,需要海外资源。
单一云厂商出过故障,迫使做灾备。
不同业务用不同云服务(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 是什么
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 代码单元。
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 项目
项目结构
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 = "arnkms111122223333: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
风险提示: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:/
优点:不用切目录。缺点:环境间差异不直观、容易误操作、不适合大差异(不同 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 stage: validate image: hashicorp/terraform:1.6.6 script: - cd environments/production - terraform init -backend=false - terraform validate - terraform fmt -check terraform 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 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
风险提示: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 输出中显示这个变量值。
实战八:迁移现有资源到 Terraform
方法 1:terraform import
# 把已有 AWS EC2 导入 terraform import aws_instance.web i-1234567890abcdef0 # 阿里云 ECS terraform import alicloud_instance.web i-bp1234567890abcdef0
导入后,Terraform 接管该资源。下次 plan 时会显示资源的当前状态(不是 Terraform 代码写的状态)。
风险提示:导入只能导入已有资源,要让 Terraform 真的不重新创建,需要把资源的当前属性写到 Terraform 代码里。
方法 2:Terraformer
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
复盘: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。
把这套流程走完,多云管理能上一个台阶。
附录:常用命令速查
# 初始化 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 showterraform state mv terraform state rm terraform state pull # 下载 state terraform state push # 上传 state(高危) # Import terraform import 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 版本字段可能略有差异,以实际版本为准。
全部0条评论
快来发表一下你的评论吧 !