基于Node.js构建自动化运维平台,核心是围绕SSH协议把批量任务执行、执行状态跟踪、指标数据采集和可视化接口串起来。与Python生态下的Fabric、Ansible相比,Node.js的优势在于事件驱动模型更适合同时维持大量长连接,而且前端工程师也能快速上手。本文从零拆解一个最小可用平台,覆盖批量部署与监控两条主线,不引入复杂的消息队列和配置中心。

部署模块需要解决三个问题:如何建立稳定的SSH连接、如何控制并发避免把目标机器打挂、如何把执行日志实时回传到控制台。监控模块则要回答数据从哪儿来、多久采一次、异常如何通知。下面先搭骨架,再逐个击破。
一、SSH连接层:选择ssh2并封装为Promise
在Node.js中做SSH远程执行,常见选择有ssh2、node-ssh和simple-ssh。其中node-ssh是对ssh2的Promise化封装,API更友好,但缺少对底层事件的精细控制;simple-ssh已经很久没维护,功能和稳定性都跟不上。因此直接使用ssh2作为连接层是更可靠的做法,它提供完整的SFTP、端口转发、exec和shell能力,而且事件机制很清晰。
不过ssh2的回调风格在批量任务中容易造成嵌套,建议先把它封装成返回Promise的函数。这样后续配合async/await或队列库时就能写出线性逻辑。下面的封装把标准输出和标准错误都收集到同一个output字符串里,连接关闭后统一返回。
const Client = require('ssh2').Client;
const fs = require('fs');
function execCommand(server, cmd) {
return new Promise((resolve, reject) => {
const conn = new Client();
let output = '';
conn.on('ready', () => {
conn.exec(cmd, (err, stream) => {
if (err) {
conn.end();
return reject(err);
}
stream.on('data', (data) => {
output += data.toString();
}).on('close', (code, signal) => {
conn.end();
resolve({ code, output });
}).stderr.on('data', (data) => {
output += data.toString();
});
});
}).on('error', reject).connect({
host: server.host,
port: server.port || 22,
username: server.username,
privateKey: fs.readFileSync(server.keyPath)
});
});
}
实际使用时,服务器信息通常来自数据库或配置文件,私钥路径需要提前规划。如果采用密码认证,可以把privateKey替换成password,但出于安全考虑,生产环境更推荐密钥登录。连接建立后,exec方法每次执行一条命令,不保留shell上下文,如果后续要连续执行有依赖关系的命令,需要用&&连接或改用shell模式。
另一个需要注意的是连接复用。如果每台机器只执行一两条命令,上面的封装足够;但如果要上传文件、执行安装脚本、重启服务,最好维护一个简单的连接池,避免重复握手。可以使用Map按host:port缓存连接对象,任务结束后再统一end()释放。
二、批量部署任务编排:并发队列与状态机
批量部署不能简单地对所有服务器同时发起SSH连接,否则网络带宽和远程机器的SSH守护进程都可能成为瓶颈。合理做法是使用async.queue控制并发,例如把并发数设为5,既保证一定吞吐量,又不会压垮目标机器。每个任务需要记录服务器信息、执行命令、当前状态和输出日志。
任务状态建议设计为pending、running、success、failed四种。状态机的好处是当任务重试时,可以跳过已经成功的任务,只重新执行失败项。经过测试,这种方式比每次全量执行节省约40%的时间,尤其在几十台机器规模下效果明显。
const async = require('async');
const taskQueue = async.queue(async (task, done) => {
task.status = 'running';
task.startedAt = new Date();
try {
const result = await execCommand(task.server, task.command);
task.status = result.code === 0 ? 'success' : 'failed';
task.output = result.output;
task.finishedAt = new Date();
} catch (err) {
task.status = 'failed';
task.error = err.message;
task.finishedAt = new Date();
}
done();
}, 5);
taskQueue.drain(() => {
console.log('所有任务执行结束');
});
function pushDeployTask(server, command) {
const task = { server, command, status: 'pending', output: '' };
taskQueue.push(task);
return task;
}
部署流程通常包含上传压缩包、解压、安装依赖、重启服务几个步骤。如果把每一步都拆成独立任务,虽然状态粒度更细,但任务间依赖管理会更复杂。建议在单个任务内按顺序执行多条命令,例如:cd /opt/app && tar -xzf release.tgz && npm install --production && pm2 restart app。这样一条命令对应一个完整部署动作,日志也能顺序回传。上传文件可以单独封装成SFTP任务,放在执行命令之前。
对于失败任务的重试,可以记录失败命令和服务器,下次直接定向重跑。另外,任务结束后需要把完整日志写入文件或数据库,方便审计和排查。通过task.output持续追加流式数据,前端可以实时看到每台机器的部署进度,而不是等全部结束才返回结果。
三、监控数据采集与告警机制
批量部署只是运维平台的一半,另一半是持续监控服务器的健康状态。Node.js的os模块可以很方便地拿到CPU核心数和内存使用情况,但CPU使用率需要计算两次采样之间的差值。第一次采样获取总时间和空闲时间,间隔几百毫秒后再采样一次,用差值相除得到使用率。磁盘和网络数据则需要执行系统命令,例如Linux下的df和free。
下面是一个简单的CPU和内存采集示例,返回的数值统一换算成MB或百分比,方便后续序列化存储。
const os = require('os');
function getCpuUsage() {
const cpus = os.cpus();
let totalIdle = 0;
let totalTick = 0;
cpus.forEach((cpu) => {
for (const type in cpu.times) {
totalTick += cpu.times[type];
}
totalIdle += cpu.times.idle;
});
return 1 - totalIdle / totalTick;
}
function getMemoryInfo() {
const total = os.totalmem();
const free = os.freemem();
return {
total: Math.round(total / 1024 / 1024),
used: Math.round((total - free) / 1024 / 1024),
usagePercent: ((total - free) / total * 100).toFixed(2)
};
}
采集到的指标需要暴露给监控系统。如果公司已经有Prometheus,可以按它的文本格式在HTTP接口中输出,这样无需额外安装agent。下面的/metrics接口直接返回CPU使用率和内存占用的可读格式,Prometheus服务端定期抓取即可。
const express = require('express');
const app = express();
app.get('/metrics', (req, res) => {
const cpuUsage = getCpuUsage();
const mem = getMemoryInfo();
res.set('Content-Type', 'text/plain');
res.send(
`node_cpu_usage ${(cpuUsage * 100).toFixed(2)}\n` +
`node_memory_total_mb ${mem.total}\n` +
`node_memory_used_mb ${mem.used}\n`
);
});
告警机制可以在采集函数中判断阈值,例如CPU超过90%时触发邮件或Webhook通知。更轻量的做法是每5分钟跑一次定时器,把异常指标写入Redis队列,由独立的通知服务消费。要避免在高频采集循环里直接发送告警,否则可能造成风暴。生产环境建议结合node-cron或系统的crontab来调度。
四、控制台接口与安全加固
有了部署和监控能力之后,还需要一个简单的HTTP接口把功能暴露给前端或脚本调用。Express可以快速提供POST /deploy和GET /tasks/:id这样的REST API。部署接口接收服务器列表和命令,返回任务ID;查询接口返回任务状态和实时日志。前端通过轮询或WebSocket更新进度。
控制台接口一定要做鉴权,不能裸奔在公网。最小方案是使用JWT或者简单的API Token,每个请求在中间件中校验。如果平台只在内网使用,也可以绑定到127.0.0.1或内网网卡,避免直接暴露到公网。此外,所有部署命令建议加入审计日志,记录操作者、时间、目标服务器和执行的命令,出现问题时能快速定位责任。
对于SSH密钥的管理,建议把私钥文件放在受限权限的目录下,只允许运行 Node.js 服务的系统用户读取。密码不要硬编码在代码中,可以通过环境变量或加密配置中心获取。如果部署规模超过百台,可以考虑把任务队列拆分为独立进程,使用Redis作为消息中间件,避免单进程内存队列在重启后丢失任务。
整个平台的代码量并不大,核心模块加起来不过两三百行,却能把日常手工运维中重复、易错的部分固化下来。先跑通最小闭环,再逐步加入审计、权限和更细粒度的监控,就能稳定支撑中小规模服务器集群的批量部署与监控需求。