「脚本是给机器跑的。但机器怎么知道脚本跑没跑?」
「看日志?」
「日志是给人看的。机器要的是接口。」

一、机器不会 SSH
「你现在的脚本,只有登录服务器才能跑。」
「但监控系统、CI/CD、K8s 探针,它们不会 SSH。它们只会发 HTTP 请求。」
脚本库(内部) → 日志文件 → 人去看
API(对外) → HTTP 接口 → 机器调用
「你前作学过 Prometheus 监控。Prometheus 怎么知道你的服务活着?靠 /metrics 或 /health 接口。今天我们就写这个。」

「那用 Python?」
「FastAPI。轻量、快、自带文档。」
二、准备 Python 环境
先确认系统里 Python 的版本:
python3 --version
输出示例:
Python 3.13.x
按 Canonical 官方的 LTS 说明:Ubuntu 26.04 LTS 默认 Python 是 3.13(24.04 才是 3.12,22.04 是 3.10)。这个数字要改对,因为后面 Dockerfile 里的基础镜像、依赖版本,都跟它对得上才顺。
安装 pip 和 venv,顺便把 pip 源配好:
sudo apt install -y python3-pip python3-venv
mkdir -p ~/.config/pip
tee ~/.config/pip/pip.conf <<-'EOF'
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
EOF
把配置写在 ~/.pip/pip.conf 这个路径虽然还能用,但早已不是推荐位置。pip 现在遵循 XDG 规范,用户级配置应该放 ~/.config/pip/pip.conf。另外 trusted-host = pypi.tuna.tsinghua.edu.cn 现在不必要了:清华源是正经 HTTPS,证书没问题。这一行只在用 http 源或自签证书的内网源时才需要。
创建虚拟环境:
python3 -m venv venv
source venv/bin/activate
「隔离依赖。系统 Python 别乱装包,每个项目一个环境,互不干扰。」
三、第一个接口
pip install fastapi uvicorn
「FastAPI 是框架,uvicorn 是服务器。」
from fastapi import FastAPI
app = FastAPI(title="Ops Scripts API", version="0.1.0")
@app.get("/")
def root():
return {"message": "Ops Scripts API is running"}
@app.get("/health")
def health():
return {"status": "ok"}
启动:
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
--reload 是开发模式,改代码自动重启。生产环境别用。
浏览器打开 http://192.168.1.100:8000/docs,能看到 Swagger UI,两个接口列在那里,点一下就能测试。
「代码即文档,不用手写 API 说明。」

四、调用脚本
在 FastAPI 里调外部脚本,核心是 subprocess.run 加超时保护:
@app.post("/check/disk", response_model=CheckResult)
def check_disk(threshold: int = 80):
script = "/home/xiaolin/ops-scripts/bin/disk-check.sh"
result = subprocess.run(
["bash", script, "-t", str(threshold)],
capture_output=True,
text=True,
timeout=30 # 超时保护,防止脚本卡死
)
return CheckResult(
status="completed",
output=result.stdout.strip() or result.stderr.strip(),
exit_code=result.returncode
)
「timeout 不是美化项,是必需品。脚本卡住 = API 卡住。」
在 /docs 里输入 threshold: 90,返回:
{ "status": "completed", "output": "所有分区正常", "exit_code": 0 }
「现在机器可以发 HTTP 请求触发检查,不用 SSH。」
这里有个容易含糊的点:threshold: int = 80 会被 FastAPI 当作查询参数——值是通过 URL 传的( ?threshold=90 )。所以它在 /docs 里出现在 Parameters 区,不是在请求体里。要放进请求体,得单独定义一个 Pydantic 模型。两种方式在真实联调时差别很大——查询参数会被记进访问日志,别把敏感信息塞进 URL。
五、最容易写错的一段:「脚本不存在」会报什么
小林把脚本路径改错,再调接口,返回:
{
"status": "completed",
"output": "bash: /home/xiaolin/ops-scripts/bin/disk-check.sh: No such file or directory",
"exit_code": 127
}
「嗯?它没报错,还说 completed?」
「因为 bash 自己启动成功了——找不到脚本的是 bash,不是 Python。」
「Python 这边什么异常都没抛,只是拿回了一个 127 的退出码。」
「那我上一版写的 except FileNotFoundError 呢?」
「永远不会命中。」

