Codex生成服务器巡检脚本:只读、可审计的AI运维实践
2026/9/10 3:49:50 网站建设 项目流程

我见过最刺激的生产事故,其实不是人干的,而是一个 AI 智能体在终端里“自由发挥”。你跟它说“看看这台服务器最近怎么这么卡”,它看完负载、内存、缓存之后,转头就给你来一句“我准备重启一下服务释放内存”。那一刻你才会真正意识到:它拿到的是 shell 权限,不是建议权限。

Codex 是 OpenAI 出的命令行编程智能体,干这类活非常顺手:读日志、看指标、写脚本、改代码,甚至直接在 shell 里执行命令。它能帮你把运维效率拉高一个量级,但如果不划清边界,“效率”和“事故”之间往往就隔一个回车键。这次我做的实验很明确:用 Codex 生成一份服务器运维巡检脚本,不暴力运维,不直接重启服务器,所有动作只读、可留痕、可审计;人只需要做两件事,审代码、按计划执行。

这篇文章不是教你“怎么把 Codex 当 root 用”,而是讲怎么把 AI 放到一个既能干活、又不容易闯祸的位置。我会从需求拆解、提示词设计、脚本实现、运行审计、常见踩坑五个部分串起来,工具是 Codex CLI,目标是 Linux 服务器。无论你是想引入 AI 做运维的工程师,还是刚接触巡检脚本的新人,这套思路应该都能直接用。

1. 别让 AI 直接操作生产服务器,先解决“权限失控”问题

1.1 传统终端操作本来就高风险,AI 让风险变成了概率问题

人手动操作服务器出事故,通常是因为疲劳、手滑、误判。比如凌晨三点接到告警,脑子里想的是看内存,手上却敲错了命令,这类故事每个运维都能讲出几个。但人工操作有一个隐藏优点:人知道自己不知道什么,遇到拿不准的命令会停下来,会犹豫,会去看文档,甚至会上网查一圈再动手。

AI Agent 不一样。它给出下一条命令的过程,本质上是一个基于历史数据生成的预测,而不是“经过完整推演的确定性结论”。它看到负载高,会自然地推断“清理缓存、重启某个服务”,因为在它训练的语料里这种操作很常见。问题是生产环境最讨厌的就是“常见的操作”,你永远不会知道哪一次常见的重启会带走一个没保存的连接,或者让某个依赖服务起不来。

更要命的是,终端里一旦允许 AI 自动执行命令,它不会像人那样有“生理性紧张”。它不会因为这台机器上有三百个在线用户就犹豫,也不会因为这条命令从未在这个环境里验证过而手抖。它只会按照对话上下文里的意图,一步步把“应该做的事”做出来,直到某个命令产生不可逆影响。

所以我的结论很直接:不要让 AI 直接操作生产服务器。不是说 AI 不行,而是说目前阶段,任何模型都无法保证自己在生产环境里的每一步判断都百分之百正确。正确的做法是给 AI 一个“安全的输出形式”,也就是代码。

1.2 Codex 的正确用法:让 AI 生产“方案”,人负责执行方案

Codex CLI 有两个很常见的使用场景,第一时间想让它直接跑,第二时间想让它生成可交付的代码。我强烈建议运维类任务默认选第二个。

把 AI 的产出限定成脚本,本质上是把“执行权”和“影响力”从 AI 手里重新收回人类手里。脚本是可阅读的,是可以做 code review 的,是可以先在测试环境跑一遍再上生产的,也是可以纳入 Git 做版本管理的。服务器真正执行的是一份经过审批和验证的“提案”,而不是 AI 临场发挥的随意命令。

这就像你不会让一个实习生直接在生产环境敲命令,但你会让实习生先把操作步骤写成文档,由你审核后再执行。Codex 在运维里的定位就应该是这个实习生:它负责把巡检思路、检查命令、告警规则整理成可执行脚本,人来确认每一行有没有问题,确认完之后再让脚本去跑。

我在这次实战里给 Codex 立的规矩就一句话:你可以读任何文件、看任何指标、写任何脚本,但所有有副作用的操作必须经过我确认,脚本默认只读,禁止重启服务,禁止修改系统配置。结果就是,它产出的巡检脚本不仅能用,还可以留下来作为长期运维资产,反复跑、反复审查。

2. 巡检清单先行:到底该让脚本检查哪些东西

2.1 单台 Linux 服务器的五层巡检模型

