跳转至

27. 自动化与脚本编写

本章介绍 RHEL 10 运维自动化的核心技能:规范化 Shell 脚本编写、Ansible 进阶应用(角色、Content Collections、ansible-navigator 现代执行工作流),以及 at/cron/systemd timer 三种定时任务调度方式的选型与实践。自动化是消除重复性手工操作、降低人为失误风险的根本途径,也是第 28 章自动化部署的直接基础。


27.1 前提条件

  • 已完成第四阶段全部配置(第 20–26 章)。
  • 通过 SecureCRT(SSH 端口 2222)以 ops 用户、密钥认证方式登录 192.168.1.100,执行 sudo -i 切换至 root 环境。
  • Ansible 已安装(第 26 章);Git 已安装(第 30 章)。

本章全局参数

参数 值
主机名 / IP linuxdc / 192.168.1.100
脚本部署目录 /usr/local/bin/
巡检日志路径 /var/log/linuxdc-health.log
Timer 执行时间 每日 06:00

27.2 Shell 脚本规范化编写

27.2.1 生产级脚本基础结构

规范的生产级 Shell 脚本应包含严格模式声明、结构化日志函数、前置检查和清晰的主逻辑分层:

tee /usr/local/bin/linuxdc-health.sh << 'SCRIPT'
#!/usr/bin/env bash
# ============================================================
# 脚本名称: linuxdc-health.sh
# 功能描述: 系统健康状态巡检
# 运行环境: RHEL 10 | 主机: linuxdc | IP: 192.168.1.100
# ============================================================

# 严格模式(生产脚本必须开启)
# -e: 遇到错误立即退出;-u: 引用未定义变量时报错;
# -o pipefail: 管道中任一命令失败则整体失败
set -euo pipefail

# --- 常量定义 ---
readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_FILE="/var/log/linuxdc-health.log"
readonly TIMESTAMP=$(date '+%F %T')

# --- 结构化日志函数 ---
log()  { echo "[${TIMESTAMP}] [INFO]  $*" | tee -a "${LOG_FILE}"; }
warn() { echo "[${TIMESTAMP}] [WARN]  $*" | tee -a "${LOG_FILE}" >&2; }
die()  { echo "[${TIMESTAMP}] [ERROR] $*" | tee -a "${LOG_FILE}" >&2; exit 1; }

# --- 前置检查 ---
[[ $EUID -ne 0 ]] && die "此脚本需要 root 权限运行,请使用 sudo 执行"

# --- 主逻辑 ---
main() {
    log "开始系统健康巡检 | 主机: $(hostname) | IP: 192.168.1.100"

    # 磁盘空间检查(超过 85% 告警)
    local disk_usage
    disk_usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
    if [[ ${disk_usage} -ge 85 ]]; then
        warn "根分区使用率 ${disk_usage}%,超过 85% 告警阈值"
    else
        log "根分区使用率 ${disk_usage}% - 正常"
    fi

    # 可用内存检查
    local mem_free
    mem_free=$(free -m | awk '/^Mem:/ {print $4}')
    log "可用内存: ${mem_free} MB"

    # 关键服务检查
    local services=("sshd" "chronyd" "firewalld")
    for svc in "${services[@]}"; do
        if systemctl is-active --quiet "${svc}"; then
            log "服务 ${svc}: 运行中"
        else
            warn "服务 ${svc}: 未运行,请立即检查"
        fi
    done

    log "巡检完成"
}

main "$@"
SCRIPT

chmod +x /usr/local/bin/linuxdc-health.sh

# 验证脚本语法无误
bash -n /usr/local/bin/linuxdc-health.sh

# 执行巡检
/usr/local/bin/linuxdc-health.sh
cat /var/log/linuxdc-health.log

27.2.2 参数处理与帮助信息

mkdir -p ~/scripts

tee ~/scripts/backup-selective.sh << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

