Skip to content

🤖 RHCE 9.0 知识库 ​

基于模拟题整理,按题号编排,每道题向外扩散关键知识点。


考试环境概览 ​

主机IP角色
workstation.lab.example.com172.25.250.9Ansible 控制节点
servera.lab.example.com172.25.250.10受管节点 (dev)
serverb.lab.example.com172.25.250.11受管节点 (test)
serverc.lab.example.com172.25.250.12受管节点 (prod)
serverd.lab.example.com172.25.250.13受管节点 (prod)
bastion.lab.example.com172.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

清单文件 ​

ini
[dev]
servera

[test]
serverb

[prod]
serverc
serverd

[balancers]
bastion

[webservers:children]
prod

配置文件 ​

ini
[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 = devopsSSH 连接用户名
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 配置 ​

yaml
# /home/devops/.ansible-navigator.yml
ansible-navigator:
  execution-environment:
    image: utility.lab.example.com/ee-supported-rhel8:latest
    pull:
      policy: missing

Podman 镜像仓库配置 ​

toml
# /etc/containers/registries.conf
[registries.search]
registries = ['utility.lab.example.com']

[registries.insecure]
registries = ['utility.lab.example.com']

验证 ​

bash
ansible all -m ping

二、创建受管节点存储库 ​

题目要求 ​

创建 /home/devops/ansible/yum_repo.yml,在所有受管节点配置两个 YUM 仓库。

Playbook ​

yaml
- 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)
gpgkeyGPG 公钥 URL
enabled是否启用仓库(yes/no)
filerepo 文件名(不带 .repo 后缀),模块自动追加 .repo

file 参数详解 ​

  • 不写 file:默认用 name 的值作为文件名
  • 两个 task 共用 file: rhel_dvd → 合并写入 /etc/yum.repos.d/rhel_dvd.repo
  • 一个 .repo 文件可以有多个 [section]
  • 不要手动加 .repo 后缀,模块会自动加

执行 ​

bash
ansible-navigator run yum_repo.yml -i inventory -m stdout
# 或
ansible-playbook yum_repo.yml

验证 ​

bash
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 ​

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

yaml
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 针对不同主机组执行不同任务。

验证 ​

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

四、使用 Timesync RHEL 系统角色 ​

题目要求 ​

创建 /home/devops/ansible/timesync.yml,使用 timesync 角色配置 NTP 时间服务器 classroom.example.com。

前置准备 ​

bash
yum -y install rhel-system-roles
cp -r /usr/share/ansible/roles/rhel-system-roles.timesync/ roles/timesync

Playbook ​

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

验证 ​

bash
ansible all -m shell -a 'chronyc sources'

五、使用 SELINUX RHEL 系统角色 ​

题目要求 ​

在所有节点使用 SELinux 角色,将 SELinux 设置为 enforcing 强制模式。

Playbook ​

yaml
- hosts: all
  vars:
    selinux_policy: targeted
    selinux_state: enforcing
  roles:
    - role: selinux
      become: true

扩展开的知识点 ​

SELinux 变量 ​

变量值含义
selinux_policytargeted策略类型(只约束特定进程)
selinux_stateenforcing强制模式(违反就阻止)

SELinux 三种状态 ​

状态命令说明
enforcingsetenforce 1强制模式
permissivesetenforce 0宽容模式(只警告不阻止)
disabled改配置文件重启完全关闭

become: true 详解 ​

  • 用 root 权限执行该角色(通过 sudo 提权)
  • SELinux 状态修改需要 root 权限
  • 跟 ansible.cfg 的 [privilege_escalation] 区别:
    • become: true 只对当前角色/play 生效
    • [privilege_escalation] 全局生效

验证 ​

bash
ansible all -m shell -a 'getenforce'

六、使用 Ansible Galaxy 安装角色 ​

题目要求 ​

从 URL 下载角色并安装到 /home/devops/ansible/roles。

实际操作 ​

bash
# 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
  • 两种安装方式:
    1. 从 Galaxy 仓库:ansible-galaxy install geerlingguy.docker
    2. 从 URL:ansible-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.gz 和 community-general-6.3.0.tar.gz 安装到 /home/devops/ansible/mycollections/。

实际操作 ​

bash
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/

扩展开的知识点 ​

两种安装方式 ​

方法命令场景
直接 URLansible-galaxy collection install URL -p dir/集合少,简单直接
requirements.ymlansible-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 列出已安装集合

验证 ​

bash
ls mycollections/ansible_collections/

八、创建和使用角色 ​

题目要求 ​

创建 apache 角色(安装 httpd、配置防火墙、部署 Jinja2 模板),在 webservers 主机组运行。

角色 tasks/main.yml ​

yaml
- 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 ​

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

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

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

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

template 模块 ​

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

firewalld 模块参数详解 ​

yaml
- firewalld:
    service: http
    permanent: yes
    immediate: yes
    state: enabled