很多人写巡检脚本习惯“想到哪写到哪”,今天加一个 CPU,明天补一个磁盘,最后脚本几百行,逻辑混乱,还不知道哪些检查才是关键的。我的做法是先分层,把巡检对象拆成五个维度,再逐层确认。

分层检查项数据源/命令重点关注
资源层CPU 负载、内存、磁盘、inode/proc/loadavg、/proc/meminfo、df、free负载是否超过核数,内存是否耗尽,磁盘/inode 是否接近满
系统层运行时间、内核版本、时间同步uptime、uname、timedatectl内核是否过旧,NTP 是否同步,系统是否长时间未重启
服务层关键 systemd 服务状态、进程数量systemctl、pgrep业务进程是否存活,依赖服务是否正常
网络层监听端口、TCP 连接数、丢包错包ss、netstat、ip -s端口是否按预期监听,连接是否异常积压
安全与备份证书到期时间、登录失败记录、备份文件时间戳openssl、lastb、stat证书是否即将过期,是否有异常登录,备份是否还在更新

之所以把“证书到期”和“备份文件时间戳”也放进巡检脚本,是因为这两类问题平时不冒烟,一旦出事就是大事。证书过期会导致线上接口突然全部报错,备份失效意味着数据恢复根本无从谈起。巡检脚本如果只盯 CPU 和内存,那它和裸奔区别不大。

2.2 脚本设计三约束:只读、幂等、可审计

确定检查项之后,还要给脚本定三条硬约束。

第一,只读优先。巡检脚本的本职是观察和记录,不是修东西。脚本执行过程中不应该修改系统配置、不应该删除文件、不应该对服务做任何启停操作。唯一允许的写入是往自己的日志目录写报告。把脚本设计成只读,等于给所有潜在风险加了一道天然防火墙。

第二,幂等可重复。同一份脚本,今天跑和明天跑,除了时间戳和具体数值,整体流程必须完全一致。不能因为上次执行留下某个状态文件就改变行为,也不能依赖交互式输入。这样才能保证定时任务可以反复执行,也能保证出问题时随时手动补跑一次。

第三,输出可审计。脚本必须把检查结果分成“正常、警告、异常”几档,并且把每次运行的时间、主机、各项指标、结论完整记录下来。审计不是只给一个人看,还要能在出问题时让其他同事也能通过日志还原当时的现场。

3. 用 Codex 生成巡检脚本:提示词设计是关键

3.1 给 Codex 的提示词模板

Codex 生成代码的质量,很大程度上取决于你给它输入的约束条件。尤其是运维脚本,提示词里如果不明确“只读、不许重启”,它很容易按自己的理解补上一堆危险操作。

我这次使用的提示词大致如下,你可以直接复制改一改再用:

你现在是一位有十年经验的 SRE,请帮我在一台 Linux 服务器上编写一份运维巡检脚本。 要求: 1. 只读检查,不得修改任何系统配置,不得重启服务,不得删除文件; 2. 检查范围包括:CPU 负载、内存使用率、磁盘和 inode 使用率、关键 systemd 服务状态、监听端口、最近 1 小时错误日志、SSL 证书到期时间、NTP 同步状态; 3. 输出到控制台的同时,把完整结果写入 /var/log/server_inspect/ 目录下带时间戳的日志文件; 4. 每类检查结果必须区分“正常 / 警告 / 异常”三档,存在异常时最终退出码不为 0; 5. 脚本使用 bash 实现,尽量兼容主流 Linux 发行版; 6. 只生成脚本内容,不用真正执行命令。

在 Codex CLI 里可以直接这样跑:

codex exec "你现在是一位有十年经验的 SRE,请帮我编写一份运维巡检脚本……"

也可以先进入交互式对话,再把提示词粘贴进去。我比较推荐交互式的方式,因为你可以在生成初稿后追加问题,比如“为什么这里用 grep 而不是 awk”“这个端口匹配会不会误伤”,让 Codex 现场解释代码逻辑。

3.2 人工审查 Codex 输出的四个重点

Codex 生成的脚本不一定完美,它甚至可能在脚本里加入你没要求的操作。所以拿到初稿后,第一件事不是执行,而是审查。

审查维度具体检查我这次的实际观察
高危命令搜索 restart、reboot、shutdown、rm -rf、mkfs、kill 等关键词初版脚本里就出现过重启 sshd 的操作,必须删掉
写操作搜索 > /etc、> /dev/sd、tee /etc、chmod、chown正确版本只允许往日志目录写文件
交互风险是否包含 ssh 远程执行、是否可能 hang 住等待输入初版有 ssh 检查,被我改成纯本地检查
变量与引用未初始化变量、反引号嵌套、缺少双引号个别路径没加引号,遇到空格目录会出错

