DevOps ·

Terraform 入门:基础语法、多云 IaC 与日常排障

Terraform 是 IaC(基础设施即代码)的事实标准。本文从安装、HCL 语法、Provider、Module、Workflow,到典型 VPC/ECS/S3 场景和最常踩的 10+ 坑整理。

Terraform 入门:基础语法、多云 IaC 与日常排障

IaC(Infrastructure as Code,基础设施即代码) 是现代运维绕不开的一环:把 VPC、云主机、数据库、负载均衡、DNS 等资源,用代码(HCL 语言)描述,放进 Git 版本管理,再由 Terraform 按依赖顺序创建/更新/销毁。手动点控制台的时代,正在被 IaC 系统性替代。

一、核心概念速览

概念说明
HCL (HashiCorp Configuration Language)Terraform 的声明式配置语言
Provider插件,对接不同云厂商(AWS/Azure/GCP/阿里云/AWS 中国/腾讯云/vSphere…)
Resource一个具体资源:aws_instancealicloud_vpcaws_s3_bucket
Data Source只读查询已有资源的信息:已有的 VPC、镜像 AMI 等
Module把一组资源打包复用,类比函数
Stateterraform.tfstate 文件,记录 Terraform 认知的”当前真实世界”,非常重要
BackendState 存放位置:本地、S3、OSS、Terraform Cloud、Consul 等
Workflowinit → plan → apply → (destroy) 固定四步
DRY / Don’t Repeat YourselfModule 就是解决重复写代码的

IaC 相对控制台的好处

  1. 可版本化:Git 提交历史 = 基础设施变更历史
  2. 可复现:一模一样的环境能在另一个 Region / 账号复制出来
  3. 计划先行plan 告诉你会改什么,不会误删
  4. 团队协作:PR 评审 + CI 门禁,比”某人登录控制台点了下”安全得多

二、安装

# macOS(推荐,支持多版本切换)
brew install tfenv
tfenv install 1.9.8
tfenv use 1.9.8

# 或直接官方
brew install hashicorp/tap/terraform

# Debian / Ubuntu
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg >/dev/null
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 update && sudo apt install -y terraform

# 验证
terraform -version
# 开启自动补全
terraform -install-autocomplete

版本锁死原则:项目里一定要用 required_version + .terraform-version 锁死 Terraform 版本,防止不同同事版本不一致报错。

三、第一个项目:阿里云 ECS + VPC

目录结构:

tf-demo/
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars

3.1 main.tf

# ① 版本 & Provider 配置
terraform {
  required_version = ">= 1.7.0, < 2.0.0"

  required_providers {
    alicloud = {
      source  = "aliyun/alicloud"
      version = "~> 1.225"
    }
  }

  # 推荐:State 放 OSS,多人协作不冲突
  # backend "oss" {
  #   bucket = "your-terraform-state"
  #   prefix = "env/dev/"
  #   region = "cn-hangzhou"
  # }
}

# ② Provider 初始化(密钥从环境变量读,别明文写!)
#  export ALICLOUD_ACCESS_KEY="xxxxxx"
#  export ALICLOUD_SECRET_KEY="xxxxxx"
#  export ALICLOUD_REGION="cn-hangzhou"
provider "alicloud" {
  region = var.region
}

# ③ 查可用镜像(Ubuntu 22.04 64位)
data "alicloud_images" "ubuntu" {
  owners      = "system"
  name_regex  = "^ubuntu_22_04_x64.*"
  most_recent = true
}

# ④ VPC
resource "alicloud_vpc" "main" {
  vpc_name   = "tf-demo-vpc"
  cidr_block = "10.0.0.0/16"
}

# ⑤ 交换机
resource "alicloud_vswitch" "main" {
  vswitch_name = "tf-demo-vsw"
  vpc_id       = alicloud_vpc.main.id
  cidr_block   = "10.0.1.0/24"
  zone_id      = data.alicloud_zones.default.zones.0.id
}

# ⑥ 查询可用区
data "alicloud_zones" "default" {
  available_instance_charge_type = "PostPaid"
  available_disk_category        = "cloud_essd"
}

# ⑦ 安全组:放通 22 / 80
resource "alicloud_security_group" "web" {
  name   = "tf-demo-sg"
  vpc_id = alicloud_vpc.main.id

  ingress {
    protocol  = "tcp"
    port_range = "22/22"
    cidr_ip  = var.my_ip
  }
  ingress {
    protocol  = "tcp"
    port_range = "80/80"
    cidr_ip  = "0.0.0.0/0"
  }
  egress {
    protocol   = "-1"
    cidr_ip    = "0.0.0.0/0"
    policy     = "accept"
  }
}

