我刚学 Terraform 时干过一件蠢事:看terraform.tfstate像个临时文件就顺手删了,下一次 apply,它把整套 VPC、交换机和 ECS 原封不动又建了一遍,还因重名卡在半空。
这篇把安装、云插件、第一个 ECS 与 init/plan/apply/destroy 四个命令串成一条闭环。走完你会明白 Terraform 到底在「对比」什么,也能拿到一份可反复创建、干净销毁的最小配置。
一、场景:手工点控制台,迟早要还的
删状态文件之所以是事故,根子在于我压根没理解 Terraform 靠什么记住资源——这个状态(State)就是它对真实世界的全部记录。要讲清这件事,得先回到一个更前置的问题:明明控制台点几下就能开机器,为什么还要写代码?
手工运维的痛点,和基础设施即代码(Infrastructure as Code,IaC)的价值,恰好一一对应:
| 手工点控制台 | Terraform 声明式配置 |
|---|---|
| 操作只在某个人脑子里,换人就断线 | 配置即代码,能进 Git、能 Review、能回滚 |
| 十台机器点出十种状态,环境漂移 | 同一份配置在任何环境跑出相同结果 |
| 改了什么全凭记忆,出事无法追溯 | 每次变更先出 diff,看清改什么再执行 |
Terraform 是 HashiCorp 出品的基础设施即代码工具,用声明式配置描述「想要的最终状态」,再由它把云资源创建、修改、销毁成这个状态。它不关心操作顺序,只关心「最终应该长什么样」。
工具定位先分清,才不会拿它干不擅长的活:
| 工具 | 定位 | 关注对象 |
|---|---|---|
| Terraform | 资源编排,调云厂商 API | 资源从生到死的生命周期 |
| Ansible | 配置管理,登录机器执行任务 | 机器内部的状态 |
| CloudFormation | 资源编排,能力近似 | 绑定 AWS 单一云 |
| Pulumi | 资源编排,用通用语言写配置 | 灵活但门槛更高 |
适用边界也很清楚:
- 适合:计算、网络、存储、数据库、DNS、权限等资源生命周期,多云统一编排与环境批量复制。
- 不适合:机器内部的精细化配置(交给 Ansible 或 cloud-init),以及一条命令就能搞定的一次性临时操作。
一句话记住这套心智模型:描述状态、对比差异、执行变更。手工脚本的创建与删除往往要各写一套逻辑,而 Terraform 一份配置双向可用。
二、一个二进制文件,装起来也有三道坎
Terraform 没有依赖库,安装就是解压、放进 PATH,卸载就是删文件。既然这么简单,为什么我敲terraform还是提示command not found?问题几乎都出在「放进 PATH」这一步。
你先到 releases.hashicorp.com 确认最新版本号,替换下面的1.16.5,再按平台选一种方式。
| 平台 | 推荐方式 | 架构包 |
|---|---|---|
| Linux | 二进制 / apt / yum | x86_64 用amd64,ARM 用arm64 |
| macOS | Homebrew / 二进制 | Intel 用amd64,M 系列用arm64 |
| Windows | 手动解压 / Chocolatey | 统一windows_amd64 |
Linux:通用二进制
任意发行版都能用,下载、解压进 PATH、清理安装包:
wgethttps://releases.hashicorp.com/terraform/1.16.5/terraform_1.16.5_linux_amd64.zipsudounzipterraform_1.16.5_linux_amd64.zip-d/usr/local/bin/rmterraform_1.16.5_linux_amd64.zipLinux:包管理器(便于后续升级)
Debian/Ubuntu 添加官方 apt 源,以后apt upgrade自动跟随新版本:
wget-O- https://apt.releases.hashicorp.com/gpg|sudogpg--dearmor-o/usr/share/keyrings/hashicorp-archive-keyring.gpgecho"deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com$(lsb_release-cs)main"|sudotee/etc/apt/sources.list.d/hashicorp.listsudoaptupdate&&sudoaptinstallterraformRHEL/CentOS/Alibaba Cloud Linux 添加官方 yum 源:
sudoyuminstall-yyum-utilssudoyum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.reposudoyuminstall-yterraformmacOS
有 Homebrew 就直接装,它会自动处理架构:
brew tap hashicorp/tap brewinstallhashicorp/tap/terraform不装 Homebrew 就手动下载二进制。Apple Silicon(M1/M2/M3)选arm64,Intel 芯片把arm64换成amd64:
curl-LOhttps://releases.hashicorp.com/terraform/1.16.5/terraform_1.16.5_darwin_arm64.zipunzipterraform_1.16.5_darwin_arm64.zipsudomkdir-p/usr/local/binsudomvterraform /usr/local/bin/Windows
手动安装以管理员身份打开 PowerShell:下载 zip、解压到固定目录、写入系统 PATH。
$version="1.16.5"$installDir="C:\Program Files\Terraform"Invoke-WebRequest"https://releases.hashicorp.com/terraform/$version/terraform_${version}_windows_amd64.zip"-OutFile terraform.zipNew-Item-ItemType Directory-Force-Path$installDir|Out-NullExpand-Archive-Path terraform.zip-DestinationPath$installDir-Force$machinePath=[Environment]::GetEnvironmentVariable("Path","Machine")if($machinePath-notlike"*$installDir*"){[Environment]::SetEnvironmentVariable("Path","$machinePath;$installDir","Machine")}不想敲命令也可以走图形界面,到 官方安装页 下载 Windows 版 ZIP 包:解压出terraform.exe放进固定目录(如C:\Program Files\Terraform),再到「设置 → 系统 → 关于 → 高级系统设置 → 环境变量」,把该目录追加到系统变量Path。已安装 Chocolatey,则以管理员身份运行 PowerShell 后执行:
choco install terraform无论哪种方式,装完都要重开一个终端,PATH 才会生效。
多版本管理
多个项目用不同版本时,用 tfenv 切换,避免「这台能跑、那台跑不了」的版本漂移:
tfenvinstall1.16.5 tfenv use1.16.5验证安装
terraform versionTerraform v1.16.5 on linux_amd64装不上时的报错基本是下面四类,按报错对号入座:
| 报错信息 | 原因 | 处理 |
|---|---|---|
command not found | 解压目录没进 PATH,或终端没重开 | 加 PATH 后重开终端 |
Exec format error | 架构包下错(amd64/arm64 不符) | 重新下载对应架构包 |
Permission denied | 二进制缺执行权限 | chmod +x terraform |
unzip: command not found | 系统没装 unzip | apt install unzip或yum install unzip |
注意:架构包必须和 CPU 对应——x86_64 用linux_amd64,鲲鹏、树莓派等 ARM 机器用linux_arm64,下错了不会在安装时报错,只会在运行时扔下一句Exec format error。
版本号能打印了,但此时的 Terraform 还只是一台空转的机器。它凭什么能去操作阿里云的 API?
三、原理:提供者才是它操作云的那只手
答案是提供者(Provider)。提供者是 Terraform 与云厂商之间的插件,把阿里云几千个 API 封装成 Terraform 能理解的资源类型。没有提供者,Terraform 就只剩一台空转的「状态对比机」。
新建一个空目录~/tf-first,在里面创建main.tf,在terraform块里声明要用的提供者:
terraform { required_version = ">= 1.5" required_providers { alicloud = { source = "aliyun/alicloud" version = "~> 1.278.0" } } } provider "alicloud" { region = "cn-hangzhou" }三个字段各有职责:
| 字段 | 含义 | 不锁定的后果 |
|---|---|---|
source | 提供者在 Registry 上的定位,格式「命名空间/名称」 | 找不到插件 |
version | 版本约束,~> 1.278.0允许 1.278.x 补丁升级 | 大版本升级常带破坏性变更 |
provider块 | 连接参数,region指定资源默认所在地域 | 资源落错地域 |
source 这一项有个时效性很强的坑:阿里云提供者已于 2026 年 3 月从hashicorp/alicloud迁移到 aliyun/alicloud,新配置一律用后者;旧配置迁移只需改 source 后重新 init,继续写旧 source 会在 init 阶段报找不到插件。
一台机器上要管理多个账号或地域,用alias声明多个实例,再在资源里显式指定:
provider "alicloud" { alias = "bj" region = "cn-beijing" } resource "alicloud_instance" "web" { provider = alicloud.bj }连接云需要凭据,AccessKey 的给法按安全等级分三档:
| 方式 | 做法 | 适用场景 |
|---|---|---|
| 环境变量 | export ALICLOUD_ACCESS_KEY=... | 本地练习、临时操作 |
| RAM 角色 | 给 ECS 挂角色,ecs_role_name免密钥 | 云上生产主机 |
| OIDC 临时凭证 | CI 通过 OIDC 换取短期凭证 | 流水线,根上消灭常驻密钥 |
环境变量注入凭据的命令:
exportALICLOUD_ACCESS_KEY="你的 AccessKey ID"exportALICLOUD_SECRET_KEY="你的 AccessKey Secret"AccessKey 绝对不能写进 .tf 文件——文件要进 Git,硬编码等于公开泄露,没有补救,只能吊销。
init 要从 registry.terraform.io 下载提供者,国内网络可能超时。在~/.terraformrc配置阿里云官方镜像即可提速:
provider_installation { network_mirror { url = "https://terraform-mirror.aliyuncs.com/" } }资源参数不需要背:打开 registry.terraform.io,找到aliyun/alicloud文档页,左侧 Resources 栏列出全部资源类型,点进去就有完整参数表、必填项和官方示例,照文档填是正常姿势。
手已经接上了,却还没告诉它「想要什么」。一台 ECS,到底该怎么声明出来?
四、方案:把「想要的状态」写进 resource
声明一台 ECS,靠的是资源(resource)块;同样一份配置能部署开发、测试、生产三套环境,前提是先把「环境相关」和「逻辑相关」分离——这是基础设施即代码的基本素养,也是 Terraform 环境复现能力的根基。
标准单模块布局用三个文件各司其职:
tf-first/ ├── main.tf # 资源定义(逻辑相关) ├── variables.tf # 可变输入(环境相关) └── outputs.tf # 对外暴露的值你在variables.tf里声明交换机 ID 这类随环境变化的值:
variable "vswitch_id" { description = "目标交换机 ID,可在 VPC 控制台查看" type = string }你在main.tf追加资源定义,这是全篇最核心的一段:
resource "alicloud_instance" "web" { instance_name = "my-first-ecs" instance_type = "ecs.t6-c1m1.large" image_id = "aliyun_2_1903_x64_20G_alibase_20230911.vhd" vswitch_id = var.vswitch_id internet_charge_type = "PayByTraffic" internet_max_bandwidth_out = 5 password = var.instance_password }你在outputs.tf声明建完后想看到的值:
output "instance_id" { value = alicloud_instance.web.id } output "public_ip" { value = alicloud_instance.web.public_ip }resource "alicloud_instance" "web"是最核心的三段式语法:
resource "alicloud_instance" "web" │ │ │ │ │ └─ 本地名称:当前配置内的唯一标识,用于互相引用 │ └─────────────────── 资源类型:提供者给出的能力,格式「云产品_资源」 └───────────────────────────────── 关键字:声明一个资源块内var.vswitch_id引用变量,alicloud_instance.web.id引用另一资源的属性。Terraform 会自动分析引用关系、构建依赖图,按正确顺序创建,不需要你手动排先后。
几个参数的真实含义:
| 参数 | 含义 |
|---|---|
instance_type | 实例规格,决定 CPU/内存 |
image_id | 镜像,决定操作系统 |
internet_charge_type = "PayByTraffic" | 按流量计费 |
internet_max_bandwidth_out = 5 | 出网带宽峰值 5 Mbps,不设则默认不分配公网 IP |
两个细节别踩:镜像 ID 不要手抄,用数据源(data source)按名称动态查;密码用sensitive标记,apply 时交互输入、输出日志自动打码:
data "alicloud_images" "ubuntu" { owners = "system" name_regex = "^ubuntu_24_04" } variable "instance_password" { type = string sensitive = true }多数依赖靠属性引用自动推导,个别「有顺序要求但无属性引用」的关联用depends_on;生命周期(Lifecycle)块则控制资源变更策略:
| 生命周期参数 | 作用 | 典型场景 |
|---|---|---|
prevent_destroy = true | 含删除该资源的计划直接失败 | 数据库等有状态资源保命 |
create_before_destroy = true | 先建新再删旧 | 无状态应用零中断滚动替换 |
ignore_changes = [tags] | 忽略指定属性差异 | 容忍控制台手工改动 |
注意:给数据库这类有状态资源加上prevent_destroy,是最便宜的后悔药——误跑 destroy 时它会在确认前直接拦下。
配置写完了,你怎么知道它这次会建什么、会不会顺手删掉没打算删的东西?
五、验证:先看 diff 再动手
Terraform 敢让你放心 apply,靠的是把「看清楚」和「动手」拆成两个命令。日常迭代的主循环就是这四板斧:
| 命令 | 作用 | 是否改动云资源 | 常用时机 |
|---|---|---|---|
init | 下载提供者、生成锁文件、初始化状态后端 | 否 | 新项目、新增依赖后 |
plan | 对比配置与状态,输出执行计划 | 否(只读) | 每次 apply 前 |
apply | 按计划调用 API 创建/变更资源 | 是 | 核对计划无误后 |
destroy | 按依赖逆序删除全部资源 | 是 | 清理练习环境 |
init:初始化
你首次执行 init,它做三件事:下载提供者插件到.terraform/、生成.terraform.lock.hcl锁文件(Lock File)、初始化状态后端。锁文件必须提交 Git,.terraform/目录则写进.gitignore。
terraform initplan:只读预览
执行计划(plan)对比配置与当前状态,用三种符号标出动作:+创建、~更新(附字段级 diff)、-删除,一致时输出No changes。它不调用任何写操作,是应用前唯一的免费安全检查。
你最好把计划固化成文件,apply 时严格按这份计划执行,杜绝「plan 之后配置又被改」的竞态:
terraform plan-out=tfplan计划末尾的汇总数字要逐一核对,多一个删除、少一个创建都和预期不符:
Plan: 1 to add, 0 to change, 0 to destroy.apply:执行
你按固化的计划执行,首次会要求输入yes确认;自动化场景才用-auto-approve跳过确认:
terraform apply tfplan terraform apply -auto-approveapply 默认按依赖图并行创建互不依赖的资源(并行度 10)。多人共用远程状态后端时会加锁,你看到Error acquiring the state lock就等他人完成再重跑,不要强行删锁文件。失败了也不用慌:修正配置后直接重跑,Terraform 会基于状态继续完成剩余工作,而不是推倒重来。
destroy:销毁
terraform destroy销毁顺序与创建相反:先删依赖别人的资源(实例),再删被依赖的资源(交换机、VPC),保证不报「资源仍被占用」。日常练习建议你练完随手 destroy,账单不心疼,也验证了配置「可生可灭」的完整性。
现在可以解开开头那个删状态文件的坑了。apply 成功后目录里的terraform.tfstate是 Terraform 的记忆,记录「真实世界当前是什么样」,plan/apply 都基于它做 diff:
- 它是唯一事实来源:删了它,Terraform 会认为所有资源都不存在,再 apply 就全部重建,这正是开头重复建资源的原因。
- 它不能进 Git:可能含密码、IP,且多人并发提交会互相覆盖。
- 团队协作必须用远程状态后端:阿里云用 OSS,同时提供存储与锁,在
terraform块声明后重新 init 即迁移:
terraform { backend "oss" { bucket = "my-tf-state" key = "prod/ecs.tfstate" region = "cn-hangzhou" } }其余高频命令构成日常排障工具箱:
| 命令 | 作用 |
|---|---|
terraform fmt | 自动格式化,提交 Git 前必跑 |
terraform validate | 静态校验语法与引用合法性 |
terraform state list | 查看状态管理着哪些资源 |
terraform show | 查看资源当前属性 |
terraform console | 交互式测试表达式 |
注意:状态文件不进 Git、不手删,单机练习无所谓,团队协作必须上 OSS 远程状态后端加锁,否则并发 apply 会互相覆盖、损坏状态。
四板斧跑通了,剩下的坑,能不能在动手前用一张表全部避开?
六、总结延伸:避坑清单与术语速查
把全篇的坑和正解汇成一张清单,贴在工位上比临时翻文档快:
| 坑 | 正解 |
|---|---|
| 版本漂移 | 锁required_version与提供者版本,提交 lock 文件,tfenv 管本机 |
| 凭据硬编码 | 一律环境变量、RAM 角色或 OIDC,绝不进 .tf |
| 状态文件裸奔 | 多人协作上 OSS 远程状态后端加锁 |
| 删状态文件重建 | 状态是唯一事实来源,不手删、不进 Git |
| 乱跑 destroy | 先 plan 看清波及范围,有状态资源加prevent_destroy |
滥用-auto-approve | 只在 CI/脚本用,手敲保留交互确认 |
| 控制台手工改资源 | 下次 apply 会被「纠正」回去,一切变更走 Terraform |
滥用-target | 跳过依赖图部分节点,生产环境易建不全,慎用 |
至此你已经走通最小闭环:安装 → 声明提供者 → 定义资源 → init/plan/apply/destroy。「描述状态、对比差异、执行变更」这套心智模型,就是基础设施即代码的全部精髓。
进阶建议按这个顺序走:
- 变量与 locals 表达式
- 远程状态后端 OSS
- module 模块化复用
- CI/CD 流水线集成
- 远程状态跨模块共享
把第一台 ECS destroy 掉,再随手建一个,手感就出来了。
术语速查表:
| 术语 | 英文 | 一句话解释 |
|---|---|---|
| 基础设施即代码 | Infrastructure as Code | 用代码而非手工操作管理基础设施 |
| 提供者 | Provider | Terraform 与云厂商 API 之间的插件 |
| 资源 | Resource | 一个被管理的云对象,如一台 ECS |
| 数据源 | Data Source | 只读查询已有信息,如按名称查镜像 |
| 状态 | State | 记录真实世界当前状态的文件,diff 的基准 |
| 执行计划 | Plan | 变更前的只读预览,含增/改/删符号 |
| 生命周期 | Lifecycle | 控制资源创建、替换、销毁策略的块 |
| 锁文件 | Lock File | 锁定提供者版本与校验和,保证构建一致 |
参考链接:
- https://developer.hashicorp.com/terraform/install
- https://registry.terraform.io/providers/aliyun/alicloud/latest/docs
- https://developer.hashicorp.com/terraform/cli/commands
- https://registry.terraform.io/providers/aliyun/alicloud/latest/docs/backends/oss