持续部署编排的另类选择:使用Node-RED进行容器化部署

在这里插入图片描述
Node-RED是在2013年IBM开源的应用于物联网的流编排引擎,但是也不仅限于物联网,这篇文章选取容器化应用持续交付的一个示例来进行说明Node-RED的使用方式。

场景说明

持续集成执行完毕之后,容器化的应用已经存储在Harbor镜像私库中,持续部署的时候需要拉取镜像、启动容器,这本来是非常简单的事情,但是需要考虑到主要是各种异常的分支的对应方法,比如:

  • 拉取镜像:镜像私库连接失败和成功的处理
  • 启动容器:确认是否存在容器,如果不存在使用docker run生成,如果存在,首先停止此容器,然后删除,停止失败和删除失败都需要进行异常处理,停止并删除成功之后启动容器
  • 结果确认: 确认容器是否启动,根据结果输出成功或失败信息

持续部署的逻辑流程

普通逻辑下,这个示例流程可能是这样的:

Created with Raphaël 2.2.0开始拉取镜像镜像拉取成功?容器已经存在?容器已经停止?删除容器容器已删除?启动容器容器成功启动?显示容器启动成功信息显示容器启动失败信息显示容器删除失败信息停止运行中的容器容器成功停止?显示容器停止失败信息显示镜像拉取失败信息yesnoyesnoyesnoyesnoyesnoyesno

当然,在实际的使用中有很多种方法会将上述这个流程变得非常简化,比如简化这个过程,如果不需要对中间结果进行确认的话,可以将所有的条件全部去除,直接上来先强制停止->强制删除之前的容器和镜像->强制拉取->强制启动,或者使用kubernetes提供的滚动升级的机制都会非常简单,但是这里主要是用于检验持续部署引擎中流水线可视化编排的能力,是否能够很轻松和简单地实现这个流程是需要确认的。

Jenkins vs Node-RED

实际上这并不是可以进行比较的两个东西,但是仅限于本文中示例说明的这个场景,似乎还真有可对比的地方。Jenkins提供可以DSL语言支持的流水线,代码及流水线,如果把流水线也看作“流”的表现形式和结果,在这个角度上是有类似的地方的,在本文指定的场景中实际上只有一个流编排引擎的能力:条件分支。

Jenkins的编排能力

Jenkins的DSL里面支持基于Stage的条件分支和并行的编排需求,但是可视化编排引擎能力稍差,比如BlueOcean插件可以在很简单地程度上进行可视化编排,但是目前还达不到易用的程度,但是毫无疑问,条件分支的支持在Jenkins中没有任何问题,比如下图为使用Jenkins中的when语句进行条件分支的示例流水线的执行情况:
在这里插入图片描述
注:详细可参看https://blog.csdn.net/liumiaocn/article/details/92817179
更多Jenkins相关的使用技巧:点击此处

Node-RED的编排能力

条件分支

更多Node-RED相关的使用技巧:点击此处

使用Node-RED进行编排

与Docker的结合

可以使用Node-RED的插件或者直接拷贝docker客户端至Node-RED容器中即可对容器进行操作,这里使用后一种方式,详细的方法可参看下文:

环境准备

  • 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

如下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的编排标准或者接入现在主流的工具还需要进一步地观察,这些都是我们在持续集成和部署实践中需要注意的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章