服务器运维中 80% 的工作都是重复劳动:更新系统、备份数据、检查服务、续签证书……本指南将带您用 Shell 脚本和定时任务彻底解放双手,打造一台真正的无人值守自动化服务器。
🤖 为什么要自动化运维
自动化运维的核心价值不只是"省时间"——更重要的是消除人为错误。凌晨 3 点的手动备份可能因为疏忽少备份了一个目录;而脚本每次都以完全相同的方式执行,没有情绪,不会忘记步骤。
效率提升
数小时的手动工作压缩为几秒钟的脚本执行,腾出时间做更有价值的事
零人为错误
脚本每次以完全相同的步骤执行,排除疲劳、疏忽带来的操作失误
无人值守
凌晨自动备份、证书到期前自动续签、服务挂了自动拉起,全程无需人工干预
可扩展性
一套脚本配合 Ansible,可以同时在 10 台、100 台服务器上批量执行相同任务
📜 Bash 脚本基础与健壮性
大多数 Shell 教程只教"能跑起来",但生产环境的脚本必须考虑:命令失败时能否正确停止?变量为空时会发生什么?同一脚本同时被触发两次如何处理?
健壮性三件套:set -euo pipefail
这三个选项是所有生产脚本的必加前缀,能捕获大多数常见的静默错误:
#!/bin/bash
# ── 健壮性三件套(每个生产脚本都应加在 shebang 后第一行)──────────────────────
set -e # 遇到任何命令返回非零退出码立即终止脚本(等同于 set -o errexit)
set -u # 引用未声明的变量时报错退出,防止 typo 引发的静默错误
set -o pipefail # 管道中任意命令失败(而非只看最后一个)都视为失败
# ── 使用双引号保护变量,防止空格和通配符展开导致的 Bug ─────────────────────────
SOURCE_DIR="/var/www/my site" # 路径中有空格
echo "$SOURCE_DIR" # ✅ 正确:输出 /var/www/my site
# echo $SOURCE_DIR # ❌ 危险:被分割成两个参数
# ── 用 ${VAR:-默认值} 为可能为空的变量提供兜底 ──────────────────────────────
BACKUP_DIR="${BACKUP_DIR:-/root/backups}" # 若环境变量未设置则用默认值
LOG_LEVEL="${LOG_LEVEL:-INFO}"
# ── 函数化:将重复逻辑封装为函数 ────────────────────────────────────────────────
log() {
# 统一日志格式:[时间戳] [级别] 消息内容
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$1] $2" | tee -a /var/log/myapp.log
}
log "INFO" "脚本开始执行"
log "ERROR" "发现异常:文件不存在" | 选项 | 作用 | 不加会怎样 |
|---|---|---|
| set -e | 任意命令失败(非零退出码)立即终止脚本 | 错误被忽略,后续命令继续执行,可能产生严重后果 |
| set -u | 引用未定义变量时报错终止 | 变量名拼错时展开为空字符串,静默地执行错误操作 |
| set -o pipefail | 管道中任意命令失败都视为整体失败 | 只检查管道最后一个命令的退出码,中间错误被掩盖 |
锁文件防重入:避免脚本并发执行
当 Cron 任务间隔较短,而某次执行时间意外超长(如网络卡顿导致备份慢),下一次 Cron 触发时上次脚本还没结束——这会导致两个备份进程同时写入同一文件,造成数据损坏。
#!/bin/bash
set -euo pipefail
# ── 锁文件机制:防止同一脚本被 Cron 或手动触发多次重叠执行 ──────────────────────
LOCK_FILE="/tmp/$(basename "$0").lock"
# 检测锁文件是否存在,且其中记录的 PID 进程仍在运行
if [ -f "$LOCK_FILE" ]; then
OLD_PID=$(cat "$LOCK_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
echo "脚本已在运行(PID: $OLD_PID),本次跳过。"
exit 0
fi
fi
# 将当前进程 PID 写入锁文件
echo $$ > "$LOCK_FILE"
# 注册 EXIT trap:无论脚本正常结束还是异常退出,都清理锁文件
trap 'rm -f "$LOCK_FILE"' EXIT
echo "获取锁成功(PID: $$),开始执行任务..."
# === 脚本核心逻辑从此开始 === 🛠️ 实战脚本库:六大常用场景
以下六个脚本覆盖了 VPS 日常运维 90% 的自动化需求,每个脚本都包含完整的逐行注释,可以直接使用或按需修改:
脚本①:系统自动安全更新
每天凌晨自动安装安全补丁,不升级内核等高风险包,含防重入锁与日志记录
#!/bin/bash
# 文件路径:/usr/local/bin/auto-update.sh
# 功能:Debian/Ubuntu 系统无人值守安全更新,含完整日志记录
set -euo pipefail
LOG="/var/log/auto-update.log"
LOCK="/tmp/auto-update.lock"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
# 防重入检查
[ -f "$LOCK" ] && kill -0 "$(cat $LOCK)" 2>/dev/null && { log "已在运行,跳过"; exit 0; }
echo $$ > "$LOCK"; trap 'rm -f "$LOCK"' EXIT
log "=== 系统更新开始 ==="
# 更新软件包索引
apt-get update -qq
# 仅安装安全更新(不升级内核等高风险包)
# unattended-upgrade 读取 /etc/apt/apt.conf.d/50unattended-upgrades 配置
apt-get install -y --only-upgrade $(apt-get --just-print upgrade 2>&1 | grep ^Inst | grep -i security | awk '{print $2}')
# 清理旧包与缓存
apt-get autoremove -y -qq
apt-get clean -qq
log "=== 系统更新完成 ===" 脚本②:网站文件 + 数据库双备份
tar 压缩文件 + mysqldump 热备,本地保留 14 天,可选 rclone 上传至云端(R2/S3/GDrive)
#!/bin/bash
# 文件路径:/usr/local/bin/website-backup.sh
# 功能:网站文件 + 数据库双备份,支持本地保留 + 可选 rclone 上传云端
set -euo pipefail
# ── 配置区 ────────────────────────────────────────────────────────────────────
WEB_DIR="/var/www/html" # 网站根目录
DB_NAME="mywebsite" # 数据库名
DB_USER="backup_user" # 专用备份账号(只需 SELECT 权限)
DB_PASS="db_password_here" # 建议改用 ~/.my.cnf 存储,避免明文
BACKUP_ROOT="/root/backups" # 本地备份根目录
KEEP_DAYS=14 # 本地保留最近 14 天的备份
DATE=$(date +%Y%m%d_%H%M%S)
LOG="/var/log/backup.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
# ── 创建本次备份目录 ──────────────────────────────────────────────────────────
BACKUP_DIR="${BACKUP_ROOT}/${DATE}"
mkdir -p "$BACKUP_DIR"
log "开始备份到 $BACKUP_DIR"
# ── 备份网站文件(排除缓存和 node_modules 以节省空间)──────────────────────────
tar --exclude='*/node_modules' --exclude='*/cache' --exclude='*/.git' -czf "${BACKUP_DIR}/web_files.tar.gz" "$WEB_DIR"
log "网站文件备份完成:$(du -sh ${BACKUP_DIR}/web_files.tar.gz | cut -f1)"
# ── 备份数据库(mysqldump 生成 SQL,gzip 压缩)────────────────────────────────
mysqldump -u "$DB_USER" -p"$DB_PASS" --single-transaction # InnoDB 热备,不锁表
--routines # 包含存储过程
--triggers # 包含触发器
"$DB_NAME" | gzip > "${BACKUP_DIR}/database.sql.gz"
log "数据库备份完成:$(du -sh ${BACKUP_DIR}/database.sql.gz | cut -f1)"
# ── 清理超过保留期的旧备份 ──────────────────────────────────────────────────────
find "$BACKUP_ROOT" -maxdepth 1 -type d -mtime "+${KEEP_DAYS}" -exec rm -rf {} +
log "已清理 ${KEEP_DAYS} 天前的旧备份"
# ── 可选:上传到 Cloudflare R2 / S3 / Google Drive(需提前配置 rclone)──────────
# rclone copy "$BACKUP_DIR" "r2:my-bucket/backups/${DATE}" --transfers=4
# log "已上传到云端存储"
log "=== 备份任务完成 ===" 脚本③:服务健康检查与自动重启
每 10 分钟巡检 Nginx/MySQL/Redis 等服务,挂掉自动重拉,推送企业微信告警
#!/bin/bash
# 文件路径:/usr/local/bin/service-health.sh
# 功能:检查关键服务是否在运行,挂了自动重启,并推送通知
set -euo pipefail
# 需要监控的服务列表(空格分隔)
SERVICES=("nginx" "mysql" "redis-server" "php8.3-fpm")
LOG="/var/log/service-health.log"
# 推送通知 Token(企业微信 or Telegram,见"Webhook 通知"章节配置)
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
send_alert() {
local msg="$1"
# 使用企业微信 Webhook 推送告警(见通知章节)
curl -s -X POST "$WEBHOOK_URL" -H 'Content-Type: application/json' -d "{"msgtype":"text","text":{"content":"[服务器告警]\n$msg"}}" > /dev/null
}
for SERVICE in "${SERVICES[@]}"; do
if ! systemctl is-active --quiet "$SERVICE"; then
log "⚠️ 服务 $SERVICE 已停止,尝试重启..."
systemctl restart "$SERVICE"
sleep 3 # 等待服务完全启动
if systemctl is-active --quiet "$SERVICE"; then
log "✅ $SERVICE 重启成功"
send_alert "服务 $SERVICE 已自动重启成功($(hostname))"
else
log "❌ $SERVICE 重启失败!请人工介入"
send_alert "⛔ 服务 $SERVICE 重启失败,需人工处理($(hostname))"
fi
fi
done 脚本④:SSL 证书自动续期
用 acme.sh 检测证书有效期,剩余不足 30 天时自动续期并 reload Nginx(零停机)
#!/bin/bash
# 文件路径:/usr/local/bin/ssl-renew.sh
# 功能:检查 SSL 证书剩余有效期,在过期前 30 天自动续期并重载 Nginx
# 推荐配合 acme.sh(比 certbot 更轻量,支持更多 CA 和 DNS API)
set -euo pipefail
LOG="/var/log/ssl-renew.log"
DOMAIN="yourdomain.com"
WARN_DAYS=30 # 剩余不足此天数时触发续期
ACME_SH="$HOME/.acme.sh/acme.sh"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
# 获取证书过期时间(通过 openssl 查询实际证书文件)
CERT_FILE="/etc/nginx/ssl/${DOMAIN}/fullchain.cer"
if [ ! -f "$CERT_FILE" ]; then
log "证书文件不存在:$CERT_FILE,跳过检查"
exit 0
fi
# 计算距离过期的剩余天数
EXPIRY_DATE=$(openssl x509 -in "$CERT_FILE" -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
log "证书 ${DOMAIN} 剩余有效期:${DAYS_LEFT} 天"
if [ "$DAYS_LEFT" -le "$WARN_DAYS" ]; then
log "开始续期证书..."
# 使用 acme.sh 续期(--force 强制续期,去掉则只在临近过期时才续期)
"$ACME_SH" --renew -d "$DOMAIN" --ecc
# 重载 Nginx 使新证书生效(reload 而非 restart,不中断现有连接)
systemctl reload nginx
log "✅ 证书续期完成,Nginx 已重载"
else
log "证书有效期充足,无需操作"
fi 脚本⑤:Cloudflare DDNS 动态 DNS 更新
适用于家宽/动态 IP 场景,自动检测 IP 变化并调用 CF API 更新 DNS A 记录
#!/bin/bash
# 文件路径:/usr/local/bin/cf-ddns.sh
# 功能:检测本机公网 IP 是否变化,若变化则自动更新 Cloudflare DNS A 记录
# 适用场景:家庭宽带、动态 IP 的低成本服务器
set -euo pipefail
# ── 配置区 ────────────────────────────────────────────────────────────────────
CF_API_TOKEN="your_cloudflare_api_token" # CF 后台 → API 令牌 → 创建令牌
ZONE_ID="your_zone_id" # CF 域名概览页右侧
RECORD_NAME="home.yourdomain.com" # 要更新的子域名
CACHE_FILE="/tmp/ddns_last_ip.cache" # 缓存上次的 IP,避免无变化时频繁调用 API
# ── 获取当前公网 IP(使用多个服务互为备份)────────────────────────────────────
CURRENT_IP=$(curl -sf https://api.ipify.org || curl -sf https://ifconfig.me || curl -sf https://ip.sb)
if [ -z "$CURRENT_IP" ]; then
echo "无法获取公网 IP,跳过本次更新" && exit 1
fi
# 若 IP 未变化则静默退出,不消耗 API 调用次数
LAST_IP=$(cat "$CACHE_FILE" 2>/dev/null || echo "")
[ "$CURRENT_IP" = "$LAST_IP" ] && exit 0
echo "IP 已变更:$LAST_IP → $CURRENT_IP,开始更新 DNS..."
# 查询 Cloudflare 上的 A 记录 ID
RECORD_ID=$(curl -sf -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=A&name=$RECORD_NAME" -H "Authorization: Bearer $CF_API_TOKEN" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
# 更新 DNS A 记录
curl -sf -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" --data "{"type":"A","name":"$RECORD_NAME","content":"$CURRENT_IP","ttl":60}" > /dev/null
echo "$CURRENT_IP" > "$CACHE_FILE"
echo "✅ DNS 更新完成:$RECORD_NAME → $CURRENT_IP" 脚本⑥:磁盘容量多级告警
自动检查所有分区,超 85% 警告、超 95% 紧急,推送 Webhook 通知,支持多分区
#!/bin/bash
# 文件路径:/usr/local/bin/disk-monitor.sh
# 功能:监控多个分区的磁盘使用率,超过阈值时推送告警
set -euo pipefail
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
THRESHOLD=85 # 告警阈值(%)
CRITICAL=95 # 紧急阈值(%),超过此值需立即处理
# 检查所有挂载分区(排除 tmpfs、devtmpfs 等虚拟文件系统)
df -h --output=pcent,target --exclude-type=tmpfs --exclude-type=devtmpfs | tail -n +2 | while read -r usage mount; do
usage_num=${usage%%%} # 去掉 % 号
if [ "$usage_num" -ge "$CRITICAL" ]; then
level="⛔ 紧急"
elif [ "$usage_num" -ge "$THRESHOLD" ]; then
level="⚠️ 警告"
else
continue # 未超阈值,跳过
fi
msg="${level}:$(hostname) 分区 ${mount} 使用率 ${usage},请立即清理!"
echo "$msg"
curl -s -X POST "$WEBHOOK_URL" -H 'Content-Type: application/json' -d "{"msgtype":"text","text":{"content":"$msg"}}" > /dev/null
done ⏰ Cron 定时任务全解析
Cron 是 Linux 内置的任务调度器,几乎所有发行版都预装,无需额外安装。通过 crontab -e 编辑当前用户的任务列表。
Cron 表达式语法图解
# ┌────────────────── 分钟 (0-59)
# │ ┌─────────────── 小时 (0-23)
# │ │ ┌──────────── 日 (1-31)
# │ │ │ ┌───────── 月 (1-12)
# │ │ │ │ ┌────── 星期 (0-6, 0=周日, 也可写 Sun/Mon/Tue...)
# │ │ │ │ │
# * * * * * /path/to/command
# 特殊符号:
# * = 每个可能的值(任意)
# , = 枚举多个值,如 1,3,5
# - = 连续范围,如 1-5(周一到周五)
# / = 步长间隔,如 */10(每 10 分钟)
# @reboot = 系统重启后执行一次(非标准但广泛支持) 10 个常用 Cron 表达式速查
| 表达式 | 含义 | 典型用途 |
|---|---|---|
| 0 3 * * * | 每天凌晨 3:00 | 系统自动更新、数据库备份 |
| 0 5 * * 1 | 每周一凌晨 5:00 | 周级全量备份、日志归档 |
| */10 * * * * | 每隔 10 分钟 | 服务健康检查、DDNS 更新 |
| 0 */6 * * * | 每 6 小时整点 | 证书检测、磁盘容量告警 |
| 30 8 * * 1-5 | 工作日每天 8:30 | 工作日定时推送日报 |
| 0 0 1 * * | 每月 1 日 00:00 | 月度全量备份、账单汇总 |
| 0 2 * * 0 | 每周日凌晨 2:00 | 周日低峰系统重启维护 |
| 5 4 * * * | 每天 4:05(错峰执行) | 多服务器错峰避免资源争抢 |
| @reboot | 系统重启后立即执行一次 | 启动时运行初始化脚本 |
| 0 4 1 1 * | 每年 1 月 1 日 4:00 | 年度统计、旧数据归档清理 |
Cron 配置示例(将脚本挂载到 crontab)
# ⚠️ 重要:cron 中必须使用绝对路径,不能用 ~/ 或相对路径
# ⚠️ 推荐将所有脚本输出重定向到日志,便于排查问题
# 每天凌晨 3:00 执行系统更新(输出追加到日志)
0 3 * * * /usr/local/bin/auto-update.sh >> /var/log/auto-update.log 2>&1
# 每天 4:15 执行网站备份(错峰,避免与系统更新冲突)
15 4 * * * /usr/local/bin/website-backup.sh >> /var/log/backup.log 2>&1
# 每 10 分钟检查服务健康状态
*/10 * * * * /usr/local/bin/service-health.sh >> /var/log/service-health.log 2>&1
# 每天 5:00 检查 SSL 证书有效期
0 5 * * * /usr/local/bin/ssl-renew.sh >> /var/log/ssl-renew.log 2>&1
# 每 5 分钟更新 DDNS(动态 IP 场景)
*/5 * * * * /usr/local/bin/cf-ddns.sh
# 每小时检查磁盘容量
0 * * * * /usr/local/bin/disk-monitor.sh
# 系统重启后立即执行一次初始化检查
@reboot /usr/local/bin/service-health.sh */2 * * * * 每 2 分钟触发),观察日志确认执行正常后再改回正常间隔。验证 cron 任务运行记录:grep CRON /var/log/syslog | tail -20 ⏱️ 进阶:Systemd Timer 定时器
Systemd Timer 是 Cron 的现代替代方案,在需要以下特性时优先选择 Timer:
完整日志集成
执行输出自动进入 journalctl,无需手动管理日志文件,支持按时间、优先级过滤
失败自动重试
配置 Restart=on-failure 后任务失败可自动重试,Cron 任务失败则静默跳过
精确到秒
OnCalendar 支持完整日历语法,可精确到秒;Cron 最小粒度为 1 分钟
第一步:创建 .service 文件(定义"做什么")
# 文件路径:/etc/systemd/system/website-backup.service
# 定义"做什么":执行备份脚本
[Unit]
Description=Website Daily Backup
# 依赖网络(若备份需要上传到远程存储)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot # 一次性任务(非长驻进程)
User=root
ExecStart=/usr/local/bin/website-backup.sh
# 标准输出和错误输出都写入 systemd journal,用 journalctl 查看
StandardOutput=journal
StandardError=journal
# 脚本失败时的重试策略
Restart=on-failure
RestartSec=60s
# 资源限制:防止备份时占用过多 CPU
CPUQuota=50% 第二步:创建 .timer 文件(定义"什么时候做")
# 文件路径:/etc/systemd/system/website-backup.timer
# 定义"什么时候做":每天凌晨 3:15 执行
[Unit]
Description=Run Website Backup Daily at 3:15 AM
[Timer]
# OnCalendar 支持完整日历语法,比 Cron 更直观
OnCalendar=*-*-* 03:15:00 # 每天 3:15
# OnCalendar=Mon *-*-* 02:00 # 每周一 2:00
# OnCalendar=*-*-1 00:00 # 每月 1 日
# Persistent=true:若上次计划时间错过(机器关机),开机后立即补充执行一次
Persistent=true
# 随机延迟 0-300 秒,避免多台服务器同时执行造成资源争抢
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
# ── 启用并立即启动定时器 ──────────────────────────────────────────────────────
# systemctl daemon-reload
# systemctl enable --now website-backup.timer
# systemctl list-timers --all # 查看所有定时器的下次执行时间
# journalctl -u website-backup # 查看执行日志 📝 进阶:日志轮转与 logrotate
每个自动化脚本都会持续写日志,不加管理的日志文件在 1-2 年内就能轻松吃掉数 GB 磁盘。logrotate 是 Linux 系统内置的日志轮转工具,几乎无需维护即可自动处理所有日志。
# 文件路径:/etc/logrotate.d/myapp
# 功能:统一管理应用产生的多个日志文件,防止日志占满磁盘
# 可以同时指定多个日志路径(支持通配符)
/var/log/backup.log
/var/log/service-health.log
/var/log/ssl-renew.log
/var/log/cf-ddns.log
{
daily # 每天轮转一次(可选:weekly/monthly/yearly)
rotate 30 # 保留最近 30 份归档(超出则删除最旧的)
compress # 使用 gzip 压缩归档文件(.log.1.gz)
delaycompress # 本次轮转的文件下次才压缩(当前 .log.1 不压缩,方便实时查看)
missingok # 若日志文件不存在,不报错直接跳过
notifempty # 若日志为空,不执行轮转(避免产生 0 字节的归档)
dateext # 归档文件名追加日期(backup.log-20260316.gz),避免数字后缀混乱
create 0640 root adm # 轮转后创建新日志,权限 640,属主 root,属组 adm
# postrotate 钩子:轮转后通知应用重新打开日志文件句柄
# 对于使用长驻进程的应用(如 Nginx),不发信号则新日志仍写入旧文件
postrotate
# 发送 USR1 信号让 Nginx 重新打开日志文件
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid) || true
endscript
} 📋 logrotate 常用指令速查
daily / weekly / monthly 轮转频率 rotate N 保留 N 份归档 compress gzip 压缩归档 delaycompress 最新一份不压缩 missingok 文件不存在不报错 notifempty 空文件不轮转 dateext 用日期作为后缀 create MODE OWNER GROUP 轮转后建新文件 postrotate / endscript 轮转后执行命令 手动触发测试:logrotate -d /etc/logrotate.d/myapp(-d 为 dry-run,不实际执行)
📲 进阶:Webhook 消息推送通知
让脚本在关键事件时主动通知您,是自动化运维的重要闭环。以下提供两种最主流的推送方案,二选一即可:
企业微信 Webhook
适合国内用户。在企业微信群中添加"群机器人",获取 Webhook URL 后即可使用。消息支持 Markdown 格式,显示在工作群中,不易错过。
配置路径:群聊 → 添加群机器人 → 复制 Webhook URL
Telegram Bot
适合国际用户。与 @BotFather 创建 Bot 后,消息推送到个人或群组频道。API 稳定可靠,消息格式支持 Markdown/HTML,免费无限制。
配置路径:Telegram → 搜索 @BotFather → /newbot → 获取 Token
企业微信 Webhook 推送函数
#!/bin/bash
# 企业微信 Webhook 消息推送函数(封装为可复用的 Shell 函数库)
# 用法:source /usr/local/lib/notify.sh,然后调用 notify_wecom "消息内容"
WECOM_WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_WECOM_KEY"
notify_wecom() {
local message="$1"
curl -sf -X POST "$WECOM_WEBHOOK" -H 'Content-Type: application/json' --data-binary "{
"msgtype": "markdown",
"markdown": {
"content": "**[VPS 自动化通知]**\n>主机:$(hostname)\n>时间:$(date '+%Y-%m-%d %H:%M:%S')\n>\n>${message}"
}
}" > /dev/null && echo "企业微信通知已发送" || echo "通知发送失败"
}
# 使用示例:
# notify_wecom "✅ 网站备份完成,大小:234 MB"
# notify_wecom "⛔ 磁盘空间告急:/ 分区使用率 96%,请立即处理" Telegram Bot 推送函数
#!/bin/bash
# Telegram Bot 消息推送函数
# 前置条件:
# 1. 与 @BotFather 对话,创建 Bot,获取 BOT_TOKEN
# 2. 与您的 Bot 发一条消息,然后访问:
# https://api.telegram.org/bot<TOKEN>/getUpdates
# 从返回 JSON 中找到 chat.id
TG_BOT_TOKEN="123456789:ABCdefGHIjklMNOpqrSTUvwxYZ"
TG_CHAT_ID="your_chat_id_here" # 个人 chat_id 或群组 chat_id
notify_telegram() {
local message="$1"
# parse_mode=Markdown 支持加粗、代码块等格式
curl -sf -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" --data-urlencode "chat_id=${TG_CHAT_ID}" --data-urlencode "parse_mode=Markdown" --data-urlencode "text=*[VPS 通知]* `$(hostname)`
时间:$(date '+%Y-%m-%d %H:%M:%S')
───
${message}" > /dev/null && echo "Telegram 通知已发送" || echo "通知发送失败"
}
# 使用示例:
# notify_telegram "✅ SSL 证书续期成功,新过期时间:2026-09-16" 🚀 进阶:Ansible 多机批量运维
当您管理的 VPS 超过 3 台时,逐台 SSH 执行命令会变得低效且容易出错。Ansible 是目前最主流的无代理(agentless)自动化工具:只需在本地安装,通过 SSH 即可批量管理所有服务器,无需在目标机器上部署任何组件。
⚡ 快速安装 Ansible(本地机器)
brew install ansible sudo apt install ansible -y pip3 install ansible Inventory 清单文件(定义您的服务器)
# 文件路径:~/playbooks/inventory.ini
# 定义您的服务器分组
[web_servers]
web1 ansible_host=1.2.3.4 ansible_user=ubuntu ansible_port=39217
web2 ansible_host=5.6.7.8 ansible_user=ubuntu ansible_port=39217
[db_servers]
db1 ansible_host=9.10.11.12 ansible_user=debian ansible_port=22
[all:vars]
# 所有主机共用的变量
ansible_ssh_private_key_file=~/.ssh/id_ed25519
# ── 执行 Playbook ──────────────────────────────────────────────────────────────
# ansible-playbook -i inventory.ini update-all.yml
#
# 先用 --check 做"预演"(dry-run),看看会做什么改动但不实际执行:
# ansible-playbook -i inventory.ini update-all.yml --check
#
# 只对某个分组执行:
# ansible-playbook -i inventory.ini update-all.yml --limit web_servers Playbook 任务文件(批量系统更新示例)
# 文件路径:~/playbooks/update-all.yml
# 功能:批量在所有 VPS 上执行系统更新
# 前置:pip install ansible && ssh-copy-id user@server1 user@server2
# (Playbook 文件以 --- 开头,这是 YAML 标准格式)
- name: 批量系统更新
hosts: all # 应用到 inventory 文件中的所有主机
become: true # 等同于 sudo,以 root 权限执行
tasks:
- name: 更新软件包索引
apt:
update_cache: true
cache_valid_time: 3600 # 若缓存不超过 1 小时则不重新拉取
- name: 升级所有软件包
apt:
upgrade: dist # dist-upgrade:处理依赖变更(比 safe 更彻底)
autoremove: true
autoclean: true
- name: 检查是否需要重启(内核更新后)
stat:
path: /var/run/reboot-required
register: reboot_required
- name: 输出重启提示
debug:
msg: "主机 {{ inventory_hostname }} 需要重启(内核已更新)"
when: reboot_required.stat.exists 🛡️ 安全最佳实践
脚本中的安全漏洞可能比代码 Bug 更危险——一个处理不当的密码变量,就可能让攻击者获取数据库访问权限。
🔐不要硬编码密码
数据库密码、API Token 等敏感信息切勿直接写在脚本文件中。推荐方案:① 使用 ~/.my.cnf 存储 MySQL 密码;② 将敏感变量写入单独的 .env 文件并设置 chmod 600;③ 使用 pass 或 vault 密钥管理工具。
📁最小化文件权限
脚本文件设置 chmod 700(仅所有者可读写执行),配置文件设置 chmod 600(仅所有者可读写)。包含密码的文件一旦权限设置过松(如 644),任意用户都能读取其中的密钥。
👤避免以 root 运行
尽量为自动化脚本创建专用低权限账号(如 backup_user、monitor_user),通过 sudo 仅授权所需命令的执行权限,而非完整 root。即使脚本被注入恶意代码,危害范围也大幅降低。
✅严格校验外部输入
若脚本接受命令行参数或读取外部文件,必须对输入进行合法性检验。例如:文件路径变量在使用前应检查是否包含 ../ 或特殊字符,防止路径遍历攻击导致任意文件读写。
🔧 脚本调试与排错工具
bash -x 执行跟踪模式
每行命令执行前都会打印出来(展开变量后的实际命令),是最直接的调试方式。
bash -x /usr/local/bin/website-backup.sh # 外部调试运行 # 或在脚本内部局部开关: set -x # 从这里开始打印执行命令 your_function set +x # 关闭打印
shellcheck 静态代码分析(强烈推荐)
在运行前发现脚本中的常见错误(未加引号的变量、废弃语法、SC2xxx 规范问题),几乎是 Shell 脚本的"编译器"。
apt install shellcheck -y # 安装 shellcheck /usr/local/bin/backup.sh # 检查脚本 # 输出示例: # ^-- SC2086: Double quote to prevent globbing and word splitting.
journalctl 查看 Systemd Timer 日志
Systemd Timer 触发的任务日志全部进入 journal,用以下命令精准查看:
journalctl -u website-backup.service # 查看该服务的全部日志 journalctl -u website-backup.service -n 50 # 最近 50 行 journalctl -u website-backup.service -f # 实时跟踪(follow) journalctl -u website-backup.service --since "2026-03-15" # 指定日期后的日志 systemctl list-timers --all # 查看所有 Timer 的下次执行时间
$? && trap 退出码检查与 cleanup
每个命令执行后都有退出码(0=成功,非0=失败)。配合 trap 实现脚本无论如何退出都能清理资源:
# 检查上一个命令是否成功
cp file1 /backup/
if [ $? -ne 0 ]; then
echo "复制失败!" && exit 1
fi
# 更简洁的写法:
cp file1 /backup/ || { echo "复制失败!"; exit 1; }
# trap 清理(脚本退出时无论成功还是失败都执行)
cleanup() { rm -f /tmp/tempfile; }
trap cleanup EXIT ❓ 常见问题解答
Cron 任务在命令行手动能运行,但 Cron 里不执行,是什么原因?
这是 Cron 最常见的坑,原因 99% 是环境变量差异。Cron 的执行环境极度精简:没有 PATH 中常用的 /usr/local/bin、没有 HOME、没有用户的 .bashrc。排查步骤:① 把命令中所有路径改为绝对路径(用 which 命令名 查询);② 在 crontab 文件顶部显式定义 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin;③ 将所有输出重定向到日志 >> /tmp/cron-debug.log 2>&1,检查实际报错信息;④ 若脚本依赖特定用户配置,改用 sudo crontab -u 用户名 -e 而非 crontab -e。
Cron 和 Systemd Timer 我应该选哪个?有没有简单的判断标准?
简单判断标准:选 Cron 当您只需要"每 X 分钟/小时/天运行某个脚本"这种简单场景,配置快、直观,几乎所有 Linux 发行版都支持;选 Systemd Timer 当您需要以下任意特性:① 任务失败需要自动重试;② 需要用 journalctl 统一查看所有任务日志;③ 任务有服务依赖(如需等网络就绪);④ 需要精确到秒的执行控制;⑤ 机器关机期间错过的任务需要开机后补跑(Persistent=true)。对于新建的重要生产任务,推荐用 Systemd Timer;对于简单的脚本调度,Cron 依然是最快的选择。
脚本中数据库密码怎么安全存储?直接写在脚本里有多危险?
直接写在脚本里极度危险:脚本文件可能被意外 cat 显示在终端、被误提交到 Git 仓库、被其他用户读取(若权限设置不当)。安全存储方案推荐按安全级别选择:
① ~/.my.cnf(最简单):[client],chmod 600,mysqldump 自动读取,无需在脚本中出现密码;
password=your_pass
② .env 文件:将密码写入 /etc/backup.env(chmod 600),在脚本中用 source /etc/backup.env 加载;
③ 系统密钥环/pass:使用 pass 命令行密码管理器,脚本中用 DB_PASS=$(pass show db/myapp) 动态读取,密码加密存储在文件系统中。
mysqldump 备份时数据库还在运行,会不会备份到不一致的数据?
对于 InnoDB 引擎(MySQL 5.5+ 的默认引擎),使用 --single-transaction 参数即可实现热备份:mysqldump 会开启一个一致性快照事务,整个导出过程看到的是同一时间点的数据,不锁表,不影响正常读写。对于 MyISAM 表(较旧的项目可能使用),--single-transaction 无效,必须加 --lock-tables 锁表备份,这期间写操作会被阻塞。推荐做法:新项目全面使用 InnoDB(MySQL 默认已是 InnoDB),配合 --single-transaction --routines --triggers 三个参数,可以做到完整且不影响业务的热备份。备份文件的完整性验证:定期尝试将备份文件恢复到测试环境,确保备份真正可用。
rclone 怎么配置?备份上传到 Cloudflare R2 或 Google Drive 的步骤是什么?
rclone 是支持 40+ 云存储服务的命令行工具,配置步骤:① 安装:curl https://rclone.org/install.sh | bash;② 交互式配置:rclone config,按提示选择存储类型(S3/R2/Drive)并输入 API 密钥;③ 测试:rclone ls remote:bucket-name。配置完成后,在备份脚本中加入:rclone copy /root/backups/ r2:my-backup-bucket/vps-backups/ --transfers=4。Cloudflare R2 特别适合:无出站流量费用(只收存储和请求费),10 GB/月免费额度,对个人 VPS 备份几乎免费。配置时选 S3 Compatible,endpoint 填 https://账户ID.r2.cloudflarestorage.com,在 R2 后台生成 API Token 作为 access_key/secret_key。
Telegram Bot 的 chat_id 怎么获取?推送给群组和个人有什么区别?
获取个人 chat_id:① 与您创建的 Bot 发送任意消息;② 浏览器访问 https://api.telegram.org/bot<TOKEN>/getUpdates;③ 在返回的 JSON 中找 message.from.id(个人 ID)或 message.chat.id(聊天 ID)。推送到群组:先将 Bot 加入群组,然后发一条 @Bot 的消息,再查 getUpdates,此时 message.chat.id 为负数(群组 ID 格式为 -100xxxxxxxxx)。将这个负数填入 TG_CHAT_ID 即可推送到群组。推荐做法:创建一个专属"运维通知"频道,将 Bot 设为管理员,所有 VPS 的告警统一推送到该频道,便于查看历史记录。
使用 Ansible 批量执行任务时,某台服务器失败了,会影响其他服务器的任务吗?
默认情况下,某台服务器失败后 Ansible 会将其标记为失败并从本次执行中排除,不会中断其他服务器的任务继续执行。如果您希望"任何一台失败则全部停止",可以设置 any_errors_fatal: true。通过 --limit 参数可以指定只对某台或某组机器执行:ansible-playbook -i inventory.ini update-all.yml --limit "web1,web2"。建议的生产实践:① 先用 --check 做 dry-run 预演;② 用 --limit 先在一台测试机上验证;③ 确认无误后再全量执行。Ansible 执行结束后的 PLAY RECAP 报告会清晰显示每台机器的 ok/changed/failed 统计。
acme.sh 和 certbot 都能续期 SSL,我应该用哪个?有何区别?
两者都能申请 Let's Encrypt 免费证书,主要区别:acme.sh:纯 Shell 脚本,无依赖,支持 ZeroSSL/Buypass 等多个 CA,内置 150+ DNS 服务商的 API(包括 Cloudflare、阿里云、腾讯云),申请泛域名证书(*.yourdomain.com)时无需开放 80/443 端口——这是 acme.sh 的核心优势,特别适合只有 SSH 无 Web 服务的服务器。certbot:EFF 官方工具,文档最丰富,有自动配置 Nginx/Apache 的插件(--nginx 参数一键自动修改 Nginx 配置),新手友好但依赖 Python 环境。推荐:VPS 运维场景选 acme.sh(更轻量灵活);初学者第一次配置 HTTPS 选 certbot with Nginx plugin(一键自动化)。
本篇学完后,下一步应该按什么顺序继续?自动化和日志分析有什么关联?
按本站 30 篇路径,第 11 篇(本篇)→ 第 12 篇(logs-and-troubleshooting)→ 第 13 篇(server-control-panels)是最紧密的进阶链条。关联逻辑:本篇的脚本每次执行都在写日志,第 12 篇教您如何读懂这些日志——从 journalctl 分析 Systemd Timer 日志到 awk/grep 提取 Nginx 访问日志中的攻击模式,掌握后您会发现自动化脚本写得更有针对性(知道该记录什么)。第 13 篇的可视化面板(1Panel/宝塔)则提供图形化的 Cron 任务管理界面,可以对本篇的脚本进行可视化监控和管理,两者互补。三篇合在一起构成完整的"运维飞轮":自动化执行 → 日志留证 → 面板监控。
这些自动化脚本适合放在哪个目录?有没有推荐的目录结构约定?
推荐遵循 Linux FHS(文件系统层次标准):/usr/local/bin/:系统级可执行脚本,放这里后全局 PATH 可直接调用,Cron 中可以直接写脚本名;/usr/local/lib/:共享函数库(如 notify.sh 通知函数),供其他脚本 source 引用;/etc/:配置文件(敏感的 .env 文件、logrotate 配置等),chmod 600;/var/log/:脚本产生的日志文件,配合 logrotate 自动管理。避免使用 /root/scripts/:虽然方便,但不符合标准,且若将来添加非 root 用户运行某些脚本时会造成权限麻烦。另外建议将所有脚本纳入 Git 版本控制(私有仓库),每次修改都 commit,这样既有备份,又有变更历史,也方便在多台服务器间同步。