# ⑧ ECS
resource "alicloud_instance" "web" {
  instance_name   = "tf-demo-web"
  image_id        = data.alicloud_images.ubuntu.images[0].id
  instance_type   = var.instance_type
  security_groups = [alicloud_security_group.web.id]
  vswitch_id      = alicloud_vswitch.main.id
  internet_max_bandwidth_out = 20   # 公网带宽 Mbps
  password        = var.ecs_password
}

3.2 variables.tf

variable "region" {
  description = "阿里云区域"
  type        = string
  default     = "cn-hangzhou"
}

variable "instance_type" {
  description = "ECS 规格"
  type        = string
  default     = "ecs.g6.large"
}

variable "my_ip" {
  description = "允许 SSH 登录的 IP(/32),生产请设具体值"
  type        = string
  default     = "0.0.0.0/0"   # 示例默认全开,生产别这么干
}

variable "ecs_password" {
  description = "ECS root 密码,敏感!请用 TF_VAR_ecs_password 环境变量或 tfvars 传"
  type        = string
  sensitive   = true
}

3.3 outputs.tf

output "vpc_id" {
  value = alicloud_vpc.main.id
}

output "ecs_public_ip" {
  value = alicloud_instance.web.public_ip
}

output "ecs_id" {
  value = alicloud_instance.web.id
}

3.4 terraform.tfvars(可选,别提交 Git)

# 本地加这个文件覆盖变量,记得加进 .gitignore
ecs_password = "ChangeMeStrongP@ss123!"
my_ip        = "1.2.3.4/32"

配套 .gitignore

.terraform/
*.tfstate*
crash.log
terraform.tfvars
.terraform.lock.hcl.bak

3.5 跑起来

# 1) 初始化:下载 provider、初始化 backend
export ALICLOUD_ACCESS_KEY="..."
export ALICLOUD_SECRET_KEY="..."
terraform init

# 2) 看计划:这一步只看、不改真实世界
terraform plan -out=plan.tfplan

# 3) 执行计划
terraform apply plan.tfplan

# 或不用 plan 文件,直接 plan + apply 二合一
terraform apply

# 4) 查看输出
terraform output

# 5) 不用了 → 销毁(一定要记得!不然扣钱)
terraform destroy

四、HCL 语法要点

4.1 基本类型

variable "tags" {
  type = map(string)
  default = {
    Project = "demo"
    Owner   = "ops"
  }
}

variable "cidrs" {
  type = list(string)
  default = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "enable_public" {
  type    = bool
  default = false
}

4.2 条件 / 动态块 / 循环

# 条件表达式
count = var.enable_public ? 1 : 0

# for_each 循环(推荐,优于 count)
resource "alicloud_vswitch" "many" {
  for_each = toset(var.cidrs)

  vswitch_name = "vsw-${each.key}"
  cidr_block   = each.value
  vpc_id       = alicloud_vpc.main.id
  zone_id      = data.alicloud_zones.default.zones[0].id
}

# 动态块:条件性生成 ingress
resource "alicloud_security_group" "web" {
  name   = "web"
  vpc_id = alicloud_vpc.main.id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      protocol   = ingress.value.protocol
      port_range = ingress.value.port_range
      cidr_ip    = ingress.value.cidr
    }
  }
}

4.3 常用函数

# 字符串
lower(var.name)
format("%s-%03d", var.prefix, 1)        # web-001
replace(var.url, "http://", "https://")

# 集合 / Map
keys(var.tags)
values(var.tags)
lookup(var.map, "key", "default")
merge({a=1}, {b=2})                      # 合并 map
flatten([[1,2],[3]])                     # [1,2,3]

# 类型转换
toset(["a","a","b"])                     # 去重
cidrsubnet("10.0.0.0/16", 8, 1)         # 10.0.1.0/24

4.4 Module

modules/
  vpc/
    main.tf
    variables.tf
    outputs.tf

根目录引用:

module "vpc" {
  source = "./modules/vpc"

  vpc_cidr     = "10.0.0.0/16"
  vsw_cidrs    = ["10.0.1.0/24", "10.0.2.0/24"]
  region       = var.region
  tags         = var.tags
}

# 使用模块输出
output "vpc_id" { value = module.vpc.vpc_id }

公共模块(Terraform Registry / 阿里云云市场 / Git 仓库):

module "vpc" {
  source  = "terraform-alicloud-modules/vpc/alicloud"
  version = "~> 2.0"
  # ...
}

五、State & Backend 最佳实践

State 是 Terraform 的命根子。 坏了丢了 = 你得手动 terraform import 到吐。

方式适用备注
本地 terraform.tfstate学习 / 单人别多人共享
S3 / OSS + DynamoDB / Tablestore 锁生产多人协作强烈推荐,防止同时 apply 冲突
Terraform Cloud / Enterprise团队级SaaS,含 workspace、审批、私有模块

