DockOne微信分享(一零八):基于Jenkins和Kubernetes的CI工作流


【编者的话】Jenkins作为最为流行的持续集成工具,在结合使用容器技术,Kubernetes集群的基础上,该如何发挥出新的能力,在应用微服务化的基础上,提供更好的CI方式,值得我们每一个开发人员去持续不断的摸索。本次分享主要介绍我司如何使用Jenkins Pipeline、Container和 Kubernetes Deployment的能力, 通过增加使用文本模版引擎, 扩展Kubernetes Config能力,完成公司产品开发CI工作流的建立。

Jenkins和Kubernetes

Jenkins作为最流行的持续集成工具,有着丰富的用户群、强大的扩展能力、丰富的插件,是开发人员最为常见的CI工具。在Jenkins 加强其Pipeline功能后,更是可以通过丰富的step库,实现各种复杂的流程。同时随着Docker的流行,Jenkins内也增加了对Docker的支持,可以实现容器内流程的执行。

而Kubernetes随着版本迭代的速度越来越快,在容器圈内的热度也越来越高,同时每次版本发布,所新增的功能也不断增加。做为当前主流的容器管理平台,其强大的能力无需在此多做介绍。

应用容器化和应用微服务化设计思考

容器化不意味着微服务化,传统单体应用也可以容器化,但是很难享受到容器化后带来的好处。微服务化也不是一定要容器化,应用拆解为微服务后,一样可以不利用容器而是通过传统的运维来完成系统构建和部署。当微服务化和容器化相结合之后,就能充分利用各方优势,带来了弹性伸缩,简化部署,易于扩展,技术兼容等优点。

我们在针对应用进行微服务化拆分的过程中,主要先考虑到的是功能点、控制对象、开发组的人员配置安排,产品路线图规划等。例如,针对现有开发组人员人数、分配和各自的技能熟练程度,就可以考虑到服务模块数量的控制,安排好服务模块开发小组;针对功能点和中远期产品规划,就可以将特定功能归纳到一个服务模块中,并在版本开发迭代的过程中,通过扩展这个服务模块的能力,来完成产品功能的开发,或者暂时将部分功能整合在一个模块中,随着功能增加或迭代开发,再进行进一步的模块拆分或拆解。

对于模块开发的要求,由于使用了容器技术,对于开发语言或特定框架的选型,可以交给具体的模块开发人员。在团队内,我们不做强制要求,但是做建议要求,避免出现过多的技术栈,导致后期的维护困难。

在我们团队内,就只集中在两种后端开发语言的使用,相应的框架或主要的开发库,也都有相应而且明确的选择。对于模块的API接口,使用REST,并且至少按照成熟度模型LEVEL2提供API。

容器环境下的编译和单元测试

我们整个CI工作流的驱动,都是由Jenkins完成,并且使用了Jenkins Pipeline。第一,Pipeline可以更好的组合job内的stage,重复利用模块间相同的部分,并且随着开发复杂度的增加来逐步增加扩展stage,实现更多所需的功能;第二,将Pipeline Groovy脚本来源设置为源代码内,可以根据源代码功能点来控制流程,同时也完成了对脚本的版本管理。

由于有容器这么个工具,我们各个模块的编译环境,单元测试环境,也都放到了容器中。各个模块均可以安装自身模块的运行特性或环境要求,准备自身的编译环境、单元测试环境、运行环境,因此,代码库内会分别留存相应的Dockerfile,通过不同的Dockerfile完成不同环境镜像的准备。同时,Jenkins现在也可以通过Docker Pipeline插件,支持在容器内运行step,因此我们利用其功能完成的实际的编译和测试流程是这样的:
  1. 使用编译环境的Dockerfile构建编译环境镜像。
  2. 使用编译环境镜像启动容器并在容器内完成编译,完成编译的中间产物也暂存在Workspace中。
  3. 使用测试环境的Dockerfile构件单元测试环境镜像。
  4. 使用单元测试环境镜像启动容器并在容器内运行单元测试,单元测试脚本来源于代码库,同时也使用到编译时生成的中间产物。
  5. 使用发布Dockerfile构建实际发布镜像并上传镜像库。


其中由于编译环境和单元测试环境不是经常变更,也可以抽出编译环境镜像准备和单元测试环境镜像准备两个步骤放到独立的CI job中去,需要时手工触发即可。

服务部署和升级

对于CI流程,在完成编译和打包后,需要做的就是服务启动和测试了。我们利用的是Kubernetes Deployment和service,在每次CI流程完成编译和打包后,通过拿到Build号,作为镜像的tag,完成镜像的上传归档;同时利用tag,修改Kubernetes中已经创建的Deployment,利用Deployment的Rolling Update,完成升级。

对Kubernetes服务模版和服务配置的扩展