参数含义等价命令
permanent: yes永久生效,重启后规则仍在firewall-cmd --add-service=http --permanent
immediate: yes立即生效,当前就加载,不用重启 firewalldfirewall-cmd --add-service=http
state: enabled启用/允许该服务--add-service
state: disabled禁用/拒绝该服务--remove-service

permanent + immediate 组合 = 既写入配置文件又立即加载,缺一不可。

验证 ​

bash
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 ​

yaml
- hosts: balancers
  roles:
    - balancer

- hosts: webservers
  roles:
    - phpinfo

扩展开的知识点 ​

角色复用 ​

  • 通过 roles 关键字引用已安装的角色
  • 角色放在 roles/ 目录下(在 ansible.cfg 中配置了 roles_path)
  • balancer 角色实现负载均衡,phpinfo 角色显示 PHP 信息

验证 ​

bash
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 ​

yaml
- 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 ​

yaml
- block:
    # 尝试创建 600m 逻辑卷
- rescue:
    # 如果 block 失败,输出提示 + 创建 400m
- always:
    # 无论成功失败都执行格式化
when: "'research' in ansible_lvm.vgs"  # 整个 block 的前置条件

关键点:when 是任务级指令,必须和 - name: 对齐(第8格),不是和模块参数对齐(第10格)。

第二个 task:卷组不存在时的处理 ​

yaml
- name: search not exists
  debug:
    msg: Volume group does not exist
  when: "'research' not in ansible_lvm.vgs"  # 和 debug: 对齐

扩展知识点 ​

block/rescue/always 错误处理 ​

关键字作用类比
block正常执行的任务try
rescueblock 中任一任务失败时执行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. 单位混淆:lvol 用 m(MiB),parted 用 MiB,效果一样都是 1024 进制
  3. 卷组不存在时的处理:需要单独的 task 用 when 判断,不能放在 block 里

验证 ​

bash
# 查看逻辑卷
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。

bash
# 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) ​

yaml
- 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 ​

yaml
- 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 不存在 ​

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

第三部分:vdb exists(关键逻辑) ​

yaml
- 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 不存在 ​

yaml
- 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
rescueblock 中任一任务失败时执行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. 单位混淆:lvol 用 m(MiB),parted 用 MiB,效果一样都是 1024 进制
  3. 卷组不存在时的处理:需要单独的 task 用 when 判断,不能放在 block 里

验证 ​

bash
# 查看逻辑卷
ssh root@servera lvs

# 查看卷组
ssh root@servera vgs

十二、生成主机文件 ​

题目要求 ​

创建 Jinja2 模板 hosts.j2 和 playbook hosts.yml,在 dev 主机组生成 /etc/myhosts 文件。

模板 hosts.j2 ​

jinja2
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.1IPv4 回环地址(本机)
localhost主机名(短名)
localhost.localdomain完全限定域名(FQDN)
localhost4IPv4 专用别名
localhost4.localdomain4IPv4 别名的 FQDN

第二行:IPv6 localhost 条目 ​

::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
字段含义
::1IPv6 回环地址(相当于 IPv4 的 127.0.0.1)
localhost主机名(和 IPv4 共用)
localhost.localdomainFQDN(和 IPv4 共用)
localhost6IPv6 专用别名
localhost6.localdomain6IPv6 别名的 FQDN

第三行:Jinja2 循环(渲染每台主机的条目) ​

jinja2
{% for host in groups.all %}
{{ hostvars[host].ansible_eth0.ipv4.address }} {{ hostvars[host].ansible_fqdn }} {{ hostvars[host].ansible_hostname }}
{% endfor %}

Jinja2 语法分解:

