🤖 RHCE 9.0 知识库¶
基于模拟题整理,按题号编排,每道题向外扩散关键知识点。
考试环境概览¶
| 主机 | IP | 角色 |
|---|---|---|
| workstation.lab.example.com | 172.25.250.9 | Ansible 控制节点 |
| servera.lab.example.com | 172.25.250.10 | 受管节点 (dev) |
| serverb.lab.example.com | 172.25.250.11 | 受管节点 (test) |
| serverc.lab.example.com | 172.25.250.12 | 受管节点 (prod) |
| serverd.lab.example.com | 172.25.250.13 | 受管节点 (prod) |
| bastion.lab.example.com | 172.25.250.254 | 受管节点 (balancers) |
关键信息:
| 项目 | 值 |
|---|---|
| root 密码 | redhat |
| Ansible 用户 | devops |
| 工作目录 | /home/devops/ansible/ |
| 所有操作 | 以 devops 用户在 ansible 目录下执行 |
| 镜像仓库 | utility.lab.example.com,用户 admin,密码 redhat |
| 内容源 | http://content.example.com |
| 评分方式 | 重置受管节点 → 从控制节点运行你的 playbook → 评估结果 |
一、安装和配置 Ansible¶
题目要求¶
- 安装 Ansible 软件包
- 创建静态清单文件
/home/devops/ansible/inventory - 创建配置文件
/home/devops/ansible/ansible.cfg
清单文件¶
配置文件¶
[defaults]
inventory = /home/devops/ansible/inventory
remote_user = devops
roles_path = /home/devops/ansible/roles
host_key_checking = false
collections_path = /home/devops/ansible/mycollections:/usr/share/ansible/collections
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
扩展开的知识点¶
ansible.cfg 配置段¶
| 配置段 | 作用 |
|---|---|
[defaults] |
全局默认配置(inventory、remote_user、roles_path 等) |
[privilege_escalation] |
权限提升专用配置(become、sudo 等) |
两个 section 是并列关系,不是嵌套。[defaults] 管"连谁",[privilege_escalation] 管"用什么身份干活"。
关键配置项¶
| 配置项 | 含义 |
|---|---|
remote_user = devops |
SSH 连接用户名 |
roles_path |
角色查找路径 |
host_key_checking = false |
跳过 SSH 主机密钥确认 |
collections_path |
集合查找路径,支持冒号分隔多个路径 |
become = True |
启用 sudo 提权 |
become_user = root |
提权目标用户 |
become_ask_pass = False |
不询问 sudo 密码(已配置 NOPASSWD) |
清单语法¶
| 语法 | 说明 |
|---|---|
[groupname] |
定义主机组 |
[groupname:children] |
定义组的组(父组包含子组成员) |
ansible-navigator 配置¶
# /home/devops/.ansible-navigator.yml
ansible-navigator:
execution-environment:
image: utility.lab.example.com/ee-supported-rhel8:latest
pull:
policy: missing
Podman 镜像仓库配置¶
# /etc/containers/registries.conf
[registries.search]
registries = ['utility.lab.example.com']
[registries.insecure]
registries = ['utility.lab.example.com']
验证¶
二、创建受管节点存储库¶
题目要求¶
创建 /home/devops/ansible/yum_repo.yml,在所有受管节点配置两个 YUM 仓库。
Playbook¶
- hosts: all
tasks:
- name: configure BaseOS repo
ansible.builtin.yum_repository:
name: rh294_BASE
description: "rh294 base software"
baseurl: http://content.example.com/rhel9.0/x86_64/dvd/BaseOS
enabled: yes
gpgcheck: yes
gpgkey: http://content.example.com/rhel9.0/x86_64/dvd/RPM-GPG-KEY-redhat-release
file: rhel_dvd
- name: configure AppStream repo
ansible.builtin.yum_repository:
name: rh294_STREAM
description: "rh294 stream software"
baseurl: http://content.example.com/rhel9.0/x86_64/dvd/AppStream
enabled: yes
gpgcheck: yes
gpgkey: http://content.example.com/rhel9.0/x86_64/dvd/RPM-GPG-KEY-redhat-release
file: rhel_dvd
扩展开的知识点¶
yum_repository 模块参数¶
| 参数 | 含义 |
|---|---|
name |
仓库标识名(.repo 文件中的 [name]) |
description |
仓库描述(显示在 dnf repolist) |
baseurl |
软件包下载地址 |
gpgcheck |
是否启用 GPG 签名检查(yes/no) |
gpgkey |
GPG 公钥 URL |
enabled |
是否启用仓库(yes/no) |
file |
repo 文件名(不带 .repo 后缀),模块自动追加 .repo |
file 参数详解¶
- 不写
file:默认用name的值作为文件名 - 两个 task 共用
file: rhel_dvd→ 合并写入/etc/yum.repos.d/rhel_dvd.repo - 一个 .repo 文件可以有多个
[section] - 不要手动加
.repo后缀,模块会自动加
执行¶
验证¶
ansible all -m shell -a 'cat /etc/yum.repos.d/rhel_dvd.repo'
ansible all -m shell -a 'yum clean all && yum makecache'
三、安装软件包¶
题目要求¶
创建 /home/devops/ansible/packages.yml:
1. 将 php 和 mariadb 安装到 dev、test、prod 主机组
2. 将 Development Tools 包组安装到 dev 主机组
3. 将 dev 主机组中所有软件包更新到最新版
Playbook¶
- hosts: dev, test, prod
tasks:
- name: install mariadb php
ansible.builtin.yum:
name: "{{ item }}"
state: present
loop:
- php
- mariadb
- hosts: dev
tasks:
- name: install Development Tools
ansible.builtin.yum:
name: "@Development Tools"
state: present
- hosts: dev
tasks:
- name: update pkgs
ansible.builtin.yum:
name: '*'
state: latest
扩展开的知识点¶
loop 循环¶
{{ item }}是 Jinja2 模板的循环变量loop列表中几个值就执行几次- 更简洁写法:
name: [php, mariadb](不需要 loop)
yum 模块 state 参数¶
| state | 行为 |
|---|---|
present |
已装就跳过,没装就装上 |
latest |
检查更新,有就更新 |
absent |
卸载 |
通配符和包组¶
| 写法 | 含义 |
|---|---|
name: '*' |
所有软件包 |
name: "@Development Tools" |
@ 前缀表示软件包组 |
一个 YAML 多个 Play¶
一个 playbook 可以包含多个 Play(用 - hosts: 分隔),每个 Play 针对不同主机组执行不同任务。
验证¶
四、使用 Timesync RHEL 系统角色¶
题目要求¶
创建 /home/devops/ansible/timesync.yml,使用 timesync 角色配置 NTP 时间服务器 classroom.example.com。
前置准备¶
yum -y install rhel-system-roles
cp -r /usr/share/ansible/roles/rhel-system-roles.timesync/ roles/timesync
Playbook¶
- hosts: all
vars:
timesync_ntp_servers:
- hostname: classroom.example.com
iburst: yes
roles:
- timesync
扩展开的知识点¶
RHEL 系统角色¶
- 安装
rhel-system-roles后,角色文件在/usr/share/ansible/roles/ - 复制到本地
roles/目录方便 playbook 引用和管理 - 复制的是角色的定义文件(tasks、defaults、templates 等),不是软件包
vars 变量¶
vars:定义变量,传递给 roles 控制角色行为timesync_ntp_servers是 timesync 角色要求的变量名iburst: yes启用快速同步(开机时连续发 8 个包快速校时)- 可以配多个 NTP 服务器,按列表顺序优先级从高到低
验证¶
五、使用 SELINUX RHEL 系统角色¶
题目要求¶
在所有节点使用 SELinux 角色,将 SELinux 设置为 enforcing 强制模式。
Playbook¶
- hosts: all
vars:
selinux_policy: targeted
selinux_state: enforcing
roles:
- role: selinux
become: true
扩展开的知识点¶
SELinux 变量¶
| 变量 | 值 | 含义 |
|---|---|---|
selinux_policy |
targeted |
策略类型(只约束特定进程) |
selinux_state |
enforcing |
强制模式(违反就阻止) |
SELinux 三种状态¶
| 状态 | 命令 | 说明 |
|---|---|---|
enforcing |
setenforce 1 |
强制模式 |
permissive |
setenforce 0 |
宽容模式(只警告不阻止) |
disabled |
改配置文件重启 | 完全关闭 |
become: true 详解¶
- 用 root 权限执行该角色(通过 sudo 提权)
- SELinux 状态修改需要 root 权限
- 跟 ansible.cfg 的
[privilege_escalation]区别: become: true只对当前角色/play 生效[privilege_escalation]全局生效
验证¶
六、使用 Ansible Galaxy 安装角色¶
题目要求¶
从 URL 下载角色并安装到 /home/devops/ansible/roles。
实际操作¶
# requirements.yml
- name: balancer
src: http://classroom.example.com/content/haproxy.tar.gz
- name: phpinfo
src: http://classroom.example.com/content/phpinfo.tar.gz
# 安装
ansible-galaxy install -r roles/requirements.yml -p roles/
扩展开的知识点¶
ansible-galaxy install¶
- Ansible Galaxy 是 Ansible 的角色市场/仓库,类似 pip/npm
- 两种安装方式:
- 从 Galaxy 仓库:
ansible-galaxy install geerlingguy.docker - 从 URL:
ansible-galaxy install -r requirements.yml -p roles/
requirements.yml 格式¶
| 字段 | 含义 |
|---|---|
name |
安装后的角色目录名 |
src |
角色包的下载地址(URL) |
命令参数¶
| 参数 | 含义 |
|---|---|
-r requirements.yml |
从清单文件批量安装 |
-p roles/ |
指定安装目录 |
流程¶
七、安装集合¶
题目要求¶
将 ansible-posix-1.5.1.tar.gz 和 community-general-6.3.0.tar.gz 安装到 /home/devops/ansible/mycollections/。
实际操作¶
ansible-galaxy collection install http://content.example.com/ansible-posix-1.5.1.tar.gz -p mycollections/
ansible-galaxy collection install http://content.example.com/community-general-6.3.0.tar.gz -p mycollections/
扩展开的知识点¶
两种安装方式¶
| 方法 | 命令 | 场景 |
|---|---|---|
| 直接 URL | ansible-galaxy collection install URL -p dir/ |
集合少,简单直接 |
| requirements.yml | ansible-galaxy collection install -r req.yml -p dir/ |
集合多,清单管理 |
安装后目录结构¶
关键知识点¶
ansible-galaxy collection install安装集合(不是角色)-p mycollections/指定安装目录- 需要在
ansible.cfg配置collections_path指向安装目录 ansible-galaxy collection list列出已安装集合
验证¶
八、创建和使用角色¶
题目要求¶
创建 apache 角色(安装 httpd、配置防火墙、部署 Jinja2 模板),在 webservers 主机组运行。
角色 tasks/main.yml¶
- name: install http
yum:
name: httpd
state: present
- name: config system service
service:
name: "{{ item }}"
state: started
enabled: yes
loop:
- httpd
- firewalld
- name: firewalld service
firewalld:
service: http
permanent: yes
immediate: yes
state: enabled
- name: user templates
template:
src: index.html.j2
dest: /var/www/html/index.html
角色 templates/index.html.j2¶
Playbook newrole.yml¶
扩展开的知识点¶
Role 目录结构(ansible-galaxy init apache)¶
自动生成标准目录:
apache/
├── tasks/main.yml ← 主任务入口(Ansible 默认读这里)
├── handlers/main.yml ← 处理器
├── templates/ ← Jinja2 模板文件
├── files/ ← 静态文件
├── vars/main.yml ← 变量
├── defaults/main.yml ← 默认变量
└── meta/main.yml ← 角色元数据
为什么写在 tasks/main.yml? Ansible 执行角色时默认读 tasks/main.yml,这是约定的入口文件,相当于程序的 main()。
常用 Facts 变量¶
| 变量 | 含义 |
|---|---|
ansible_fqdn |
主机完全限定域名 |
ansible_default_ipv4.address |
主机 IP 地址 |
ansible_hostname |
主机短名称 |
ansible_distribution |
发行版名称 |
Jinja2 模板(.j2 文件)¶
index.html.j2 是 Jinja2 模板,用变量动态生成文件:
ansible_fqdn→ 被管节点的完全限定域名ansible_default_ipv4.address→ 被管节点的 IP 地址- 每台机器执行时自动替换为自己的值,生成专属页面
在 tasks/main.yml 中用 template 模块部署:
template 模块¶
- 将 Jinja2 模板渲染后复制到目标主机
src: 模板文件路径(相对于templates/)dest: 目标路径
firewalld 模块参数详解¶
| 参数 | 含义 | 等价命令 |
|---|---|---|
permanent: yes |
永久生效,重启后规则仍在 | firewall-cmd --add-service=http --permanent |
immediate: yes |
立即生效,当前就加载,不用重启 firewalld | firewall-cmd --add-service=http |
state: enabled |
启用/允许该服务 | --add-service |
state: disabled |
禁用/拒绝该服务 | --remove-service |
permanent + immediate 组合 = 既写入配置文件又立即加载,缺一不可。
验证¶
curl serverc
# Welcome to serverc.lab.example.com on 172.25.250.12
curl serverd
# Welcome to serverd.lab.example.com on 172.25.250.13
九、从 Ansible Galaxy 使用角色¶
题目要求¶
创建 /home/devops/ansible/roles.yml,在 balancers 组使用 balancer 角色,在 webservers 组使用 phpinfo 角色。
Playbook¶
扩展开的知识点¶
角色复用¶
- 通过 roles 关键字引用已安装的角色
- 角色放在
roles/目录下(在ansible.cfg中配置了roles_path) - balancer 角色实现负载均衡,phpinfo 角色显示 PHP 信息
验证¶
curl http://bastion.lab.example.com/
# 轮流返回 serverc 和 serverd 的 Welcome 页面
curl http://serverc.lab.example.com/hello.php
# Hello PHP World from serverc.lab.example.com
十、创建和使用逻辑卷¶
题目要求¶
创建 /home/devops/ansible/lv.yml,在 research 卷组中创建 600MiB 的 data 逻辑卷,使用 ext4 格式化。如果无法创建 600MiB 则创建 400MiB,如果卷组不存在则报错。
题目分析¶
- 目标:在
research卷组中创建逻辑卷data - 容错:600m 不行就创建 400m(block/rescue)
- 异常处理:卷组不存在时报错提示(when 判断)
- 格式化:无论成功失败都格式化为 ext4(always)
完整流程¶
Playbook¶
- hosts: all
tasks:
- block:
- name: create lvm 600m
lvol:
vg: research
lv: data
size: 600m
rescue:
- name: output fail msg
debug:
msg: Could not create logical volume of that size
- name: create lvm 400m
lvol:
vg: research
lv: data
size: 400m
always:
- name: format lvm
filesystem:
fstype: ext4
dev: /dev/research/data
when: "'research' in ansible_lvm.vgs"
- name: search not exists
debug:
msg: Volume group does not exist
when: "'research' not in ansible_lvm.vgs"
代码逐行解析¶
第一个 task:block/rescue/always¶
- block:
# 尝试创建 600m 逻辑卷
- rescue:
# 如果 block 失败,输出提示 + 创建 400m
- always:
# 无论成功失败都执行格式化
when: "'research' in ansible_lvm.vgs" # 整个 block 的前置条件
关键点:when 是任务级指令,必须和 - name: 对齐(第8格),不是和模块参数对齐(第10格)。
第二个 task:卷组不存在时的处理¶
- name: search not exists
debug:
msg: Volume group does not exist
when: "'research' not in ansible_lvm.vgs" # 和 debug: 对齐
扩展知识点¶
block/rescue/always 错误处理¶
| 关键字 | 作用 | 类比 |
|---|---|---|
block |
正常执行的任务 | try |
rescue |
block 中任一任务失败时执行 | catch |
always |
无论成功失败都执行 | finally |
Facts 变量判断¶
| 变量 | 用途 |
|---|---|
ansible_lvm.vgs |
包含所有卷组信息的字典 |
'research' in ansible_lvm.vgs |
判断卷组是否存在 |
lvol 模块¶
| 参数 | 含义 |
|---|---|
vg |
卷组名 |
lv |
逻辑卷名 |
size |
大小(m = MiB,按 1024 进制计算) |
filesystem 模块¶
| 参数 | 含义 |
|---|---|
fstype |
文件系统类型(ext4/xfs 等) |
dev |
设备路径(/dev/research/data) |
常见错误¶
when缩进错误:必须和- name:对齐,不能和模块参数对齐- 单位混淆:
lvol用m(MiB),parted用MiB,效果一样都是 1024 进制 - 卷组不存在时的处理:需要单独的 task 用
when判断,不能放在 block 里
验证¶
十一、创建分区¶
题目要求¶
创建 /home/devops/ansible/parted.yml,在 dev 主机组上操作:
- 如果 /dev/vdd 存在,创建 1500MiB 分区,失败则创建 800MiB
- 如果 /dev/vdb 存在,创建 1500MiB 分区,失败则创建 800MiB
- 分区格式化为 ext4 并挂载到 /mnt/fs01
题目分析¶
- 目标:在指定磁盘上创建分区并挂载
- 容错:1500MiB 不行就创建 800MiB(block/rescue)
- 条件判断:先判断磁盘是否存在(when + ansible_devices)
- 格式化+挂载:无论成功失败都执行(always)
- 关键逻辑:vdb 只在 vdd 不存在时才操作(避免两个磁盘挂载到同一点)
准备工作(重置磁盘)¶
正式考试不需要做,但练习时需要先清理上一题的 LVM。
# 1. 删除逻辑卷
ansible all -m shell -a 'lvremove /dev/research/data -y'
# 2. 删除卷组
ansible all -m shell -a 'vgremove research -y'
# 3. 删除物理卷
ansible all -m shell -a 'pvremove /dev/vdb1'
# 4. 用零填充磁盘开头,清除分区表
ansible all -m shell -a 'dd if=/dev/zero of=/dev/vdb bs=512 count=1'
# 5. 验证:fdisk -l /dev/vdb 没有分区信息即为成功
ansible all -m shell -a 'fdisk -l /dev/vdb'
完整 Playbook(parted.yml)¶
- hosts: dev
tasks:
- name: vdd exists
block:
- name: Create 1500m
parted:
device: /dev/vdd
number: 1
state: present
part_end: 1501MiB
rescue:
- name: Output fail msg
debug:
msg: Could not create partition of that size
- name: Create 800m
parted:
device: /dev/vdd
number: 1
state: present
part_end: 801MiB
always:
- name: format partition
filesystem:
fstype: ext4
dev: /dev/vdd1
- name: mount device
mount:
path: /mnt/fs01
src: /dev/vdd1
fstype: ext4
opts: defaults
state: mounted
when: "'vdd' in ansible_devices"
- name: vdd not exits
debug:
msg: disk /dev/vdd does not exist
when: "'vdd' not in ansible_devices"
- name: vdb exists
block:
- name: Create 1500m
parted:
device: /dev/vdb
number: 1
state: present
part_end: 1501MiB
rescue:
- name: Output fail msg
debug:
msg: Could not create partition of that size
- name: Create 800m
parted:
device: /dev/vdb
number: 1
state: present
part_end: 801MiB
always:
- name: format partition
filesystem:
fstype: ext4
dev: /dev/vdb1
- name: mount device
mount:
path: /mnt/fs01
src: /dev/vdb1
fstype: ext4
opts: defaults
state: mounted
when: "'vdb' in ansible_devices and 'vdd' not in ansible_devices"
- name: vdb not exists
debug:
msg: disk /dev/vdb does not exist
when: "'vdb' not in ansible_devices and 'vdd' not in ansible_devices"
代码逐行解析¶
第一部分:vdd exists¶
- name: vdd exists # 给 task 起个名字,更清晰
block:
- name: Create 1500m
parted:
device: /dev/vdd # 操作的磁盘
number: 1 # 分区号(第一个分区)
state: present # present = 创建分区
part_end: 1501MiB # ⚠️ 参数名是 part_end,不是 parted_end
rescue:
- name: Output fail msg
debug:
msg: Could not create partition of that size
- name: Create 800m
parted:
device: /dev/vdd
number: 1
state: present
part_end: 801MiB # 退而求其次
always:
- name: format partition
filesystem:
fstype: ext4
dev: /dev/vdd1 # ⚠️ 分区设备 = 磁盘 + 分区号
- name: mount device
mount:
path: /mnt/fs01 # 挂载点
src: /dev/vdd1 # 设备路径
fstype: ext4 # 文件系统类型
opts: defaults # 挂载选项(默认)
state: mounted # 挂载并写入 fstab
when: "'vdd' in ansible_devices" # 只在 vdd 存在时执行
关键点:
- part_end: 1501MiB 而不是 1500MiB,多 1MiB 避免边界计算问题
- when 和 - name: / block: 对齐(任务级指令),不是和模块参数对齐
- always 里的任务无论 block 成功还是失败都会执行
第二部分:vdd 不存在¶
第三部分:vdb exists(关键逻辑)¶
- name: vdb exists
block:
# ... 同 vdd 的处理 ...
# ⚠️ 关键:复合条件!
when: "'vdb' in ansible_devices and 'vdd' not in ansible_devices"
为什么 vdb 的条件要加 and 'vdd' not in ansible_devices?
因为 vdd 和 vdb 都挂载到 /mnt/fs01,只能选一块盘:
- vdd 存在 → 用 vdd
- vdd 不存在,vdb 存在 → 用 vdb
- 都不存在 → 都跳过
第四部分:vdb 不存在¶
- name: vdb not exists
debug:
msg: disk /dev/vdb does not exist
when: "'vdb' not in ansible_devices and 'vdd' not in ansible_devices"
⚠️ 注意:vdb 的 when 条件里
and前后都是 vdd,这是题目原文的 typo,正常应该是一个 vdb 一个 vdd。
扩展知识点¶
block/rescue/always 错误处理¶
| 关键字 | 作用 | 类比 |
|---|---|---|
block |
正常执行的任务 | try |
rescue |
block 中任一任务失败时执行 | catch |
always |
无论成功失败都执行 | finally |
Facts 变量判断¶
| 变量 | 用途 |
|---|---|
ansible_lvm.vgs |
包含所有卷组信息的字典 |
'research' in ansible_lvm.vgs |
判断卷组是否存在 |
lvol 模块¶
| 参数 | 含义 |
|---|---|
vg |
卷组名 |
lv |
逻辑卷名 |
size |
大小(m = MiB,按 1024 进制计算) |
filesystem 模块¶
| 参数 | 含义 |
|---|---|
fstype |
文件系统类型(ext4/xfs 等) |
dev |
设备路径(/dev/research/data) |
常见错误¶
when缩进错误:必须和- name:对齐,不能和模块参数对齐- 单位混淆:
lvol用m(MiB),parted用MiB,效果一样都是 1024 进制 - 卷组不存在时的处理:需要单独的 task 用
when判断,不能放在 block 里
验证¶
十二、生成主机文件¶
题目要求¶
创建 Jinja2 模板 hosts.j2 和 playbook hosts.yml,在 dev 主机组生成 /etc/myhosts 文件。
模板 hosts.j2¶
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
{% for host in groups.all %}
{{ hostvars[host].ansible_eth0.ipv4.address }} {{ hostvars[host].ansible_fqdn }} {{ hostvars[host].ansible_hostname }}
{% endfor %}
模板逐行解析¶
第一行:IPv4 localhost 条目¶
| 字段 | 含义 |
|---|---|
127.0.0.1 |
IPv4 回环地址(本机) |
localhost |
主机名(短名) |
localhost.localdomain |
完全限定域名(FQDN) |
localhost4 |
IPv4 专用别名 |
localhost4.localdomain4 |
IPv4 别名的 FQDN |
第二行:IPv6 localhost 条目¶
| 字段 | 含义 |
|---|---|
::1 |
IPv6 回环地址(相当于 IPv4 的 127.0.0.1) |
localhost |
主机名(和 IPv4 共用) |
localhost.localdomain |
FQDN(和 IPv4 共用) |
localhost6 |
IPv6 专用别名 |
localhost6.localdomain6 |
IPv6 别名的 FQDN |
第三行:Jinja2 循环(渲染每台主机的条目)¶
{% for host in groups.all %}
{{ hostvars[host].ansible_eth0.ipv4.address }} {{ hostvars[host].ansible_fqdn }} {{ hostvars[host].ansible_hostname }}
{% endfor %}
Jinja2 语法分解:
循环体内的变量:
{{ hostvars[host].ansible_eth0.ipv4.address }} ← 该主机的 IPv4 地址
{{ hostvars[host].ansible_fqdn }} ← 该主机的完全限定域名
{{ hostvars[host].ansible_hostname }} ← 该主机的主机名
hostvars[host] 结构图:
hostvars["servera"]
├── ansible_eth0
│ └── ipv4
│ └── address: "172.25.250.10"
├── ansible_fqdn: "servera.lab.example.com"
└── ansible_hostname: "servera"
渲染结果示例(假设 dev 组有 servera 和 serverb):
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
172.25.250.10 servera.lab.example.com servera
172.25.250.11 serverb.lab.example.com serverb
Playbook¶
- hosts: all
- hosts: dev
tasks:
- name: copy hosts.j2 to dev
template:
src: hosts.j2
dest: /etc/myhosts
Playbook 逐行解析¶
- hosts: all # 第一个 play:空 play,触发 gather_facts 采集所有主机信息
- hosts: dev # 第二个 play:在 dev 主机组上执行
tasks:
- name: copy hosts.j2 to dev
template: # template 模块:渲染 Jinja2 模板并复制
src: hosts.j2 # 源模板文件
dest: /etc/myhosts # 目标路径
为什么第一个 play 是 hosts: all 且没有 tasks?
- Ansible 默认在执行 play 时
gather_facts: true - 这个空 play 触发收集所有主机的 Facts(IP、主机名、FQDN 等)
- 第二个 play 渲染模板时需要引用
hostvars[host].ansible_eth0.ipv4.address - 如果不先采集 facts,模板里的变量会是空的
为什么第二个 play 在 dev 上执行,但模板里写的是 groups.all?
template模块只在 dev 主机上运行,但模板里的groups.all可以访问所有主机的变量- 这样就能在 dev 主机上生成包含所有主机信息的 hosts 文件
扩展知识点¶
Jinja2 模板语法¶
| 语法 | 含义 | 示例 |
|---|---|---|
{% %} |
逻辑语句(循环、条件) | {% for %}, {% if %} |
{{ }} |
变量输出 | {{ host }} |
{# #} |
注释 | {# 这是注释 #} |
常用 Jinja2 语句¶
{# 循环 #}
{% for item in list %}
{{ item }}
{% endfor %}
{# 条件 #}
{% if condition %}
...
{% elif other %}
...
{% else %}
...
{% endif %}
Ansible Facts 变量¶
| 变量 | 含义 |
|---|---|
groups.all |
所有主机列表 |
groups.dev |
dev 主机组的主机列表 |
hostvars |
所有主机的变量字典 |
hostvars[host].ansible_eth0.ipv4.address |
指定主机的 IPv4 地址 |
hostvars[host].ansible_fqdn |
指定主机的完全限定域名 |
hostvars[host].ansible_hostname |
指定主机的主机名 |
inventory_hostname |
当前主机的主机名 |
template 模块 vs copy 模块¶
| 模块 | 用途 |
|---|---|
template |
渲染 Jinja2 模板,支持变量替换 |
copy |
直接复制文件,不支持变量 |
验证¶
# 在 dev 主机上查看生成的文件
ssh root@servera 'cat /etc/myhosts'
# 预期输出:
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
172.25.250.10 servera.lab.example.com servera
172.25.250.11 serverb.lab.example.com serverb
常见错误¶
- 没有第一个空 play:模板渲染时变量为空,生成的文件没有主机信息
- 模板路径错误:
src: hosts.j2要确保文件在 playbook 同目录或 roles 目录下 - FQDN 拼写:
ansible_fqdn不是ansible_domain|
十三、修改文件内容¶
题目要求¶
创建 /home/devops/ansible/issue.yml,根据主机组不同写入不同的 /etc/issue:
- dev → "Development"
- test → "Test"
- prod → "Production"
Playbook¶
- hosts: all
tasks:
- name: write Development to dev
copy:
content: "Development\n"
dest: /etc/issue
when: "'dev' in group_names"
- name: write Test to test
copy:
content: "Test\n"
dest: /etc/issue
when: "'test' in group_names"
- name: write Production to prod
copy:
content: "Production\n"
dest: /etc/issue
when: "'prod' in group_names"
扩展开的知识点¶
when 条件判断¶
| 条件 | 含义 |
|---|---|
'dev' in group_names |
当前主机属于 dev 组 |
group_names |
当前主机所属的所有组列表 |
copy 模块 content 参数¶
content:直接写入字符串内容(不需要源文件)dest:目标文件路径
验证¶
十四、创建 Web 内容目录¶
题目要求¶
创建 Playbook,在 dev 主机组:
1. 安装启动 httpd
2. 创建 /webdev 目录(SGID),拥有组 devops
3. 符号链接 /var/www/html/webdev → /webdev
4. 创建 /webdev/index.html 内容为 "Development"
Playbook¶
- hosts: dev
tasks:
- name: install httpd
yum:
name: httpd
state: present
- name: enable httpd
systemd:
name: httpd
enabled: yes
state: started
- name: enable 80/tcp
firewalld:
service: http
immediate: yes
permanent: yes
state: enabled
- name: Create webdev directory
file:
path: /webdev
state: directory
owner: root
group: devops
mode: '2775'
setype: httpd_sys_content_t
- name: Create file
copy:
content: "Development\n"
dest: /webdev/index.html
setype: httpd_sys_content_t
- name: Create soft link
file:
src: /webdev
dest: /var/www/html/webdev
state: link
扩展开的知识点¶
HTTPD 工作原理与软链接¶
为什么需要软链接?
httpd 默认网站根目录:/var/www/html/
访问 http://servera.lab.example.com/webdev/
↓
httpd 实际查找:/var/www/html/webdev/
但题目要求文件放在 /webdev/(根目录下),不是 /var/www/html/webdev/
实际文件位置:/webdev/index.html ← 根目录下的 webdev
httpd 期望位置:/var/www/html/webdev/ ← 网站根目录下
两个位置不同!httpd 找不到文件!
解决方案:创建软链接
- name: Create soft link
file:
src: /webdev # 原始位置(实际文件在这)
dest: /var/www/html/webdev # httpd 期望的位置
state: link # 创建符号链接
流程图:
用户访问 http://servera.lab.example.com/webdev/
↓
httpd 查找 /var/www/html/webdev/
↓
发现是软链接 → 跳转到 /webdev/
↓
读取 /webdev/index.html
↓
返回 "Development"
一句话理解:内容放在 /webdev,但 httpd 只认 /var/www/html/,所以用软链接“骗”一下 httpd。
文件权限 mode¶
| mode | 含义 |
|---|---|
2775 |
SGID(2)+ rwxrwxr-x |
2(首位)= SGID:在该目录下创建的文件继承目录的组7= rwx(owner)7= rwx(group)5= r-x(other)
SELinux 上下文¶
| 参数 | 含义 |
|---|---|
setype: httpd_sys_content_t |
设置 SELinux 类型标签为 Web 内容 |
符号链接 file 模块参数¶
| 参数 | 含义 |
|---|---|
src |
原始文件/目录路径 |
dest |
链接文件路径 |
state: link |
创建符号链接 |
state: hard |
创建硬链接 |
state: absent |
删除 |
符号链接 vs 硬链接¶
| 类型 | 特点 |
|---|---|
| 符号链接(软链接) | 独立文件,指向源文件路径,源文件删除后链接失效 |
| 硬链接 | 和源文件共享 inode,源文件删除后仍可访问 |
验证¶
十五、生成硬件报告¶
题目要求¶
创建 /home/devops/ansible/hwreport.yml,生成 /root/hwreport.txt,包含:主机名、内存、BIOS 版本、vda/vdb 磁盘大小。
Playbook¶
- hosts: all
tasks:
- name: Create report file
get_url:
url: http://172.25.254.254/content/hwreport.empty
dest: /root/hwreport.txt
- name: Get inventory_hostname
replace:
path: /root/hwreport.txt
regexp: 'inventoryhostname'
replace: '{{ inventory_hostname }}'
- name: Get memory total size
replace:
path: /root/hwreport.txt
regexp: 'memory_in_MB'
replace: "{{ ansible_memtotal_mb | string }}"
- name: Get bios version
replace:
path: /root/hwreport.txt
regexp: 'BIOS_version'
replace: "{{ ansible_bios_version }}"
- name: Get disk vda size
replace:
path: /root/hwreport.txt
regexp: 'disk_vda_size'
replace: "{{ ansible_devices.vda.size | default('NONE') }}"
- name: Get disk vdb size
replace:
path: /root/hwreport.txt
regexp: 'disk_vdb_size'
replace: "{{ ansible_devices.vdb.size | default('NONE') }}"
代码逐行解析¶
replace 模块基本逻辑¶
文件里预先有一堆占位符(如 memory_in_MB、BIOS_version 等),playbook 的任务就是把它们替换成真实的系统信息。
各变量详解¶
| 变量 | 含义 | 类型 |
|---|---|---|
inventory_hostname |
inventory 中定义的主机名 | 字符串,直接用 |
ansible_memtotal_mb |
目标主机总内存(MB),如 2048 | 整数(int) |
ansible_bios_version |
BIOS/固件版本号 | 字符串 |
ansible_devices.vda.size |
第一块虚拟磁盘大小 | 字符串 |
ansible_devices.vdb.size |
第二块虚拟磁盘大小 | 字符串 |
ansible_memtotal_mb | string 详解 ⭐¶
ansible_memtotal_mb:setup 模块自动采集的 facts,返回整数(如 2048)| string:Jinja2 类型转换过滤器,2048(int) →"2048"(string)- 为什么加:
replace模块的replace参数期望字符串,直接用整数可能报类型错误 - 类比 Python:
str(2048)的效果 - 考试里如果忘了加
| string,可能不会报错(不同版本行为不同),但加上是保险写法
default('NONE') 过滤器¶
- 用法:
{{ ansible_devices.vda.size | default('NONE') }} - 含义:如果值不存在(比如机器没有 vda 盘),返回
'NONE'而不是报 UndefinedError - 用于处理硬件项不存在的情况(如 bastion 没有 vdb 磁盘)
扩展开的知识点¶
get_url 模块¶
- 从 URL 下载文件到目标主机
replace 模块¶
| 参数 | 含义 |
|---|---|
path |
要修改的文件路径 |
regexp |
正则表达式匹配 |
replace |
替换内容 |
default 过滤器¶
{{ var | default('NONE') }}:如果变量不存在则使用默认值- 用于处理硬件项不存在的情况(如 bastion 没有 vdb 磁盘)
常用 Facts 变量¶
| 变量 | 含义 |
|---|---|
ansible_memtotal_mb |
内存总量(MB) |
ansible_bios_version |
BIOS 版本 |
ansible_devices.vda.size |
vda 磁盘大小 |
验证¶
十六、创建密码库¶
题目要求¶
创建 Ansible Vault 存储密码,加密密码为 fgydsxsxsbxs。
实际操作¶
# 创建变量文件
vim locker.yml
# 内容:
# pw_developer: Imadev
# pw_manager: Imamgr
# 创建密码文件
echo fgydsxsxsbxs > secret.txt
# 加密
ansible-vault encrypt --vault-password-file=secret.txt locker.yml
扩展开的知识点¶
ansible-vault 命令¶
| 命令 | 作用 |
|---|---|
ansible-vault encrypt |
加密文件 |
ansible-vault decrypt |
解密文件 |
ansible-vault view |
查看加密文件内容 |
ansible-vault edit |
编辑加密文件 |
ansible-vault rekey |
更改密码 |
ansible-vault create |
创建新加密文件 |
运行 playbook 时解密¶
ansible-playbook --vault-password-file=secret.txt playbook.yml
ansible-playbook --ask-vault-pass playbook.yml
十七、创建用户账户¶
题目要求¶
创建 /home/devops/ansible/users.yml,根据 user_list.yml 和 locker.yml 创建用户。
Playbook¶
- hosts: dev, test
vars_files:
- locker.yml
- user_list.yml
tasks:
- name: Ensure group "devops" exists
group:
name: devops
state: present
- name: Create user in developer
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: devops
password: "{{ pw_developer | password_hash('sha512') }}"
loop: "{{ users }}"
when: item.job == 'developer'
- hosts: prod
vars_files:
- locker.yml
- user_list.yml
tasks:
- name: Ensure group "opsmgr" exists
group:
name: opsmgr
state: present
- name: Create user in manager
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: opsmgr
password: "{{ pw_manager | password_hash('sha512') }}"
loop: "{{ users }}"
when: item.job == 'manager'
代码逐行解析¶
item 变量的来源 ⭐¶
关键在 loop: "{{ users }}":
users是从user_list.yml加载的变量,结构是一个列表(list),每个元素是一个字典(dict)user_list.yml内容示例:
users:
- name: alice
uid: 1010
job: developer
- name: bob
uid: 1011
job: manager
- name: charlie
uid: 1012
job: developer
- 当 Ansible 执行
loop: "{{ users }}"时,遍历列表,每次循环把当前元素赋值给item变量
| 循环次数 | item 的值 |
item.name |
item.job |
|---|---|---|---|
| 第1次 | {name: alice, uid: 1010, job: developer} |
alice | developer |
| 第2次 | {name: bob, uid: 1011, job: manager} |
bob | manager |
| 第3次 | {name: charlie, uid: 1012, job: developer} |
charlie | developer |
item.job 属性详解¶
item是字典,item.job用点号访问字典的键- 类比 Python:
item["job"]等价于item.job when: item.job == 'developer'按条件过滤,只处理 job 为 developer 的用户
loop + when 执行逻辑¶
loop 遍历 users 列表
│
├─ item = {name: alice, job: developer}
│ when: item.job == 'developer' → ✅ 执行创建任务
│
├─ item = {name: bob, job: manager}
│ when: item.job == 'developer' → ❌ 跳过
│
└─ item = {name: charlie, job: developer}
when: item.job == 'developer' → ✅ 执行创建任务
一句话:item 是 loop 循环自动提供的变量,每次循环指向列表中的当前元素。item.job 就是取这个元素字典里 job 键的值。
when 为什么不需要 {{ }}?⭐¶
| 位置 | 是否自动解析 | 写法 |
|---|---|---|
when: |
✅ 自动当成 Jinja2 表达式 | when: item.job == 'developer' |
loop: |
✅ 自动当成 Jinja2 表达式 | loop: "{{ users }}" |
| 模块参数(name、uid 等) | ❌ 默认是字符串,不会自动解析 | name: "{{ item.name }}" |
原因:
- when 是 Ansible 关键字,从设计上就知道后面要跟条件表达式,自动解析
- 模块参数(如 user 的 name)是模块自己定义的,Ansible 不知道你传的是变量还是字面字符串,必须用 {{ }} 显式插值
类比 Python:
一句话记住:when 后面永远不需要 {{ }},它是关键字自动解析。模块参数默认是字符串,要插变量必须加 {{ }}。
扩展开的知识点¶
vars_files¶
- 从外部 YAML 文件加载变量
- 可以加载加密的 Vault 文件
- 变量在 play 级别可用
password_hash 过滤器¶
{{ pw | password_hash('sha512') }}:生成 SHA512 哈希密码- 配合 user 模块的
password参数使用
user 模块¶
| 参数 | 含义 |
|---|---|
name |
用户名 |
uid |
用户 ID |
groups |
附加组 |
password |
加密后的密码字符串 |
执行方式¶
ansible-playbook --vault-password-file=secret.txt users.yml
ansible-navigator run users.yml -m stdout --vault-password-file=secret.txt
十八、更新 Ansible 库的密钥¶
题目要求¶
更改 Vault 密码:旧密码 insecure4sure → 新密码 bbe2de98389b。
实际操作¶
ansible-vault rekey salaries.yml
# Vault password: insecure4sure
# New Vault password: bbe2de98389b
# Confirm New Vault password: bbe2de98389b
扩展开的知识点¶
ansible-vault rekey¶
- 更改 Vault 加密文件的密码
- 交互式输入旧密码和新密码
- 文件内容不变,只改变加密密钥
十九、创建计划任务¶
题目要求¶
为 natasha 用户创建计划任务,每隔 2 分钟执行 echo hello,playbook 文件为 cron.yml,在 dev 组运行。
Playbook¶
- hosts: dev
tasks:
- name: create natasha
user:
name: natasha
state: present
- name: create cron tasks
cron:
name: "exec tasks every 2 minute"
minute: "*/2"
user: natasha
job: "echo hello"
扩展开的知识点¶
cron 模块¶
| 参数 | 含义 | 取值 |
|---|---|---|
name |
任务描述(用于标识和删除) | 任意字符串 |
minute |
分钟 | */2(每2分钟) |
hour |
小时 | *(每小时) |
day |
日 | *(每天) |
month |
月 | *(每月) |
weekday |
星期 | *(每天) |
user |
执行用户 | 用户名 |
job |
执行的命令 | 命令字符串 |
state |
absent 删除任务 | present/absent |
删除任务¶
验证¶
综合速查¶
执行 playbook 两种方式¶
# 方式一:ansible-navigator(容器执行环境)
ansible-navigator run playbook.yml -m stdout
ansible-navigator run playbook.yml -i inventory -m stdout
# 方式二:ansible-playbook(直接执行)
ansible-playbook playbook.yml
ansible-playbook playbook.yml --vault-password-file=secret.txt
ansible-navigator 注意事项¶
- 需要正确的 PATH 环境变量(包含 ansible/python 路径)
su - devops(login shell,完整加载环境变量)✅su devops(非 login shell)❌- 使用
-m stdout参数让输出直接在终端显示
常用验证命令¶
ansible all -m ping # 连接测试
ansible all -m shell -a 'command' # 批量执行命令
ansible-inventory --list # 查看清单
ansible-navigator images # 获取导航器镜像
podman login utility.lab.example.com # 登录镜像仓库
二十、补充知识点(2026-05-12)¶
Roles.yml 中空 Play 的作用¶
- hosts: webservers # 空 play,没有 roles
- hosts: balancers
roles:
- balancer
- hosts: webservers
roles:
- phpinfo
第一个 - hosts: webservers 为什么是空的?
目的:提前采集 webservers 的 facts(IP、主机名等)。
Ansible 默认在执行 play 时自动 gather_facts。这个 play 虽然没写 roles 和任务,但执行后 webservers 的 facts 就被采集了。
为什么需要? 后面的 balancer 角色要配置负载均衡,需要知道 webservers 的 IP 地址(写进 httpd 的 BalancerMember 配置)。如果不先采集 facts,balancer 角色拿不到 IP,模板渲染会出错。
常见套路:先跑一个空 play 采集目标主机 facts,后面再用。
| play | 作用 |
|---|---|
hosts: webservers(空) |
采集 facts,让 balancer 能拿到 webservers 的 IP |
hosts: balancers + balancer |
配置负载均衡 |
hosts: webservers + phpinfo |
配置 phpinfo 页面 |
Balancer 角色中的 haproxy + httpd 配合¶
场景:bastion 主机作为负载均衡器,需要同时运行 haproxy 和 httpd。
原理: - haproxy 监听 80 端口,负责请求分发(负载均衡) - httpd 提供本地服务(如 stats 页面或 fallback 页面) - 两个服务必须同时运行才能实现完整的负载均衡效果
常见问题:多次运行 ansible-playbook 后,可能出现 haproxy 占用 80 端口导致 httpd 无法启动的情况。
排查步骤:
# 1. 检查端口占用
ssh root@bastion 'ss -tlnp | grep :80'
# 2. 如果 haproxy 占用 80 端口,先停掉
ssh root@bastion 'systemctl stop haproxy && systemctl disable haproxy'
# 3. 杀掉残留进程
ssh root@bastion 'fuser -k 80/tcp'
# 4. 重新跑 playbook
ansible-playbook roles.yml
关键教训:
- Connection refused 不一定是防火墙问题,先查端口占用
- 多次运行 ansible playbook 可能导致服务状态混乱
- 排查顺序:端口占用 → 服务状态 → 防火墙 → SELinux
- Galaxy 下载的 role 可能同时管理多个服务(haproxy + httpd),要注意服务间的依赖关系
LVM 模块的单位差异(parted vs lvol)¶
问题:为什么 parted 模块用 MiB,而 lvol 模块用 m?
答案:两个模块的单位约定不同,但实际效果一样,都是按 1024 进制计算。
| 模块 | 单位 | 含义 |
|---|---|---|
parted |
MiB |
Mebibyte,1 MiB = 1024² bytes(二进制,标准命名) |
lvol |
m |
LVM 内部按 MiB 计算,简写为 m |
实际对比:
parted: part_start: 1MiB # 1 × 1024 × 1024 bytes
lvol: size: 600m # 600 × 1024 × 1024 bytes(LVM 按 MiB 算)
考试速记:
- parted → 写 MiB(如 600MiB)
- lvol → 写 m(如 600m)
- 两个效果一样,按模块约定写就行
parted 和 lvol 创建逻辑卷的流程¶
典型步骤:
1. parted 创建分区(part_start: 1MiB, part_end: 600MiB)
2. lvg 创建卷组(vg: vg-data, pvs: /dev/sdb1)
3. lvol 创建逻辑卷(lv: lv-data, size: 500m, vg: vg-data)
注意:分区大小和逻辑卷大小可以不一样,分区是物理层面,逻辑卷是逻辑层面,逻辑卷不能超过卷组总大小。
dd 命令清除分区表¶
命令:dd if=/dev/zero of=/dev/vdb bs=512 count=1
| 参数 | 含义 |
|---|---|
dd |
底层磁盘复制工具 |
if=/dev/zero |
输入源,/dev/zero 提供无限空字节(\x00) |
of=/dev/vdb |
输出目标,直接写到 vdb 磁盘 |
bs=512 |
每次读写 512 字节(= 1 个扇区) |
count=1 |
只写 1 次 |
效果:vdb 第 1 个扇区(MBR 分区表位置)被全写成零,分区表消失。
为什么用 dd 而不是 mkfs?
- dd 是最底层操作,直接写磁盘原始数据,不需要文件系统存在
- mkfs 需要先有分区才能格式化,dd 连分区表都能干掉
- 考试里清分区表最快的方式
⚠️ 危险:if 和 of 写反就全完了,千万别搞错盘符!
验证是否清除成功:
成功标志:没有Device Boot / Start / End 分区表头出现。
RHCE 第11题:关键逻辑补充¶
核心逻辑:vdd 和 vdb 二选一,vdd 优先。
为什么? 两块盘都挂载到 /mnt/fs01,只能选一块。
part_end 用 1501MiB 而不是 1500MiB:多 1MiB 避免边界计算问题。
准备工作(重置磁盘):
lvremove /dev/research/data -y
vgremove research -y
pvremove /dev/vdb1
dd if=/dev/zero of=/dev/vdb bs=512 count=1 # 清除分区表
验证:fdisk -l /dev/vdb 没有分区信息即为成功。
parted 模块参数名变更¶
正确参数名:part_end(不是 parted_end)
# 正确写法(考试推荐)
parted:
device: /dev/vdd
number: 1
state: present
part_end: 1501MiB
# ❌ 错误写法
parted:
device: /dev/vdd
number: 1
state: present
parted_end: 1501MiB # 这个参数名不对!
给 task 起名字的好处:- name: vdd exists 比直接写 - block: 更清晰,执行时报错也更容易定位。
二十一、补充知识点(2026-05-16)¶
ansible-navigator 与 ansible-playbook 的区别¶
| ansible-playbook | ansible-navigator | |
|---|---|---|
| 执行环境 | 宿主机本地 | 容器内(Podman) |
| 输出 | 直接打到终端 | 默认进交互界面,加 -m stdout 才打到终端 |
| 环境隔离 | ❌ | ✅ |
| 配置文件 | ansible.cfg | ansible-navigator.yml(指定容器镜像) |
考试关键:配置文件 ansible-navigator.yml 指定容器镜像地址,考试镜像在 registry.lab.example.com。
ansible-navigator run site.yml -i inventory -m stdout # 跑 playbook
ansible-navigator logs # 查看日志
ansible-navigator explore # 交互式调试
navigator 为什么加 -i inventory¶
ansible-playbook(本机执行)→ 读ansible.cfg里的 inventory 配置,自动找到ansible-navigator(容器里执行)→ 容器内"当前目录"可能和宿主机不同,ansible.cfg里的相对路径可能找不到-i inventory是显式指定 inventory 路径的保险写法,防止No inventory was parsed警告- 简单记:本地跑一般不用加,容器跑加
-i更稳
ansible all -m shell 远程批量执行¶
ansible all -m shell -a 'cat /etc/yum.repos.d/rhel_dvd.repo'
ansible all -m shell -a 'yum clean all && yum makecache'
all= 所有主机(可换成组名或单台主机)-m shell= 使用 shell 模块(支持管道、重定向、$变量)-a '命令'= 传给模块的参数- shell vs command 模块:shell 支持管道/重定向但安全性低,command 不支持管道但更安全
Ansible yml 中双引号和单引号的区别¶
大多数情况没区别,YAML 对字符串处理宽松。真正有区别的三个场景:
| 场景 | 双引号 | 单引号 |
|---|---|---|
特殊字符(: #) |
✅ 正常解析 | 必须加引号 |
转义(\n \t) |
✅ 支持转义 | 不转义,字面量 |
变量替换({{ var }}) |
✅ 会替换 | 不替换,当普通字符串 |
考试建议:统一用单引号最省心,不容易出坑。
RHCE 第四题验证方式¶
任务一:install Development Tools
ansible dev -m shell -a 'rpm -q "Development Tools"'
ansible dev -m shell -a 'yum groupinfo "Development Tools"' # 更靠谱
任务二:update pkgs
Ansible roles 两种写法 + become 位置¶
become 的位置区别:
# become 在 role 下面 → 只对这个 role 生效
roles:
- role: selinux
become: true
# become 在 play 顶层 → 对整个 play 所有 task 生效
roles:
- selinux
become: true
多个 role 时,become 放 role 下面更精准,只给需要的角色提权。
安装集合用 yml 文件的写法¶
方法一:requirements.yml(推荐)
collections:
- name: ansible-posix
source: http://content.example.com/ansible-posix-1.5.1.tar.gz
- name: community.general
source: http://content.example.com/community-general-6.3.0.tar.gz
方法二:playbook 里用 collections 关键字
- name: Install Collections
hosts: localhost
connection: local
collections:
- name: ansible-posix
source: http://content.example.com/ansible-posix-1.5.1.tar.gz
区别:requirements.yml 纯下载集合更简洁,考试更常用。
Ansible playbook 中 name 和 hosts 的顺序¶
- YAML 的 play 是字典(key-value 对),key 的顺序随便写
- name: xxx开头和- hosts: xxx开头效果完全一样- 参考书多用
hosts在前只是习惯/风格,不是语法要求 - 结论:两种写法都合法,考试里用哪种都行
二十二、补充知识点(2026-05-17)¶
Ansible IPv4 变量两种写法对比¶
| 变量 | 含义 | 适用场景 |
|---|---|---|
ansible_default_ipv4.address |
系统默认路由的 IP | 通用,不依赖网卡名 |
ansible_eth0.ipv4.address |
eth0 网卡的 IP | 考试环境网卡就叫 eth0 |
考试中的使用:
- CE 第 8 题(template)用 ansible_default_ipv4.address
- CE 第 12 题(hosts 模板)用 hostvars[host].ansible_eth0.ipv4.address
- 生产环境优先用 ansible_default_ipv4.address,不绑死网卡名
CE 第 12 题 - hosts 文件模板原理补充¶
Playbook 两个 play 的作用:
1. hosts: all(空 play)→ 触发 gather_facts,控制节点 SSH 到所有主机采集信息
2. hosts: dev(执行 template)→ 只在 dev 组机器上渲染并写入 /etc/myhosts
关键理解:
- gather_facts 在所有主机上采集,但 template 只在目标主机上执行
- 模板里 groups.all 能访问所有主机的 facts,所以 dev 主机上能生成包含全部主机信息的 hosts 文件
service vs systemd 模块区别¶
| 模块 | 调用 | 特点 | 适用场景 |
|---|---|---|---|
service |
SysV init 脚本 | 兼容老系统 | 老版本 RHEL |
systemd |
systemctl | 支持 daemon_reload | RHEL 9,生产环境推荐 |
考试要点:
- 考试里两个都能得分,start/enable 效果一样
- 必须用 systemd 的场景:改了 unit 文件后需要 daemon_reload
CE 第 17 题 - Vault 加密文件如何读取变量¶
完整流程:
1. locker.yml 用 ansible-vault encrypt 加密 → 磁盘上是乱码
2. 执行时加 --vault-password-file=secret.txt 提供密码
3. Ansible 在内存中解密 locker.yml,变量 pw_developer/pw_manager 正常可用
4. 磁盘上的 locker.yml 始终保持加密状态
设计目的: - 密码文件(secret.txt)可以提交 git - vault 密码本地保管不泄露
Playbook 变量加载:
vars_files:
- locker.yml # 加密的变量文件
- user_list.yml # 明文变量文件
loop: "{{ users }}" # 遍历 users 列表
when: item.job == 'developer' # 条件过滤
password_hash 过滤器: {{ pw_developer | password_hash('sha512') }} # 明文转 SHA512 哈希
二十三、RHCE 变题分析 (2026-05-24)¶
题目 1:生成主机文件¶
- j2 模板 + playbook,部分可直接下载 hosts.yml
- 模板用
groups['dev']遍历主机组
题目 2:创建用户账户(新增 30 天过期)¶
新增字段:
| 字段 | 含义 |
|---|---|
password_expire_max: 30 |
密码最长有效期(/etc/shadow 第5字段) |
expires |
账号过期时间(/etc/shadow 第8字段) |
两个字段区别:
password_expire_max= 密码本身有效期,30天后必须改密码expires= 账号有效期,30天后账号失效
playbook 结构:
- Play 1: dev + test 主机组 → developer 用户 → devops 组
- Play 2: prod 主机组 → manager 用户 → opsmgr 组
- 两个 play 都用 vars_files 引入 locker.yml + user_list.yml
- when: item.job == "developer/manager" 过滤用户
执行命令:
ansible-navigator run users.yml -m stdout --vault-password-file=secret.txt
# 或
ansible-playbook --vault-password-file=secret.txt users.yml
二十四、expires 字段详解 (2026-05-24)¶
表达式:lookup('pipe', 'expr ($(date +%s) + 2592000) / 86400')
逐层拆解:
| 步骤 | 命令 | 说明 |
|---|---|---|
| 1 | date +%s |
获取当前 Unix 时间戳(秒数) |
| 2 | + 2592000 |
加上 30 天的秒数 |
| 3 | / 86400 |
转换成天数(86400 秒 = 1 天) |
| 4 | expr |
Linux 算术计算 |
| 5 | lookup('pipe', ...) |
Ansible 执行 shell 命令获取输出 |
最终结果:从 1970-01-01 起的天数,对应 /etc/shadow 第 8 字段(账号过期日期)
⚠️ expr 计算顺序:expr $(date +%s) + 2592000 / 86400 实际先算除法,可能有误。正确应加括号。但考试照抄图片答案即可。
二十五、Ansible lookup 详解 (2026-05-24)¶
表达式:lookup('pipe', 'expr $(date +%s) + 2592000')
逐个拆解:
| 部分 | 含义 |
|---|---|
lookup |
Ansible 查找函数,从外部获取数据 |
'pipe' |
lookup 类型,表示执行 shell 命令并返回输出 |
'expr $(date +%s) + 2592000' |
要执行的 shell 命令 |
lookup 常见类型:
| 类型 | 用途 |
|---|---|
file |
读文件内容 |
env |
读环境变量 |
pipe |
执行 shell 命令,返回输出 |
csvfile |
读 CSV 文件 |
ini |
读 ini 配置文件 |
执行流程:Ansible → 执行 shell 命令 → 拿到输出 → 填入变量