我们在实际使用Kubernetes Deployment 升级的方式进行服务部署的过程中,发现其中还是存在很多不方便的地方。例如:Kubernetes内的同名问题,Kubernetes Deployment升级时的镜像tag变更问题,等等各处需要随着CI流程可能存在变更的地方。例如在有相同名字的Deployment存在的情况下,后来的Deployment会无法创建,这导致如果想以启动新的Deployment的方式来测试某个版本,需要修改名称,对于与Deployment相关的service也一样,在启动新的命名后的Deployment,也需要启动与其对应的service用于暴露服务。

对于Deployment升级所需的镜像tag修改,需要每次随着CI生成了新的镜像tag而做变更,因而每次需要修改相应yaml文件内的镜像tag,修改为实际CI流程中生成的值,然后再使用升级功能完成服务升级。

针对这些问题,我们使用了一套文本模板引擎,部署或升级用的yaml文件本身写成为模板,可能有变化或者需要根据CI流程变化的位置,使用模板标识占位,而具体的模板数据内容,则或者通过Jenkins的CI流程获取,或者使用特定的配置文件读取,或者从具体的输入参数来获取;

同时,模板数据内容,也会在实际部署时,做为Kubernetes的Configmap设置到系统中,因此数据内容也可以通过Kubernetes使用Configmap的方式来用到环境变量或启动命令中。通过文本模板引擎,将模板和数据合并后,生成的yaml文件,再作为后续Kubernetes操作所使用的内容。

通过利用这种方式,我们把需要部署的内容分离成了模板和配置;模板一般在服务架构,使用的镜像名,启动方式或配置参数没有大的变化的情况下保持不变,而通过不同配置的灵活使用,完成服务升级或拉起新部署,完成不同数据存储使用的指向,完成对各模块内部配置的修改。

通过利用这种方式,我们的可修改的内容,从Configmap本身只能覆盖到的环境变量或启动命令这块,扩展到了启动名称、Label、镜像等YAML文件内的各个可填值处,以此来解决同名,镜像修改,Label增加或变更等各种使用Kubernetes时碰到的问题。

自动化测试

在通过Jenkins拉动完成编译打包和服务升级部署后,就可以拉动自动化测试了。测试框架我们选择了使用Robotframework。测试脚本通过Kubernetes service获取到服务的具体暴露端口,然后再根据测试脚本依次执行针对API的测试。

测试脚本的来源,部分是从各模块代码库中,由各模块开发人员提交的针对自身模块的API测试,部分是由测试人员完成撰写提交的针对跨模块的测试。针对自动化测试这块,我们的完成度并不是很高,仅仅是搭建起了基本的运行框架,能够与整个流程对接上。

版本发布

由于开发的产品本身就是由若干镜像构成,因此产品发布,可以归结为镜像的发布。在测试通过后,可以简单的利用镜像复制能力,将测试通过的相关镜像的版本,通过镜像库间的复制,由开发测试所用的内部镜像库,复制到外部发布镜像库,就可以完成版本发布,同时可以通过复制时的tag控制,发布为指定的版本号。

总结

如上介绍,说明了我们在自身开发过程中建立CI流程的做法。对于整个流程的建立,我们并没有太多需要特殊化处理的地方;对于各项工具的使用,也没有太多突出之处;我们仅仅根据自身需求,建立了和开发过程适配的CI流。在此介绍我们的CI流程的建立,也是希望抛砖引玉,能从更多处获得交流和向大家学习的机会。

Q&A

Q:关于Jenkins和Docker集成的几个插件可以分享一下吗?

A:Docker Pipeline、Docker Plugin、docker-build-step这几个插件。
Q:请问有容云的镜像复制大体思路是什么?目前我们是测试、预发布、发布共享一个仓库。通过辅助模块标签实现的。

A:镜像复制功能,简单来说实现了不同项目间的镜像克隆,根据我们镜像仓库的设计,对不同阶段(开发、测试、可上线)的镜像以不同项目分类,基于镜像复制功能即可快速实现不同阶段的产品发布,也就是镜像发布,此功能可下载AppHouse版本进行试用。
Q:贵公司的内部仓库和外部仓库镜像是实时同步的吗? 你们的配置文件是通过配置中心管理还是镜像间的环境变量实现的?

A:不是实时同步的,而是通过了我们公司镜像库产品的镜像复制能力实现的。目前开发流程中的产品运行配置是通过自身增强的配置文件能力实现的,配置会用来修改应用部署的YAML文件,也会生成为ConfigMap。
Q:有没有一个简单的sample,可以上手跟着练习一下?

A:基于Kubernetes的CICD产品即将发布,会提供对应的demo演示平台,请及时关注,谢谢!
Q:Kubernetes的YAML的部署文件的模板怎么去替换占位符?旧版本的容器怎么处理?

A:使用了自身开发的一套文本模板引擎,其实类似Web框架中的模板引擎,完成模板和配置的合并,通过使用配置中的key-value,替换掉模板中key的占位符。另外由于是使用的Kubernetes deployment的rolling update,升级完成后旧版本的容器/pod会由Kubernetes自行删除。
Q:传统单体应用如何容器化改造,可否分阶段实施?

