Appearance
系统交互
Node.js 做运维脚本时经常要读取环境变量、调用系统命令并处理退出码,后续再交给 systemd 或定时任务运行。
一、环境变量
配置从环境变量进入进程。
js
const port = Number(process.env.PORT || 3000);
const logLevel = process.env.LOG_LEVEL || "info";
console.log({ port, logLevel });必填变量单独封装。
js
export function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required`); // 启动阶段缺配置直接失败
}
return value;
}布尔值按明确值解析,不直接拿字符串真假来判断。
js
function readBooleanEnv(name, defaultValue = false) {
const value = process.env[name];
if (value === undefined) {
return defaultValue;
}
return value === "true"; // 只有明确写 true 才当成 true
}
const dryRun = readBooleanEnv("DRY_RUN", false);
console.log({ dryRun });"false" 是非空字符串,直接放进 if (process.env.DRY_RUN) 会被当成真。
二、execFile
execFile 适合执行明确的命令和参数,不经过 shell。
js
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const { stdout } = await execFileAsync("node", ["-v"]);
console.log(stdout.trim());检查 systemd 服务状态:
js
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
async function isActive(service) {
try {
const { stdout } = await execFileAsync("systemctl", ["is-active", service]);
return stdout.trim() === "active"; // systemctl 输出 active/inactive/failed
} catch {
return false; // is-active 非 active 时退出码非 0,这里转成 false
}
}
console.log(await isActive("nginx"));外部传入的参数不拼进一整条 shell 字符串。用参数数组传,参数就是参数,也少一类命令注入问题。
三、spawn
需要实时看到输出时用 spawn。
js
import { spawn } from "node:child_process";
const child = spawn("ping", ["-c", "4", "127.0.0.1"], {
stdio: "inherit" // 子进程输出直接继承当前终端
});
child.on("exit", (code) => {
process.exit(code ?? 1); // 把子进程退出码传给当前脚本
});spawn 适合执行长时间命令,比如备份、同步、构建。
四、exec
exec 会经过 shell,适合确实需要管道、重定向、通配符的场景。
js
import { exec } from "node:child_process";
import { promisify } from "node:util";
const execAsync = promisify(exec);
const { stdout } = await execAsync("ps aux | grep nginx | head");
console.log(stdout);用户输入不进 exec 字符串。
js
// 反例:service 来自参数时可能注入额外命令
// await execAsync(`systemctl restart ${service}`);替代写法:
js
await execFileAsync("systemctl", ["restart", service]); // 参数作为独立元素传入五、超时
命令执行要考虑卡住。
js
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
await execFileAsync("curl", ["-fsS", "http://127.0.0.1:3000/healthz"], {
timeout: 5000 // 超过 5 秒会终止子进程
});curl 参数含义:
text
-f HTTP 4xx/5xx 时返回失败退出码
-s 静默模式,不输出进度条
-S 静默时仍显示错误信息六、标准输出格式
机器读的结果适合输出 JSON。内部工具早晚会被别的脚本接上,结构化数据能少写很多 awk/sed 文本拆分。
js
const result = {
service: "nginx",
active: true,
checkedAt: new Date().toISOString()
};
console.log(JSON.stringify(result)); // 一行 JSON,方便日志采集和管道处理给人看的表格可以用 TSV。
js
const rows = [
["service", "status"],
["nginx", "active"],
["sshd", "active"]
];
for (const row of rows) {
console.log(row.join("\t")); // tab 分隔,终端和 awk 都好处理
}七、systemd 服务
长期运行的 Node.js 服务可以交给 systemd。
ini
[Unit]
Description=Node Ops Service
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/node-ops
Environment=PORT=3000
ExecStart=/usr/local/node/bin/node server.js
Restart=always
RestartSec=5
User=nodeapp
Group=nodeapp
[Install]
WantedBy=multi-user.target字段说明:
text
WorkingDirectory 进程启动后的工作目录,影响 process.cwd()
Environment 注入环境变量
ExecStart 启动命令,写绝对路径少踩 PATH 的坑
Restart 异常退出后自动拉起
User/Group 用低权限用户运行,避免默认 root管理命令:
bash
systemctl daemon-reload
systemctl start node-ops
systemctl enable node-ops
journalctl -u node-ops -f八、定时任务
简单脚本可以由 crontab 调度。
bash
*/5 * * * * /usr/local/bin/node /opt/check-service/index.js >>/var/log/check-service.log 2>&1这里把 stdout 和 stderr 都追加到日志文件。正式环境我更偏向 systemd timer 或 Kubernetes CronJob,失败状态、重试和日志入口都好查。crontab 不是不能用,只是看上次有没有跑成会麻烦一点。
systemd oneshot:
ini
[Unit]
Description=Run service check
[Service]
Type=oneshot
WorkingDirectory=/opt/check-service
ExecStart=/usr/local/bin/node index.jstimer:
ini
[Unit]
Description=Run service check every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Unit=check-service.service
[Install]
WantedBy=timers.target九、一个巡检脚本
配置文件:
json
{
"services": ["nginx", "sshd", "crond"]
}脚本:
js
import { readFile } from "node:fs/promises";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
async function isActive(service) {
try {
const { stdout } = await execFileAsync("systemctl", ["is-active", service], {
timeout: 3000 // 单个服务检查最多等 3 秒
});
return stdout.trim() === "active";
} catch {
return false;
}
}
async function main() {
const text = await readFile("services.json", "utf8");
const config = JSON.parse(text);
const results = [];
for (const service of config.services) {
results.push({
service,
active: await isActive(service)
});
}
console.log(JSON.stringify(results, null, 2));
}
main().catch((err) => {
console.error(err.message);
process.exit(1);
});