强大的自动化工作流工具,你值得拥有

背景

最近在家忙着开发telegram机器人项目,每次部署到nas上时,都会有一个比较重复的步骤:

  1. 推送到远程触发ci构建docker镜像,等待镜像构建完成
  2. 打开终端ssh进入nas,然后cd到机器人项目的目录
  3. 运行docker-compose下拉命令和重新构建命令

重复次数多了之后,我就想能不能找个工具代替(没办法我实在是懒),而且我在外面的时候就没办法进入nas所在的局域网重启了。

我需要一个按钮,按下去,程序就帮我执行完后面的2步。

其实这最后2步也可以挪到ci中完成,但我就是想找一个独立的工具,不去依赖ci。

于是我就想,应该已经有这样的工具了,配置好任务的触发器,触发之后就自动执行写好的脚本。

带着这个想法,我就立马去搜索关键词,最终搜到了n8n,基于node写的工作流自动化工具。

刚好第二天无意中看到群里有人转了奇舞精选的一篇【2024年最受欢迎的前段项目】n8n赫然在列。

好家伙,那我不得高低学一下。

搭建自动化工作流

n8n有两种方式使用,一种是直接使用官方网站,一种是在自己服务器上搭建n8n,我毫不犹疑就选了后者。毕竟这样可控制的程度更高一些

我们可以直接使用npm启动n8n,

只需要运行:

npx n8n

然后注册一个账号,进入主页就是这样子:

为了方便搭建和运行,我决定部署到在nas上,用docker容器运行n8n。

接下来整理一波n8n需要实现的工作流:

  1. 外部某个事件触发
  2. 进入到某个文件夹,执行重启脚本
  3. 发送邮件提示已经执行完成

开整,我们先点击右上角的【Create Workflow】新建一个工作流,然后点击中间的添加第一步,右侧会弹出可选的触发器:

app event的列表我看了一遍,全是国外的应用,看来国外开源项目的唯一缺点就是在国内水土不服。

那就只能用webhook call了,用它来新建一个触发节点,然后我们可以看到:

不得不说界面真的挺简洁明了。大概的用法就是,只要请求提供的webhook url,n8n就会从这个webhook节点开始执行任务。

接下来就是执行脚本,继续添加下一步,选择【Execute Command】

然后cd到某个目录,执行restart.sh脚本

restart.sh脚本内容如下:

# 拉取最新的镜像
docker-compose pull
# 重新创建并启动
docker-compose up -d --force-recreate

到这里我突然想到一个问题,就是我搭建的n8n是在nas里基于容器运行的,在容器内会存在命令空间隔离问题,执行docker命令肯定会提示command not found。

于是就想着如何在容器内,也能够控制宿主的docker去拉取某个镜像和重启,但是困难重重:

首先挂载宿主机的docker.sock文件,然后通过Node.js的dockerode库调用docker api去重启容器

由于我是用docker-compose.yml去管理容器,所以换成dockerode-compose去操作,本来以为这样就没问题了,然而打脸来的太快,不知道为啥报错了,过程折腾了好久,最终还是放弃了调用docker api的方案。

然后我回顾一下,如果容器直接通过ssh登录到宿主机上,也可以直接执行脚本了。

只能先这样了,如果大家有什么更好的方法,也可以在评论区发出来我学习一波。

首先问一下ai,在容器里如何获取宿主机的ip:

然后在容器内测试一下这个命令,确实可行!

再整理一下最新的工作流方案:

  1. 外部某个事件触发
  2. 执行js代码,通过child_process执行命令获取宿主机的ip地址
  3. 通过ssh2进入宿主机,运行命令重启容器。

好的,接下来创建一个Code节点:

获取宿主机ip的代码如下:

const { execSync } = require('child_process');

// 获取宿主机ip的命令
const findIpCommand = "ip route | awk '/default/ { print $3 }'";

const ip = execSync(findIpCommand, { encoding: 'utf8' }).trim();
console.log('ip', ip);

if (!ip) {
  throw new Error('宿主ip为空');
}
return {
  ip,
};

运行前,需要配置n8n支持使用内部和外部的模块,具体可以看这里:modules-in-code-node

由于n8n我是用docker-compose去部署的,所以需要在evnironment字段加上以下的值:

environment:
    - NODE_FUNCTION_ALLOW_EXTERNAL=*
    - NODE_FUNCTION_ALLOW_BUILTIN=*

