Ansible Docker 动态清单
Ansible Docker 动态清单
[TOC]
概述
动态清单就是让 Ansible 自己去问 Docker:“你现在有哪些容器在跑?“,把答案自动当成主机列表
环境准备
| 组件 | 情况 |
|---|---|
| 虚拟机 | Ubuntu 24.04,IP 192.168.122.211 |
| Docker | 已安装(/var/run/docker.sock) |
| Ansible | pipx 安装(/root/.local/bin/) |
| 容器 | myweb(nginx)、mypy(python |
'当前容器状态'root@m01 ~# docker psCONTAINER ID IMAGE NAMES973a18250fe8 python:alpine3.24 mypyb009b9d26bd3 nginx:latest myweb
1)初始动态清单'第一个动态清单'# 让 Ansible 自动发现这两个正在运行的容器root@m01 ~# pwd/rootmkdir -p /root/inventoryroot@m01 ~# mkdir -p /root/inventoryroot@m01 ~# cat > /root/inventory/docker.yml << 'EOF'plugin: community.docker.docker_containersdocker_host: unix:///var/run/docker.sockEOFroot@m01 ~# ansible-inventory -i ~/inventory/docker.yml --list❌ 报错✅ 插件的判断规则是:文件名必须包含 `.docker.` 这个子串🌰 `docker.yml` 里没有 `.docker.`,插件不认识它,Ansible 就把它当普通 YAML 清单去解析
2)改正root@m01 ~# mv /root/inventory/docker.yml /root/inventory/docker.docker.ymlroot@m01 ~# ansible-inventory -i ~/inventory/docker.docker.yml --list'注意名字是已经更正过的名字'Failed to parse inventory with 'auto' plugin.Failed to import the required Python library (requests)😭 还是不对 ❌ 👆现象:文件名改成 docker.docker.yml 后,插件已经能识别了,但仍然报错:
排错
requests 库缺失 ✅
'排查过程'1)怀疑 docker SDK 没装root@m01 ~# python3 -c "import docker"ModuleNotFoundError: No module named 'docker'✅ 确实没装,但不是首要问题'这个python3是系统里面的,不是Ansible中的'
2)怀疑 collection 没装'Collection(集合)——它是 Ansible 的包管理单位'root@m01 ~# ansible-galaxy collection list | grep dockercommunity.docker 5.2.0# 装了(v5.2.0),排除
3)开详细模式看真实错误✅ '-vvv' --》 定位到 requests 缺失oot@m01 ~# ansible-inventory -i /root/inventory/docker.docker.yml --list -vvv关键行:ModuleNotFoundError: No module named 'requests'"注意报错里的 Python 路径:"# /root/.local/share/pipx/venvs/ansible/bin/python根因:pipx 安装的 Ansible 跑在独立的虚拟环境里(/root/.local/share/pipx/venvs/ansible/)
community.docker插件调用 Docker API 时底层依赖 Python 的requests库- 但 pipx 的虚拟环境里没有得用
pipx inject专门给 Ansible 的环境装一份 - 他没有
requests库 &docker SDK
- 但 pipx 的虚拟环境里没有得用
关键认知:pipx 安装的工具,它的 Python 虚拟环境是隔离的
-
Ubuntu 24.04 的系统 Python 是”externally managed”,直接
pip install根本不让装 -
就算用
--break-system-packages强行装进系统 Python,pipx 里的 Ansible 也照样看不到- 必须用
pipx inject把依赖注入进 Ansible 自己的虚拟环境
- 必须用
4)修复:✅ 把 requests 和 docker SDK 注入到 pipx 的 Ansible 虚拟环境root@m01 ~# pipx inject ansible requests injected package requests into venv ansibledone! ✨ 🌟 ✨root@m01 ~# pipx inject ansible docker injected package docker into venv ansibledone! ✨ 🌟 ✨
哈哈...
5)验证:root@m01 ~# ansible-inventory -i /root/inventory/docker.docker.yml --list✅ 正常输出容器清单{ "_meta": { "hostvars": { "mypy": { "ansible_connection": { "__ansible_unsafe": "community.docker.docker_api" }, .................... }, "myweb": { "ansible_connection": { "__ansible_unsafe": "community.docker.docker_api" }, ..................... }-
光有主机清单,
ansible_connection也没配,还不能执行任何任务 -
接下来就要引入过滤器和连接配置,让这个清单真正可用
过滤器(filters)
- 需求: 容器停了的不应该出现在清单里 —> 只想要”正在运行”的
初尝试——直接翻车
plugin: community.docker.docker_containersdocker_host: unix:///var/run/docker.sock
filters: - include: "status=running"root@m01 ~# vim ./inventory/docker.docker.ymlroot@m01 ~# ansible-inventory -i ~/inventory/docker.docker.yml --listCould not evaluate filter condition 'status=running' ❌过滤器表达式是 Jinja2 语法,status=running 有 3 个问题:
| 错误 ❌ | 原因 | 修正 |
|---|---|---|
status | 没有这个变量;Docker inspect 的 State 键经 slugify 后叫 docker_state | docker_state |
= | Jinja2 里 = 是赋值,比较用 == | == |
running | 裸词在 Jinja2 里是变量名(未定义),字符串必须加引号 | "running" |
三条一起改掉 → 'docker_state == "running"',但往下看——
修正后”通了”——其实没生效
filters: - include: 'docker_state == "running"'外层单引号,里面双引号 "running"
root@m01 ~# ansible-inventory -i ~/inventory/docker.docker.yml --graph# 树状图方式显示@all: |--@ungrouped: | |--mypy | |--myweb# 两个都在,看起来"对了"问题出在两层:
第一层:docker_state 不是字符串,是 Docker inspect 返回的 对象:
docker_state = { "Status": "running", "Running": true, "Paused": false, ...}所以 docker_state == "running" = 拿对象跟字符串比 → 永远 False —> 这条 include 从来没命中过
第二层:既然条件永远不命中,为什么两个容器还在?停掉一个试试:
root@m01 ~# docker stop mypyroot@m01 ~# ansible-inventory -i ~/inventory/docker.docker.yml --graph@all: |--@ungrouped: | |--mypy # ← 停了还在!说明过滤器完全没干活 | |--myweb→ 引出核心机制 ↓
核心机制:include / exclude + 默认放行
filters 是一个列表,每一项有且仅有 include 或 exclude 之一,对每个容器独立求值:
对【每个容器】独立执行: 遍历 filters 列表(从上到下): include 条件 = True → 入选,立即停止 exclude 条件 = True → 排除,立即停止 ⚠️ 走到列表末尾,'没有一条为 True' → 入选(默认放行)| 关键字 | 角色 | 条件成立时 |
|---|---|---|
include | 白名单 | 入选,不再看后面的 filter |
exclude | 黑名单 | 排除,不再看后面的 filter |
以 include: '"mypy" in docker_name' 为例,两个容器各走各的:
mypy: filter[0] include: "mypy" in "mypy" → True → 入选 ✅
myweb: filter[0] include: "mypy" in "myweb" → False → 继续 走到头了,没有一条 True → 默认放行 → 混进来了!加上 exclude: true 兜底:
myweb: filter[0] include: "mypy" in "myweb" → False → 继续 filter[1] exclude: true → True → 排除 ❌ ← 被拦住!📌 mypy 在 filter[0] 就入选了,根本走不到这里必须有一条为 true,否则就会混进来。
- 所有 filter 条件都是
false→ 走到末尾无结论 → 默认放行 exclude: true= 永远为true的守门员,保证最后一定有一条命中include只能主动请人进来,拦不住不匹配的
正确写法
只要运行中的容器——两种方式:
| 方式 | 写法 | 说明 |
|---|---|---|
| 排除法 ✅ 推荐 | exclude: 'not docker_state.Running' | Running 是==布尔值==,逐个推演:stopped → Running=false → not false=true → 命中 → 踢掉 ❌running → Running=true → not true=false → 不命中 → 留下 ✅一行搞定 |
| include + 兜底 | include: 'docker_state.Running' exclude: true | running → Running=true → 第 1 条命中 → 入选stopped → Running=false → 第 1 条不命中 → 走到第 2 条 exclude: true 命中 → 踢掉 |
单写 include: 'docker_state.Running' 不行——stopped 的 Running 为 false,include 不成立 → ==默认放行==
'验证排除法'root@m01 ~# docker stop mypyroot@m01 ~# cat > /root/inventory/docker.docker.yml << 'EOF'plugin: community.docker.docker_containersdocker_host: unix:///var/run/docker.sock
filters: - exclude: 'not docker_state.Running'
compose: ansible_connection: community.docker.dockerEOF
root@m01 ~# ansible-inventory -i ~/inventory/docker.docker.yml --graph@all: |--@ungrouped: | |--myweb # ← mypy 被正确排除 ✅过滤器(filters)字符串比较:用 in 别用 ==
由于 __ansible_unsafe 包装,过滤器中 == 比较字符串不可靠:
| 表达式 | 可用? | 替代 |
|---|---|---|
docker_name == "mypy" | ❌ | "mypy" in docker_name |
docker_name.startswith("my") | ❌ | "my" in docker_name |
docker_state.Status == "running" | ❌ | "running" in docker_state.Status ⚠️ ==字符串== |
"mypy" in docker_name | ✅ | — |
docker_state.Running ⚠️ ==布尔值== | ✅ | — |
记一个规则:==过滤器(filters)==中字符串判断用 in 别用 == —> ==groups 不受此影响==(走另一条求值路径)
组合条件(and / or)
filters: - include: 'docker_state.Running and ("nginx" in docker_config.Image)' - exclude: trueand— 两边都满足才留下or— 任意一边满足就留下not— 取反
root@m01 ~# ansible-inventory --graph@all: |--@ungrouped: | |--myweb # ← 只有 nginx + 运行中| 容器 | Running | docker_config.Image | 结果 |
|---|---|---|---|
| myweb | true | nginx:latest | ✅ |
| mypy | true | python:3.12-alpine | ❌ |
| old-nginx (停) | false | nginx:1.27 | ❌ |
完整示例:
filters: - include: '"my" in docker_name' # 只要名字含 my 的 - exclude: '"test" in docker_name' # 排除含 test 的 - exclude: true # 兜底连接容器
发现容器了,试试能不能”连上”——用 ping 模块测试
配置 ansible.cfg
root@m01 ~# ansible --version | grep 'config file' config file = /etc/ansible/ansible.cfg# 现在使用的是系统级配置文件
1)创建一个项目级 `ansible.cfg`,省得每次敲 `-i`root@m01 ~# cat > /root/ansible.cfg << 'EOF'[defaults]inventory = /root/inventory/docker.docker.ymlinterpreter_python = auto_silent
[privilege_escalation]become = truebecome_method = sudobecome_user = rootbecome_ask_pass = falseEOF报错 A:sudo: not found
root@m01 ~# ansible all -m pingmypy | FAILED! => { "module_stderr": "/bin/sh: sudo: not found", "msg": "Module result deserialization failed: ...", "rc": 127}myweb | FAILED! => { "module_stderr": "/bin/sh: 1: sudo: not found", ...}所有容器都失败 ❌
为什么? ansible.cfg 里配了 become = true(开启 sudo 提权),Ansible 每次执行命令前都会先敲 sudo
但 Docker 容器里根本没有 sudo 这个命令——追求体积小的镜像(nginx、alpine)不可能装 sudo
⚠️ 更重要的是:容器里的进程本身就是 root,根本不需要提权
管理虚拟机 vs 管理容器,配置文件是不一样的:
| 配置项 | 虚拟机 | 容器 |
|---|---|---|
| 连接方式 | SSH | Docker API |
| 默认用户 | test(普通用户) | root |
| 需要 sudo? | 是 | 否 |
| privilege_escalation | 需要 | 不要配! |
✅ 修正
root@m01 ~# cat > /root/ansible.cfg << 'EOF'[defaults]inventory = /root/inventory/docker.docker.ymlinterpreter_python = auto_silentEOF🐎 去掉整个 `[privilege_escalation]` 区块报错 B:/usr/bin/python3: not found
修正后再试:
root@m01 ~# ansible all -m pingmyweb | FAILED! => { "module_stderr": "/bin/sh: 1: /usr/bin/python3: not found\n", "rc": 127}mypy | SUCCESS => { "ping": "pong"}✅ python 容器通了,nginx 容器还是失败,但'报错内容变了'——> 不是 `sudo` 问题了,是找不到 Python为什么❓
ping模块本质上是一个 Python 脚本- Ansible 把它上传到目标主机,然后找一个 Python 解释器来执行
ansible ping 模块的执行过程: 1. 把 ping 模块(Python 代码)传进容器 2. 在容器里找 Python 解释器(由 interpreter_python 配置决定) 3. 用 Python 执行这段代码 4. 返回结果mypy(python镜像)→ 自带 Python → ✅ myweb(nginx 镜像)→ 没有 Python → ❌
Docker 连接插件(community.docker.docker)只替换了传输层(“怎么把命令送进容器”),并没有替换执行层(“用什么来跑模块”)
SSH 连接: ansible --网络--> SSH服务 --> 容器(需要 Python)Docker 连接: ansible --docker exec--> 容器(同样需要 Python) ↑ 唯一的例外:'raw 模块'✅ 解决方案:raw 模块
raw 模块不传 Python 代码,直接跑 shell 命令——不需要 Python:
root@m01 ~# ansible all -m raw -a 'echo hello'mypy | CHANGED | rc=0 >>hellomyweb | CHANGED | rc=0 >>hello==============================================root@m01 ~# ansible all -m raw -a 'whoami'mypy | CHANGED | rc=0 >>rootmyweb | CHANGED | rc=0 >>root==============================================root@m01 ~# ansible myweb -m raw -a 'nginx -v'myweb | CHANGED | rc=0 >>nginx version: nginx/1.31.1适用场景:操作 nginx、redis、mysql 等镜像——这些镜像追求体积小,不带 Python
✅ 日常管理它们就靠 raw 模块
汇总:
| 模块 | 需要 Python? | 能用于 nginx 容器? |
|---|---|---|
raw | ❌ 不需要 | ✅ |
ping | ✅ 需要 | ❌ |
command / shell | ✅ 需要 | ❌ |
setup(收集信息) | ✅ 需要 | ❌ |
'在nginx容器中安装python'1)进入判断系统root@m01 ~# docker exec -it myweb /bin/bashroot@ee5d00b5904c:/# ls /etc/ | grep -E 'apt|yum'aptroot@ee5d00b5904c:/# cat /etc/apt/sources.list.d/debian.sourcesTypes: deb# http://snapshot.debian.org/archive/debian/20260610T000000ZURIs: http://deb.debian.org/debian 📌............Types: deb# http://snapshot.debian.org/archive/debian-security/20260610T000000ZURIs: http://deb.debian.org/debian-security 📌
2)替换源root@ee5d00b5904c:/# sed -i 's#deb.debian.org#mirrors.aliyun.com#g' /etc/apt/sources.list.d/debian.sourcesroot@ee5d00b5904c:/# cat /etc/apt/sources.list.d/debian.sourcesTypes: deb# http://snapshot.debian.org/archive/debian/20260610T000000ZURIs: http://mirrors.aliyun.com/debianSuites: trixie trixie-updatesComponents: mainSigned-By: /usr/share/keyrings/debian-archive-keyring.pgp
Types: deb# http://snapshot.debian.org/archive/debian-security/20260610T000000ZURIs: http://mirrors.aliyun.com/debian-securitySuites: trixie-securityComponents: mainSigned-By: /usr/share/keyrings/debian-archive-keyring.pgp
3)下载安装root@ee5d00b5904c:/# apt update && apt -y install python3root@ee5d00b5904c:/# python3 -VPython 3.13.5root@ee5d00b5904c:/# exitexit
4)退出测试root@m01 ~# ansible all -m ping✅ ansible.cfg中配置了interpreter_python=auto_silent# 可以自动识别受控节点的python环境myweb | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3.13" ⭕ }, "changed": false, "ping": "pong"}mypy | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/local/bin/python3.12" }, "changed": false, "ping": "pong"}动态分组(groups)
==需求==:容器多了以后,需要分组管理——“web 容器归一组,python 容器归一组”:
ansible web_servers -m raw -a 'nginx -s reload' # 只重载 web 组ansible py_apps -m ping # 只 ping python 组⚠️ 关键认知
-
filters(过滤器) 和 groups 共享同一套
full_facts,变量完全一样- 区别在于角色:filters 控制谁入选,groups 控制入选后怎么分队
-
--host输出只是 inventory 视图,groups 能用的变量远多于--host
方法一:按名称 / 镜像分组
full_facts 中已经包含了所有 inspect 数据,直接引用即可:
plugin: community.docker.docker_containersdocker_host: unix:///var/run/docker.sock
filters: - exclude: 'not docker_state.Running'
compose: ansible_connection: community.docker.docker # 显式指定连接方式
groups: web_servers: 'docker_name == "myweb"' py_apps: 'docker_name == "mypy"' nginx_hosts: '"nginx" in docker_config.Image' # 镜像中in(包含)nginx # 所有基于 nginx镜像 创建的容器分一组in— 子串包含,不是完全相等,“my” 能同时命中 mypy 和 myweb==— ==完全相等==,但 filter 中不可靠(groups 中没问题)
groups(动态分组)要求没有那么严格 —> == 和 in 都可以用
== 在 filter 中不可靠
root@m01 ~# docker run -d --name web01 nginx:latest'再创建一个web容器'root@m01 ~# ansible-inventory --graph@all: |--@ungrouped: |--@nginx_hosts: | |--web01 ✅ | |--myweb ✅ |--@web_servers: | |--myweb |--@py_apps: | |--mypy方法二:按标签分组
Docker 标签存在 docker_config.Labels 中,给容器打标签并分组:
'清理环境'root@m01 ~# docker rm -f $(docker ps -aq)
1)创建容器 & 带标签⚠️ Docker 不支持修改已有容器的标签,要删除标签只能 `docker rm -f` 后重建root@m01 ~# docker run -d --name myweb --label group=web_servers nginx:latestroot@m01 ~# docker run -d --name test_web --label group=test nginx:latestroot@m01 ~# docker run -d --name mypy --label group=py_apps python:3.12-alpine tail -f /etc/hosts# 得阻塞住进程
2)查看标签root@m01 ~# docker inspect myweb --format '{{json .Config.Labels}}'{"group":"web_servers","maintainer":"NGINX Docker Maintainers <docker-maint@nginx.com>"}root@m01 ~# docker inspect test_web --format '{{json .Config.Labels}}'{"group":"test","maintainer":"NGINX Docker Maintainers <docker-maint@nginx.com>"}root@m01 ~# docker inspect mypy --format '{{json .Config.Labels}}'{"group":"py_apps"}groups: py_apps: 'docker_config.Labels.group == "py_apps"' test_group: 'docker_config.Labels.group == "test"' # ==(完全相等) nginx_hosts: '"nginx" in docker_config.Image' # 镜像中in(包含)nginx- groups(动态分组) 中直接
.group不会报错- 宽松求值,取不到返回 undefined,==不炸== ✅
- filter(过滤器) 必须用
.get("group", ""),不能直接.group- 没有该标签的容器
Labels是空 dict{},直接取属性会报错 ❌
- 没有该标签的容器
💡 两者都可以用 .get() 👇 这种方式 —> 更显式
groups:test_group: 'docker_config.Labels.get("group", "") == "test"'3)测试运行root@m01 ~# ansible-inventory --graph@all: |--@ungrouped: |--@test_group: | |--test_web 🔄 |--@nginx_hosts: | |--test_web 🔄 | |--myweb |--@py_apps: | |--mypy✅ 一个容器可以同时属于多个组 --> 'test_web'
4)创建一个'不带标签'的# 并再次验证root@m01 ~# docker run -d --name web01 nginx:latestroot@m01 ~# ansible-inventory --graph@all: |--@ungrouped: |--@nginx_hosts: | |--web01 | |--test_web 🔄 | |--myweb |--@test_group: | |--test_web 🔄 |--@py_apps: | |--mypy
5)停止并再次验证root@m01 ~# docker stop -t 0 web01web01root@m01 ~# ansible-inventory --graph@all: |--@ungrouped: |--@test_group: | |--test_web 🔄 |--@nginx_hosts: | |--test_web 🔄 | |--myweb |--@py_apps: | |--mypy✅ '没有web01了'⭐compose 的真正用途
root@m01 ~# ansible-inventory --host test_web{ "ansible_connection": "community.docker.docker_api", "ansible_docker_api_version": "auto", "ansible_docker_docker_host": "unix:///var/run/docker.sock", "ansible_docker_timeout": 60, "ansible_docker_tls": false, "ansible_docker_use_ssh_client": false, "ansible_docker_validate_certs": false, "ansible_host": "test_web", "docker_name": "test_web", 📌 # 容器名 "docker_short_id": "0e4c6edb0722d" 📌 # 容器id} 🤡 # 现在关于docker的变量只有这两个-
简化别名:把
docker_config.Image映射为my_img,表达式更短 -
计算新变量:通过 Jinja2 模板组合多个字段
-
写入 inventory:compose 变量会出现在
--host输出中,供 playbook 使用
compose: ansible_connection: community.docker.docker # 指定连接方式 my_img: docker_config.Image # 简化变量 is_test: 'docker_config.Labels.group == "test"' # 计算布尔标记 my_group: docker_config.Labels.group # 暴露到 --hostroot@m01 ~# vim inventory/docker.docker.ymlroot@m01 ~# ansible-inventory --host test_web{ ................................ "ansible_host": "test_web", "docker_name": "test_web", "docker_short_id": "0e4c6edb0722d", "my_img": "nginx:latest" 😍 "is_test": true, 😍 "my_group": "test", 😍}✨变量速查
| 变量 | filters | groups | --host | 类型 | 备注 |
|---|---|---|---|---|---|
docker_name | ✅ | ✅ | ✅ | 字符串 | 内置 |
docker_short_id | ✅ | ✅ | ✅ | 字符串 | 内置 |
docker_state.Running | ✅ | ✅ | ❌ | ==布尔值== | true / false |
docker_state.Status | ✅(用 in) | ✅ | ❌ | 字符串 | "running" / "exited" |
docker_config.Image 🥳 | ✅ | ✅ | ❌ | 字符串 | "nginx:latest" |
docker_config.Labels.<key> | ✅(必须 .get) | ✅ | ❌ | 字符串 | 没该标签的容器 Labels = {} |
docker_image 🤡 | ✅ | ✅ | ❌ | 字符串 (SHA256) | ⚠️ 是哈希,镜像名在 docker_config.Image |
字符串判断差异:
- filter:
==不可靠,用"xxx" in var(双引号 +in)⚠️ 外面单引号包裹 - group:
==和in都可以 ✅
groups 工作流程
1. Docker 插件拿到所有容器 ↓2. filters 对 full_facts 筛一轮 → 决定哪些容器"入选" ↓3. compose 把计算结果注入 full_facts ↓4. groups 对 full_facts 逐个判断 → 表达式为 true 就加入对应组 ↓5. 一个容器可以同时属于多个组✅ 最终配置汇总
'项目目录最终两个文件:'root@m01 ~# tree ././├── ansible.cfg ✅ # 项目级配置└── inventory └── docker.docker.yml ✅ # 动态清单
2 directories, 2 filesansible.cfg:
[defaults]inventory = /root/inventory/docker.docker.ymlinterpreter_python = auto_silent
# 注意:没有 [privilege_escalation]!# 容器里是 root,没有 sudo,不需要提权docker.docker.yml:
plugin: community.docker.docker_containersdocker_host: unix:///var/run/docker.sock
filters: - exclude: 'not docker_state.Running'
compose: ansible_connection: community.docker.docker # 指定连接方式 my_img: docker_config.Image # 简化变量 is_test: 'docker_config.Labels.group == "test"' # 计算布尔标记 my_group: docker_config.Labels.group # 暴露到 --host
groups: py_apps: 'my_group == "py_apps"' test_group: 'my_group == "test"' # ==(完全相等) nginx_hosts: '"nginx" in my_img' # 镜像中in(包含)nginxroot@m01 ~# vim ./inventory/docker.docker.ymlroot@m01 ~# ansible-inventory --graph@all: |--@ungrouped: |--@test_group: | |--test_web |--@nginx_hosts: | |--test_web | |--myweb |--@py_apps: | |--mypy这套写法的优点:
- compose 把又长又深的路径(
docker_config.Labels.group、docker_config.Image)映射成短别名(my_group、my_img)- groups 表达式立刻干净了 & 好读得多
- 而且 compose 变量会写进 inventory,
--host也能看到,调试和 playbook 引用都方便
📌 一句话:filters 控制”谁入选”,compose 给入选的容器起别名 / 算新值,groups 用这些简洁变量分队
✅ 一个容器属于多个组完全没问题
Podman ⬌ Docker 对照
| 配置项 | Podman | Docker |
|---|---|---|
| 插件 | containers.podman.podman_containers | community.docker.docker_containers |
| 连接方式 | connection_plugin: containers.podman.podman | compose: ansible_connection: community.docker.docker |
| 控制启停 | include_stopped: false | filters: - exclude: 'not docker_state.Running' |
| 镜像变量 | podman_image(镜像名) | docker_config.Image(镜像名,可直接用);docker_image 是 SHA256 |
| 状态变量 | podman_status | docker_state(对象,取 .Status 或 .Running) |
| 标签变量 | podman_labels | docker_config.Labels.<key>(直接可用) |
主机模式(Host Patterns)
有了分组之后,下一步就是精准定位——“想对谁执行就对谁执行”
Ansible 提供了一套主机模式语法,支持单个主机、多个主机、组、排除、交集、切片等操作
模式速查表
| 描述 | 模式 | 目标 |
|---|---|---|
| 所有主机 | all 或 * | 清单中的全部主机 |
| 单个主机 | host1 | 指定主机名 |
| 多台主机 | host1:host2 或 host1,host2 | 冒号或逗号分隔 |
| 一个组 | webservers | 该组所有成员 |
| 多个组(==并集==) | group1:group2 | 两组所有成员合并 |
| 排除主机 | group:!host | 从组中剔除某台主机 |
| 排除组 | group1:!group2 | 从 group1 中踢掉 group2 所有成员 |
| 组的交集(==交集==) | group1:&group2 | 同时属于两个组的主机 |
| 组位置(切片) | group[0]、group[0:2] | 按索引取组内主机 |
| 正==则==匹配 | ~pattern | 匹配主机名符合正则的 |
模式中包含特殊字符(!、&、*、~)时,shell 可能会抢着解释,必须用引号包裹:
root@m01 ~# ansible-inventory --graph@all: |--@ungrouped: |--@test_group: | |--test_web |--@nginx_hosts: | |--test_web | |--myweb |--@py_apps: | |--mypyroot@m01 ~# ansible nginx_hosts:!test_web -m raw -a 'echo hi''冒号 :! 感叹号'-bash: !test_web: event not found❌ 不加引号 — shell 把 ! 当历史展开
root@m01 ~# ansible 'nginx_hosts:!test_web' -m raw -a 'echo hi'# nginx_hosts组中排除test_web --> 只剩mywebmyweb | CHANGED | rc=0 >>hi✅ 加引号基础定位:all / 单主机 / 多主机
'环境确认 — 当前有 3 台运行中的容器'[root@m01 ~]# ansible-inventory --graph@all: |--@ungrouped: |--@test_group: | |--test_web 🔄 |--@nginx_hosts: | |--test_web 🔄 # 同时属于 test_group 和 nginx_hosts | |--myweb |--@py_apps: | |--mypy① 所有主机 all
[root@m01 ~]# ansible all -m raw -a 'echo hello'test_web | CHANGED | rc=0 >>hellomypy | CHANGED | rc=0 >>hellomyweb | CHANGED | rc=0 >>hello② 单台主机
root@m01 ~# ansible mypy -m raw -a 'hostname'mypy | CHANGED | rc=0 >>d01c8848c7ea # ← 容器 ID(hostname 在容器里就是容器 ID)③ 多台主机 — 冒号 : 或逗号 ,
root@m01 ~# ansible 'myweb:test_web' -m raw -a 'hostname'⚠️ '别忘记加单引号'myweb | CHANGED | rc=0 >>3c6b015c1a82test_web | CHANGED | rc=0 >>0e4c6edb0722
root@m01 ~# ansible 'myweb,test_web' -m raw -a 'hostname'✅ '逗号效果完全一样'myweb | CHANGED | rc=0 >>3c6b015c1a82test_web | CHANGED | rc=0 >>0e4c6edb0722, 和 : 效果相同,但 : 还能跟 !、& 组合 → 建议统一用冒号
组定位:单组 / 并集
④ 针对某个组
root@m01 ~# ansible nginx_hosts -m raw -a 'echo nginx组'test_web | CHANGED | rc=0 >>nginx组myweb | CHANGED | rc=0 >>nginx组⑤ 多个组的并集
root@m01 ~# ansible 'nginx_hosts:py_apps' -m raw -a 'echo 并集'myweb | CHANGED | rc=0 >>并集test_web | CHANGED | rc=0 >>并集mypy | CHANGED | rc=0 >>并集✅ '3 台都在:nginx 组 2 台 + python 组 1 台'排除:! 操作符
从组中踢掉特定主机或组:
'⑥ 从组中排除单台主机'root@m01 ~# ansible 'nginx_hosts:!test_web' -m raw -a 'echo 排除test_web后'myweb | CHANGED | rc=0 >>排除test_web后# ✅ test_web 没了,只剩 myweb============================='踢掉test_group'root@m01 ~# ansible 'nginx_hosts:!test_group' -m raw -a 'echo 踢掉test_group'myweb | CHANGED | rc=0 >>踢掉test_group# test_group只有test_web一台主机| 步骤 | 集合 |
|---|---|
初始 nginx_hosts | {myweb, test_web} |
执行 :!test_web | 踢掉 test_web |
| 最终 | {myweb} |
交集:& 操作符
取两个组的公共成员:
'⑦ 组的交集'root@m01 ~# ansible 'nginx_hosts:&test_group' -m raw -a 'echo 交集'test_web | CHANGED | rc=0 >>交集# ✅ 只有 test_web:它同时属于 nginx_hosts 和 test_group| 步骤 | 集合 |
|---|---|
nginx_hosts | {myweb, test_web} |
test_group | {test_web} |
| 交集 | {test_web} ✅ |
组合运算:先交集再排除
! 和 & 可以组合,按从左到右顺序执行:
'⑧ 组合 — 交集后排除'root@m01 ~# docker run -d --name test02 --label group=test nginx:latestroot@m01 ~# ansible-inventory --graph@all: |--@ungrouped: |--@test_group: | |--test02 | |--test_web |--@nginx_hosts: | |--test02 | |--test_web | |--myweb# 场景:取 nginx_hosts ∩ test_group 的交集, 再排除test_webroot@m01 ~# ansible 'nginx_hosts:&test_group:!test_web' -m raw -a 'echo 交集并排除后的结果'test02 | CHANGED | rc=0 >>交集并排除后的结果组位置 / 切片:[n] 语法
索引从 0 开始 —> 单个索引 [n] 与 Python 一致,但切片有一个关键区别:
Ansible 切片 [start:stop] 的 stop 是包含的(inclusive),不是 Python 的排除(exclusive)!
Python list[0:1] | Ansible group[0:1] | |
|---|---|---|
| 语义 | [0, 1) 不含 stop | [0, 1] 含 stop |
| 结果 | 1 个元素(索引 0) | 2 个元素(索引 0 和 1) |
'nginx_hosts 组共 3 台,按顺序:' [0] test02 [1] test_web [2] myweb
'⑨ 单个索引 — 与 Python 完全一致'[root@m01 ~]# ansible 'nginx_hosts[0]' -m raw -a 'echo 索引0'test02 | CHANGED | rc=0 >>索引0
[root@m01 ~]# ansible 'nginx_hosts[1]' -m raw -a 'echo 索引1'test_web | CHANGED | rc=0 >>索引1
[root@m01 ~]# ansible 'nginx_hosts[-1]' -m raw -a 'echo 倒数第一'myweb | CHANGED | rc=0 >>倒数第一
'⑩ 切片 — ⚠️ stop 也是包含的(两端都算!)'# 用实验来证明,对照 Python 的预期:
[root@m01 ~]# ansible 'nginx_hosts[0:0]' -m raw -a 'echo 0:0'test02 | CHANGED | rc=0 >>0:0# ← 1 台!Python [0:0] 是空的,Ansible 两端都包含 → {索引0}
[root@m01 ~]# ansible 'nginx_hosts[0:1]' -m raw -a 'echo 0:1'test02 | CHANGED | rc=0 >>0:1test_web | CHANGED | rc=0 >>0:1# ← 2 台!Python [0:1] 只取索引0,Ansible stop=1 也包含 → {0,1}
[root@m01 ~]# ansible 'nginx_hosts[0:2]' -m raw -a 'echo 0:2'test02 | CHANGED | rc=0 >>0:2test_web | CHANGED | rc=0 >>0:2myweb | CHANGED | rc=0 >>0:2✅ 3 台全部!Python [0:2] 只取 0,1,Ansible stop=2 包含 → {0,1,2}
[root@m01 ~]# ansible 'nginx_hosts[1:2]' -m raw -a 'echo 1:2'test_web | CHANGED | rc=0 >>1:2myweb | CHANGED | rc=0 >>1:2# ← 2 台!Python [1:2] 只取索引1,Ansible → {1,2}⭐--limit:无需改命令,临时缩小范围
模式可以直接写在命令行上,也可以通过 --limit 叠加过滤——不需要修改模式本身:
'⑪ --limit 排除主机'root@m01 ~# ansible all -m raw -a 'echo hi' --limit 'all:!test_web'myweb | CHANGED | rc=0 >>hitest02 | CHANGED | rc=0 >>himypy | CHANGED | rc=0 >>hi# test_web 被 limit 排除 ✅
'⑫ --limit 指定组'[root@m01 ~]# ansible all -m raw -a 'echo mixed' --limit 'py_apps:nginx_hosts'test02 | CHANGED | rc=0 >>mixedtest_web | CHANGED | rc=0 >>mixedmyweb | CHANGED | rc=0 >>mixedmypy | CHANGED | rc=0 >>mixed
'⑬ --limit 正则排除'📌 `~` 前缀 = 正则匹配📌 `~` 还可以和排除 `!` 组合:root@m01 ~# ansible all -m raw -a 'echo 排除以test开头的组' --limit 'all:!~test.*'⚠️ 关键发现:`~` 隐式锚定开头(自带 `^`) `~test` == `~^test`myweb | CHANGED | rc=0 >>排除以test开头的组mypy | CHANGED | rc=0 >>排除以test开头的组# ✅ test 开头的主机全被排除(test_web、test02 都不见了)其实不用 --limit 更快:~ 可以直接写在命令行模式里,一行搞定:
📌 --limit 场景:命令行模式已经写好了不想改,临时叠加一个 --limit 做二次过滤
| 写法 | 效果 |
|---|---|
'~test.*' | 匹配主机名符合正则的 |
'~.*py$' | 匹配以 py 结尾的主机 |
'all:!~test.*' | 排除匹配正则的主机 |
'组名:!~test.*' | 组内排除 |
[root@m01 ~]# ansible '~test.*' -m raw -a 'echo 匹配正则'test02 | CHANGED | rc=0 >>匹配正则test_web | CHANGED | rc=0 >>匹配正则# ← 只有 test 开头的主机被命中 ✅
[root@m01 ~]# ansible 'all:!~test.*' -m raw -a 'echo 排除test开头的'⚠️ `~` 隐式锚定开头(自带 `^`)myweb | CHANGED | rc=0 >>排除test开头的mypy | CHANGED | rc=0 >>排除test开头的从文件读取主机列表:--limit @文件
'⑭ 把目标主机写进文件'[root@m01 ~]# cat > /root/host.txt << 'EOF'mywebmypyEOF
[root@m01 ~]# ansible all -m raw -a 'echo 来自文件' --limit @/root/host.txtmyweb | CHANGED | rc=0 >>来自文件mypy | CHANGED | rc=0 >>来自文件# ← test_web、test02 不在文件中,不执行 ✅小结
核心规则速记:
| 符号 | 含义 | 类比 |
|---|---|---|
: | 并集(分隔) | 集合 ∪ |
:! | 排除 | 集合 − |
:& | 交集 | 集合 ∩ |
[n] | 取单个,索引第 n 台 | Python list[n] |
[m:n] ⚠️ | 切片,两端都包含 | Python list[m:n+1] |
--limit | 叠加过滤(不影响命令行里的模式) | 二次筛选 |
实战建议:
- 日常操作:
ansible <组名> -m raw -a '命令'最常用 - 调试单台:
ansible <主机名> -m raw -a '命令' - 排除故障机:用
'组名:!故障机' - 快速过滤:
'~test.*'直接正则匹配主机名,秒定位一类主机 - 批量排除:
'all:!~test.*'直接写在模式里即可,不用--limit - 批量读取:把主机名写进文件,
--limit @文件 - ⚠️ 切片取 N 台:
group[0:N-1],stop 是包含的!
命令速查
# -------- 清单 --------ansible-inventory --graph # 树状图ansible-inventory --list # JSON 详情ansible-inventory --host myweb # 某个主机的所有变量ansible-inventory --list -vvv # 详细模式排错
# -------- 执行 --------ansible all -m ping # ping(需要 Python)ansible all -m raw -a 'whoami' # raw(不需要 Python)ansible all -m debug -a "var=ansible_connection" # 查看连接方式ansible web_servers -m raw -a 'nginx -v' # 针对某个组执行
# -------- 主机模式 --------ansible 'group:!host' -m raw -a 'cmd' # 从组中排除某台主机ansible 'g1:&g2' -m raw -a 'cmd' # 两个组的交集ansible 'group[0]' -m raw -a 'cmd' # 组内第一台ansible 'group[0:1]' -m raw -a 'cmd' # 前两台(⚠️ stop 包含,[0:1]=索引0和1)ansible '~test.*' -m raw -a 'cmd' # 正则过滤(直接模式,更快 ⚡)ansible 'all:!~test.*' -m raw -a 'cmd' # 正则排除(直接模式,不用 --limit)ansible all -m raw -a 'cmd' --limit @file # 从文件读取主机列表ansible all -m raw -a 'cmd' --limit 'all:!~pattern.*' # --limit 正则排除
# -------- 环境 --------ansible --version # 版本 + 当前使用的配置文件ansible-galaxy collection list | grep docker # 确认 Docker 插件已安装文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!