我那次让 Codex 初版生成后,它在脚本尾部顺手加了一句类似systemctl restart sshd的操作,理由是“为了确保配置生效”。可问题是我根本没有让它改任何配置,它自己脑补了一个重启。这段要是没人审查直接跑,影响面就完全失控了。

所以请记住:Codex 输出的脚本是“草稿”,不是“成品”。AI 写代码再快,人工审查这一步也绝对不能省。

4. 巡检脚本核心实现与审计设计

4.1 脚本骨架:头部、日志函数、结果汇总

在跟 Codex 来回迭代了好几轮之后,我保留了一份可以直接上生产用的脚本基准版。这里把核心结构拆开讲,方便你理解为什么每一段要这么写。

脚本头部和公共函数是整个巡检脚本的地基:

#!/usr/bin/env bash # # server_inspect.sh # 用途:服务器日常巡检,只读检查,输出审计日志 # 用法:bash server_inspect.sh # set -uo pipefail REPORT_DIR="${REPORT_DIR:-/var/log/server_inspect}" STAMP="$(date +%Y%m%d_%H%M%S)" REPORT_FILE="${REPORT_DIR}/inspect_${STAMP}.log" mkdir -p "$REPORT_DIR" FAILED=0 say() { printf '\033[1;32m[OK]\033[0m %s\n' "$*" | tee -a "$REPORT_FILE"; } warn() { printf '\033[1;33m[!!]\033[0m %s\n' "$*" | tee -a "$REPORT_FILE"; } fail() { printf '\033[1;31m[XX]\033[0m %s\n' "$*" | tee -a "$REPORT_FILE"; FAILED=1; }

第一行 shebang 必须用#!/usr/bin/env bash,而不是#!/bin/sh,因为脚本里用了 bash 的进程替换和数组特性,sh 跑起来会报语法错误。

set -uo pipefail也是有讲究的。我没有用set -e,因为巡检脚本里很多检查命令本来就可能返回非零状态,比如“grep 没匹配到”“journalctl 没日志”,如果开了set -e,脚本会在第一次“没查出问题”时直接退出,后面的检查全部被跳过。-u是为了抓住未定义变量,pipefail是为了防止管道命令的失败被掩盖。

sayfail里用了tee -a,这样每一条输出既出现在屏幕上,又写入日志文件。最终通过FAILED变量汇总所有异常,决定退出码。

4.2 各检查模块是怎么实现的

资源层的检查是最基础也最关键的。CPU 负载不能只看数值,要结合核数算百分比:

LOAD1=$(awk '{print $1}' /proc/loadavg) CORES=$(nproc 2>/dev/null || echo 1) LOAD_PCT=$(awk -v a="$LOAD1" -v c="$CORES" 'BEGIN{printf "%d", a/c*100}') if (( LOAD_PCT >= 80 )); then fail "CPU 1分钟负载偏高: $LOAD1 / $CORES 核" else say "CPU 1分钟负载: $LOAD1 / $CORES 核" fi

这里用 1 分钟负载而不是 5 或 15 分钟,是因为巡检脚本通常按小时或半小时跑一次,1 分钟负载更能反映当前是否已经有异常趋势。

内存检查我推荐直接读取/proc/meminfo里的 MemAvailable 字段,它比 free 命令里的“可用内存”更贴近真实可用量:

read -r MEM_TOTAL_KB MEM_AVAIL_KB < <(awk '/MemTotal/{t=$2}/MemAvailable/{a=$2}END{print t, a}' /proc/meminfo) MEM_USED_PCT=$(( (MEM_TOTAL_KB - MEM_AVAIL_KB) * 100 / MEM_TOTAL_KB )) if (( MEM_USED_PCT >= 90 )); then fail "内存使用率 ${MEM_USED_PCT}%" else say "内存使用率 ${MEM_USED_PCT}%" fi

磁盘检查要同时看容量和 inode,两个/根分区都是必查项:

while read -r fs used avail; do if (( "${used%\%}" >= 85 )); then fail "磁盘 $fs 使用率 ${used}" else say "磁盘 $fs 使用率 ${used} 剩余 ${avail}" fi done < <(df -hP / | awk 'NR==2{print $6, $5, $4}') INODE_PCT=$(df -iP / | awk 'NR==2{print $5}') if (( "${INODE_PCT%\%}" >= 90 )); then fail "inode 使用率 $INODE_PCT" else say "inode 使用率 $INODE_PCT" fi

