刚接手一个新环境时我习惯先看看手上这批机器到底是什么底细——操作系统版本、CPU架构、内存多大、磁盘剩多少、Python装在哪个路径。放在以前要么登录每台机器敲命令要么写个脚本批量去跑怎么都不够优雅。后来用上Ansible我发现自己可以在完全不了解目标主机的情况下直接写playbook去操作它全靠facts在背后帮我兜底。这篇文章就围绕Ansible facts展开讲清楚它是什么、怎么用、能拿哪些关键信息以及我在实际项目中踩过的那些坑。适合刚学Ansible不久、想真正掌控inventory的读者也适合已经写了不少playbook但总觉得糊里糊涂的人。1. 理解factsAnsible为什么自带“主机体检报告”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 filteransible_os_family ansible test_host -m setup -a filteransible_memtotal_mb注意这里的filter是“包含匹配”不是精确匹配。比如filteransible_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]新版Ansible2.5推荐通过ansible_facts[hostname]这种形式访问不用再裸用老式的ansible_hostname。两种写法目前都兼容但建议在新项目里统一用ansible_facts字典语义更清晰也能避免变量名冲突。还有一个值得注意的点setup模块支持参数所以你可以强制刷新缓存里的factsansible 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 默认采集的代价每执行一次playbookAnsible都会对每台目标机器生成一个新的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_subsethardwaregather_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 factslocal 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_subsetmin减少采集范围。调高超时时间配置如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缓存任务里取不到factsgather_facts被设为false检查playbook开头gather_facts配置修改了主机信息但没变化facts缓存未刷新执行setup --flush-cachelocal 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任务把需要引用的字段先打出来跑一遍确认无误后再删掉。这个方法帮我省掉了大量“靠猜字段名排错”的时间。