27. 自动化与脚本编写
本章介绍 RHEL 9 运维自动化的核心技能:规范化 Shell 脚本编写、Ansible 进阶应用,以及 systemd timer 替代传统 cron 的最佳实践。自动化是消除重复性手工操作、降低人为失误风险的根本途径,也是第 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 9 | 主机: 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.4 systemd Timer(替代 cron)
systemd timer 是 RHEL 9 推荐的定时任务实现方式,相比 cron 具备完整的日志集成、依赖管理和错误处理能力。
27.4.1 创建 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.2 验证 OnCalendar 语法
# 验证 timer 时间表达式是否正确
systemd-analyze calendar "*-*-* 06:00:00"
systemd-analyze calendar "Mon,Wed,Fri *-*-* 02:30:00"
systemd-analyze calendar "monthly"
27.4.3 cron 与 systemd timer 对比
| 特性 | cron | systemd timer |
|---|---|---|
| 日志管理 | 需手动重定向 | 自动写入 journal,journalctl 直接查询 |
| 依赖管理 | 不支持 | 完整 systemd 依赖链(After=、Requires=) |
| 错过执行处理 | 直接跳过 | Persistent=true 在下次启动时补执行 |
| 随机延迟 | 需手动 sleep |
RandomizedDelaySec 原生支持 |
| 监控集成 | 困难 | 原生 systemctl status 查看状态 |
| 错误通知 | 需额外配置 | 结合 OnFailure= 单元自动通知 |
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
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)则直接跳过错过的触发点,等待下一次计划时间,可能导致连续多天未执行而不自知。
27.9 章节总结
本章构建了 RHEL 9 自动化运维的完整技能体系:规范化 Shell 脚本(严格模式 set -euo pipefail、结构化日志函数、参数处理、错误捕获与信号处理)、Ansible 进阶应用(变量与条件判断、handlers 机制、vault 敏感数据加密),以及 systemd timer 替代 cron 的最佳实践(含 Persistent、RandomizedDelaySec 等生产要素)。这些技能是第 28 章自动化部署与镜像管理的直接基础。