六、健康检查和版本
from datetime import datetime
@app.get("/health")
def health():
return {
"status": "ok",
"timestamp": datetime.now().isoformat(),
"version": app.version
}
返回示例:
{
"status": "ok",
"timestamp": "2026-10-04T20:15:30.123456",
"version": "0.2.0"
}
「监控系统会定期调这个接口。返回 ok 就说明服务活着。」
为什么健康检查里要有「版本」?这不是装饰。真实场景里最常见的困惑是:「我明明发新版了,怎么行为没变?」——十有八九是流量打到了旧实例,或者容器没重启成功。这时候一个带上版本号的 /health,五秒钟就能定位问题。
七、容器化准备
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
-i https://pypi.tuna.tsinghua.edu.cn/simple
COPY main.py .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
让「本机开发环境」和「容器运行环境」的 Python 版本一致,能避免掉一整类「本地好的、容器里报错」的问题。另外注意 CMD 里没有 --reload——这是对的,生产环境要关掉。
pip freeze > requirements.txt
docker build -t ops-api:v0.2.0 .
docker run -d --name ops-api -p 8000:8000 ops-api:v0.2.0
「但注意架构。你本地是 ARM64,私有云是 x86_64。」
「第 9 话会优化这个 Dockerfile——多阶段构建、非 root 用户。」
八、常见问题
Q1:为什么不用 Flask 或者 python -m http.server?
python -m http.server:只能发静态文件,做不了 API;
- Flask:也很好,但要自己装插件才有文档,类型校验也要额外引入;
- FastAPI:自带
/docs、自带类型校验、原生异步。
不是 Flask 不好,是这个场景下 FastAPI 更顺手。
Q2:--reload 为什么生产环境不能用?
两个原因:
- 它会额外起一个 reloader 进程监视文件变化——多占资源,还可能干扰信号处理;
- 更严重的是它需要文件系统监听——容器里如果挂载了某些文件系统,监听会失效或报错。
Q3:0.0.0.0 和 127.0.0.1 该绑哪个?
| 绑定地址 |
谁能访问 |
127.0.0.1:8000 |
只有本机 |
0.0.0.0:8000 |
所有网卡 |
一旦服务要对外或进容器,必须是 0.0.0.0——容器内部的 127.0.0.1 是「容器自己」,外面的端口映射根本转发不进去。
Q4:脚本卡住了,API 会怎样?
没有 timeout 的话,API 就跟着卡住。一个慢请求会占住一个工作进程,几个慢请求就能把整个服务堵死。所以 timeout=30 不是可选的美化项,是必需品。超时要抛 504——它明确告诉调用方「上游太慢」,而不是含糊的 500。
Q5:这个 API 会不会有安全问题?
会,而且很现实。想一想:这个接口能执行服务器上的脚本。现在这个版本没有任何鉴权——任何能访问 8000 端口的人,都能触发磁盘检查。虽然当前脚本是只读的,但只要将来有人加一个「清日志」「重启服务」的接口,这就是一个远程命令执行漏洞。
后面要补的:① 鉴权(API Key / Token);② 命令白名单(只允许执行固定几个脚本,绝不拼接用户输入);③ 网络层限制。
九、下话
第 9 话:容器化 API——多阶段构建、非 root。
老周看了看小林的 Dockerfile,皱了皱眉。
「能跑。但这个箱子太重了,而且太危险了。」
「危险?」
「默认情况下,容器里的进程是 root。下话见。」
这一话写的其实不是「跑题去学 Python」,而是给前面搭好的脚本库装一个「对外遥控器」。没有它,脚本永远只能靠人去点。而 /health 这个接口看起来最简单,却是监控接入、容器编排探针、负载均衡摘除的共同入口。先把这个写好,后面 CI/CD 整条链路才串得起来。