usage() {
    cat << EOF
用法: $0 [选项] <目标目录>

选项:
  -d, --dest   DIR    备份存储目录(默认: /backup)
  -r, --retain DAYS   保留天数(默认: 7)
  -h, --help          显示此帮助信息

示例:
  $0 /etc
  $0 -d /nas/backup -r 30 /opt/app
EOF
    exit 0
}

# 默认值
DEST="/backup"
RETAIN=7
TARGET=""

# 参数解析
while [[ $# -gt 0 ]]; do
    case $1 in
        -d|--dest)    DEST="$2";    shift 2 ;;
        -r|--retain)  RETAIN="$2";  shift 2 ;;
        -h|--help)    usage ;;
        -*)           echo "错误: 未知选项 $1"; usage ;;
        *)            TARGET="$1";  shift ;;
    esac
done

[[ -z "${TARGET}" ]]  && { echo "错误: 未指定目标目录"; usage; }
[[ -d "${TARGET}" ]]  || { echo "错误: ${TARGET} 不存在或不是目录"; exit 1; }

DATE=$(date +%F)
mkdir -p "${DEST}"
tar -czpf "${DEST}/$(basename "${TARGET}")-${DATE}.tar.gz" "${TARGET}"
find "${DEST}" -name "*.tar.gz" -mtime +"${RETAIN}" -delete

echo "备份完成: ${DEST}/$(basename "${TARGET}")-${DATE}.tar.gz"
SCRIPT

chmod +x ~/scripts/backup-selective.sh

# 验证帮助信息
~/scripts/backup-selective.sh --help

27.2.3 错误处理与信号捕获

tee ~/scripts/safe-deploy.sh << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

TMPDIR=$(mktemp -d)
LOCKFILE="/tmp/deploy.lock"

# 清理函数(脚本退出、被 Ctrl+C 中断或收到 TERM 信号时均触发)
cleanup() {
    local exit_code=$?
    rm -rf "${TMPDIR}"
    rm -f  "${LOCKFILE}"
    [[ ${exit_code} -ne 0 ]] && echo "部署因错误退出(代码 ${exit_code}),临时文件已清理"
}
trap cleanup EXIT INT TERM

# 防止并发执行(文件锁机制)
if [[ -f "${LOCKFILE}" ]]; then
    echo "另一个部署进程正在运行(PID: $(cat "${LOCKFILE}")),退出"
    exit 1
fi
echo $$ > "${LOCKFILE}"

log() { echo "[$(date '+%F %T')] $*"; }
log "部署开始,临时目录: ${TMPDIR}"

# 此处放置实际部署逻辑
# cp /opt/release/app.tar.gz "${TMPDIR}/"
# tar -xzf "${TMPDIR}/app.tar.gz" -C /opt/app/

log "部署完成"
SCRIPT

chmod +x ~/scripts/safe-deploy.sh
bash -n ~/scripts/safe-deploy.sh

27.3 Ansible 进阶应用

27.3.1 变量、Facts 与条件判断

tee ~/ansible-playbooks/conditional-deploy.yml << 'EOF'
---
- name: 条件化部署示例
  hosts: webservers
  become: yes

  vars:
    app_version: "2.1.0"
    deploy_env: "production"
    backup_before_deploy: true

  tasks:
    - name: 部署前备份(仅生产环境执行)
      archive:
        path: /opt/app
        dest: "/backup/app-{{ ansible_date_time.date }}.tar.gz"
      when:
        - deploy_env == "production"
        - backup_before_deploy | bool

    - name: 安装 Python3 依赖
      dnf:
        name:
          - python3
          - python3-pip
        state: present

    - name: 检查当前应用版本
      command: /opt/app/bin/app --version
      register: current_version
      ignore_errors: yes
      changed_when: false

    - name: 输出版本对比信息
      debug:
        msg: >
          当前版本: {{ current_version.stdout | default('未安装') }},
          目标版本: {{ app_version }}
      when: current_version.stdout | default('') != app_version
EOF

27.3.2 Handlers 与通知机制

Handlers 只在被 notify 触发且对应任务实际产生变更时才执行,且无论被触发多少次,每个 handler 在一次 play 中只执行一次(在所有 task 完成后统一运行),避免配置多次变更导致服务被多次重启。