A:可以的,容器化改造或微服务化改造有很多实施方法,例如逐渐重构拆解,或新增模块进行微服务化和容器化,或开发新的模块替代原有应用的功能点,等等。各团队均可以选择合适自身的流程来进行改造。
Q:数据库可以容器化吗?

A:可通过将数据卷挂入容器的方式将数据库容器化,但是现在实际项目中还很少见。
Q:有这样一个场景,两个服务有依赖关系,服务A依赖于服务B,如何保证服务A和B的启动顺序的?

A:良好的设计是使得A服务启动后自行完成对B服务的检测发现和调用,而不是强依赖其启动顺序.
Q:Kubernetes的服务的模板和配置,这个模板怎么来的,是用户自己编排?还是自己事先准备好的?配置数据是怎么存储的?

A:因为当前模板和配置只用来启动我们自身开发的应用,因此这个模板是我们自己为我们的应用准备的。配置数据以文件的形式存储,但同时在使用文本引擎做模板和配置合并时,也可以接受参数作为配置。
Q:什么是CI和CD,这个搞不懂?

A:CI更多是偏向应用编译,代码检查,单元测试等动作,CD是偏向于应用部署,运行流程。我们的开发过程在编译打包完成后,实际也会将应用跑起来用于测试,也可以算是针对测试的CD。
Q:使用容器打包Jenkins流程的主要收益是什么?

A:由于不同程序对于编译环境的依赖各有不同,原有使用Jenkins方法是在Jenkins node上完成环境准备,现在可以利用容器完成环境准备,对于Jenkins node的依赖可以进一步降低。同时环境变更也可以由开发人员自行控制。
Q:多编译环境是用的不同镜像么?如何处理Pipeline处理编译环境的问题?

A:是的。由于我们本身产品开发各个模块有各个模块的开发语言和框架,因此各模块都要维护自身的编译环境镜像。使用Pipeline在进行编译时,是通过使用镜像运行容器然后在容器内编译的方式来使用编译环境的。
Q:请问Jenkins也是部署在Docker里面的吗?如果Jenkins在Docker里面怎么样在Docker里面使用Docker执行CI?

A:是的,我们也在摸索将Jenkins本身放到容器中运行。在这种情况下,Jenkins容器内使用root权限,挂载docker.sock和Docker数据目录到容器中就可以了。
Q:使用Pipeline先构建编译环境镜像,再编译,是否会导致整个流程需要很长时间?是否有优化方案?

A:编译镜像由于不会经常变动,因此这个镜像的构建通常使用cache就能直接完成,另外我们也把编译环境镜像打包这个步骤抽出来单独作为job执行了,这样在实际编译流程中就无需再进行编译环境构建。
Q:Jenkins和Kubernetes的用户是怎么管理的?我的期望是用户只能看到自己得资源,别的用户是没有权限的。

A:我们本身只是使用这两种工具,在开发环境中不对外,所有不存在用户管理的问题。在我们公司正在开发的CICD产品中,对这块有我们自身理解基础上的设计。
Q:Jenkins的持续集成是怎么实现的?比如不同的源码仓库的提交触发,如GitHub、GitLab版本号怎么控制的?

A:Jenkins的CI流程触发可以有很多种,代码提交触发,定时触发,手动触发。版本号的控制也可以有很多方案,比如使用job的编号,使用Git的commit号,使用时间戳等等。
Q:容器化后发布也要通过Jenkins,感觉Docker的发布没有Jenkins方便,除了容器化的可移植,还有什么原因值得推进项目容器化?

A:应用容器化,其实更多的是看重应用在容器管理平台上运行起来后所获得的能力,例如在Kubernetes上运行后的水平扩展,服务发现,滚动升级,等等。
Q:Kubernetes update需要制定新的镜像才能做滚服更新(升级),如果只是更新了ConfigMap,有办法做滚服更新吗?

A:我们的CI流程完成后,各模块的镜像tag会发生变化,我们利用具体生成的tag生成配置,然后部署的YAML文件写为模板,镜像的具体tag会根据每次CI流程生成的配置不同而组合为不同的YAML文件,然后使用组合后的yaml,即tag已经变更为最新版本的YAML文件进行应用的滚动升级。
Q:Pipeline采用在镜像里构建的方案,是怎么实现的?用Jenkins现成的插件 or 用Groovy脚本实现?

A:使用了Jenkins的Docker插件,同时使用方式是将相应命令写在Groovy脚本里,例如:stage('Build'){ docker.image('golang:1.7').inside { sh './script/build.sh' } }
以上内容根据2017年2月28日晚微信群分享内容整理。分享人黄文俊,有容云资深系统架构师。主要负责容器云平台产品架构及设计,8年工作经验,有着企业级存储,云计算解决方案相关理解。关注于微服务设计思考,开发流程优化,Docker及Kubernetes技术在实际环境中的应用。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesz,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册