Node-RED是在2013年IBM开源的应用于物联网的流编排引擎,但是也不仅限于物联网,这篇文章选取容器化应用持续交付的一个示例来进行说明Node-RED的使用方式。
持续集成执行完毕之后,容器化的应用已经存储在Harbor镜像私库中,持续部署的时候需要拉取镜像、启动容器,这本来是非常简单的事情,但是需要考虑到主要是各种异常的分支的对应方法,比如:
- 拉取镜像:镜像私库连接失败和成功的处理
- 启动容器:确认是否存在容器,如果不存在使用docker run生成,如果存在,首先停止此容器,然后删除,停止失败和删除失败都需要进行异常处理,停止并删除成功之后启动容器
- 结果确认: 确认容器是否启动,根据结果输出成功或失败信息
普通逻辑下,这个示例流程可能是这样的:
当然,在实际的使用中有很多种方法会将上述这个流程变得非常简化,比如简化这个过程,如果不需要对中间结果进行确认的话,可以将所有的条件全部去除,直接上来先强制停止->强制删除之前的容器和镜像->强制拉取->强制启动,或者使用kubernetes提供的滚动升级的机制都会非常简单,但是这里主要是用于检验持续部署引擎中流水线可视化编排的能力,是否能够很轻松和简单地实现这个流程是需要确认的。
Jenkins vs Node-RED实际上这并不是可以进行比较的两个东西,但是仅限于本文中示例说明的这个场景,似乎还真有可对比的地方。Jenkins提供可以DSL语言支持的流水线,代码及流水线,如果把流水线也看作“流”的表现形式和结果,在这个角度上是有类似的地方的,在本文指定的场景中实际上只有一个流编排引擎的能力:条件分支。
Jenkins的编排能力
Jenkins的DSL里面支持基于Stage的条件分支和并行的编排需求,但是可视化编排引擎能力稍差,比如BlueOcean插件可以在很简单地程度上进行可视化编排,但是目前还达不到易用的程度,但是毫无疑问,条件分支的支持在Jenkins中没有任何问题,比如下图为使用Jenkins中的when语句进行条件分支的示例流水线的执行情况:
注:详细可参看https://blog.csdn.net/liumiaocn/article/details/92817179
更多Jenkins相关的使用技巧:点击此处
-
可以根据需要进行分支处理
详细可参看:https://liumiaocn.blog.csdn.net/article/details/104745053
-
也可以使用多路输入进行统一处理
详细可参看:https://liumiaocn.blog.csdn.net/article/details/104740766
更多Node-RED相关的使用技巧:点击此处
使用Node-RED进行编排 与Docker的结合可以使用Node-RED的插件或者直接拷贝docker客户端至Node-RED容器中即可对容器进行操作,这里使用后一种方式,详细的方法可参看下文:
- https://blog.csdn.net/liumiaocn/article/details/104965769
- Node-RED 以容器方式启动Node-RED服务,启动命令如下所示:
启动命令:docker run -it -p 1880:1880 -v $PWD/data:/data -e DOCKER_HOST=tcp://192.168.31.242:2375 -e TZ=Asia/Shanghai --name nodered -d nodered/node-red:1.0.4
注:DOCKER_HOST环境变量的设定请根据自己机器的配置进行设定
示例实现
参照前面一些Node-RED系列的文章可以非常容易的实现如下的可视化编排, 拉取镜像也直接拉取一个nginx的版本,这里已1.13为例进行拉取和启动:
这里为了简单演示,直接在命令行中硬编码的方式实现容器名称和映射端口号,启动容器的设定如下所示:
如下JSON格式的flow,导入之后即可即可使用。
[{"id":"d9908e57.2efd4","type":"inject","z":"c205d2ff.b4322","name":"触发器","topic":"","payload":"","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":150,"y":60,"wires":[["4af508a3.75bed8"]]},{"id":"4af508a3.75bed8","type":"exec","z":"c205d2ff.b4322","command":"docker pull nginx:1.13","addpay":true,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"拉取镜像","x":340,"y":60,"wires":[[],[],["8e27643a.f72f68"]]},{"id":"8e27643a.f72f68","type":"switch","z":"c205d2ff.b4322","name":"拉取镜像成功?","property":"payload.code","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"neq","v":"0","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":540,"y":60,"wires":[["fbc7e47e.300d68","3f88a226.a0e27e"],["d6bf6546.e90028"]]},{"id":"fbc7e47e.300d68","type":"debug","z":"c205d2ff.b4322","name":"拉取镜像成功结果显示","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":760,"y":20,"wires":[]},{"id":"d6bf6546.e90028","type":"debug","z":"c205d2ff.b4322","name":"拉取镜像失败结果显示","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":760,"y":100,"wires":[]},{"id":"3f88a226.a0e27e","type":"exec","z":"c205d2ff.b4322","command":"docker ps -a |grep nginx","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"容器存在性确认","x":160,"y":220,"wires":[[],[],["f04db033.03b9d"]]},{"id":"f04db033.03b9d","type":"switch","z":"c205d2ff.b4322","name":"容器已经存在?","property":"payload.code","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"neq","v":"0","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":360,"y":220,"wires":[["41de4ca4.2bc304"],["370847f2.635c28","de314625.b32708"]]},{"id":"370847f2.635c28","type":"exec","z":"c205d2ff.b4322","command":"docker run -p 8088:80 --name=nginx -d nginx:1.13","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"启动容器","x":600,"y":200,"wires":[[],[],["fc7c7676.7747b8"]]},{"id":"fc7c7676.7747b8","type":"switch","z":"c205d2ff.b4322","name":"结果判断","property":"payload.code","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"neq","v":"0","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":800,"y":200,"wires":[["e7f38a59.27c928"],["594e0886.2aa498"]]},{"id":"e7f38a59.27c928","type":"debug","z":"c205d2ff.b4322","name":"显示容器正常启动信息","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1000,"y":180,"wires":[]},{"id":"594e0886.2aa498","type":"debug","z":"c205d2ff.b4322","name":"显示容器启动失败信息","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1000,"y":220,"wires":[]},{"id":"de314625.b32708","type":"debug","z":"c205d2ff.b4322","name":"容器不存在","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":610,"y":280,"wires":[]},{"id":"f4e42833.ee4448","type":"exec","z":"c205d2ff.b4322","command":"docker stop nginx;","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"停止容器","x":160,"y":540,"wires":[[],[],["3e4a962a.01903a"]]},{"id":"3e4a962a.01903a","type":"switch","z":"c205d2ff.b4322","name":"容器停止成功?","property":"payload.code","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"neq","v":"0","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":340,"y":540,"wires":[["e5baa05e.433b9","84ae2749.94ee28"],["f5d2345c.4f0d98"]]},{"id":"e5baa05e.433b9","type":"debug","z":"c205d2ff.b4322","name":"显示容器停止成功信息","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":580,"y":500,"wires":[]},{"id":"f5d2345c.4f0d98","type":"debug","z":"c205d2ff.b4322","name":"显示容器停止失败信息","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":580,"y":580,"wires":[]},{"id":"41de4ca4.2bc304","type":"exec","z":"c205d2ff.b4322","command":"docker ps |grep nginx","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"容器停止状态确认","x":170,"y":380,"wires":[[],[],["827bee6d.6d1d1"]]},{"id":"827bee6d.6d1d1","type":"switch","z":"c205d2ff.b4322","name":"容器已停止?","property":"payload.code","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"neq","v":"0","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":360,"y":380,"wires":[["db3dca72.6a5d98","f4e42833.ee4448"],["370847f2.635c28","eb12796d.538858"]]},{"id":"db3dca72.6a5d98","type":"debug","z":"c205d2ff.b4322","name":"存在启动的容器需要停止","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":630,"y":340,"wires":[]},{"id":"eb12796d.538858","type":"debug","z":"c205d2ff.b4322","name":"没有启动的容器需要停止","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":630,"y":420,"wires":[]},{"id":"84ae2749.94ee28","type":"exec","z":"c205d2ff.b4322","command":"docker rm nginx;","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"删除容器","x":140,"y":720,"wires":[[],[],["8515b044.05477"]]},{"id":"8515b044.05477","type":"switch","z":"c205d2ff.b4322","name":"容器删除成功?","property":"payload.code","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"neq","v":"0","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":320,"y":720,"wires":[["61927cc0.f243e4","370847f2.635c28"],["9b26c2ff.a7d18"]]},{"id":"61927cc0.f243e4","type":"debug","z":"c205d2ff.b4322","name":"显示容器删除成功信息","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":560,"y":680,"wires":[]},{"id":"9b26c2ff.a7d18","type":"debug","z":"c205d2ff.b4322","name":"显示容器删除失败信息","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":560,"y":760,"wires":[]}]结果确认
- 执行前确认
liumiaocn:~ liumiao$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e290de2ccd69 nodered/node-red:1.0.4 "npm start -- --user…" 11 minutes ago Up 11 minutes (healthy) 0.0.0.0:1880->1880/tcp nodered liumiaocn:~ liumiao$
-
第一次执行
说明:这里出现了一个很显眼的错误信息,这是因为使用dokcer ps |grep nginx确认是否有nginx为名称的容器在运行中的命令执行的时候没有发现这样的容器所产生的,所以这并不是一个问题,exec组件会根据返回值进行状态显示而已,结合docker ps命令在宿主机器上可以看到名为nginx的容器已经启动起来了
liumiaocn:~ liumiao$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4728e76addcf nginx:1.13 "nginx -g 'daemon of…" 11 seconds ago Up 10 seconds 0.0.0.0:8088->80/tcp nginx e290de2ccd69 nodered/node-red:1.0.4 "npm start -- --user…" 44 minutes ago Up 44 minutes (healthy) 0.0.0.0:1880->1880/tcp nodered liumiaocn:~ liumiao$
-
第二次执行
结合docker ps命令在宿主机器上可以看到nginx容器是启动状态,但是Container ID已经发生了变化,说明原有的的nginx容器已经被停止和删除,然后重新启动的nginx容器。
liumiaocn:~ liumiao$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 122c01aa206d nginx:1.13 "nginx -g 'daemon of…" 22 seconds ago Up 20 seconds 0.0.0.0:8088->80/tcp nginx e290de2ccd69 nodered/node-red:1.0.4 "npm start -- --user…" 46 minutes ago Up 46 minutes (healthy) 0.0.0.0:1880->1880/tcp nodered liumiaocn:~ liumiao$总结
这篇文章使用了一个简单地示例对Node-RED的编排进行了说明,当然在使用的过程中还有很多需要注意的,比如参数的传入方法,比如异常的处理机制,但是相信这样一个简单的示例就能够说明流编排引擎在持续集成和部署中的作用,鉴于Jenkins目前的可视化编排功能较弱,所以很多平台都是基于此进行强化,但是后续的CDF时候能够提供一种标准的DSL的编排标准或者接入现在主流的工具还需要进一步地观察,这些都是我们在持续集成和部署实践中需要注意的。