jenkins & docker 集群部署实践

小TOT 创建于 2018-01-23

关于docker的基本知识已经在5分钟学docker介绍过了。这里不再重复,这篇文章主要讲述jenkins与docker结合使用,做到docker项目的持续集成。文章会介绍开发环境、正式环境如何使用jenkins将项目部署到docker swarm集群。

概述

使用docker部署项目需要解决两个基本问题:镜像的构建分发,docker集群部署。

通常在开发或者测试等非正式环境,项目的修改频繁,导致构建镜像的频率会非常的高,一天可能几十次的镜像构建。如果在网络条件较差的情况下,频繁的构建发布将非常影响效率。如果场景允许的情况下,镜像中心尽量不跨网络,这样省去向镜像仓库推送和拉取镜像的时间。

关于镜像的部署,建议开发、测试环境和正式环境采用相同的架构,避免出现不同架构环境出现的特定问题。比如正式环境采用负载均衡的话,测试开发环境也需要使用负载均衡,至少运行两个实例,很多问题在单机模式情况下永远不会出现,一旦在多实例模式运行问题就会马上暴露出来,比如比较常见的session问题,比如定时任务重复执行的问题,这在单机模式转集群模式的时候都需要考虑。另外需要注意的测试环境明显的部署频率高于正式环境,建议动态生成docker-compose.yml去更新集群中的镜像,完成集群更新。

镜像构建

使用docker第一个需要解决的问题就是镜像的构建。项目在开发和测试环境参数配置会有很多差异,比如数据库的链接url,比如spring 的profile,另外程序在docker容器中运行时的命令也可能会有差异。这些都需要我们在构建镜像时注意。

使用jenkins构建镜像

使用jenkins构建docker 镜像大致有两个思路,1,使用Execute Shell插件执行docker的镜像构建命令。这个应该是一个万能的方法,只要能通过命令解决的都可以使用该方法。2,使用jenkins docker 镜像构建插件(本质上和第一种方法没有任何区别,只是提供了一个规范化的参数录入)。

  • 使用Execute Shell构建。完成程序打包后,执行docker的镜像构建命令构建镜像。如下图:

shell-build 图中的${version}是一个jenkins变量,用于动态指定镜像版本号。若需要将镜像push到远程镜像,执行相关命令即可。如果有推送需求建议使用插件Docker build and publish。若仅保存在本地,使用Execute shell更具方便。

  • 使用插件构建。这里使用Docker build and publish构建镜像并推送镜像。如下图:

plugin-build 图中的${version}是一个jenkins变量,用于动态指定镜像版本号。若远程参考需要权限则需要添加Registry credentials。

备注

注意由于docker在docker用户组运行,因此运行docker命令的用户需要在docker用户组下,使用groups jenkins查看jenkins所在的组。使用gpasswd -a userName groupname将用户添加到指定的组,使用gpasswd -d userName groupname将用户从指定组删除。重新设置了jenkins用户组需要重启一下jenkins服务时修改生效

镜像部署

镜像使用docker stack部署。部署时需要一份集群定义文件,即swarm服务定义文件【docker-compose.yml】,该文件定义了swarm如何去构建集群。集群部署相关命令:

docker stack deploy -c ${docker-compose.yml文件} --with-registry-auth ${appname集群名称}

运行该命令创建或者更新集群。 下面是一分集群定义文件实例,更多关于docker-compose请查阅官方文档

version: "3"
# 应用内的服务列表
services:
  #第一个服务,名字为web
  web:
    # 服务运行的镜像
    image: username/repo:tag

    ## 部署设置
    deploy:
      # 定义运行的实例个数
      replicas: 5
      # 服务重启策略:当服务停止就尝试重启
      restart_policy:
        condition: on-failure

      # 资源设置,这里设置了cpu,和内存资源限制
      resources:
        limits:
          cpus: "0.1"
          memory: 50M
    ## 设置了服务的端口设置,这里将在容器内的8080端口绑定到宿主机的80端口上。
    ## 这样访问宿主机的80端口即访问服务的8080端口
    ports:
      - "80:8080"
    networks:
      - webnet # 指定集群使用的网络

