VPSKnow

Linux 自动化运维脚本完全指南

中级
45分钟

服务器运维中 80% 的工作都是重复劳动:更新系统、备份数据、检查服务、续签证书……本指南将带您用 Shell 脚本和定时任务彻底解放双手,打造一台真正的无人值守自动化服务器

🤖 为什么要自动化运维

自动化运维的核心价值不只是"省时间"——更重要的是消除人为错误。凌晨 3 点的手动备份可能因为疏忽少备份了一个目录;而脚本每次都以完全相同的方式执行,没有情绪,不会忘记步骤。

效率提升

数小时的手动工作压缩为几秒钟的脚本执行,腾出时间做更有价值的事

🎯

零人为错误

脚本每次以完全相同的步骤执行,排除疲劳、疏忽带来的操作失误

无人值守

凌晨自动备份、证书到期前自动续签、服务挂了自动拉起,全程无需人工干预

📈

可扩展性

一套脚本配合 Ansible,可以同时在 10 台、100 台服务器上批量执行相同任务

📜 Bash 脚本基础与健壮性

大多数 Shell 教程只教"能跑起来",但生产环境的脚本必须考虑:命令失败时能否正确停止?变量为空时会发生什么?同一脚本同时被触发两次如何处理?

健壮性三件套:set -euo pipefail

这三个选项是所有生产脚本的必加前缀,能捕获大多数常见的静默错误:

bash 健壮性模板
#!/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% 的自动化需求,每个脚本都包含完整的逐行注释,可以直接使用或按需修改:

🔄

脚本①:系统自动安全更新

每天凌晨自动安装安全补丁,不升级内核等高风险包,含防重入锁与日志记录

/usr/local/bin/auto-update.sh
#!/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)

/usr/local/bin/website-backup.sh
#!/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 等服务,挂掉自动重拉,推送企业微信告警

/usr/local/bin/service-health.sh
#!/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(零停机)

/usr/local/bin/ssl-renew.sh
#!/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 记录

/usr/local/bin/cf-ddns.sh
#!/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 通知,支持多分区

/usr/local/bin/disk-monitor.sh
#!/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 表达式语法图解

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)

sudo crontab -e
# ⚠️  重要: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
💡 调试技巧: 刚添加的 cron 任务不确定是否生效?先手动执行一次脚本确认无误,再把 Cron 间隔临时改短(如 */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
# 文件路径:/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
# 文件路径:/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
# 文件路径:/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 推送函数

/usr/local/lib/notify.sh(可被其他脚本 source 引入)
#!/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 推送函数

/usr/local/lib/notify.sh(Telegram 版本)
#!/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(本地机器)

macOS: brew install ansible
Ubuntu/Debian: sudo apt install ansible -y
Python pip(任意平台): pip3 install ansible

Inventory 清单文件(定义您的服务器)

~/playbooks/inventory.ini
# 文件路径:~/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
# 文件路径:~/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]
password=your_pass
,chmod 600,mysqldump 自动读取,无需在脚本中出现密码;
.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=4Cloudflare 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,这样既有备份,又有变更历史,也方便在多台服务器间同步。