概述
凌晨两点,告警群炸了。一台刚上线两小时的 Web 服务器 CPU 飙到 100%,SSH 进去一看,Nginx 没装,但端口被一个旧版本 Apache 占着。翻部署日志发现:Terraform 10 分钟前做了一次 terraform apply,把这台机器重建了(因为有人改了 instance_type),但 Ansible 的配置脚本没有跟着跑——机器是新机器,上面的软件配置全丢了。
这不是个例。用 Terraform 建云主机、用 Ansible 做配置管理,这个组合在 2026 年已经是运维团队的标配。但"标配"不等于"配好了"——大量团队把这两个工具简单堆在一起,没有清晰的协同设计,结果就是基础设施"管了两遍":Terraform 建了一遍资源,Ansible 又配了一遍,两边都不知道对方在干什么,配置漂移、状态不一致、流水线断裂,全来了。
这篇文章拆解 Terraform + Ansible 协同的 5 个核心决策。每个决策都来自实际生产环境的踩坑经验,不是理论推演。如果你的团队正在用或准备用这个组合,这些决策点会帮你少走至少半年的弯路。
一、职责边界:Terraform 管什么,Ansible 管什么
先说清楚一个最基本但最容易被忽视的问题:这两个工具的职责边界到底画在哪里。
Terraform 的强项是基础设施编排——创建云主机、虚拟机、网络、存储、负载均衡、安全组、数据库实例、K8s 集群。它是声明式的,你告诉它"我要 3 台 4C8G 的云主机在这个可用区",它就去创建。它管理资源的整个生命周期:创建、修改、销毁。
Ansible 的强项是系统配置和应用交付——操作系统初始化、账号权限、内核参数、防火墙规则、软件包安装、中间件部署、配置文件渲染、服务启停、定时任务。它是过程式的(虽然也有幂等性设计),你告诉它"先装 Nginx,再拷配置,然后启动服务",它按顺序执行。
两个工具各管一摊,看起来很清楚。但实际生产中,边界经常模糊。举几个常见的"灰色地带":
| 配置项 | Terraform 能做 | Ansible 能做 | 应该谁管 |
|---|---|---|---|
| 创建云主机 | ✅ 原生支持 | ❌ 不适合 | Terraform |
| 安装 Nginx | ✅ remote-exec | ✅ 原生支持 | Ansible |
| 配置安全组规则 | ✅ 原生支持 | ⚠️ 能做但不推荐 | Terraform |
| 渲染 Nginx 配置文件 | ⚠️ local-exec 拼接 | ✅ Jinja2 模板 | Ansible |
| 磁盘挂载和格式化 | ⚠️ 能做但粗糙 | ✅ 精细控制 | Ansible |
| 创建 DNS 记录 | ✅ 原生支持 | ⚠️ 能做但不推荐 | Terraform |
| 用户和 SSH 密钥 | ✅ 能做 | ✅ 原生支持 | Ansible |
决策 1:不要用 Terraform provisioner 做复杂配置
Terraform 提供了 remote-exec 和 local-exec 两个 provisioner,可以在资源创建后执行命令或脚本。很多团队一上来就在 Terraform 里写一长串 remote-exec 块,用 shell 脚本装软件、配防火墙、改内核参数。
这个做法在测试环境能跑通,到了生产环境就是定时炸弹:
# ❌ 反面示例:在 Terraform 里做复杂配置
resource "alicloud_instance" "web" {
instance_type = "ecs.g6.large"
# ... 其他配置
provisioner "remote-exec" {
inline = [
"yum install -y nginx",
"systemctl enable nginx",
"systemctl start nginx",
"echo 'net.ipv4.tcp_tw_reuse = 1' >> /etc/sysctl.conf",
"sysctl -p",
# ... 还有 50 行
]
}
}
问题出在哪?
第一,provisioner 只在资源创建时执行。如果你后续修改了 instance_type 导致实例重建,provisioner 会再跑一次。但如果你只是修改了某个不相关的参数(比如加个 tag),Terraform 不会触发重建,provisioner 也不跑——这时候你的配置就和预期不一致了。
第二,provisioner 不在 Terraform 的状态管理范围内。Terraform 的核心机制是"声明式 + 状态追踪":你说要 3 台机器,它就保证 3 台;少了就补,多了就删。但 provisioner 执行的命令不在状态里,Terraform 不知道这些命令是否执行过、执行是否成功、配置是否还生效。漂移了也检测不到。
第三,provisioner 让 Terraform 变成了过程式工具。Terraform 的价值在于声明式——你描述"最终状态",工具自己算怎么达到。但 provisioner 强行塞入了一堆命令式逻辑,破坏了这个模型。
正确做法是:Terraform 只管到"资源创建完成",然后通过 output 暴露必要信息(IP、主机名、角色标签),交给 Ansible 做后续配置。
# ✅ 正确做法:Terraform 只管建资源,输出给 Ansible
resource "alicloud_instance" "web" {
count = 3
instance_type = "ecs.g6.large"
# ... 其他配置
}
output "web_instance_ips" {
value = alicloud_instance.web[*].public_ip
}
output "web_instance_roles" {
value = alicloud_instance.web[*].tags.Role
}
实践经验:在某出行项目的 K8s 迁移中,我们最初用 Terraform provisioner 做 node 初始化,结果每次
terraform plan都报一堆 provisioner 变更,计划评审完全没法看。后来把初始化逻辑全部迁移到 Ansible,Terraform 只管建 ECS 和安全组,terraform plan输出从 200 多行降到 30 行,评审效率直接翻倍。(相关文章:Terraform 基础设施即代码入门)
划定边界的实操规则
我在团队里推行过一条规则,效果很好:
“云资源生命周期归 Terraform,操作系统到应用层归 Ansible,两层之间只通过 output 和 dynamic inventory 传递数据。”
具体来说:
- Terraform 管:云主机、VPC、子网、安全组、SLB、RDS 实例、DNS 记录、对象存储桶
- Ansible 管:操作系统初始化、内核参数、用户和权限、软件安装、配置文件、服务管理、应用部署
- 交接点:Terraform
output暴露 IP 和元数据 → Ansible 动态清单消费这些数据
这条规则画完,两个工具的边界就清楚了。团队成员遇到"这个配置归谁管"的问题,先看它属于哪个层——云资源层还是操作系统层——答案自然就出来了。
二、动态清单:让 Terraform 输出自动变成 Ansible Inventory
边界画好了,下一个问题是:Terraform 建完 50 台云主机,Ansible 怎么知道这些机器的 IP 和分组?
最原始的做法是手动维护 Ansible 的 hosts 文件。Terraform 建完机器后,人工把 IP 拷到 hosts 文件里,然后跑 Ansible。这在机器少的时候还能用,但一旦 Terraform 做了弹性扩缩容,静态清单就废了——新机器加进来了但 hosts 文件没更新,Ansible 跑不到新机器。
决策 2:用 Terraform State 作为 Ansible 动态清单的数据源
Ansible 原生支持动态清单(Dynamic Inventory)。动态清单的本质是:Ansible 在执行 playbook 前,先调用一个外部脚本或插件,从数据源拉取当前主机列表和分组信息。
数据源可以选三个:
| 数据源 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| Terraform State | 用 terraform-inventory 或自写脚本读 state 文件 | 实时准确,与 TF 完全同步 | 需要 state 文件访问权限 |
| 云 API | 用 Ansible 云插件(如 alicloud/aws_ec2) | 不依赖 TF,独立查询 | 分组信息依赖 tag,与 TF 分组可能不一致 |
| Terraform Output + 本地文件 | TF output 写到 JSON 文件,Ansible 读文件 | 简单直接,依赖少 | 不是实时的,需要手动触发 output |
我推荐用** Terraform State 作为数据源**,原因是:Terraform State 是基础设施的"真实来源",它精确记录了当前有哪些资源、它们的属性是什么。用 State 做清单,Ansible 看到的就是 Terraform 实际管理的资源,不会出现"云上有但清单里没有"或"清单里有但云上已删"的情况。
具体实现方式有两种。
方式一:terraform-inventory 工具
这是一个开源工具,直接读 Terraform State 文件,生成 Ansible 动态清单:
# 安装 terraform-inventory
# https://github.com/adammck/terraform-inventory
wget https://github.com/adammck/terraform-inventory/releases/download/v0.2.1/terraform-inventory_0.2.1_linux_amd64.tar.gz
tar xzf terraform-inventory_0.2.1_linux_amd64.tar.gz
mv terraform-inventory /usr/local/bin/
# 使用方式:指定 Terraform state 文件所在目录
ansible-playbook -i /path/to/terraform/envs/prod site.yml
terraform-inventory 会自动读 state 文件,按 Terraform resource 的 type 和 name 生成 Ansible 分组。比如 alicloud_instance.web 会生成一个叫 web 的主机组。
方式二:自写动态清单脚本
如果 terraform-inventory 的分组逻辑不满足需求,可以自己写一个。脚本只需要实现两个参数:--list 返回所有主机和分组,--host <hostname> 返回单台主机的变量。
#!/usr/bin/env python3
"""Terraform State → Ansible 动态清单脚本"""
import json
import subprocess
import sys
def get_state():
"""读取 Terraform State"""
result = subprocess.run(
["terraform", "show", "-json"],
capture_output=True,
text=True,
cwd="/path/to/terraform/envs/prod"
)
return json.loads(result.stdout)
def build_inventory(state):
"""从 state 构建 Ansible 清单"""
inventory = {"_meta": {"hostvars": {}}, "all": {"children": []}}
for resource in state.get("values", {}).get("root_module", {}).get("resources", []):
if resource["type"] == "alicloud_instance":
# 用 tags.Role 做分组
role = resource.get("values", {}).get("tags", {}).get("Role", "ungrouped")
if role not in inventory:
inventory[role] = {"hosts": []}
inventory["all"]["children"].append(role)
ip = resource["values"]["public_ip"]
hostname = resource["values"]["instance_name"]
inventory[role]["hosts"].append(hostname)
inventory["_meta"]["hostvars"][hostname] = {
"ansible_host": ip,
"instance_id": resource["values"]["id"],
"instance_type": resource["values"]["instance_type"]
}
return inventory
if __name__ == "__main__":
if len(sys.argv) == 2 and sys.argv[1] == "--list":
state = get_state()
inventory = build_inventory(state)
print(json.dumps(inventory, indent=2))
elif len(sys.argv) == 3 and sys.argv[1] == "--host":
# 单台主机变量查询
print(json.dumps({}))
这个脚本的关键设计是:用 Terraform 资源上的 tags 做分组。在 Terraform 里给每台机器打上 Role=web 或 Role=db 的标签,脚本读 state 时按 Role 分组,Ansible 就能直接用 hosts: web 或 hosts: db 来执行 playbook。
踩坑提醒:动态清单脚本读的是 Terraform State 文件。如果你的 State 存在远程后端(如 OSS/S3),脚本需要能访问到。在生产环境中,我推荐把 State 存在远程后端并配置适当的权限——既能多人协作,又能避免 State 文件在本地丢失。更多 State 管理的实践可以看相关文章:状态文件丢了怎么办。
实测对比:动态清单 vs 静态清单
在一个 50 台云主机的环境中实测:
| 指标 | 静态清单(手动维护) | 动态清单(Terraform State) |
|---|---|---|
| 清单更新耗时 | 人工 5-15 分钟 | 0(自动) |
| 机器遗漏率 | 约 8%(人为疏漏) | 0% |
| 弹性扩容后生效时间 | 需要人工介入 | 下次 playbook 执行即生效 |
| 分组一致性 | 依赖人工,常出错 | 与 Terraform 完全一致 |
动态清单的另一个好处是弹性扩容场景下的自动适配。当 Terraform 做了 count = 5 → count = 8 的扩容后,Ansible 下次执行时,动态清单会自动包含新增的 3 台机器,不需要任何人工干预。
三、编排顺序与触发:谁先谁后,中间出错了怎么办
边界和清单解决了,下一个问题是:Terraform 和 Ansible 在时间上怎么编排?谁先执行,中间怎么传递数据,出错了怎么处理?
决策 3:用两阶段流水线隔离 Terraform 和 Ansible,中间加验证关卡
最直觉的方案是在 Terraform 里直接触发 Ansible——用 local-exec provisioner 在 terraform apply 之后调 ansible-playbook。这个方案看起来简单,但生产环境中有三个硬伤:
错误传播不可控:Terraform apply 成功了,但 Ansible playbook 失败了。此时资源已创建,但没配置,处于半成品状态。Terraform 的 provisioner 失败会导致整个 apply 标记为失败,但资源已经创建了——你没法回滚。
执行时间过长:Terraform apply 本身就要几分钟(等云 API 响应),再加上 Ansible 配置 50 台机器可能要 10-20 分钟。一个命令要等 25 分钟,中间任何中断都会导致状态不一致。
调试困难:Terraform 的输出和 Ansible 的输出混在一起,出问题时分不清是哪个环节的锅。
正确做法是把 Terraform 和 Ansible 拆成流水线的两个阶段,中间用一个验证关卡连接:
Stage 1: Terraform Plan → Review → Terraform Apply
↓
验证关卡:检查资源是否真的创建成功(API 查询确认)
↓
Stage 2: Ansible Dynamic Inventory Refresh → Ansible Playbook
↓
验证关卡:检查关键服务是否正常运行(健康检查)
这个设计的核心是隔离 + 验证。Terraform 只管建资源,建完后进入验证关卡——用云 API 确认实例状态是 Running,SSH 端口可达。验证通过后,Ansible 才开始工作。Ansible 配置完后也有验证关卡——检查关键端口是否监听、服务是否 healthy。
以下是用 GitLab CI 实现这个两阶段流水线的示例:
# .gitlab-ci.yml - Terraform + Ansible 两阶段流水线
stages:
- terraform
- verify_infra
- configure
- verify_app
variables:
TF_DIR: "terraform/envs/prod"
ANSIBLE_DIR: "ansible"
# Stage 1: Terraform
terraform_plan:
stage: terraform
script:
- cd $TF_DIR
- terraform init
- terraform plan -out=tfplan
artifacts:
paths:
- $TF_DIR/tfplan
only:
- main
terraform_apply:
stage: terraform
script:
- cd $TF_DIR
- terraform init
- terraform apply -auto-approve tfplan
# 输出主机信息到文件,供后续阶段使用
- terraform output -json > ../../$ANSIBLE_DIR/tf_outputs.json
dependencies:
- terraform_plan
only:
- main
when: manual # 需要人工确认后才执行 apply
# 验证关卡:检查基础设施是否就绪
verify_infra:
stage: verify_infra
script:
- python3 scripts/verify_infra.py --tf-output $ANSIBLE_DIR/tf_outputs.json
only:
- main
# Stage 2: Ansible
ansible_configure:
stage: configure
script:
- cd $ANSIBLE_DIR
# 刷新动态清单
- ansible-inventory -i inventory_terraform.py --list > /dev/null
# 执行 playbook
- ansible-playbook -i inventory_terraform.py site.yml --limit "tag_Role_web"
only:
- main
# 验证关卡:检查应用是否正常
verify_app:
stage: verify_app
script:
- python3 scripts/verify_app.py --tf-output $ANSIBLE_DIR/tf_outputs.json
only:
- main
这个流水线设计有几个关键点:
人工确认关卡:terraform_apply 设了 when: manual,plan 通过后需要人工确认才执行 apply。这是因为 Terraform apply 是不可逆操作——资源一旦创建就会计费。在自研 Go CI/CD 调度引擎时,我们的部署耗时从 1.5h 降到 5min,但关键操作(如基础设施变更)始终保留人工确认环节。速度和安全不是对立的——快的是执行速度,安全的是确认流程。
验证脚本:verify_infra.py 的核心逻辑是查询云 API,确认每台实例都是 Running 状态,且 SSH 端口可达。verify_app.py 检查 HTTP 健康检查接口返回 200。
# scripts/verify_infra.py
"""验证基础设施是否就绪"""
import json
import socket
import sys
import time
def check_ssh_reachable(ip, port=22, timeout=5):
"""检查 SSH 端口是否可达"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((ip, port))
sock.close()
return result == 0
except Exception:
return False
def main():
with open(sys.argv[2]) as f:
outputs = json.load(f)
ips = outputs.get("web_instance_ips", {}).get("value", [])
all_ready = True
for ip in ips:
# 最多等待 120 秒
for attempt in range(24):
if check_ssh_reachable(ip):
print(f"[OK] {ip}:22 SSH 可达")
break
time.sleep(5)
else:
print(f"[FAIL] {ip}:22 SSH 不可达,等待 120s 后超时")
all_ready = False
if not all_ready:
print("基础设施验证失败,阻断后续 Ansible 配置阶段")
sys.exit(1)
print("全部实例 SSH 可达,基础设施就绪")
if __name__ == "__main__":
main()
踩坑提醒:云主机创建后到 SSH 可达之间有延迟。阿里云 ECS 通常在创建后 30-60 秒才完成初始化,AWS EC2 在 60-90 秒。如果不加验证关卡直接跑 Ansible,第一批 SSH 连接会失败,playbook 报错退出。验证关卡的价值就在这里——等机器真的就绪了再开始配置。
四、配置漂移治理:两个工具同时改一台机器怎么办
这是 Terraform + Ansible 协同中最棘手的问题。
假设这个场景:Terraform 创建了一台云主机,并配置了一个安全组规则——开放 80 和 443 端口。Ansible 在这台机器上做了系统初始化,其中包含一个 nftables 防火墙规则——也管理端口开放。
现在有人在运维工单里说"临时开一下 8080 端口给测试用"。运维同学直接 SSH 上去,nftables add rule 开了 8080。过两天 Terraform 做 plan,发现安全组没变(因为 8080 是主机内的 nftables 规则,不在 Terraform 管理范围),不做任何操作。但 Ansible 下次跑初始化 playbook 时,因为 8080 不在 Ansible 模板里,把它删了。测试同学发现端口又关了,又来找运维开。
这就是配置漂移。根本原因是资源所有权不清晰。
决策 4:建立资源所有权矩阵,明确每类配置由谁管理
解决配置漂移的核心不是工具,是所有权规则。我推行的做法是维护一张"资源所有权矩阵":
| 配置层 | 所有者 | 允许的变更途径 | 漂移检测方式 |
|---|---|---|---|
| 云主机/网络/存储 | Terraform | 修改 TF 代码 → PR → apply | terraform plan |
| 安全组规则 | Terraform | 修改 TF 代码 → PR → apply | terraform plan |
| OS 内核参数 | Ansible | 修改 Ansible role → PR → playbook | ansible --check |
| 用户和 SSH 密钥 | Ansible | 修改 Ansible role → PR → playbook | ansible --check |
| 防火墙规则(主机内) | Ansible | 修改 Ansible role → PR → playbook | ansible --check |
| 应用配置文件 | Ansible | 修改 Ansible template → PR → playbook | ansible --check |
这张表的核心原则是:每一项配置只有一个所有者。不允许"Terraform 也能改,Ansible 也能改"的情况。如果两个工具都声称管理同一项配置,漂移只是时间问题。
漂移检测的具体做法:
Terraform 侧:定期执行 terraform plan(比如每天凌晨跑一次),如果 plan 显示有变更,说明实际资源偏离了声明的状态——有人手动改了,或者 Ansible 改了不该改的东西。
# 漂移检测脚本(可放入 cron 或 CI 定时任务)
#!/bin/bash
cd /path/to/terraform/envs/prod
terraform init -input=false
terraform plan -detailed-exitcode -out=/dev/null
EXIT_CODE=$?
# 0: 无变更, 1: 有错误, 2: 有漂移
if [ $EXIT_CODE -eq 2 ]; then
echo "⚠️ 检测到 Terraform 漂移!"
terraform plan -no-color > /tmp/drift_report.txt
# 发送到告警渠道
curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=$DINGTALK_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"Terraform 漂移告警:$ENV 环境\n$(cat /tmp/drift_report.txt | head -50)\"}}"
fi
Ansible 侧:用 ansible-playbook --check 做干跑模式检测。--check 模式只检测不修改,会报告哪些任务的状态与预期不符。如果报告有变化,说明实际配置偏离了 playbook 声明的状态。
# Ansible 漂移检测
ansible-playbook -i inventory_terraform.py site.yml --check --diff
# --check: 不实际执行修改,只检测
# --diff: 显示预期的差异
实践经验:在某个电商平台,我们推了一个规则——所有临时变更必须通过 Ansible ad-hoc 命令操作并记录到 git 提交历史里,不允许直接 SSH 手改。一开始运维同学觉得麻烦,但实施一个月后,配置漂移事件从每周 3-4 起降到 0。关键是让"通过工具变更"比"手动变更"更方便——把常用 Ansible role 封装成一键执行的脚本,比 SSH 进去手敲还快。这种思路在相关文章:IaC 测试体系中有更详细的讨论。
Terraform 和 Ansible 都能管的配置怎么办
有些配置确实是"灰色地带"——Terraform 和 Ansible 都有能力管理。比如 SSH 密钥分发,Terraform 可以通过 cloud-init 或 alicloud_key_pair 资源管理,Ansible 也可以通过 authorized_key 模块管理。
我的建议是:在操作系统能感知的层面,优先用 Ansible;在云 API 能感知的层面,优先用 Terraform。
SSH 密钥是个好例子。云平台的 SSH 密钥绑定(如阿里云 ECS 的 key pair)归 Terraform 管——它通过云 API 绑定密钥到实例。但实例内部的 authorized_keys 文件归 Ansible 管——它通过 SSH 连接直接操作文件。这两个层面不冲突,各自管各自的。
五、CI/CD 集成:从手动脚本到自动化流水线
前面几个决策解决了"怎么协同"的问题,但协同的最终目标是让整个流程自动化——开发提交代码,CI/CD 自动执行 Terraform plan、人工 review、apply、Ansible 配置、验证,全程不需要 SSH 进任何机器。
决策 5:把 TF + Ansible 协同流水线纳入 IaC 测试体系
很多团队的 TF + Ansible 流水线缺少测试环节。Terraform 代码改了,直接 apply;Ansible playbook 改了,直接跑。出问题就回滚——但"回滚"在基础设施领域是个很重的操作。
完整的测试体系应该包含四层:
Layer 1: 代码检查
├── Terraform: terraform fmt + terraform validate + tflint
├── Ansible: ansible-lint + yamllint
└── 通用: shellcheck(对 shell 脚本)
Layer 2: 计划验证
├── Terraform: terraform plan(在 CI 里跑,输出给人工 review)
└── Ansible: ansible-playbook --check(干跑模式,检测预期变更)
Layer 3: 集成测试
├── 在测试环境执行完整的 apply + playbook
├── 验证服务可用性
└── 测试通过后自动 destroy 测试环境
Layer 4: 生产部署
├── 人工确认 plan
├── apply + playbook
├── 健康检查
└── 失败时自动回滚
以下是 CI 流水线中 Terraform 代码检查的配置:
# .gitlab-ci.yml - IaC 代码检查阶段
lint_terraform:
stage: lint
image: hashicorp/terraform:1.6
script:
- cd terraform/envs/prod
- terraform fmt -check -recursive
- terraform init -backend=false
- terraform validate
only:
- merge_requests
lint_ansible:
stage: lint
image: ansible/ansible:latest
script:
- cd ansible
- ansible-lint site.yml roles/
- yamllint -d relaxed .
only:
- merge_requests
集成测试阶段的实现思路:在测试环境执行完整的 Terraform apply + Ansible playbook,验证服务可用性后自动 destroy 测试资源。这个阶段的执行时间通常在 5-10 分钟(取决于云资源创建速度和 Ansible playbook 复杂度),但它能在合并到 main 分支前发现 90% 的集成问题。
# scripts/integration_test.py
"""集成测试:建临时环境 → 配置 → 验证 → 销毁"""
import subprocess
import sys
import json
def run(cmd, cwd=None):
"""执行命令,返回 (returncode, stdout, stderr)"""
result = subprocess.run(
cmd, shell=True, cwd=cwd,
capture_output=True, text=True, timeout=600
)
return result.returncode, result.stdout, result.stderr
def main():
test_dir = "terraform/envs/test"
# 1. 建测试环境
code, out, err = run("terraform apply -auto-approve", cwd=test_dir)
if code != 0:
print(f"TF apply 失败: {err}")
sys.exit(1)
try:
# 2. 获取测试环境主机信息
code, out, _ = run("terraform output -json", cwd=test_dir)
outputs = json.loads(out)
# 3. Ansible 配置
code, out, err = run(
"ansible-playbook -i inventory_terraform.py site.yml",
cwd="ansible"
)
if code != 0:
print(f"Ansible 执行失败: {err}")
sys.exit(1)
# 4. 验证服务可用性
ips = outputs.get("test_instance_ips", {}).get("value", [])
for ip in ips:
code, _, _ = run(f"curl -sf http://{ip}:80/healthz")
if code != 0:
print(f"健康检查失败: {ip}")
sys.exit(1)
print("✅ 集成测试通过")
finally:
# 5. 无论成功失败,都销毁测试环境
run("terraform destroy -auto-approve", cwd=test_dir)
if __name__ == "__main__":
main()
回滚策略
基础设施的回滚比应用回滚复杂得多。应用回滚是切镜像版本,但 Terraform apply 后创建了新资源、修改了配置,回滚意味着要回到之前的状态。
我推荐分层回滚策略:
| 故障范围 | 回滚方式 | 执行时间 | 影响范围 |
|---|---|---|---|
| Ansible 配置错误 | 回滚 Ansible 代码 → 重新执行 playbook | 2-5 分钟 | 只影响 OS 配置 |
| Terraform 资源配置错误 | 回滚 TF 代码 → terraform apply | 5-15 分钟 | 可能涉及资源重建 |
| 资源被误删 | 从 State 恢复 → terraform apply | 10-30 分钟 | 可能丢失数据 |
| 灾难性故障 | 从备份恢复 + 重新 apply + 配置 | 30 分钟以上 | 全量恢复 |
关键原则:先试 Ansible 回滚,不行再试 Terraform 回滚。因为 Ansible 回滚快、影响面小。只有基础设施本身有问题时才动 Terraform。
实战经验:在某个项目的 CI/CD 平台建设中,我们遇到过一次 Ansible playbook 的 template 变量引用错误,导致 20 台机器的 Nginx 配置文件渲染失败。因为流水线有
--diff输出,第一时间发现了问题。回滚方式很简单——git revert 掉那个提交,重新跑流水线,5 分钟内 20 台机器全部恢复。如果没有分层回滚策略,可能会去动 Terraform(重建实例),那就要 30 分钟以上,而且数据可能丢失。
六、性能对比与生产环境注意事项
工具性能对比
在 50 台云主机环境下的实测数据:
| 操作 | Terraform | Ansible | 说明 |
|---|---|---|---|
| 创建 50 台 ECS | 4-6 分钟 | — | 受云 API 限速影响 |
| 配置 50 台主机 | — | 8-12 分钟 | 受 SSH 连接和任务数影响 |
| 漂移检测 | 15-30 秒 | 3-5 分钟 | TF plan 快,Ansible check 慢 |
| 回滚配置 | 5-15 分钟 | 2-5 分钟 | Ansible 回滚更快 |
| 清理销毁 | 3-5 分钟 | — | TF destroy 受 API 限速影响 |
Ansible 大规模性能优化
当主机数量超过 100 台时,Ansible 的默认配置会成为瓶颈。以下是三个关键优化点:
1. 调整 fork 数
# ansible.cfg
[defaults]
forks = 50 # 默认是 5,50 台以上建议调到 30-50
fork 数控制 Ansible 同时连接多少台主机执行任务。默认 5 太保守了——50 台机器每个任务要分 10 批跑。调到 50 后,一轮就能全部覆盖。但不要无限调高——每 fork 一个进程都要占内存,200 台机器设 200 fork 可能把 Ansible 控制机的内存吃满。
2. 开启 SSH 多路复用
# ansible.cfg
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=300s
pipelining = True
SSH 多路复用(ControlMaster)让 Ansible 对同一台主机复用 SSH 连接,避免每个 task 都重新建立连接。pipelining = True 减少临时文件传输,直接通过 SSH 管道传输模块代码。这两个配置加起来,50 台机器的 playbook 执行时间能缩短 40-50%。
3. 用回调插件优化输出
# ansible.cfg
[defaults]
stdout_callback = yaml # 默认是 default,yaml 格式更易读
State 文件安全
Terraform State 文件包含基础设施的完整配置——IP 地址、实例 ID、甚至可能包含密码和密钥。这是 Terraform + Ansible 协同中安全风险最高的文件。
生产环境必须做到:
- State 存远程:用 OSS/S3 + DynamoDB(用于锁)作为 backend,不要把 State 文件留在本地磁盘
- 加密存储:开启 backend 的服务端加密(SSE-S3 或 SSE-KMS)
- 访问控制:State 文件的读写权限只给 CI/CD 服务账号和少数 SRE,其他人不给
- 敏感变量脱敏:用
sensitive = true标记 output 中的敏感字段,避免在日志中暴露
# Terraform backend 配置
terraform {
backend "oss" {
bucket = "sre-tfstate-prod"
prefix = "terraform/prod"
key = "terraform.tfstate"
region = "cn-hangzhou"
encrypt = true
acl = "private"
}
}
# 标记敏感输出
output "db_password" {
value = alicloud_rds_instance.main.password
sensitive = true # 不会在 terraform output 中显示
}
安全提醒:Ansible 侧的敏感数据也要加密。用 Ansible Vault 加密包含密码、密钥的变量文件。不要明文存储在任何地方——哪怕是在 CI 变量里,也要用最小权限原则限制谁能看到。(相关文章:Ansible Vault 密码管理)
总结
Terraform + Ansible 的协同不是"把两个工具放到一起"那么简单。核心是在五个决策点上做对:
职责边界:Terraform 管云资源生命周期,Ansible 管操作系统到应用层。不要用 Terraform provisioner 做复杂配置——它不在状态管理范围内,漂移了检测不到。
动态清单:用 Terraform State 作为 Ansible 动态清单的数据源,让清单自动同步。避免手动维护 hosts 文件——一旦弹性扩缩容,静态清单就废了。
两阶段流水线:Terraform 和 Ansible 拆成流水线的两个阶段,中间加验证关卡。不要用 provisioner 在 Terraform 里直接触发 Ansible——错误传播不可控,调试困难。
配置漂移治理:建立资源所有权矩阵,每项配置只有一个所有者。Terraform 定期 plan 检测漂移,Ansible 定期
--check检测偏离。IaC 测试体系:四层测试——代码检查、计划验证、集成测试、生产部署。在合并到 main 分支前,先在测试环境跑一遍完整的 apply + playbook。
这五个决策的共同逻辑是:隔离 + 所有权 + 自动化验证。两个工具各自管好自己的领域,通过自动化流水线连接,中间有验证关卡兜底。不要追求"一个工具搞定一切"——Terraform 试图管配置会变成 Terraform 写 shell 脚本,Ansible 试图管基础设施会变成 Ansible 调云 API。各用所长,边界清晰,协同才能真正发挥威力。
最后说一句:工具协同的本质是团队协作。Terraform 和 Ansible 的边界,其实是"基础设施团队"和"配置管理团队"的边界。如果这两个团队各自为政、没有共同的代码仓库和流水线,再好的工具设计也会被组织壁垒打碎。推行 IaC 协同时,先统一仓库、统一流水线、统一所有权规则——工具是次要的,流程和规则才是根本。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Ansible vs. Terraform — Red Hat Ansible 官方博客,阐述了 Ansible 作为编排器与 Terraform 协同的定位
- HashiCorp and Red Hat, better together — HashiCorp 官方博客,讨论了 Terraform 与 Ansible 在 Day 1 和 Day 2 运维中的分工与集成方向
- Working with dynamic inventory — Ansible Community Documentation — Ansible 官方文档,动态清单的工作原理与插件机制
- Terraform vs Ansible - Infrastructure as Code Showdown — 对比了 Terraform 和 Ansible 在安全合规、CI/CD 集成方面的差异,提供了 GitOps 流程的实战案例
- IaC 双引擎:Terraform + Ansible 完整最佳实践 — CSDN 博客,提供了 Terraform 输出注入 Ansible Inventory 的具体实现方案
- 告别手写 Inventory:Terraform 与 Ansible 在 Azure 上的联动 — CSDN 博客,动态清单在云环境中的自动归组实践
- tads-boilerplate — GitHub 开源项目,Terraform + Ansible + Docker Swarm 的完整脚手架示例