# 这里定义了集群中的网络
networks:
  webnet:

在生产环境我们可以单独来维护这份文件,使用git进行管理,在发版的时候修改响应docker-compose.yml文件即可完成镜像的修改,集群的弹性扩展。但是由于测试环境的特殊性,可能每天就需要进行几十次的版本发布,由于我们不能使用同一个版本号(比如latest),去更新我们的集群运行镜像,因此需要频繁的修改docker-compose.yml文件中的镜像版本。因此测试环境建议动态修改docker-compose.yml中的版本号。根据场景需要,开发环境的docker发布采用这样的思路。

  • 首先创建一个jenkins job1。专门用来构建镜像,并且镜像的版本号与jenkins构建号或者其他指标(入日期)相关。
  • 创建另外一个jenkins job2,job2读取job1的构建变量作为动态参数,使用动态参数动态生成版本号替换docker-compose.yml文件中的镜像版本号。构建时,选择要构建的版本参数,执行docker stack deploy命令更新镜像到指定版本。

根据以上思路大致分为两个问题:

  1. 构建镜像的版本控制,这里我们需要版本号可读性较高
  2. 运行镜像时动态生成docker-compose.yml文件.

构建镜像时动态生成版本号

第一问题使用jenkins插件Version Number Plug-In来解决,可以到jenkins插件市场下载安装。插件相关截图: 插件截图

插件Version Number Plug-In的使用截图

使用截图

使用Execute shell构建镜像

镜像构建命令

构建结果的展示

构建结果

选择部署构建好的镜像

这里需要解决的主要问题就是将上述构建的镜像进行选择部署,选择的参数我们使用上述job经过处理的展示名称,比如2018-01-01的第一次构建我们定义为20180101-1。这里我们使用Run parameter参数读取其他job的构建参数。使用截图如下 参数配置

得到版本号后,接下来就需要替换docker-compose.yml中的版本号了。替换文本在linux有很多方案。这里使用了sed命令。截图如下

## 替换yml文件中的镜像版本,latest-->指定版本
sed -e "s/latest/v.${imgBuildNo_NAME}/g" alpha-dev-app.yml > app.yml

有了docker-compose.yml文件,执行docker stack deploy命令即可更新集群。

备注

这里大家可能会有疑问,测试环境我们可以将版本号定义为latest,这样每次发布就发latest版本就好了。听起来确实是一个好主意,经过实践,使用stack部署,在不更改镜像版本号的情况下(一直都是latest版本),即便是之前引用的镜像已经过时,本地镜像仓库已经存在更新的latest镜像,再次执行docker stack deploy命令集群不会更新运行镜像。更多详情看查看链接1链接2

后记

上述描述了使用jenkins部署docker 集群的大致过程,主要描述的是在开发环境下的应用。由于开发环境部署比较频繁,相对来讲,开发环境的jenkins & docker配置相对复杂。主要两个难点。1,动态生成镜像版本号。2,将生成的版本号参数化到部署job,做到灵活控制发布的版本。对于以上的问题,上面已经做了大致介绍。对于正式环境,由于发版频率较低,我们可以将上述1,2问题人工化。比如镜像的版本号,我们可以根据需求号作为依据来定义,在构建时手动输入。部署时,更新docker-compose.yml文件,指定要发布的镜像版本号,再使用git进行版本控制,部署时使用最新的版本即可。而不需要使用jenkins动态的生成。

一些tips

在使用中,遇到了一些问题。记录下来避免再踩坑。

  1. 若镜像中挂载了宿主机目录,需要确保运行镜像的每个节点都有相应的目录,否则镜像不会分配到该机器中运行。出现的这样的问题,很难在stack的错误日志中发现问题。
  2. 不同swarm node拉取镜像的问题。正式环境的镜像我们通常都是私密镜像,需要登录授权的,在部署时请添加--with-registry-auth参数。在部署时需要确保每个swarm节点所在的机器,都已经登录过对应的镜像仓库。由于登录信息保存在${user_home}/.docker/config.json里面,因此还必须保证用户一致。