jinja2
{% for host in groups.all %}    ← 循环开始:遍历所有主机
  ...                           ← 循环体:对每台主机执行
{% 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 ​

yaml
- hosts: all

- hosts: dev
  tasks:
    - name: copy hosts.j2 to dev
      template:
        src: hosts.j2
        dest: /etc/myhosts

Playbook 逐行解析 ​

yaml
- 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 语句 ​

jinja2
{# 循环 #}
{% for item in list %}
  {{ item }}
{% endfor %}

{# 条件 #}
{% if condition %}
  ...
{% elif other %}
  ...
{% else %}
  ...
{% endif %}

Ansible Facts 变量 ​

变量含义
groups.all所有主机列表
groups.devdev 主机组的主机列表
hostvars所有主机的变量字典
hostvars[host].ansible_eth0.ipv4.address指定主机的 IPv4 地址
hostvars[host].ansible_fqdn指定主机的完全限定域名
hostvars[host].ansible_hostname指定主机的主机名
inventory_hostname当前主机的主机名

template 模块 vs copy 模块 ​

模块用途
template渲染 Jinja2 模板,支持变量替换
copy直接复制文件,不支持变量

验证 ​

bash
# 在 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 ​

yaml
- 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:目标文件路径

验证 ​

bash
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 ​

yaml
- 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 找不到文件!

解决方案:创建软链接

yaml
- 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含义
2775SGID(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,源文件删除后仍可访问

验证 ​

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

十五、生成硬件报告 ​

题目要求 ​

创建 /home/devops/ansible/hwreport.yml,生成 /root/hwreport.txt,包含:主机名、内存、BIOS 版本、vda/vdb 磁盘大小。

Playbook ​

yaml
- 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 模块基本逻辑 ​

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

文件里预先有一堆占位符(如 memory_in_MB、BIOS_version 等),playbook 的任务就是把它们替换成真实的系统信息。

各变量详解 ​

变量含义类型
inventory_hostnameinventory 中定义的主机名字符串,直接用
ansible_memtotal_mb目标主机总内存(MB),如 2048整数(int)
ansible_bios_versionBIOS/固件版本号字符串
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_versionBIOS 版本
ansible_devices.vda.sizevda 磁盘大小

验证 ​

bash
ansible all -m shell -a 'cat /root/hwreport.txt'

十六、创建密码库 ​

题目要求 ​

创建 Ansible Vault 存储密码,加密密码为 fgydsxsxsbxs。

实际操作 ​

bash
# 创建变量文件
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 时解密 ​

bash
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 ​

yaml
- 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 内容示例:
yaml
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.nameitem.job
第1次{name: alice, uid: 1010, job: developer}alicedeveloper
第2次{name: bob, uid: 1011, job: manager}bobmanager
第3次{name: charlie, uid: 1012, job: developer}charliedeveloper

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 为什么不需要 {{ }}?⭐ ​

yaml
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:

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加密后的密码字符串

执行方式 ​

bash
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。

实际操作 ​

bash
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 ​

yaml
- 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执行的命令命令字符串
stateabsent 删除任务present/absent

删除任务 ​

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

验证 ​

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

综合速查 ​

执行 playbook 两种方式 ​

bash
# 方式一: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 参数让输出直接在终端显示

常用验证命令 ​

bash
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 的作用 ​

yaml
- 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 无法启动的情况。

排查步骤:

bash
# 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 进制计算。

模块单位含义
partedMiBMebibyte,1 MiB = 1024² bytes(二进制,标准命名)
lvolmLVM 内部按 MiB 计算,简写为 m

实际对比:

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

验证是否清除成功:

bash
fdisk -l /dev/vdb

成功标志:没有 Device Boot / Start / End 分区表头出现。

RHCE 第11题:关键逻辑补充 ​

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

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

为什么? 两块盘都挂载到 /mnt/fs01,只能选一块。

part_end 用 1501MiB 而不是 1500MiB:多 1MiB 避免边界计算问题。

准备工作(重置磁盘):

bash
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)

yaml
# 正确写法(考试推荐)
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-playbookansible-navigator
执行环境宿主机本地容器内(Podman)
输出直接打到终端默认进交互界面,加 -m stdout 才打到终端
环境隔离❌✅
配置文件ansible.cfgansible-navigator.yml(指定容器镜像)

考试关键:配置文件 ansible-navigator.yml 指定容器镜像地址,考试镜像在 registry.lab.example.com。

bash
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 远程批量执行 ​

bash
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

bash
ansible dev -m shell -a 'rpm -q "Development Tools"'
ansible dev -m shell -a 'yum groupinfo "Development Tools"'  # 更靠谱

任务二:update pkgs

bash
ansible dev -m shell -a 'yum check-update'  # 空输出 = 全部最新
# 退出码 0 = 没有可更新的包 ✅
# 退出码 100 = 还有包没更新 ❌

Ansible roles 两种写法 + become 位置 ​

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

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

become 的位置区别:

yaml
# become 在 role 下面 → 只对这个 role 生效
roles:
  - role: selinux
    become: true

# become 在 play 顶层 → 对整个 play 所有 task 生效
roles:
  - selinux
become: true

多个 role 时,become 放 role 下面更精准,只给需要的角色提权。

安装集合用 yml 文件的写法 ​

方法一:requirements.yml(推荐)

yaml
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 关键字

yaml
- 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.addresseth0 网卡的 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 模块区别 ​

模块调用特点适用场景
serviceSysV init 脚本兼容老系统老版本 RHEL
systemdsystemctl支持 daemon_reloadRHEL 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 变量加载:

yaml
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" 过滤用户

执行命令:

bash
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')

逐层拆解:

步骤命令说明
1date +%s获取当前 Unix 时间戳(秒数)
2+ 2592000加上 30 天的秒数
3/ 86400转换成天数(86400 秒 = 1 天)
4exprLinux 算术计算
5lookup('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')

逐个拆解:

部分含义
lookupAnsible 查找函数,从外部获取数据
'pipe'lookup 类型,表示执行 shell 命令并返回输出
'expr $(date +%s) + 2592000'要执行的 shell 命令

lookup 常见类型:

类型用途
file读文件内容
env读环境变量
pipe执行 shell 命令,返回输出
csvfile读 CSV 文件
ini读 ini 配置文件

执行流程:Ansible → 执行 shell 命令 → 拿到输出 → 填入变量

小弟二号打造的知识体系