大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Provisioners 预置器
这一课你将能做到
- 区分 local-exec、remote-exec、file 三种预置器,知道它们各自在哪里执行
- 用
connection块和 remote-exec 在新建实例上完成首次引导 - 说清楚预置器的触发时机限制,并给出「不用预置器」的优先替代方案
为什么要学
Terraform 的本职是「调用云 API 建资源」,资源建完那一刻它的活就干完了。但真实需求常想多走一步:实例起来后自动装 JDK、把应用 jar 传上去、启动服务;或者资源建好后在执行机上调用一次第三方 API 注册 CMDB。这些 Terraform 管不到的「资源内部动作」,就是 provisioner 的地盘。
必须同时强调另一半:HashiCorp 官方文档明确把 provisioner 列为「最后手段(last resort)」。预置器脚本藏在资源生命周期里、只在创建时跑、失败处理棘手,是大量「在我机器上能建,在 CI 里建坏了」的根源。本站架构(ECS Fargate)基本不用预置器——应用交付靠镜像和任务定义。学这一课的目标,是需要时会用,更重要的是知道什么时候不该用。
先建立图像
terraform apply(在执行机:你的笔记本 / CI Runner)
│
├─ 调用 AWS API 创建 EC2 成功
│
├─ local-exec → 命令在「执行机」上跑(你本机/Runner)
│
├─ file → 经 SSH/WinRM 把文件从执行机传到目标机
│
└─ remote-exec → 经 SSH/WinRM 在「目标 EC2」上跑命令
触发时机:
creation-time provisioner:只在资源「新建」后执行一次
update(原地改)不执行 ← 最易踩坑
destroy-time provisioner :资源「删除前」执行(when = destroy)
跟着做
步骤 1:local-exec——在执行机上跑命令
local-exec 不需要目标机,最简单。常见用途:apply 后本地通知、调用没有 provider 的内部 API。
resource "aws_s3_bucket" "assets" {
bucket = "coderoadmap-provision-demo-2026"
provisioner "local-exec" {
command = "echo 桶 ${self.bucket} 已创建 >> provision.log"
}
}
注意 self.bucket:预置器内部用 self 引用所属资源的属性(只能在 provisioner 里用)。local-exec 默认在资源创建成功后执行,失败会让整个 apply 失败。
步骤 2:remote-exec + connection 在目标机引导
要在 EC2 上跑命令,先告诉 Terraform「怎么连上去」:
resource "aws_instance" "web" {
ami = "ami-09c3834b0881dbf81"
instance_type = "t3.micro"
key_name = aws_key_pair.deploy.key_name
vpc_security_group_ids = [aws_security_group.web.id]
# 关联连接信息
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/deploy.pem")
host = self.public_ip
}
# 把本地脚本传上去
provisioner "file" {
source = "${path.module}/scripts/bootstrap.sh"
destination = "/tmp/bootstrap.sh"
}
# 在目标机执行
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/bootstrap.sh",
"/tmp/bootstrap.sh ${var.db_address}",
]
}
tags = { Name = "coderoadmap-bootstrap-demo" }
}
scripts/bootstrap.sh:
#!/usr/bin/env bash
set -euo pipefail
DB_ADDRESS="$1"
sudo dnf install -y java-17-amazon-corretto-headless
echo "spring.datasource.url=jdbc:mysql://${DB_ADDRESS}:3306/coderoadmap" \
| sudo tee /etc/coderoadmap.conf
执行顺序严格按 provisioner 块书写顺序:先 file 传脚本,再 remote-exec 执行。安全组必须放行执行机到 22 端口的入站,否则 SSH 超时。
步骤 3:失败策略与销毁时预置器
远程机器网络抖动导致脚本失败时,默认整个 apply 失败、资源标记 tainted(下次重建)。确认脚本失败不影响资源可用性时可放宽:
provisioner "remote-exec" {
when = failure # 仅在前面的 provisioner 失败时执行(补救)
inline = ["/usr/bin/logger 'bootstrap failed'"]
on_failure = continue # 失败也不中断 apply
}
provisioner "remote-exec" {
when = destroy # 删除资源前执行:优雅下线、注销服务
inline = ["/opt/app/deregister.sh"]
}
when = destroy 的销毁预置器,在 terraform destroy 时经同样的 connection 连上去执行;若实例已经不存在会失败,此时可用 on_failure = continue 容错。
步骤 4:用 null_resource 让脚本可重复触发
预置器依附具体资源,资源不重建就不再执行。想把「跑脚本」本身当作一个可控制的对象,用 null provider 的 null_resource:
resource "null_resource" "register_cmdb" {
triggers = {
instance_id = aws_instance.web.id
config_hash = filemd5("${path.module}/scripts/register.sh")
}
provisioner "local-exec" {
command = "${path.module}/scripts/register.sh ${aws_instance.web.id}"
}
}
triggers 是一组字符串映射:任何一个值变化,Terraform 就销毁旧的 null_resource、新建并重新执行预置器;不变则跳过。它是「把外部脚本挂进 Terraform 依赖图」的标准做法,也是 1.7 中替代旧 taint 重跑脚本的方式。
步骤 5:优先选择替代方案
本站以及成熟团队的优先级阶梯:
| 需求 | 首选(非预置器) | 预置器为什么差 |
|---|---|---|
| EC2 首次装软件/配置 | launch template 里的 user_data + cloud-init | user_data 由 AWS 原生记录、可审计、可更新 |
| 机器持续保持配置 | 黄金镜像(Packer/Image Builder)、Ansible | 预置器只在创建时跑,后续漂移无人管 |
| 跑应用服务 | ECS Fargate / EKS + 容器镜像 | 应用交付与资源 provision 本就该解耦 |
| 资源间编排/等待 | 原生资源引用、time_sleep、depends_on | 脚本里 sleep/轮询脱离状态管理 |
| 调用外部 API | 对应平台的 provider;其次 external data source | local-exec 无返回值,错误难追踪 |
本站 ECS 架构下,EC2/预置器只在构建机、堡垒机等少数场景出现;应用层零 SSH、零 remote-exec。
讲清楚要点
- 预置器不属于声明式模型的核心:它是「逃生舱」,配置里出现 provisioner 就该自问有没有原生做法。
- 只在创建时执行:这是语义层面最重要的事实——更新实例标签、改安全组都不会重跑引导脚本,期望「每次 apply 都同步配置」必须走 user_data 触发或配置管理工具。
- 失败会污染资源:creation provisioner 失败时资源被标记为需要 replace;CI 里要提供清晰日志,否则重试成本高。
- self 只在预置器作用域可用:
self.public_ip不能拿到普通表达式中,普通引用写资源全名。 - 可测试性差:脚本逻辑在 Terraform 之外,
terraform validate管不到;脚本越短越好,复杂逻辑移进镜像或独立流水线。
你可能会懵的地方
-
为什么我改了 remote-exec 脚本,apply 却不执行? provisioner 只在资源创建时运行,脚本内容变化不触发资源重建。要强制重跑:
terraform apply -replace=aws_instance.web,或改用 null_resource + triggers 管理脚本。 -
user_data 改了会自动在老实例上生效吗? 不会。AWS 的 user_data 也只在首次启动默认执行一次(配合 cloud-init 的 per-once 标记);改 user_data 会让 plan 显示实例替换,靠新模板滚动换新,而不是 SSH 上去改。
-
remote-exec 和我手动 SSH 上去执行有什么区别? 技术上完全一样,都是走 SSH。区别只是动作被 Terraform 编排和记录;但它依然是非声明式的副作用,出问题时 Terraform 无法感知机器内部状态。
-
file provisioner 能传目录吗? 可以传目录(source 指向目录时递归上传),但大文件/多文件场景建议走 S3 + 启动脚本拉取,避免 SSH 通道成为瓶颈。
-
Windows 机器怎么办? connection
type = "winrm",remote-exec 执行 PowerShell;同样优先用 user_data(PowerShell 脚本)和 SSM Automation。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 用 remote-exec 部署并启动应用 | 不可滚动、不可观测、绑定单机 | 容器化走 ECS/EKS,镜像即交付物 |
| 以为 provisioner 每次 apply 都跑 | 只在创建时执行一次 | 用 user_data/黄金镜像,或 null_resource triggers |
| connection 里写死私钥路径给 CI 用 | Runner 上路径不存在 | 私钥由 CI secret 写临时文件或用 SSM Session |
| on_failure=continue 掩盖引导失败 | 带病实例进入流量 | 默认 fail;补救脚本只做告警不改判定 |
| 长脚本内联在 inline 列表 | 难评审、难测试 | file 传脚本文件,脚本单独版本管理 |
卡点急救
- SSH 超时 / handshake failed:安全组没放行 Runner 出口 IP 的 22 端口、密钥用户不对(Amazon Linux 2023 是 ec2-user,Ubuntu 是 ubuntu)、私钥内容未正确读取。先用同参数
ssh -i key user@host在终端验证连通性。 - remote-exec 报命令非零退出但日志看不清:脚本开头
set -euxo pipefail打开命令回显;所有输出会进 Terraform 日志,必要时加TF_LOG=TRACE排查连接层。 - 销毁卡在 destroy provisioner(机器已没了):给销毁预置器加
on_failure = continue,或临时从配置移除该 provisioner 后再 destroy(state 更新后即不再尝试连接)。
面试常问
-
Terraform 的 provisioner 有哪些?为什么官方不推荐? local-exec(执行机本地)、remote-exec(SSH/WinRM 到目标)、file(传文件)。不推荐是因为它只在创建时运行、属命令式副作用、失败处理复杂、破坏声明式的可预期性;应优先 user_data/cloud-init、黄金镜像、容器编排与专用 provider。
-
要在 EC2 启动后自动装 JDK 并下发配置,你有哪几种方案? 首选 launch template + user_data(cloud-init),或用 Packer 预制带 JDK 的 AMI;持续配置漂移用 Ansible/SSM;remote-exec 只作为无法使用前述方案时的最后手段。
-
null_resource 的 triggers 有什么用? 把任意外部命令纳入 Terraform 图;triggers 中值(如脚本哈希、目标资源 id)变化时资源重建并重跑 provisioner,未变化则跳过,从而实现可声明、可重跑的外部动作。
自测三问
- local-exec 和 remote-exec 的执行位置分别是什么?谁需要安全组放行?
- 为什么「更新安全组」不会触发实例上的 remote-exec?想重跑有哪些正当方式?
- 本站 ECS Fargate 部署应用,为什么天然避开了绝大多数 provisioner 场景?
小结一句话
Provisioner 是 Terraform 在「资源已建好」之后执行命令的逃生舱:local/remote-exec 和 file 经 connection 工作、只在创建时触发,但能用 user_data、黄金镜像和容器编排解决的,就不要把副作用塞进资源生命周期。
延伸
- Provisioners 官方文档(含 last resort 说明)
- local-exec 与 remote-exec 参考
- null_resource:triggers 机制
- 下一课:Data Sources——只读查询 AWS 现有信息(AMI、调用者身份、托管策略),配合 preconditions 做前置断言。