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_instance、alicloud_vpc、aws_s3_bucket |
| Data Source | 只读查询已有资源的信息:已有的 VPC、镜像 AMI 等 |
| Module | 把一组资源打包复用,类比函数 |
| State | terraform.tfstate 文件,记录 Terraform 认知的”当前真实世界”,非常重要 |
| Backend | State 存放位置:本地、S3、OSS、Terraform Cloud、Consul 等 |
| Workflow | init → plan → apply → (destroy) 固定四步 |
| DRY / Don’t Repeat Yourself | Module 就是解决重复写代码的 |
IaC 相对控制台的好处
- 可版本化:Git 提交历史 = 基础设施变更历史
- 可复现:一模一样的环境能在另一个 Region / 账号复制出来
- 计划先行:
plan告诉你会改什么,不会误删 - 团队协作: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 -json;terraform 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 apply | Provider Bug / 云侧异步操作延迟。一般 terraform apply 再跑一次就好,否则锁定 Provider 版本 / 加 time_sleep 等待 |
Cycle: xxx -> yyy -> xxx | 资源循环依赖。打断循环:用 depends_on 明确顺序,或拆成 data / resource 分开 |
Invalid index / out of range | count / 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.tfvars。TF_VAR_xxx 环境变量和 -var 同优先级。 |
6.3 排障通用流程
- 先 plan:看 Terraform 认为”世界应该是怎样”的
- 开 DEBUG 日志:
TF_LOG=DEBUG能看到每次对云 API 的调用和返回 - 对照云控制台 / CLI:资源真实状态到底是什么?会不会是手动改了?
terraform state list/state show:确认 state 里有无该资源- Drift 修复:要么把代码改成跟控制台一致,要么
replace重建 - 锁定版本:版本漂移是很多诡异 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 + 自动化
七、进阶方向
- Workspace 隔离环境:
terraform workspace new dev/staging/prod - Terragrunt:给 Terraform 包一层,解决 “多账号多环境 DRY 不够” 的问题
- CDK for Terraform (CDKTF):用 TypeScript/Python 写 HCL,对程序员友好
- OPA / Checkov / tfsec:IaC 静态扫描,检测安全漏洞和不合规配置
- Infracost:
plan之后顺便告诉你”这一改要多花多少钱” - Policy as Code (Sentinel):在 apply 前跑自定义规则
参考资料
- 官方文档:https://developer.hashicorp.com/terraform/docs
- 阿里云 Provider:https://registry.terraform.io/providers/aliyun/alicloud/latest/docs
- AWS Provider:https://registry.terraform.io/providers/hashicorp/aws/latest/docs
- tfsec(安全扫描):https://github.com/aquasecurity/tfsec
- Infracost(云成本):https://www.infracost.io/