OSS + 表格存储锁示例:

terraform {
  backend "oss" {
    bucket        = "your-tf-state-bucket"
    prefix        = "network/dev"
    region        = "cn-hangzhou"
    key           = "terraform.tfstate"
    # 加锁,防止并发修改
    tablestore_endpoint = "https://tf-state-lock.cn-hangzhou.ots.aliyuncs.com"
    tablestore_table    = "tf-state-locks"
  }
}

六、日常排障手册

6.1 调试手段

需求方法
查看详细日志TF_LOG=TRACE terraform apply 2>&1 | tee tf.log(TRACE/DEBUG/INFO/WARN/ERROR)
plan 到底会改什么terraform plan -detailed-exitcode-out=plan.tfplan 存下来再 terraform show plan.tfplan
看真实资源与 state 的差异terraform plan;有差异就是 drift
想强行重建某个资源terraform apply -replace=module.vpc.alicloud_instance.web[0](旧命令:taint 已弃用)
只 apply 某个资源terraform apply -target=alicloud_instance.web(慎用,会破坏依赖一致性)
某资源已手动建好,想纳入管理terraform import RESOURCE_TYPE.NAME 实例ID,再 terraform show 把属性抄回代码
看输出 JSON 给 CI/脚本处理terraform output -jsonterraform show -json

6.2 常见报错对照表

报错关键字根因 / 排查方式
Failed to query available provider packages网络不通(国内阿里云镜像源需要加 TF_CLI_CONFIG_FILE)。设 TF_REGISTRY_DISCOVERY_RETRY=3 或用镜像 network-mirror
Error: Error acquiring the state lock别人正在 apply / 上次中途挂了锁没释放。terraform force-unlock <LOCK_ID> 解锁前先确认真的没人在用
No configuration files当前目录没有 .tf 文件,或者你在错的目录跑
Provider produced inconsistent result after applyProvider Bug / 云侧异步操作延迟。一般 terraform apply 再跑一次就好,否则锁定 Provider 版本 / 加 time_sleep 等待
Cycle: xxx -> yyy -> xxx资源循环依赖。打断循环:用 depends_on 明确顺序,或拆成 data / resource 分开
Invalid index / out of rangecount / for_each 引用了空列表。加 try()try(aws_subnet.private[0].id, "N/A")
Error: Invalid legacy provider address升级 Terraform 0.12/13 → 1.x 没清干净,跑 terraform state replace-provider ...terraform providers schema -json
改了代码,apply 说 No changes. Why?代码没保存 / 跑的不是这个目录 / State 已经 drift 了。跑 terraform plan -refresh=false 再对比
timeout while waiting for state云侧 API 慢。在 resource 上加 timeouts { create = "30m" delete = "1h" }
Resource already exists手动建过同名资源。要么删,要么 import 进来
变量找不到但明明设了优先级:-var > -var-file > *.auto.tfvars > terraform.tfvarsTF_VAR_xxx 环境变量和 -var 同优先级。

6.3 排障通用流程

  1. 先 plan:看 Terraform 认为”世界应该是怎样”的
  2. 开 DEBUG 日志TF_LOG=DEBUG 能看到每次对云 API 的调用和返回
  3. 对照云控制台 / CLI:资源真实状态到底是什么?会不会是手动改了?
  4. terraform state list / state show:确认 state 里有无该资源
  5. Drift 修复:要么把代码改成跟控制台一致,要么 replace 重建
  6. 锁定版本:版本漂移是很多诡异 Bug 的源头,required_providers.version 锁死,.terraform.lock.hcl 提交 Git

6.4 常用安全技巧

  • 最小化权限:给 Terraform 的 AK 只给需要的 RAM 策略
  • 敏感变量:加 sensitive = true;输出时会自动打码
  • Secret 放密钥管理:用 data "alicloud_kms_secret" / AWS Secrets Manager 取,别写进 tfvars
  • CI 门禁terraform fmt -check -recursive + terraform validate + terraform plan 作为必跑步骤
  • 团队 apply:用 Atlantis / Terrakube / Terraform Cloud,所有变更走 PR + 自动化

七、进阶方向

  1. Workspace 隔离环境terraform workspace new dev/staging/prod
  2. Terragrunt:给 Terraform 包一层,解决 “多账号多环境 DRY 不够” 的问题
  3. CDK for Terraform (CDKTF):用 TypeScript/Python 写 HCL,对程序员友好
  4. OPA / Checkov / tfsec:IaC 静态扫描,检测安全漏洞和不合规配置
  5. Infracostplan 之后顺便告诉你”这一改要多花多少钱”
  6. Policy as Code (Sentinel):在 apply 前跑自定义规则

参考资料

问问 AI