如果引入了外部的模块,比如后面会用到的ssh2,需要先用npm安装好,因为我是用的Docker,所以我这边是选择新建一个Dockerfile,引入n8n的镜像,然后运行安装依赖的代码:

FROM docker.n8n.io/n8nio/n8n:latest
RUN npm config set registry https://registry.npmmirror.com
RUN npm install -g ssh2

所以我目前的docker-compose.yml内容如下:

version: '3.8'
services:
  n8n:
    container_name: n8n
    build: .
    restart: 'unless-stopped'
    ports:
      - '5678:5678'
    volumes:
      - ./data:/root/.n8n
    environment:
      # 允许http的方式登录
      - N8N_SECURE_COOKIE=false
      # 支持所有内部和外部的包
      - NODE_FUNCTION_ALLOW_EXTERNAL=*
      - NODE_FUNCTION_ALLOW_BUILTIN=*

重启n8n之后,测试运行以下这个节点,输出也没问题:

这个节点会自动把return的数据传给下一个节点,接下来就是拿到传入的ip然后通过ssh2来连接宿主机。

我们继续创建新的Core节点,代码如下:

const { Client } = require('ssh2');

const conn = new Client();
// n8n内部提供获取传入节点数据的方式,具体可以看
// https://docs.n8n.io/code/builtin/current-node-input/
// 这里拿到上一个节点写入的ip属性
const host = $json.ip;
const port = 22;
const username = 'root';
const password = 'xxxx';
// docker镜像源的用户名
const dockerUser = 'xxxxx';
// docker镜像源的密码
const dockerPassword = 'xxxx';

const loginCommand = `docker login -u ${dockerUser} -p ${dockerPassword} xxxx`;
const cdCommand = 'cd /volume1/docker/xxx';
const pullCommand = 'docker-compose pull';
const recreateCommand = 'docker-compose up -d --force-recreate';
const allCommand = `${loginCommand} && ${cdCommand} && ${pullCommand} && ${recreateCommand}`;

return new Promise((resolve) => {
  conn
  .on('ready', () => {
    console.log('Client :: ready');
    conn.shell((err, stream) => {
      stream
        .on('close', (code, signal) => {
          console.log('执行完成');
          conn.end();
        })
        .on('data', (data) => {
          console.log(data.toString());
        })
        .stderr.on('data', (data) => {
          console.log('STDERR: ' + data);
        });

      //执行命令 
      stream.write(allCommand + '\n');
      // 退出
      stream.write('exit\n');
    });
  }).on('close', () => {
    resolve([
      {
        msg: 'success',
        code: 0,
      }
    ]);
  })
  .connect({
    host,
    port,
    username,
    password,
  });
})

测试一下试试:

也是顺利执行完成,在nas管理页面也看到镜像拉取到最新的版本,并且自动重新启动了。

最后我加上发送邮件的节点,运行到这个节点就给我的邮箱发送邮件通知,这个节点搭建也比较简单,我这边就不展示了。

目前我的重启容器工作流如下:

触发的方式就是直接请求从Webhook节点里拿到的生产环境url:

当ci的docker镜像构建完之后,就可以通过curl <webhook_url>的方式触发nas拉取镜像并重启容器。

其实还需要实现一个步骤就是内网穿透,需要把这个webhook_url暴露给外网环境,否则只能在内网里触发webhook。目前碍于篇幅的原因,我就不在这篇文章里展开啦。

总结

经过以上的步骤,已经大致摸清了n8n这个自动化工具的使用流程。它的用户界面让我印象特别深刻,特别的简洁明了,让人一目了然整个工作流的执行顺序和操作流程,让一个没使用过的人都能轻易上手创建工作流。

不仅如此,这个工具内置了许多的触发器和节点类型,功能已经相当完善,可以满足大多数人的使用需求。在官方的示例中,甚至可以跟AI结合,接收excel表格并按要求处理后导出,还有更多强大的功能等待着我们去发现和发掘。

在使用过程中,我也发现了一些问题:

  1. 代码节点运行js时,只能运行cjs规范的代码,目前暂不支持esm规范。
  2. 能作为触发器的app基本都是海外,国内的还未适配。

但是这并不影响我对这款工具的喜爱。

希望后面能够引入AI助手,我们只需要动动嘴,AI就会帮我们创建好工作流。

这样不就更加接近漫威世界里钢铁侠的贾维斯了?