跳转至

🤖 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

题目要求

  1. 安装 Ansible 软件包
  2. 创建静态清单文件 /home/devops/ansible/inventory
  3. 创建配置文件 /home/devops/ansible/ansible.cfg

清单文件

[dev]
servera

[test]
serverb

[prod]
serverc
serverd

[balancers]
bastion

[webservers:children]
prod

配置文件

[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']

验证

ansible all -m ping

二、创建受管节点存储库

题目要求

创建 /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-navigator run yum_repo.yml -i inventory -m stdout
# 或
ansible-playbook yum_repo.yml

验证

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 循环

name: "{{ item }}"
loop:
  - php
  - mariadb
  • {{ item }} 是 Jinja2 模板的循环变量
  • loop 列表中几个值就执行几次
  • 更简洁写法:name: [php, mariadb](不需要 loop)

yum 模块 state 参数

state 行为
present 已装就跳过,没装就装上
latest 检查更新,有就更新
absent 卸载

通配符和包组

写法 含义
name: '*' 所有软件包
name: "@Development Tools" @ 前缀表示软件包组

一个 YAML 多个 Play

一个 playbook 可以包含多个 Play(用 - hosts: 分隔),每个 Play 针对不同主机组执行不同任务。

验证

ansible dev,test,prod -m shell -a 'rpm -q php'

四、使用 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 服务器,按列表顺序优先级从高到低

验证

ansible all -m shell -a 'chronyc sources'

五、使用 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 all -m shell -a 'getenforce'

六、使用 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
  • 从 URLansible-galaxy install -r requirements.yml -p roles/

requirements.yml 格式

字段 含义
name 安装后的角色目录名
src 角色包的下载地址(URL)

命令参数

参数 含义
-r requirements.yml 从清单文件批量安装
-p roles/ 指定安装目录

流程

读取清单 → 下载 tar.gz → 解压到 roles/ 目录

七、安装集合

题目要求

ansible-posix-1.5.1.tar.gzcommunity-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/ 集合多,清单管理

安装后目录结构

mycollections/
└── ansible_collections/
    ├── ansible.posix/
    └── community.general/

关键知识点

  • ansible-galaxy collection install 安装集合(不是角色)
  • -p mycollections/ 指定安装目录
  • 需要在 ansible.cfg 配置 collections_path 指向安装目录
  • ansible-galaxy collection list 列出已安装集合

验证

ls mycollections/ansible_collections/

八、创建和使用角色

题目要求

创建 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

Welcome to {{ ansible_fqdn }} on {{ ansible_default_ipv4.address }}

Playbook newrole.yml

- name: use apache role
  hosts: webservers
  roles:
    - apache

扩展开的知识点

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 模板,用变量动态生成文件:

welcome to {{ ansible_fqdn }} on {{ ansible_default_ipv4.address }}

  • ansible_fqdn → 被管节点的完全限定域名
  • ansible_default_ipv4.address → 被管节点的 IP 地址
  • 每台机器执行时自动替换为自己的值,生成专属页面

tasks/main.yml 中用 template 模块部署:

- template:
    src: index.html.j2
    dest: /var/www/html/index.html

template 模块

  • 将 Jinja2 模板渲染后复制到目标主机
  • src: 模板文件路径(相对于 templates/
  • dest: 目标路径

firewalld 模块参数详解

- firewalld:
    service: http
    permanent: yes
    immediate: yes
    state: enabled
参数 含义 等价命令
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

- hosts: balancers
  roles:
    - balancer

- hosts: webservers
  roles:
    - phpinfo

扩展开的知识点

角色复用

  • 通过 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,如果卷组不存在则报错。

题目分析

  1. 目标:在 research 卷组中创建逻辑卷 data
  2. 容错:600m 不行就创建 400m(block/rescue)
  3. 异常处理:卷组不存在时报错提示(when 判断)
  4. 格式化:无论成功失败都格式化为 ext4(always)

完整流程

判断卷组是否存在
    ├─ 存在 → 尝试创建 600m 逻辑卷
    │         ├─ 成功 → 格式化 ext4
    │         └─ 失败 → 创建 400m → 格式化 ext4
    └─ 不存在 → 输出错误信息

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)

常见错误

  1. when 缩进错误:必须和 - name: 对齐,不能和模块参数对齐
  2. 单位混淆lvolm(MiB),partedMiB,效果一样都是 1024 进制
  3. 卷组不存在时的处理:需要单独的 task 用 when 判断,不能放在 block 里

验证

# 查看逻辑卷
ssh root@servera lvs

# 查看卷组
ssh root@servera vgs

十一、创建分区

题目要求

创建 /home/devops/ansible/parted.yml,在 dev 主机组上操作: - 如果 /dev/vdd 存在,创建 1500MiB 分区,失败则创建 800MiB - 如果 /dev/vdb 存在,创建 1500MiB 分区,失败则创建 800MiB - 分区格式化为 ext4 并挂载到 /mnt/fs01

题目分析

  1. 目标:在指定磁盘上创建分区并挂载
  2. 容错:1500MiB 不行就创建 800MiB(block/rescue)
  3. 条件判断:先判断磁盘是否存在(when + ansible_devices)
  4. 格式化+挂载:无论成功失败都执行(always)
  5. 关键逻辑: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 不存在

- name: vdd not exits
  debug:
    msg: disk /dev/vdd does not exist
  when: "'vdd' not in ansible_devices"

第三部分: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)

