怎么使用Terraform统一管理多云资源

描述

问题背景

很多公司过去几年里一步步从单云走向多云,原因各有不同:

业务出海,需要在多个地域部署。

监管要求数据在境内保存,但海外用户多,需要海外资源。

单一云厂商出过故障,迫使做灾备。

不同业务用不同云服务(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 show 
terraform 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 版本字段可能略有差异,以实际版本为准。

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分