tee ~/ansible-playbooks/nginx-config.yml << 'EOF'
---
- name: Nginx 配置管理
  hosts: webservers
  become: yes

  handlers:
    - name: reload nginx
      systemd:
        name: nginx
        state: reloaded

    - name: restart nginx
      systemd:
        name: nginx
        state: restarted

  tasks:
    - name: 安装 Nginx
      dnf:
        name: nginx
        state: present
      notify: restart nginx

    - name: 部署 Nginx 配置文件
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        validate: 'nginx -t -c %s'
      notify: reload nginx

    - name: 确保 Nginx 运行并开机自启
      systemd:
        name: nginx
        state: started
        enabled: yes
EOF

27.3.3 使用 ansible-vault 加密敏感数据

mkdir -p ~/ansible-playbooks/vault

# 创建加密变量文件(存储数据库密码、API 密钥等敏感信息)
ansible-vault create ~/ansible-playbooks/vault/secrets.yml
# 输入 Vault 密码后,在编辑器中添加:
# db_password: "Db@Secure2025!"
# api_key: "sk-xxxxxxxxxxxxxxxx"

# 在 Playbook 中通过 vars_files 引用加密文件:
# vars_files:
#   - vault/secrets.yml

# 执行时输入 Vault 密码解密
ansible-playbook site.yml --ask-vault-pass

# CI/CD 场景使用密码文件(不交互输入)
echo "vault_password_here" > ~/.vault_pass
chmod 600 ~/.vault_pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass

# 查看加密文件内容(需 Vault 密码)
ansible-vault view ~/ansible-playbooks/vault/secrets.yml

# 修改加密文件
ansible-vault edit ~/ansible-playbooks/vault/secrets.yml

27.3.4 角色(Roles)与 Content Collections

大型 Playbook 应拆分为角色(可复用的标准化目录结构),而非堆积在单一文件中。

# 初始化角色标准目录结构
ansible-galaxy init roles/webserver
tree roles/webserver
# roles/webserver/
# ├── tasks/main.yml      # 主任务列表
# ├── handlers/main.yml   # 该角色专属 handlers
# ├── templates/          # Jinja2 模板
# ├── files/              # 静态文件
# ├── vars/main.yml       # 角色内变量(优先级较高)
# ├── defaults/main.yml   # 角色默认变量(优先级最低,便于外部覆盖)
# └── meta/main.yml       # 角色元信息与依赖声明
# site.yml 中引用角色
- hosts: webservers
  roles:
    - webserver
    - { role: firewall, firewall_port: 8080 }

Content Collections 是官方或社区发布的模块/角色/插件打包分发单元,用于获取标准 ansible-core 之外的扩展模块:

# 安装 Collection(如 community.general 提供大量通用模块)
ansible-galaxy collection install community.general

# 查看已安装的 Collection
ansible-galaxy collection list
# Playbook 中通过完全限定名(FQCN)调用 Collection 内模块
- hosts: all
  tasks:
    - name: 使用 community.general 集合设置时区
      community.general.timezone:
        name: Asia/Shanghai

27.3.5 ansible-navigator:现代 Playbook 执行入口

RHCE(EX294)当前基于 Ansible Automation Platform,ansible-navigator 是官方推荐的 Playbook 运行与排障工具,通常配合 Execution Environment(预置好 Ansible 及依赖的容器镜像)使用,取代直接调用 ansible-playbook。

dnf install -y ansible-navigator

项目根目录创建 ansible-navigator.yml:

ansible-navigator:
  execution-environment:
    enabled: true
    image: registry.redhat.io/ansible-automation-platform/ee-supported-rhel10:latest
    pull:
      policy: missing
  mode: stdout          # stdout:直接输出(类似 ansible-playbook);不设置则进入交互式 TUI
# 以 stdout 模式运行(日常使用,行为接近 ansible-playbook)
ansible-navigator run site.yml -m stdout

