概述

凌晨两点,告警群炸了。一台刚上线两小时的 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-execlocal-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 Stateterraform-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=webRole=db 的标签,脚本读 state 时按 Role 分组,Ansible 就能直接用 hosts: webhosts: 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。这个方案看起来简单,但生产环境中有三个硬伤:

  1. 错误传播不可控:Terraform apply 成功了,但 Ansible playbook 失败了。此时资源已创建,但没配置,处于半成品状态。Terraform 的 provisioner 失败会导致整个 apply 标记为失败,但资源已经创建了——你没法回滚。

  2. 执行时间过长:Terraform apply 本身就要几分钟(等云 API 响应),再加上 Ansible 配置 50 台机器可能要 10-20 分钟。一个命令要等 25 分钟,中间任何中断都会导致状态不一致。

  3. 调试困难: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 → applyterraform plan
安全组规则Terraform修改 TF 代码 → PR → applyterraform plan
OS 内核参数Ansible修改 Ansible role → PR → playbookansible --check
用户和 SSH 密钥Ansible修改 Ansible role → PR → playbookansible --check
防火墙规则(主机内)Ansible修改 Ansible role → PR → playbookansible --check
应用配置文件Ansible修改 Ansible template → PR → playbookansible --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 代码 → 重新执行 playbook2-5 分钟只影响 OS 配置
Terraform 资源配置错误回滚 TF 代码 → terraform apply5-15 分钟可能涉及资源重建
资源被误删从 State 恢复 → terraform apply10-30 分钟可能丢失数据
灾难性故障从备份恢复 + 重新 apply + 配置30 分钟以上全量恢复

关键原则:先试 Ansible 回滚,不行再试 Terraform 回滚。因为 Ansible 回滚快、影响面小。只有基础设施本身有问题时才动 Terraform。

实战经验:在某个项目的 CI/CD 平台建设中,我们遇到过一次 Ansible playbook 的 template 变量引用错误,导致 20 台机器的 Nginx 配置文件渲染失败。因为流水线有 --diff 输出,第一时间发现了问题。回滚方式很简单——git revert 掉那个提交,重新跑流水线,5 分钟内 20 台机器全部恢复。如果没有分层回滚策略,可能会去动 Terraform(重建实例),那就要 30 分钟以上,而且数据可能丢失。

六、性能对比与生产环境注意事项

工具性能对比

在 50 台云主机环境下的实测数据:

操作TerraformAnsible说明
创建 50 台 ECS4-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 协同中安全风险最高的文件。

生产环境必须做到:

  1. State 存远程:用 OSS/S3 + DynamoDB(用于锁)作为 backend,不要把 State 文件留在本地磁盘
  2. 加密存储:开启 backend 的服务端加密(SSE-S3 或 SSE-KMS)
  3. 访问控制:State 文件的读写权限只给 CI/CD 服务账号和少数 SRE,其他人不给
  4. 敏感变量脱敏:用 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 的协同不是"把两个工具放到一起"那么简单。核心是在五个决策点上做对:

  1. 职责边界:Terraform 管云资源生命周期,Ansible 管操作系统到应用层。不要用 Terraform provisioner 做复杂配置——它不在状态管理范围内,漂移了检测不到。

  2. 动态清单:用 Terraform State 作为 Ansible 动态清单的数据源,让清单自动同步。避免手动维护 hosts 文件——一旦弹性扩缩容,静态清单就废了。

  3. 两阶段流水线:Terraform 和 Ansible 拆成流水线的两个阶段,中间加验证关卡。不要用 provisioner 在 Terraform 里直接触发 Ansible——错误传播不可控,调试困难。

  4. 配置漂移治理:建立资源所有权矩阵,每项配置只有一个所有者。Terraform 定期 plan 检测漂移,Ansible 定期 --check 检测偏离。

  5. IaC 测试体系:四层测试——代码检查、计划验证、集成测试、生产部署。在合并到 main 分支前,先在测试环境跑一遍完整的 apply + playbook。

这五个决策的共同逻辑是:隔离 + 所有权 + 自动化验证。两个工具各自管好自己的领域,通过自动化流水线连接,中间有验证关卡兜底。不要追求"一个工具搞定一切"——Terraform 试图管配置会变成 Terraform 写 shell 脚本,Ansible 试图管基础设施会变成 Ansible 调云 API。各用所长,边界清晰,协同才能真正发挥威力。

最后说一句:工具协同的本质是团队协作。Terraform 和 Ansible 的边界,其实是"基础设施团队"和"配置管理团队"的边界。如果这两个团队各自为政、没有共同的代码仓库和流水线,再好的工具设计也会被组织壁垒打碎。推行 IaC 协同时,先统一仓库、统一流水线、统一所有权规则——工具是次要的,流程和规则才是根本。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Ansible vs. Terraform — Red Hat Ansible 官方博客,阐述了 Ansible 作为编排器与 Terraform 协同的定位
  2. HashiCorp and Red Hat, better together — HashiCorp 官方博客,讨论了 Terraform 与 Ansible 在 Day 1 和 Day 2 运维中的分工与集成方向
  3. Working with dynamic inventory — Ansible Community Documentation — Ansible 官方文档,动态清单的工作原理与插件机制
  4. Terraform vs Ansible - Infrastructure as Code Showdown — 对比了 Terraform 和 Ansible 在安全合规、CI/CD 集成方面的差异,提供了 GitOps 流程的实战案例
  5. IaC 双引擎:Terraform + Ansible 完整最佳实践 — CSDN 博客,提供了 Terraform 输出注入 Ansible Inventory 的具体实现方案
  6. 告别手写 Inventory:Terraform 与 Ansible 在 Azure 上的联动 — CSDN 博客,动态清单在云环境中的自动归组实践
  7. tads-boilerplate — GitHub 开源项目,Terraform + Ansible + Docker Swarm 的完整脚手架示例