Appearance
实战:运维工具
这一篇把前面八篇的内容串成一个完整的小工具:CLI 入口(commander)→ 核心业务函数 → 配置文件读取 → 同时支持 CLI、定时任务和 Web 面板三种运行方式。核心原则是:业务逻辑不依赖运行方式,CLI、定时任务和 HTTP 只是同一套函数的三个外壳。
项目结构
text
ops-tool/
package.json
bin/
ops-tool.js CLI 入口——只处理参数和输出
src/
check-service.js 核心检查逻辑——不写 console.log
config.js 配置读取和校验
server.js Web 面板入口bin/ 是约定的 CLI 可执行文件目录,src/ 放核心函数。CLI 和 Web 面板各有一个入口,都引用 src/ 的函数。
初始化项目
bash
mkdir ops-tool && cd ops-tool
npm init -y
npm install commanderpackage.json:
json
{
"name": "ops-tool",
"version": "0.1.0",
"type": "module",
"bin": {
"ops-tool": "./bin/ops-tool.js"
},
"scripts": {
"start": "node bin/ops-tool.js",
"server": "node server.js"
},
"dependencies": {
"commander": "^12.0.0"
}
}bin 字段告诉 npm:装这个包时,把 bin/ops-tool.js 链到 /usr/local/bin/ops-tool,终端里就能直接敲 ops-tool 了。
核心检查函数
src/check-service.js:
js
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
// promisify 把 callback 风格函数转成返回 Promise 的函数
export async function checkService(service) {
try {
const { stdout } = await execFileAsync(
"systemctl",
["is-active", service],
{ timeout: 3000 }
);
return {
service,
active: stdout.trim() === "active",
checkedAt: new Date().toISOString()
};
} catch (err) {
// 不向上抛异常——巡检场景希望一个服务失败不影响其他
return {
service,
active: false,
error: err.message,
checkedAt: new Date().toISOString()
};
}
}
export async function checkAll(services) {
// 串行检查——输出顺序稳定,且不会对 systemctl 产生并发压力
const results = [];
for (const service of services) {
results.push(await checkService(service));
}
return results;
}promisify 适用于 Node.js 内置的 callback 风格 API(fs.*、child_process.* 等)。它把最后一个参数是 (err, result) => {} 回调的函数转成 Promise——前提是那个函数遵循这种约定。
配置读取
src/config.js:
js
import { readFile } from "node:fs/promises";
export async function loadConfig(file) {
try {
const text = await readFile(file, "utf8");
const config = JSON.parse(text);
if (!Array.isArray(config.services)) {
throw new Error("config.services must be an array");
}
if (config.services.some((s) => typeof s !== "string" || s.length === 0)) {
throw new Error("each service must be a non-empty string");
}
return config;
} catch (err) {
// 补文件名上下文——排查多个配置文件时快速定位
throw new Error(`failed to load config ${file}: ${err.message}`);
}
}配置文件 ops-tool.json:
json
{
"services": ["nginx", "sshd", "crond"]
}CLI 入口
bin/ops-tool.js:
js
#!/usr/bin/env node
// shebang 行——让 Linux 直接用 node 执行这个文件
import { Command } from "commander";
import { checkService, checkAll } from "../src/check-service.js";
import { loadConfig } from "../src/config.js";
const program = new Command();
program
.name("ops-tool")
.description("ops helper")
.version("0.1.0");
// check-service 子命令:手动指定要查哪些服务
program
.command("check-service")
.argument("<services...>") // 必填参数,可以传多个
.option("--json", "output json") // 可选 flag
.action(async (services, options) => {
const results = [];
for (const service of services) {
results.push(await checkService(service));
}
if (options.json) {
console.log(JSON.stringify(results, null, 2));
} else {
for (const r of results) {
const status = r.active ? "active" : (r.error || "inactive");
console.log(`${r.service}\t${status}`);
}
}
});
// check 子命令:从配置文件读服务列表
program
.command("check")
.option("-c, --config <file>", "config file", "ops-tool.json")
.option("--json", "output json")
.action(async (options) => {
const config = await loadConfig(options.config);
const results = await checkAll(config.services);
if (options.json) {
console.log(JSON.stringify(results, null, 2));
} else {
for (const r of results) {
const status = r.active ? "active" : (r.error || "inactive");
console.log(`${r.service}\t${status}`);
}
}
});
// parseAsync() 是 commander 9.x+ 的异步解析——支持 async action
program.parseAsync().catch((err) => {
console.error(err.message);
process.exit(1);
});使用:
bash
# 本地开发
node bin/ops-tool.js check-service nginx sshd
node bin/ops-tool.js check --config ops-tool.json --json
# 全局安装后
chmod +x bin/ops-tool.js
npm link
ops-tool check-service nginx sshd --jsonnpm link 在当前项目建全局软链接,开发机上可以像已安装的命令一样调 ops-tool,改代码立刻生效,不需要每次都 npm install -g。
Web 面板
server.js:
js
import express from "express";
import { checkAll } from "./src/check-service.js";
import { loadConfig } from "./src/config.js";
const app = express();
const port = Number(process.env.PORT || 3000);
// 封装异步路由,省掉重复 try/catch + next(err)
function asyncHandler(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
app.get("/healthz", (req, res) => {
res.json({ ok: true });
});
app.get("/api/services", asyncHandler(async (req, res) => {
const config = await loadConfig("ops-tool.json");
const results = await checkAll(config.services);
res.json(results);
}));
// 简单 HTML 面板——只读展示
app.get("/", asyncHandler(async (req, res) => {
const config = await loadConfig("ops-tool.json");
const results = await checkAll(config.services);
const rows = results.map((r) => {
const status = r.active
? "active"
: (r.error ? `error: ${r.error}` : "inactive");
return `<tr><td>${r.service}</td><td>${status}</td></tr>`;
}).join("");
res.type("html").send(`<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>Ops Panel</title>
<style>body{font-family:system-ui;padding:2em} table{border-collapse:collapse}
td{padding:8px 16px;border:1px solid #ccc}</style></head>
<body>
<h1>Service Status</h1><table><tbody>${rows}</tbody></table>
<p><small>checked at ${new Date().toISOString()}</small></p>
</body></html>`);
}));
// 统一错误处理
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: "internal server error" });
});
const server = app.listen(port, "0.0.0.0", () => {
console.log(`ops panel listening on port ${port}`);
});
// 优雅退出
process.on("SIGTERM", () => {
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10_000).unref();
});这个面板只做只读展示。加了操作按钮(重启服务、执行命令)时,要同时加鉴权、审计和操作确认——内部平台出事通常不是技术复杂度,是操作入口太随意。
定时任务
crontab 方式:
bash
*/5 * * * * /usr/local/bin/ops-tool check --config /etc/ops-tool/config.json --json >>/var/log/ops-tool.log 2>&1systemd timer 方式(推荐——日志、状态、上次执行时间都更好查):
ini
# /etc/systemd/system/ops-tool.service
[Unit]
Description=Run ops-tool check
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ops-tool check --config /etc/ops-tool/config.json --jsonini
# /etc/systemd/system/ops-tool.timer
[Unit]
Description=Run ops-tool every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Unit=ops-tool.service
[Install]
WantedBy=timers.targetbash
systemctl daemon-reload
systemctl enable --now ops-tool.timer
systemctl list-timers ops-tool.timer
systemctl status ops-tool.service发布
内网不能直接 npm install 时,打包分发:
bash
# 在构建机或开发机上准备好依赖
npm ci --omit=dev
# 整体打包项目目录
tar czf ops-tool.tar.gz \
package.json package-lock.json \
bin/ src/ node_modules/目标服务器:
bash
tar xzf ops-tool.tar.gz -C /opt/ops-tool
cd /opt/ops-tool
node bin/ops-tool.js check --config /etc/ops-tool/config.json --json也可以用 npm pack 生成 .tgz 包:
bash
npm pack # 生成 ops-tool-0.1.0.tgz
npm install -g ./ops-tool-0.1.0.tgz # 全局安装
ops-tool check-service nginx设计要点回顾
这个小工具的几个设计选择:
核心函数不写 console.log、不读 process.argv——
checkService返回数据,输出格式由 CLI 层决定。同一天写 CLI 和 API 时不用写两遍逻辑捕获异常返回结果对象——巡检场景的预期是"一个服务失败不影响其他";如果抛异常,
checkAll里一个服务挂了后面全停CLI 输出格式稳定——JSON 和 TSV 都不在中间加装饰文字。有人接了管道处理这些输出后,格式就成了隐式接口
async/await 贯穿始终——从文件读取到命令执行到 HTTP 响应,统一的异步模型比 callback 嵌套好排错
优雅退出——Web 面板处理 SIGTERM,容器和 systemd 能干净退出,不会残留半截连接和临时文件
Node.js 在运维里比较合适的位置:读配置、调命令、调 API、出报告、做轻量 HTTP 服务。当工具需要大量 CPU 计算、复杂的后台调度、强类型安全的业务逻辑时,换 Go/Rust/Java 比用 Node.js 硬撑更省维护成本。