# 交互模式:可逐项查看 play、task、结果详情(默认模式,不加 -m stdout)
ansible-navigator run site.yml

# 查看/搜索已安装 Collection 中的模块文档
ansible-navigator doc community.general.timezone

# 交互式浏览清单
ansible-navigator inventory -i inventory.ini

# 交互模式下常用按键:数字键进入对应 play/task;:back 返回上一级;:q 退出

何时用 ansible-navigator,何时用 ansible-playbook

日常学习与理解 Playbook 执行逻辑(尤其排错时查看每个 task 的详细结果)优先用 ansible-navigator 交互模式;自动化脚本、CI/CD 流水线中调用则使用 -m stdout 模式,行为与 ansible-playbook 基本等价。RHCE 备考应两种方式都能熟练操作。


27.4 定时任务调度:at、cron 与 systemd Timer

RHEL 10 提供三种互补的定时任务机制:at(一次性任务)、cron(周期性任务的传统方案)、systemd timer(周期性任务的现代推荐方案)。三者均为 RHCSA 官方考点,需全部掌握。

27.4.1 at:一次性任务调度

# 安装并启动 atd 服务(部分精简安装环境需要)
dnf install -y at
systemctl enable --now atd

# 调度一次性任务:10 分钟后执行
echo "systemctl restart httpd" | at now + 10 minutes

# 交互式输入命令(Ctrl+D 结束输入)
at 23:30
at> tar -czf /backup/manual-$(date +%F).tar.gz /data
at> <按 Ctrl+D>

# 查看待执行的 at 任务队列
atq

# 查看指定任务的具体内容
at -c <任务编号>

# 取消任务
atrm <任务编号>

at 与 cron/systemd timer 的核心区别

at 只执行一次,执行后任务自动从队列移除,适合"今晚 23:30 执行一次性维护脚本"这类场景;cron 和 systemd timer 都是周期性重复执行。三者选型:临时/一次性任务用 at;简单周期任务、需要跨发行版兼容用 cron;生产环境周期任务、需要依赖管理与完善日志则用 systemd timer。

27.4.2 crontab:传统周期任务调度

# 编辑当前用户的 crontab(不要直接编辑 /var/spool/cron 下的文件)
crontab -e
# 格式:分 时 日 月 周 命令
# 每天凌晨 2:30 执行备份脚本
30 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# 每 15 分钟执行一次健康检查
*/15 * * * * /opt/scripts/healthcheck.sh

# 每周一凌晨 3:00 执行日志归档
0 3 * * 1 /opt/scripts/log-archive.sh
# 查看当前用户的 crontab
crontab -l

# 删除当前用户的所有 crontab 任务
crontab -r

# 系统级别定时任务(无需指定用户,直接在文件中声明用户名)
vim /etc/cron.d/system-tasks
# /etc/cron.d 格式比用户 crontab 多一列"执行用户"
0 4 * * * root /opt/scripts/system-cleanup.sh

RHCSA 常见失分点

crontab -e 修改的是当前登录用户的任务;以 root 身份 crontab -e 和以普通用户 crontab -e 操作的是完全不同的两份任务表。考试环境中必须先确认题目要求"哪个用户"的定时任务,用 whoami 核实当前身份后再编辑。

27.4.3 systemd Timer(周期任务现代方案)

systemd timer 是 RHEL 10 推荐的周期性任务实现方式,相比 cron 具备完整的日志集成、依赖管理和错误处理能力。

27.4.4 创建 systemd Timer

以每日健康巡检任务为例:

# 创建 service unit(定义实际执行的工作)
tee /etc/systemd/system/health-check.service << 'EOF'
[Unit]
Description=System Health Check
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/linuxdc-health.sh
StandardOutput=journal
StandardError=journal
EOF

# 创建 timer unit(定义触发时间和策略)
tee /etc/systemd/system/health-check.timer << 'EOF'
[Unit]
Description=Daily System Health Check Timer
Requires=health-check.service

