
Terraform 和 Ansible 经常一起出现在基础设施自动化方案中。一个常见解释是：Terraform 负责创建虚拟机，Ansible 负责登录虚拟机安装软件。这个说法适合作为入门，但没有解释为什么两者适合这样的分工，也容易让人误以为它们的能力边界就是「云资源」和「主机内部」。

两者都能调用云 API，也都能描述期望状态。更稳定的区分方式是看它们如何认识一个被管理的对象：Terraform 把对象建模为有身份、依赖关系和生命周期的资源；Ansible 面向一组目标执行 Playbook，通过任务把目标调整到指定状态。

<!--more-->

## 两者都属于 IaC 吗

HashiCorp 将 Terraform 定义为 Infrastructure as Code 工具：使用人类可读的配置文件定义云端和本地资源，并在资源的整个生命周期中进行供应和管理。Terraform 管理的对象既可以是计算、存储和网络，也可以是 DNS、SaaS 功能以及其他提供 API 的服务。完整定义见 [What is Terraform](https://developer.hashicorp.com/terraform/intro)。

Ansible 官方把 Ansible 定义为自动化工具。Playbook 可以声明本地或远程系统的期望状态，常见用途包括系统配置管理、应用部署、滚动更新和复杂任务编排。完整定义见 [Introduction to Ansible](https://docs.ansible.com/projects/ansible/latest/getting_started/introduction.html)。

从「用可版本化的代码管理基础设施」这个广义定义看，两者都可以用于 IaC。Red Hat 的官方对比也把 Terraform 和 Ansible 都放在 IaC 自动化体系中，但将 Terraform 的主要角色归为基础设施供应，将 Ansible 的主要角色归为配置、部署和编排。

因此，IaC 是一种工程实践，不是一条严格的产品分类线。只讨论「是不是 IaC」不足以解释两者的差异，需要继续看它们的执行模型。

## Terraform 如何管理资源

Terraform 配置中的核心对象是 `resource`。下面的配置声明 10 台 AWS EC2 实例：

```hcl
resource "aws_instance" "web" {
  count         = 10
  ami           = "ami-12345678"
  instance_type = "t3.small"
}
```

Terraform Provider 调用远端 API 创建实例，并在 state 中保存配置资源与真实对象之间的绑定关系，例如：

```text
aws_instance.web[0] <-> i-0123456789abcdef0
aws_instance.web[1] <-> i-0123456789abcdef1
```

根据 [Terraform state 文档](https://developer.hashicorp.com/terraform/language/state)，state 的首要用途是保存远端对象与配置中资源实例的绑定。没有这个映射，Terraform 下次运行时无法可靠判断哪台真实实例对应 `aws_instance.web[0]`。

一次典型运行会同时涉及三类信息：

1. 配置描述期望管理的资源及其参数。
2. State 保存 Terraform 对资源身份和元数据的记录。
3. Provider 从云平台或其他服务读取远端对象，并调用 API 实施变更。

`terraform plan` 根据这些信息生成执行计划，列出需要创建、原地更新、替换或销毁的资源。Terraform 还会构建资源依赖图，按照依赖顺序执行操作，并尽量并行处理互不依赖的资源。

例如，把实例数量从 10 改成 8 后，Terraform 能判断应该销毁哪两个实例；修改某个不能原地更新的参数时，Provider 可以要求 Terraform 替换对应实例。这里管理的不是一串 API 调用，而是一组具有稳定身份和生命周期的资源。

## Ansible 如何管理目标系统

Ansible 的核心模型由三部分组成：

- Inventory 定义本次自动化涉及哪些主机、设备或分组。
- Playbook 定义对哪些目标执行哪些 Play。
- Task 调用 Module 检查或修改目标状态。

例如，在 `web` 组的机器上安装并启动 Nginx：

```yaml
- name: Configure web servers
  hosts: web
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Start nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true
```

`package` 和 `service` 模块会读取目标机器的当前状态。Nginx 已经安装且服务状态符合要求时，任务通常不会重复修改系统。这就是 Ansible 官方强调的幂等性。

但幂等性是 Module 对单个任务语义的实现，不等于 Terraform 的资源生命周期管理。Ansible 可以收集 Facts、缓存信息，并由 Automation Platform 保存 Inventory 和执行记录，但 Ansible Core 通常不会持久维护一份「Playbook 对象与所有远端对象的身份绑定和所有权图」。每次执行仍然以 Inventory 选出的目标和当前 Playbook 中的任务为中心。

Ansible 也不只管理 Linux 服务器。Inventory 中的目标可以是网络设备、Windows 主机、云服务或本机；Module 既可以通过 SSH、WinRM 等连接修改目标，也可以直接调用 API。

## 删除一段代码时发生什么

删除声明后的行为最能体现两种模型的区别。

假设从 Terraform 配置中删除一个仍在 state 里的资源：

```hcl
# 删除整个 aws_instance.web 资源块
```

Terraform 会把它理解为「这个受管理资源不应再存在」，并在执行计划中提出销毁远端对象。若只想停止管理而保留真实资源，需要显式把资源移出 state，而不是直接删除配置后执行 `apply`。

再看 Ansible。假设从 Playbook 中删除安装 Nginx 的任务：

```yaml
# 删除 Install nginx 任务
```

Ansible 下次运行时只是不再执行这个任务，不会根据历史执行记录自动卸载 Nginx。如果目标状态是删除软件，需要明确写出反向操作：

```yaml
- name: Remove nginx
  ansible.builtin.package:
    name: nginx
    state: absent
```

两种行为背后的差异是：Terraform 持续跟踪纳入 state 的资源，并根据配置计算其生命周期；Ansible 执行当前 Playbook 中存在的任务，不会自动推导已经移除任务的反向操作。

```mermaid
flowchart TB
    subgraph Terraform
        TFConfig["配置中的资源"]
        TFState["State 中的资源绑定"]
        Remote["远端真实资源"]
        Plan["Plan：创建 / 更新 / 替换 / 销毁"]
        TFConfig --> Plan
        TFState --> Plan
        Remote --> Plan
    end

    subgraph Ansible
        Inventory["Inventory：选择目标"]
        Playbook["Playbook：定义任务"]
        Current["目标当前状态"]
        Execute["Module：检查并执行任务"]
        Inventory --> Execute
        Playbook --> Execute
        Current --> Execute
    end
```

## 几个容易混淆的区别

### Terraform 是声明式，Ansible 是命令式

这个说法不准确。

Terraform 配置确实主要描述基础设施的最终状态。Ansible Playbook 有明确的任务顺序，也能执行任意命令，因此可以表达过程；但 `package: state=present`、`service: state=started` 等 Module 同样声明了期望状态，并具有幂等语义。

更准确的说法是：Terraform 的核心是根据资源图规划生命周期变更；Ansible 的核心是按 Playbook 对目标执行任务，其中许多任务以幂等方式收敛状态。

### Terraform 有状态，Ansible 无状态

这个说法可以帮助记忆，但要加上范围。

Terraform 必须使用 state 保存资源绑定，这是其核心工作模型的一部分。Ansible 可以缓存 Facts、保存执行结果，也可以使用外部系统保存 Inventory，因此不能说它完全不保存任何状态。关键在于 Ansible 没有用一份等价于 Terraform state 的持久资源所有权图来计算完整生命周期。

### Terraform 管云，Ansible 管服务器

这只是最常见的分工，不是能力边界。

Terraform 可以管理 Kubernetes、GitHub 仓库和 SaaS 配置；Ansible 也可以创建云资源、配置网络设备或调用服务 API。真正需要考虑的是任务更接近资源生命周期，还是更接近配置变更和操作流程。

### Terraform 对应不可变基础设施，Ansible 对应可变基础设施

这是一种常见倾向，也不是严格定义。Terraform 更适合在不能原地修改时替换资源；Ansible 更擅长在现有机器上更新软件和配置。不过，Terraform Provider 可以原地更新资源，Ansible 也可以构建镜像或替换实例。是否采用不可变基础设施仍然取决于整体交付方案。

## 两者如何协作

一个典型流程是 Terraform 创建基础设施，Ansible 接管系统配置：

```text
Terraform
├── 创建 VPC、子网和安全组
├── 创建 10 台虚拟机
├── 创建负载均衡器
└── 输出实例地址
          │
          ▼
Ansible Inventory
          │
          ▼
Ansible Playbook
├── 创建系统用户
├── 安装 Nginx
├── 下发配置文件
└── 启动和更新服务
```

Red Hat 在 [Ansible vs. Terraform](https://www.redhat.com/en/topics/automation/ansible-vs-terraform) 中将这种分工描述为：Terraform 负责 Day 0 的基础设施供应，Ansible 负责 Day 1 的系统配置，并继续承担 Day 2 的补丁、升级和配置变更。

实现两者之间的交接有多种方式：

- Terraform 输出主机地址，生成或更新 Ansible Inventory。
- Ansible 使用云厂商的 Dynamic Inventory 插件直接查询实例和标签。
- CI/CD 系统先执行 `terraform apply`，成功后再运行 `ansible-playbook`。
- Ansible Automation Platform 编排包含 Terraform 在内的完整操作流程。

小规模环境可以直接使用 Terraform output 生成 Inventory。环境扩大以后，根据云标签动态发现主机通常更容易维护，也避免把易变 IP 长期复制在多个文件中。

协作时还需要明确所有权。Terraform 创建的云资源尽量继续由 Terraform 管理，避免 Ansible 在没有同步 Terraform 配置和 state 的情况下修改同一组生命周期属性。否则下一次 `terraform plan` 可能把这些修改识别为漂移并尝试恢复。

HashiCorp 也建议谨慎使用 Terraform 的 `local-exec`、`remote-exec` 等 Provisioner。Terraform 无法可靠建模任意脚本的行为和副作用，机器初始化应优先使用镜像、cloud-init 或专用配置管理工具。参见 [Terraform Provisioners](https://developer.hashicorp.com/terraform/language/provisioners)。

## 本地实验：Terraform 创建容器，Ansible 安装 Nginx

Docker 不能用来创建虚拟机。Docker Provider 创建的是容器、镜像和 Docker 网络，容器与虚拟机的隔离模型不同。不过，容器启动快，适合在本机演示 Terraform 和 Ansible 如何交接目标。

这个实验完成以下操作：

1. Terraform 创建一个 bridge 网络和 3 个 Debian 容器。
2. Terraform 把容器名称输出为 Ansible Inventory。
3. Ansible 进入 3 个容器，安装并启动 Nginx。
4. 本机通过 `8080`、`8081` 和 `8082` 端口访问它们。

Terraform Docker Provider 当前提供 `docker_network` 和 `docker_container` 等资源，具体字段可参考 [Docker Provider 文档](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs)；Ansible 使用 [`community.docker.docker` connection](https://docs.ansible.com/projects/ansible/latest/collections/community/docker/docker_connection.html)，通过本机 Docker CLI 对已有容器执行任务。

### 前置条件

本机需要安装并启动：

- Docker Engine 或 Docker Desktop。
- Terraform。
- Ansible。
- `community.docker` collection。

安装 Ansible collection：

```bash
ansible-galaxy collection install community.docker
```

示例目录只需要两个手写文件。`inventory.ini` 在 Terraform 执行后生成：

```text
terraform-ansible-demo/
├── main.tf
└── configure.yml
```

### 用 Terraform 创建网络和容器

完整配置可以直接下载：[main.tf](https://jimyag.com/posts/terraform-vs-ansible/files/main.tf)。内容如下：

```hcl
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "4.5.0"
    }
  }
}

provider "docker" {}

resource "docker_network" "lab" {
  name = "tf-ansible-lab"
}

resource "docker_image" "node" {
  name = "debian:12-slim"
}

resource "docker_container" "web" {
  count = 3

  name    = "web-${count.index + 1}"
  image   = docker_image.node.image_id
  command = ["sleep", "infinity"]

  networks_advanced {
    name = docker_network.lab.name
  }

  ports {
    internal = 80
    external = 8080 + count.index
    ip       = "127.0.0.1"
  }
}

output "ansible_inventory" {
  value = join("\n", concat(
    ["[web]"],
    [for container in docker_container.web :
      "${container.name} ansible_connection=community.docker.docker"
    ]
  ))
}
```

这里已经能看到 Terraform 的资源关系：3 个 `docker_container.web` 实例依赖 `docker_image.node` 和 `docker_network.lab`。容器端口映射到本机回环地址，不对局域网开放。

执行 Terraform，并把 output 转成 Inventory 文件：

```bash
terraform init
terraform plan
terraform apply
terraform output -raw ansible_inventory > inventory.ini
```

生成的 `inventory.ini` 内容如下：

```ini
[web]
web-1 ansible_connection=community.docker.docker
web-2 ansible_connection=community.docker.docker
web-3 ansible_connection=community.docker.docker
```

这里没有配置 SSH。Ansible connection plugin 把 Inventory 中的主机名当作容器名，通过 `docker exec` 进入目标容器。

### 用 Ansible 安装 Nginx

完整 Playbook 可以直接下载：[configure.yml](https://jimyag.com/posts/terraform-vs-ansible/files/configure.yml)。内容如下：

```yaml
- name: Configure web containers
  hosts: web
  gather_facts: false
  vars:
    ansible_python_interpreter: /usr/bin/python3
  pre_tasks:
    - name: Install Python required by Ansible modules
      ansible.builtin.raw: |
        if command -v python3 >/dev/null 2>&1 && python3 -c 'import apt' >/dev/null 2>&1; then
          echo PYTHON_READY
        else
          apt-get update
          DEBIAN_FRONTEND=noninteractive apt-get install -y python3 python3-apt
          echo PYTHON_INSTALLED
        fi
      register: python_bootstrap
      changed_when: "'PYTHON_INSTALLED' in python_bootstrap.stdout"

    - name: Gather facts after installing Python
      ansible.builtin.setup:

  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Write home page
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        content: "served by {{ inventory_hostname }}\n"
        mode: "0644"

    - name: Start nginx when it is not running
      ansible.builtin.shell: |
        if test -f /run/nginx.pid && kill -0 "$(cat /run/nginx.pid)" 2>/dev/null; then
          echo NGINX_RUNNING
        else
          nginx
          echo NGINX_STARTED
        fi
      register: nginx_start
      changed_when: "'NGINX_STARTED' in nginx_start.stdout"
```

多数 Ansible Module 需要目标中存在 Python，而 `debian:12-slim` 默认没有提供完整的 Python 环境。因此第一个 `raw` 任务先安装 `python3` 和 `python3-apt`，再执行 `setup`、`apt` 和 `copy` 等 Module。这个容器没有 systemd，所以示例使用 `nginx` 命令启动进程，并显式判断是否已经运行。

运行 Playbook：

```bash
ansible-playbook -i inventory.ini configure.yml
```

然后验证 3 个容器：

```bash
curl http://127.0.0.1:8080
curl http://127.0.0.1:8081
curl http://127.0.0.1:8082
```

预期分别返回：

```text
served by web-1
served by web-2
served by web-3
```

再次执行 Playbook 时，`apt`、`copy` 和启动任务应该保持 `ok`，这可以观察 Ansible Module 的幂等行为。实验结束后由 Terraform 销毁容器和网络：

```bash
terraform destroy
```

### 实际验证结果

本文示例在 2026 年 8 月 23 日使用以下版本完成了本地端到端验证：

- Terraform `1.15.9`，运行于官方 `linux_arm64` 容器。
- Terraform Docker Provider `4.5.0`。
- Docker client/server `29.4.0`。
- ansible-core `2.21.3`。
- community.docker `5.2.2`。

`terraform fmt -check`、`terraform validate`、`plan` 和 `apply` 均成功，首次 Apply 创建了 5 个资源。Ansible 首次执行时，每个容器均为 `ok=5 changed=4`；三个 HTTP 地址分别返回 `served by web-1`、`served by web-2` 和 `served by web-3`。第二次执行 Playbook 时，三个容器均为 `ok=5 changed=0`。

Ansible 完成容器内部配置后，再次执行 `terraform plan` 得到 `No changes`。这也验证了前面的边界：Terraform 管理容器、网络和镜像的生命周期，但不会把 Ansible 安装到容器可写层中的 Nginx 识别为 Terraform 配置变化。最后执行 `terraform destroy`，5 个资源均成功销毁，并确认测试容器和网络没有残留。

这个实验展示的是两个控制面的交接，不是容器部署的生产实践。Ansible 安装的软件写入容器可写层，Terraform 不会把这些包建模为 `docker_container` 的属性；容器被替换后，Nginx 也会丢失。生产环境通常应当用 Dockerfile 构建包含应用和依赖的不可变镜像。若要演示真实虚拟机，可以把 Docker Provider 换成 AWS、OpenStack 或 libvirt Provider，再让 Ansible 通过 SSH 连接 Terraform 创建的实例，整体交接方式不变。

## 应该选择哪个工具

如果主要问题是下面这些，通常优先考虑 Terraform：

- 某个云资源是否应该存在。
- 资源之间有什么依赖关系。
- 修改参数时应该更新还是替换资源。
- 如何预览一批资源的创建、修改和销毁计划。
- 如何长期管理资源身份和基础设施漂移。

如果主要问题是下面这些，通常优先考虑 Ansible：

- 在一组目标上安装哪些软件。
- 配置文件应该包含什么内容。
- 服务应该以什么状态运行。
- 发布时需要按照什么顺序操作多台机器。
- 如何编排补丁、备份、部署和故障处理流程。

很多生产环境不需要二选一。Terraform 管理资源生命周期，Ansible 管理目标配置和操作流程，二者之间通过明确的输出和 Inventory 交接。这个边界也解释了最初的例子：Terraform 创建 10 台虚拟机，Ansible 在这 10 台机器中安装 Nginx，是符合两者核心模型的分工。

