刚接手一个新环境时,我习惯先看看手上这批机器到底是什么底细——操作系统版本、CPU架构、内存多大、磁盘剩多少、Python装在哪个路径。放在以前,要么登录每台机器敲命令,要么写个脚本批量去跑,怎么都不够优雅。后来用上Ansible,我发现自己可以在完全不了解目标主机的情况下,直接写playbook去操作它,全靠facts在背后帮我兜底。
这篇文章就围绕Ansible facts展开,讲清楚它是什么、怎么用、能拿哪些关键信息,以及我在实际项目中踩过的那些坑。适合刚学Ansible不久、想真正掌控inventory的读者,也适合已经写了不少playbook但总觉得糊里糊涂的人。
1. 理解facts:Ansible为什么自带“主机体检报告”
1.1 facts到底是什么
Ansible在运行playbook时,默认会在执行任务之前,先把目标主机“扫”一遍,收集这台机器几乎所有的基本信息——主机名、操作系统、IP地址、内存和磁盘容量、CPU型号与核数、已安装的软件包、挂载的文件系统、系统启动时间等等。这一整套自动采集到的信息,就是facts。
听起来很抽象?你可以把它理解成Ansible给每台被管机器自动生成的一份“体检报告”。报告内容包括硬件配置、系统环境、网络状态等几十个大类、几百个小项。这些数据会被放进一个叫ansible_facts的变量结构中,后续的任务可以直接引用,比如“只在CentOS系统上执行某操作”“获取这台机器的默认IPv4地址并写入配置”。
我在刚开始学Ansible时有个认知误区:以为inventory文件里写的那些ansible_host、ansible_user就是facts。其实那是连接参数,作用在使用SSH连上目标机器。而facts是在连接成功之后,由Ansible主动在远端执行内置模块采集来的真实信息,两者完全不同。
1.2 孤立的facts与全局变量体系
facts之所以重要,首先是因为它让playbook具备了“感知环境”的能力。同一套playbook下发到不同的机器组,它会根据facts自动调整行为,不需要人为为每组机器各写一套逻辑。
其次,facts是临时性的。主机重启、网络变更、软件安装之后,facts都会随之变化。每次执行playbook时重新采集,能拿到机器的最新状态,这一点对自动化运维很关键。
另外,facts不只是给“自动化任务”用的。你可以利用gather_facts收集到的信息做资产盘点,把机器信息导出成清单,辅助运维报表或设备台账建设。
要理解facts的定位:它是Ansible从inventory静态描述走向动态感知的关键桥梁。
2. 如何采集facts并查看完整信息
2.1 使用setup模块手动采集
执行playbook时,facts默认是自动采集的。如果你想单独查看某一台机器或一组机器的facts,不需要写playbook,直接用setup模块就能完成:
ansible test_host -m setup这条命令会对test_host主机执行facts收集,并把完整结果以JSON格式打印到终端。由于信息量很大,终端输出会非常长。我常用参数来精确过滤需要的内容:
ansible test_host -m setup -a "filter=ansible_os_family" ansible test_host -m setup -a "filter=ansible_memtotal_mb"注意这里的filter是“包含匹配”,不是精确匹配。比如filter=ansible_eth会把所有网卡相关字段都匹配出来。想知道这台机器有哪些虚拟网卡、哪些物理网卡,嗯,用这个filter直接列出来了。
2.2 playbook中控制facts的采集
在playbook里,facts采集由gather_facts控制,默认值为true:
- hosts: all gather_facts: true tasks: - name: Show hostname debug: var: ansible_facts['hostname']新版Ansible(2.5+)推荐通过ansible_facts['hostname']这种形式访问,不用再裸用老式的ansible_hostname。两种写法目前都兼容,但建议在新项目里统一用ansible_facts字典,语义更清晰,也能避免变量名冲突。
还有一个值得注意的点:setup模块支持参数,所以你可以强制刷新缓存里的facts:
ansible test_host -m setup --flush-cache这个命令会忽略已有的facts缓存,强制重新采集一遍。后面讲缓存机制时,这个操作很有用。
2.3 gather_facts设为false的场景
不是每个playbook都需要facts。比如你只是批量重启服务、拷贝几个静态文件,机器的操作系统版本、内存情况根本用不上。这种情况下,把gather_facts关掉能省掉不少时间,尤其是管理几百台机器时,效果非常明显。
- hosts: all gather_facts: false tasks: - name: Copy a file copy: src: /local/path/file dest: /opt/file从反馈时间上看,关闭facts采集后,单台机器可以省下1到3秒。机器数量越多,节省的时间越可观。但有一个前提:任务里千万不能引用任何facts变量,否则会直接报“变量未定义”的错误,到时候还得回头开启采集。
2.4 哪些facts值得关注的一览表
facts字段非常多,为了不让初学者迷路,我整理了一份高频字段速查:
| 分类 | 字段名 | 含义 |
|---|---|---|
| 系统 | ansible_facts['distribution'] | 发行版名称,如CentOS、Ubuntu |
| 系统 | ansible_facts['distribution_version'] | 发行版版本号 |
| 系统 | ansible_facts['os_family'] | 系统家族,如RedHat、Debian |
| 网络 | ansible_facts['default_ipv4']['address'] | 默认路由对应的IPv4地址 |
| 网络 | ansible_facts['dns']['nameservers'] | 当前使用的DNS服务器列表 |
| 硬件 | ansible_facts['memtotal_mb'] | 总内存大小(MB) |
| 硬件 | ansible_facts['processor_cores'] | 物理CPU核心数 |
| 硬件 | ansible_facts['processor_count'] | 逻辑CPU数量 |
| 存储 | ansible_facts['mounts'] | 挂载点详细信息列表 |
| 存储 | ansible_facts['devices']['sda']['size'] | 第一块磁盘容量 |
| 时间 | ansible_facts['date_time']['iso8601'] | 系统当前时间(ISO格式) |
| 包管理 | ansible_facts['pkg_mgr'] | 使用的包管理器名称 |
这份表只是冰山一角。实际上facts里还包含每个网卡的MAC地址、IPv6地址、系统时区、SELinux状态、Python解释器路径等几百个字段。能用好其中二三十个,日常自动化任务基本就够了。
3. 核心字段解析:从系统到网络、从存储到虚拟化
3.1 系统基础信息判断
写playbook时,最常见的需求是“根据系统类型做不同处理”。比如同一套部署脚本,在RedHat系用yum安装依赖,在Debian系用apt。以下是两种判断写法:
- name: Install package according to OS family yum: name: httpd state: present when: ansible_facts['os_family'] == "RedHat" - name: Install package according to OS family apt: name: apache2 state: present when: ansible_facts['os_family'] == "Debian"使用场景再宽一点,比如某个任务只允许在CentOS 7以上执行,直接组合使用:
when: - ansible_facts['distribution'] == "CentOS" - ansible_facts['distribution_major_version'] is version('7', '>=')要留意一个问题:CentOS和RedHat的os_family都是RedHat,而某些Linux分支(比如Fedora)也是RedHat家族,但软件包管理上有差异。判断时需要组合多个字段,切忌只凭一个os_family下结论。
3.2 网络信息与IP地址获取
另一个高频需求是把目标机器的IP地址动态写入配置或服务注册。假设我要获取默认IPv4地址,可以这样写:
- name: Show default IP debug: msg: "The default IP is {{ ansible_facts['default_ipv4']['address'] }}"如果你有多个网卡要单独获取,字段会变成这样:
ansible_facts['ansible_eth0']['ipv4']['address']不过这里我需要特别提醒:在Ansible 2.12之前的版本里,ansible_facts['ansible_eth0']这种写法可能会因为内部变量排序问题出现错误,推荐使用无前缀形式ansible_facts['eth0']。我当时升级Ansible版本时踩过这个坑,查了很久才发现是facts索引的问题。
获取所有网卡的所有地址,可以对ansible_facts['interfaces']列表做遍历。这个字段返回的是所有网卡名称的列表,你可以结合具体网卡继续向下读取。
3.3 硬件资源与存储信息
在做容量规划或资源检查时,内存和磁盘是最常关注的。以下是我检查物理内存和磁盘的常用片段:
- name: Check memory threshold debug: msg: "Memory too low! Current {{ ansible_facts['memtotal_mb'] }} MB" when: ansible_facts['memtotal_mb'] < 2048磁盘挂在facts里比较复杂,mounts是一个列表,每个元素包含mount、device、fstype、size_total、size_available等信息。想判断根分区是否剩余空间不足,用when引用:
when: item.size_available < 1073741824这里size_available的单位是字节,不要和人的直觉搞混。1GB等于1073741824字节,但有些文件系统会按1024进制,直接用这个裸数字比较更准确。
如果你只关心磁盘设备本身而不关心挂载点,devices字典里按块设备名称排列,比如/dev/sda的对应键是sda,里面包含分区表、大小、旋转类型等字段。遍历时会非常详细。
3.4 内核与虚拟化、云环境信息
知道目标机器是虚拟机还是物理机,对不少运维场景很重要。比如某些驱动安装、性能调优任务需要区分虚拟化平台。可以通过virtualization_type字段做判断:
- name: Run only on virtual machines debug: msg: "This is a VM running on {{ ansible_facts['virtualization_type'] }}" when: ansible_facts['virtualization_type'] in ['VMware', 'kvm', 'xen']如果机器部署在云平台上,比如AWS或阿里云,facts里还有对应的ansible_facts['ec2']或厂商特定的元数据字段,能拿到的信息比物理机丰富不少。这在自动扩缩容或云资源自动化场景下很有用。
值得注意的是,不同平台收集到的facts覆盖范围不一样,比如在容器环境下virtualization_type可能是docker或containerd。写playbook时不要想当然认为某些字段一定存在,必要时先打印一遍实测数据。
4. facts的实战应用:条件判断、模板渲染与自省
4.1 条件判断让playbook“看懂”机器
facts最常见的用途就是配合when做条件执行。除了基础的OS判断,我举一个更实际的生产场景:某服务基于systemd管理,但老旧的CentOS 6并没有systemd,需要退回到service模块。可以这样实现:
- name: Start service with systemd systemd: name: my_service state: restarted when: ansible_facts['service_mgr'] == 'systemd' - name: Start service with sysvinit service: name: my_service state: restarted when: ansible_facts['service_mgr'] == 'sysvinit'这里的service_mgr字段记录了系统使用哪种服务管理器,Ansible收集facts时自动识别。利用这种机制,同一套playbook可以覆盖新旧两代系统,不用维护两套文件。
4.2 把facts渲染进模板文件
另一个高频场景是把facts变量带进Jinja2模板中。比如我要在每台机器上生成一个包含自身主机名和IP的配置文件:
# server.conf hostname={{ ansible_facts['hostname'] }} ip={{ ansible_facts['default_ipv4']['address'] }} platform={{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}对应的playbook任务如下:
- name: Generate config with facts template: src: server.conf.j2 dest: /etc/my_app/server.conf这种做法可以动态生成每台机器专属的配置,不需要在inventory里手工定义一堆变量。模板中还可以使用Jinja2过滤器,比如把内存大小从字节转成GB,或者对字符串做拼接处理,让配置更易读。
4.3 用facts做动态资产清单
我见过不少团队用Ansible做资产盘点。其实不需要额外开发,直接利用setup模块加上模板,就可以生成一份格式化的服务器清单。下面是一个思路:
- hosts: all tasks: - name: Generate inventory report local_action: module: template src: inventory_report.j2 dest: "./reports/{{ inventory_hostname }}_{{ ansible_facts['date_time']['date'] }}.txt"模板里按表格列出主机名、IP、CPU、内存、磁盘等信息。当机器数量增多时,还可以直接用add_host和group_by基于facts动态分组,实现真正的“inventory自适应”。
这种玩法对我来说帮助最大,因为它让静态的inventory变成了一套能实时反映真实环境的数据源,省去了额外开发CMDB的成本。
5. 性能优化与缓存:大型环境下的facts策略
5.1 默认采集的代价
每执行一次playbook,Ansible都会对每台目标机器生成一个新的SSH连接并运行fact收集。在管理几十台以内的机器时,这个开销并不明显;但当你管理几百上千台机器时,facts收集时间会成为整个playbook运行的大头。
我自己在一组约300台机器的环境里实测过,仅gather_facts阶段就要占用整个执行时间的三分之一左右。如果你的任务根本用不到facts,关掉它是零成本优化;如果必须要用,则需要考虑缓存方案。
5.2 启用facts缓存:redis方案
Ansible支持多种fact缓存后端,我用得最顺的是redis,适合中大型环境。配置方法在ansible.cfg中完成:
[defaults] gathering = smart fact_caching = redis fact_caching_timeout = 86400这里gathering = smart表示如果facts缓存存在且未过期,就直接使用缓存结果;只有缓存缺失或过期时才重新采集。fact_caching_timeout设置缓存有效期,单位为秒,86400就是24小时。
使用redis作为缓存后端,需要提前在控制机安装redis并启动服务。另外一个容易被忽略的细节:如果修改了目标机器的网络或系统配置,旧的facts缓存不会自动失效,需要手动执行ansible test_host -m setup --flush-cache来刷新,否则任务会一直基于旧数据运行。
如果你不想引入redis,用JSON文件做缓存也可以,适合小型环境:
[defaults] fact_caching = jsonfile fact_caching_connection = /tmp/ansible/facts_cache fact_caching_timeout = 86400这种方式配置更简单,不需要额外的服务依赖。
5.3 减少facts采集的另一条路:按需加载
除了缓存,我还会配合gather_facts: false,然后在具体任务里按需用setup模块手动采集个别机器,甚至用booleans精简。
setup模块还支持gather_subset参数,只采集特定子集,例如只采集硬件信息:
ansible test_host -m setup -a "gather_subset=hardware"gather_subset可用的值包括all、min、hardware、network、virtual等。min是最小集,速度最快。如果只需要少数几个字段,它比全量采集快出不少。
但注意:gather_subset和filter并不完全相同。filter是在收集结果之后做字段过滤,数据采集动作依然全量执行;gather_subset则是在源头控制采集范围,性能收益更大。
6. 自定义facts:让机器汇报私有状态
6.1 local facts机制
有些信息Ansible的默认采集是拿不到的,比如你只有通过某个脚本才能获取的业务自定义状态,像是数据库集群的主备角色、应用当前的运行模式。这时候可以用local facts把信息注入facts体系。
local facts的存放位置是目标机器的/etc/ansible/facts.d/目录。目录下的每个.fact文件会被Ansible在收集facts时解析,并把文件内容放进ansible_facts['local']字典中。
假设你写了一个脚本,输出以下JSON到/etc/ansible/facts.d/cluster_info.fact:
{ "role": "primary", "status": "healthy", "sync_lag_seconds": 0 }之后在playbook里就可以直接引用:
- name: Check cluster role debug: msg: "This node is {{ ansible_facts['local']['cluster_info']['role'] }}"local facts的价值是把业务信息变成Ansible原生可读的数据结构,既可以用在条件判断里,也可以渲染到模板中。
6.2 用INI格式定义local facts
local facts文件除了JSON,还支持INI格式。比如:
[web] port = 8080 ssl_enabled = false引用方式变为:
ansible_facts['local']['web']['port']INI格式更简洁,适合写少量键值对;JSON格式表达能力更强,适合嵌套结构。两种混用场景也存在,看团队习惯。
6.3 写local facts文件的注意事项
写local facts时,有几个坑我踩过:
- 文件扩展名必须是
.fact,但内容既可以是可以直接解析的静态数据,也可以是可执行脚本,Ansible会判断文件内容类型。 - 如果是脚本,需要确保该脚本有执行权限,并且以输出合法JSON为最终结果,否则会被Ansible当作普通文件尝试解析,报错率很高。
- local facts目录如果不存在,Ansible会自动创建,但目标机器必须有写权限。使用非root用户管理时,需要检查sudo规则。
- 脚本型local facts不能使用Jinja2模板渲染,因为它在facts收集阶段只是被执行。如果你需要动态渲染,建议把渲染好的配置文件用
template模块下发到.fact路径。
local facts用得好,可以让每台机器把自己最核心的业务状态主动告诉Ansible,而不用通过复杂的inventory变量或外部CMDB去同步。
7. 常见问题与排查技巧实录
7.1 为什么我拿不到某个facts字段
新手常遇到“明明在文档里看到这个字段,但执行playbook时报变量未定义”。原因通常有几个:
- Ansible版本差异。不同版本的facts字段名可能有调整,过滤字段时建议先跑一次
setup模块看实际输出,用哪个字段直接对照结果来。 - 平台差异。Windows、网络设备的facts结构和Linux完全不同,不能照搬Linux字段。
- 使用了
gather_facts: false但某个任务里又引用了facts变量。排查时先检查play的gather_facts设置。 - namespaced版本问题。老式写法
ansible_hostname在某些情况下还能用,但如果你用ansible_facts['ansible_hostname']反而可能取不到值,因为嵌套关系变了。推荐写ansible_facts['hostname']。
排查这类问题,最快的办法就是先用setup模块过滤一下,实际看看数据长什么样,而不是反复猜字段名。
7.2 变量优先级冲突:同名变量覆盖facts
在Ansible中,playbook中定义的变量优先级高于facts。如果你在vars里定义了一个server_ip,而facts里也有同名key,任务里引用时会优先使用vars里的值。这一点需要小心,避免引发“看起来没生效”的诡异现象。
建议给自定义变量起有区分度的名字,尽量不要和facts字段重合。如果确实需要覆盖,要清楚自己正在做什么,否则排查半天找不到原因。
7.3 gather_facts超时问题
管理大量机器时,gather_facts偶尔出现超时或连接中断。主要诱因是目标机器的SSH配置、网络延迟、Python版本差异等。可以从几方面入手:
- 控制机到目标机器的网络质量,如果SSH本身连接慢,facts采集自然慢。
- 目标机器的Python路径不一致时,Ansible会自动探测解释器,这会增加耗时。可以在ansible.cfg中显式设置
ansible_python_interpreter。 - 尝试用
gather_subset=min减少采集范围。 - 调高超时时间配置,如
timeout = 30。
实际项目中,我发现很多超时并不是Ansible本身慢,而是因为目标机器负载过高导致执行环境响应慢。这种情况下,适当调低并发数,用串行分批执行反而更稳定。
7.4 缓存中的facts过期
使用facts缓存后,有一个很容易被忽略的问题:目标机器变化了,Ansible却仍然按旧数据执行。比如新加了IP、扩容了内存、装了新网卡,但缓存没刷新。
这时候手动刷新单台机器:
ansible target_host -m setup --flush-cache刷新后重新跑playbook,就能看到新数据。
如果大批量刷新,注意控制并发,避免短时间内对大量目标机器发起密集的fact收集请求,导致目标机器或网络负载过高。
7.5 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 变量未定义 | 引用了不存在的facts字段 | 用setup模块过滤观察实际字段 |
| 与文档字段不一致 | Ansible版本差异 | 升级到较新版本或按实际输出为准 |
| 每次playbook执行很慢 | 未使用facts缓存 | 在ansible.cfg开启redis或jsonfile缓存 |
| 任务里取不到facts | gather_facts被设为false | 检查playbook开头gather_facts配置 |
| 修改了主机信息但没变化 | facts缓存未刷新 | 执行setup --flush-cache |
| local facts不生效 | 文件扩展名不对或没有执行权限 | 检查.fact后缀和权限 |
8. 经验总结:我的facts使用守则
写到这里,我把这几年使用facts的几条个人心得整理一下,不一定都是大道理,但都是实操换来的。
第一,不到万不得已不要全量打印facts。很多云平台的日志系统会收集完整执行输出,直接debug一个完整的ansible_facts会把几百行JSON灌进日志,排查问题反而困难。需要什么就filter什么。
第二,facts采集不是免费的午餐。如果你管理的主机比较多,且每个playbook都默认采集facts,批量任务的整体耗时会上涨得非常快。用不到facts的playbook,第一行就写gather_facts: false,按需开启。
第三,local facts是你的好朋友。当你觉得默认facts不够用时,先别急着引入复杂的外部系统,写一个脚本输出JSON放到facts.d目录,事情就解决了一大半。
第四,版本升级后务必回归测试facts用法。Ansible在2.5、2.8、2.12这几个版本里都对facts做了结构调整,老写法不一定在新版本还能跑。每次升级Ansible主版本,我都会先跑一遍setup模块的抽样测试,确认核心字段没有变化。
最后分享一个小技巧:在编写新的playbook时,我会先加一个临时的debug任务,把需要引用的字段先打出来跑一遍,确认无误后再删掉。这个方法帮我省掉了大量“靠猜字段名排错”的时间。