[Timer]
# 每天 06:00 执行
OnCalendar=*-*-* 06:00:00
# 系统启动 5 分钟后补执行一次(处理开机时错过的触发)
OnBootSec=5min
# 错过的触发点在下次系统启动时补执行
Persistent=true
# 随机延迟最多 5 分钟(分散多节点同时触发的压力)
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable --now health-check.timer

# 验证 timer 状态和下次触发时间
systemctl list-timers health-check.timer

# 手动触发测试
systemctl start health-check.service
journalctl -u health-check.service -n 20

27.4.5 验证 OnCalendar 语法

# 验证 timer 时间表达式是否正确
systemd-analyze calendar "*-*-* 06:00:00"
systemd-analyze calendar "Mon,Wed,Fri *-*-* 02:30:00"
systemd-analyze calendar "monthly"

27.4.6 at/cron/systemd timer 三者对比

特性 at cron systemd timer
执行次数 仅一次 周期重复 周期重复
日志管理 需手动重定向 需手动重定向 自动写入 journal,journalctl 直接查询
依赖管理 不支持 不支持 完整 systemd 依赖链(After=、Requires=)
错过执行处理 不适用(一次性) 直接跳过 Persistent=true 在下次启动时补执行
随机延迟 不适用 需手动 sleep RandomizedDelaySec 原生支持
监控集成 atq 查看队列 困难 原生 systemctl status 查看状态
典型场景 临时/一次性维护任务 简单周期任务、跨平台脚本迁移 生产环境周期任务首选

27.5 Git 自动化部署钩子

# 在服务端裸仓库中创建 post-receive 钩子
mkdir -p /opt/repos/myapp.git/hooks

tee /opt/repos/myapp.git/hooks/post-receive << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

DEPLOY_DIR="/opt/app/myapp"
BRANCH="main"
LOG="/var/log/git-deploy.log"

while read -r old_rev new_rev ref_name; do
    branch="${ref_name##refs/heads/}"
    [[ "${branch}" != "${BRANCH}" ]] && continue

    echo "$(date '+%F %T') 收到推送,分支: ${branch},版本: ${new_rev:0:8}" >> "${LOG}"
    git --work-tree="${DEPLOY_DIR}" --git-dir="$(pwd)" checkout -f "${BRANCH}"

    # 重载应用服务(优先 reload,避免中断连接)
    systemctl reload nginx 2>/dev/null || systemctl restart nginx

    echo "$(date '+%F %T') 部署完成" >> "${LOG}"
done
SCRIPT

chmod +x /opt/repos/myapp.git/hooks/post-receive

27.6 常见问题与排查

问题现象 排查步骤
脚本 set -e 下意外退出 对允许失败的命令追加 \|\| true;或局部 set +e … set -e 包裹
Ansible Playbook 不幂等 使用 stat/command 模块检查状态再决定执行;避免裸 command/shell
systemd timer 未触发 systemctl list-timers;systemd-analyze calendar 验证语法
ansible-vault 密码遗忘 无法恢复,需重新创建加密文件;务必将密码存入密码管理器
# 验证 OnCalendar 语法
systemd-analyze calendar "*-*-* 06:00:00"

# 脚本语法检查
bash -n /usr/local/bin/linuxdc-health.sh

# Ansible 连通性测试
ansible webservers -m ping -v
ansible-playbook site.yml --check --diff

27.7 实践任务

  1. 登录 linuxdc(SecureCRT 端口 2222,ops 用户,密钥认证,sudo -i 切换 root)。
  2. 按照 27.2.1 创建 /usr/local/bin/linuxdc-health.sh,执行后查看 /var/log/linuxdc-health.log。
  3. 创建参数化备份脚本 backup-selective.sh,测试 -h 帮助输出和实际备份功能。
  4. 编写含 handlers 的 Ansible Playbook,确保 Nginx 配置变更时自动触发 reload。
  5. 配置 health-check.timer,验证下次触发时间,手动触发并查看日志:

    systemctl list-timers health-check.timer
    systemctl start health-check.service
    journalctl -u health-check.service -n 10
    
  6. 用 at 调度一个 5 分钟后执行的一次性任务(如写入一条测试日志),用 atq 确认已进入队列,等待触发后验证结果;再用 crontab -e 为 ops 用户添加一条每 5 分钟执行一次的 crontab 任务,观察其执行日志。


