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 实践任务
- 登录
linuxdc(SecureCRT 端口 2222,ops用户,密钥认证,sudo -i切换 root)。 - 按照 27.2.1 创建
/usr/local/bin/linuxdc-health.sh,执行后查看/var/log/linuxdc-health.log。 - 创建参数化备份脚本
backup-selective.sh,测试-h帮助输出和实际备份功能。 - 编写含 handlers 的 Ansible Playbook,确保 Nginx 配置变更时自动触发 reload。
-
配置
health-check.timer,验证下次触发时间,手动触发并查看日志:systemctl list-timers health-check.timer systemctl start health-check.service journalctl -u health-check.service -n 10 -
用
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)考试的核心能力覆盖范围。