很多新手只看磁盘容量,不看 inode。inode 满了以后,即使磁盘还有空间,也无法创建任何新文件,这种故障一旦发生会非常难排查。

服务状态检查用 systemctl 是最标准的做法,关键服务可以根据业务改:

for svc in ssh cron rsyslog; do if systemctl is-active --quiet "$svc" 2>/dev/null; then say "服务 ${svc} 运行中" else fail "服务 ${svc} 未运行" fi done

网络层检查我不建议一股脑把所有端口全打出来,那样日志会很乱。只检查你关心的几个端口即可:

for port in 22 80 443; do if ss -tln 2>/dev/null | awk '{print $4}' | grep -q ":${port}$"; then say "端口 ${port} 正在监听" else warn "端口 ${port} 未监听" fi done

注意 grep 里的正则一定要写成:${port}$,否则会误匹配像 :2222、:8080 这类包含相同前缀的端口。

证书到期检查也是运维巡检里的刚需:

CERT_FILES=$(find /etc/ssl/certs /etc/pki/tls/certs -maxdepth 2 -name '*.pem' -o -name '*.crt' 2>/dev/null | head -5) for cert in $CERT_FILES; do end=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | cut -d= -f2) [[ -z "$end" ]] && continue days=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 )) if (( days < 30 )); then warn "证书 ${cert} 剩余 $days 天到期" else say "证书 ${cert} 剩余 $days 天" fi done

日志错误检查使用 journalctl 即可,最近 1 小时有 error 级别日志就提示:

if command -v journalctl >/dev/null 2>&1; then err_count=$(journalctl --since "1 hour ago" -p err --no-pager 2>/dev/null | wc -l) if (( err_count > 0 )); then fail "最近 1 小时错误日志 ${err_count} 条" else say "最近 1 小时无错误日志" fi fi

脚本结尾统一汇总:

echo "" echo "巡检报告: $REPORT_FILE" exit "$FAILED"

如果FAILED是 1,脚本退出码为 1。定时任务和监控系统可以基于退出码判断是否需要告警。

4.3 日志文件为什么要按时间戳命名

审计的核心其实是日志的可追溯性。我让 Codex 把报告文件命名为inspect_YYYYmmdd_HHMMSS.log,每跑一次就生成一份独立的巡检快照。这样做有三个显而易见的好处。

第一,出问题时可以直接翻到故障时间点前后那一份日志,看当时的系统状态是什么样,不需要从一堆混杂输出里大海捞针。第二,不同时间点的日志可以做对比,比如内存使用率从上周的 60% 涨到这周的 85%,趋势立刻就能看出来。第三,日志不会互相覆盖,历史记录天然保留,这对合规审计也很有价值。

想要快速找到历次巡检里的异常项,可以用一条命令:

grep "\[XX\]" /var/log/server_inspect/inspect_*.log

这个设计的核心思路就是:巡检日志不是给人看的流水账,它是可以被检索、被对比、被追溯的审计轨迹。

5. 让脚本稳定跑进定时任务:执行、排错与闭环

5.1 首次上线前的三个验证步骤

脚本写出来只是完成了三分之一,验证才是重头戏。

第一步,先在测试环境跑一遍。我自己的习惯是先在本地虚拟机或容器里执行,用bash -n server_inspect.sh做语法检查,再实际运行一遍确认输出格式和退出码符合预期。测试环境跑通了,再谈生产环境。

第二步,用普通用户执行一次。巡检脚本不一定要 root 才能跑,很多信息/proc下普通用户也能读。但 journalctl 和 systemctl 的部分操作在非 root 环境下可能拿不到完整数据。我在普通用户下跑了一次,发现 journalctl 直接报没有权限,于是把用户加到了 systemd-journal 组,同时确认 systemctl is-active 即使非 root 也能正常返回结果。

第三步,连续跑三到五次,检查输出是否有随机波动。如果发现某次运行多了一条 “磁盘使用率警告”,另一次又消失了,多半是脚本里有竞态条件或者依赖了上次运行留下的临时文件。巡检脚本应该是确定的,同样的状态应该得到同样的结论。

5.2 巡检脚本常见问题速查表

这一节是基于我这几次踩坑经历整理出来的问题清单,强烈建议你贴到自己的运维笔记里。