27.8 自测问题

Q1:Shell 脚本中 set -euo pipefail 三个选项各有什么作用?

-e:任意命令返回非零退出码时立即终止脚本,防止错误被忽略后继续执行导致状态异常;-u:引用未定义变量时报错退出,防止拼写错误的变量名被当作空字符串静默处理;-o pipefail:管道 cmd1 | cmd2 中若 cmd1 失败,整个管道返回非零(默认只检查最后一条命令),防止管道前段的错误被掩盖。三者组合是生产脚本严格模式的安全基线。

Q2:Ansible handlers 与普通 task 的核心区别是什么?

普通 task 按声明顺序逐条执行;handlers 只在被 notify 通知且对应任务实际产生变更(changed)时才执行,且无论在一次 play 中被触发多少次,每个 handler 只执行一次(在所有 task 完成后统一运行)。这一机制避免了因配置文件多次变更导致服务被多次不必要地重启,是 Playbook 幂等性设计的关键支撑。

Q3:systemd timer 的 Persistent=true 解决了什么问题?

若 timer 应触发时系统处于关机状态(如每日 06:00 执行,但服务器在此期间停机),Persistent=true 会在系统下次启动时立即补执行一次错过的任务,确保定期巡检、备份等关键任务不被遗漏。默认(Persistent=false)则直接跳过错过的触发点,等待下一次计划时间,可能导致连续多天未执行而不自知。

Q4:为什么大型 Playbook 建议拆分为角色(Roles),而不是写在一个文件里?

角色提供标准化的目录结构(tasks/handlers/templates/files/vars/defaults),使配置逻辑、模板资源、变量按功能边界清晰隔离,便于团队协作、复用(同一角色可应用于不同 play)和版本化维护。单文件 Playbook 在项目变大后难以定位问题、容易产生变量命名冲突,且无法被其他项目直接复用。

Q5:ansible-navigator 与 ansible-playbook 的核心区别是什么?

ansible-playbook 直接使用控制节点本地安装的 Ansible 环境执行;ansible-navigator 通常基于 Execution Environment(预置版本化 Ansible 及依赖的容器镜像)运行,避免了控制节点本地环境差异导致的"在我机器上能跑"问题,并提供交互式 TUI 便于逐项查看 play/task 执行详情,是 Ansible Automation Platform 生态下的官方推荐入口。两者可执行同一份 Playbook,ansible-navigator run <playbook> -m stdout 的行为与 ansible-playbook <playbook> 基本等价。

Q6:什么场景应该用 at 而不是 cron 或 systemd timer?

at 专为一次性任务设计:任务执行完毕后自动从队列移除,不会重复触发。典型场景如"今晚 23:30 执行一次性的紧急维护脚本""2 小时后自动重启某服务"。cron 和 systemd timer 都面向周期性重复任务(如每天/每小时执行)。若用 cron 实现一次性任务,需要任务自行在结尾删除对应的 crontab 条目,既繁琐又容易遗漏;at 天然满足这类需求,是三者中最简单直接的选择。


27.9 章节总结

本章构建了 RHEL 10 自动化运维的完整技能体系:规范化 Shell 脚本(严格模式 set -euo pipefail、结构化日志函数、参数处理、错误捕获与信号处理)、Ansible 进阶应用(变量与条件判断、handlers 机制、vault 敏感数据加密、角色化组织、Content Collections 扩展模块、ansible-navigator 现代执行工作流),以及 at(一次性任务)、cron(传统周期任务)、systemd timer(现代周期任务,含 Persistent、RandomizedDelaySec 等生产要素)三种定时调度方式的选型与实践。这些技能是第 28 章自动化部署与镜像管理的直接基础,也是 RHCSA(EX200)与 RHCE(EX294)考试的核心能力覆盖范围。