常见错误

  1. when 缩进错误:必须和 - name: 对齐,不能和模块参数对齐
  2. 单位混淆lvolm(MiB),partedMiB,效果一样都是 1024 进制
  3. 卷组不存在时的处理:需要单独的 task 用 when 判断,不能放在 block 里

验证

# 查看逻辑卷
ssh root@servera lvs

# 查看卷组
ssh root@servera vgs

十二、生成主机文件

题目要求

创建 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 localhost localhost.localdomain localhost4 localhost4.localdomain4
字段 含义
127.0.0.1 IPv4 回环地址(本机)
localhost 主机名(短名)
localhost.localdomain 完全限定域名(FQDN)
localhost4 IPv4 专用别名
localhost4.localdomain4 IPv4 别名的 FQDN

第二行:IPv6 localhost 条目

::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
字段 含义
::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 语法分解

{% for host in groups.all %}    ← 循环开始:遍历所有主机
  ...                           ← 循环体:对每台主机执行
{% endfor %}                    ← 循环结束

循环体内的变量

{{ 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?

  1. Ansible 默认在执行 play 时 gather_facts: true
  2. 这个空 play 触发收集所有主机的 Facts(IP、主机名、FQDN 等)
  3. 第二个 play 渲染模板时需要引用 hostvars[host].ansible_eth0.ipv4.address
  4. 如果不先采集 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

常见错误

  1. 没有第一个空 play:模板渲染时变量为空,生成的文件没有主机信息
  2. 模板路径错误src: hosts.j2 要确保文件在 playbook 同目录或 roles 目录下
  3. 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:目标文件路径

验证

ansible dev,test,prod -m shell -a 'cat /etc/issue'

十四、创建 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,源文件删除后仍可访问

验证

curl http://servera.lab.example.com/webdev/index.html

十五、生成硬件报告

题目要求

创建 /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 模块基本逻辑

replace:
  path: /root/hwreport.txt      # 目标文件
  regexp: '占位符文本'           # 用正则找到要替换的字符串
  replace: "实际值"              # 替换成什么

文件里预先有一堆占位符(如 memory_in_MBBIOS_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 all -m shell -a 'cat /root/hwreport.txt'

十六、创建密码库

题目要求

创建 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.ymllocker.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' → ✅ 执行创建任务

一句话itemloop 循环自动提供的变量,每次循环指向列表中的当前元素。item.job 就是取这个元素字典里 job 键的值。

when 为什么不需要 {{ }}?⭐

when: item.job == 'developer'      # ✅ 不需要 {{ }}
name: "{{ item.name }}"            # ❌ 模块参数必须加 {{ }}
位置 是否自动解析 写法
when: ✅ 自动当成 Jinja2 表达式 when: item.job == 'developer'
loop: ✅ 自动当成 Jinja2 表达式 loop: "{{ users }}"
模块参数(name、uid 等) ❌ 默认是字符串,不会自动解析 name: "{{ item.name }}"

原因: - when 是 Ansible 关键字,从设计上就知道后面要跟条件表达式,自动解析 - 模块参数(如 user 的 name)是模块自己定义的,Ansible 不知道你传的是变量还是字面字符串,必须用 {{ }} 显式插值

类比 Python

# when 类比 — 直接写表达式
if item.job == 'developer':

# name 类比 — 字符串里要插值
name = f"{item['name']}"

一句话记住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

删除任务

- cron:
    name: "exec tasks every 2 minute"
    state: absent

验证

crontab -l -u natasha
# #Ansible: exec tasks every 2 minute
# */2 * * * * echo hello

综合速查

执行 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 写反就全完了,千万别搞错盘符!

验证是否清除成功

fdisk -l /dev/vdb
成功标志:没有 Device Boot / Start / End 分区表头出现。

RHCE 第11题:关键逻辑补充

核心逻辑:vdd 和 vdb 二选一,vdd 优先。

when: "'vdb' in ansible_devices and 'vdd' not in ansible_devices"

为什么? 两块盘都挂载到 /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                               # 交互式调试
  • 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 dev -m shell -a 'yum check-update'  # 空输出 = 全部最新
# 退出码 0 = 没有可更新的包 ✅
# 退出码 100 = 还有包没更新 ❌

Ansible roles 两种写法 + become 位置

# 写法一:完整写法
roles:
  - role: selinux

# 写法二:简写(效果完全一样)
roles:
  - selinux

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.ymlansible-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 命令 → 拿到输出 → 填入变量