问题现象根本原因解决方案
脚本执行到一半就退出使用了 set -e,某个检查命令返回非零改用 set -uo pipefail,不要全局开 -e
journalctl 报没有权限非 root 用户不在 systemd-journal 组将用户加入 systemd-journal 组,或使用 sudo 执行
df 命令长时间卡住服务器挂了 NFS,网络文件系统无响应对远程挂载点单独处理,给 df 添加 timeout 限制
inode 使用率显示异常只看了磁盘容量,没看文件系统 inode 数在检查中同时加入 df -iP 的 inode 输出
端口匹配出问题grep 正则没有加结尾符,误匹配 2222使用 :22$ 这类精确匹配,或改用 ss 的 state 过滤
日志颜色串全是转义符tee 直接把 ANSI 色码写入了日志文件不做处理也可以,或用 sed 去掉控制字符再落盘
cron 里跑不正常但手动正常cron 的 PATH 环境与终端不同脚本开头 export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
自动执行时重复跑多份上一次脚本还没结束,下一次任务又开始了使用 flock 文件锁,防止脚本并发执行
磁盘满了导致日志写不进去日志目录没有做轮转和清理对 /var/log/server_inspect 配置 logrotate 或定期清理策略
AI 生成脚本里多出意外操作模型脑补了人类没有要求的步骤上线前务必 grep 高危关键词,逐行做 code review

第 10 条其实是最容易忽略的,我单独提一下。Codex 生成脚本再快,它也只是按照概率补全上下文,它不知道这台服务器上跑的是什么业务,也不知道你对配置变更的容忍度。任何由 AI 生成的运维脚本,第一版都必须过一次人工审查,重点就是查那些你并没有要求它做,但它自己“顺手”加进去的操作。

5.3 配置定时任务时的细节

巡检脚本要发挥价值,必须周期性地跑。我的习惯是每天至少跑一次,默认放在凌晨或业务低峰期。

cron 配置可以写成这样:

30 3 * * * /usr/local/bin/server_inspect.sh >> /dev/null 2>&1

这条表示每天凌晨 3 点 30 分执行一次巡检,并把标准输出和错误输出都丢弃,因为脚本内部已经用 tee 把日志写入了报告文件。如果你希望定时任务失败时能被监控系统感知,可以在脚本外层再包一层检测:

30 3 * * * /usr/local/bin/server_inspect.sh || echo "巡检执行失败"

另外,如果担心脚本还没跑完下一次任务又启动,可以用 flock 做执行锁:

30 3 * * * /usr/bin/flock -n /tmp/server_inspect.lock /usr/local/bin/server_inspect.sh

这样即使上次执行卡住或超时,也不会出现两个巡检进程同时跑、互相干扰日志的情况。

脚本执行完成后,还可以考虑把异常结果转发到内部告警群。最简单的方式是在脚本末尾加一步判断,如果FAILED非零,就调用一个通知脚本发送摘要。不过这一步我建议单独做,不要在巡检脚本里堆太多跟系统检查无关的业务逻辑。

6. 后续扩展与我的真实体会

Codex 做好巡检脚本只是整个运维自动化的第一小步。脚本跑稳之后,可以继续让 Codex 基于同一套巡检结果生成趋势报表,或者把日志里的关键指标解析成 JSON,上报给监控平台做可视化和阈值告警。甚至下一步可以让 Codex 根据巡检发现自动生成修复建议草案,再由人工确认执行。每次扩展的边界都一样:AI 负责分析和产出,人负责拍板和执行。

我个人的实操体会是,把 Codex 从一个“直接执行者”降级成“方案生成器”,效率优势一点没少,但安全感至少翻了几倍。巡检脚本里那些重复枯燥的检查逻辑,人写要半小时,它几十秒就能出一版;而我在旁边要做的,只是花几分钟做一次认真的审查。

最后一次补充一个实用小技巧:我每次让 Codex 改脚本,都会要求它同时更新脚本头部的注释块,把变更原因写清楚。这样两周之后再看这份脚本,每一块功能为什么存在、什么时间加的、解决了什么问题,全都一目了然。审计的核心不是“留了多少日志”,而是“后来的人是否还能读懂”。

新服务器到手,我现在第一句话都是:先给我写一份可审计的巡检脚本,跑通、留痕、再逐步加检查项。别让 AI 直接重启服务器,这是我用 Codex 做运维这段时间最想提醒自己的一件事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询