实战总结之 利用Docker、Docker Compose &Rancher构建持续部署

前言:

本文由John Patterson 、 Chris Lunsford写于2016年4月4日,译者有容云张向波 ,转载请注明出处。(原文链接见文末)

 

作者John Patterson和Chris Lunsford 运营了一家提供运营和基础架构服务的公司,本文是他们给大家分享的内容:关于如何通过使用Docker、Docker-Compose和Rancher来实现容器部署落地。

 

我们想跟你一起从头开始体验整个过程,特别是之间遇到的一些痛点和所做的决策。目前,已经有许多的资源和工具可以与Docker一起配合来搭建持续集成和部署工作流程。搭建一个部署工作流程较简单,但是搭建一个部署系统相对比较复杂,因为很多相关的工作流程在已有的环境中已经实现,并且存在很多依赖关系,同时还要让开发和运维部门启用新的工作流程。希望我们在搭建部署系统中的经验能够帮您跳过项目中的那些坑。

 

在这篇文章中,我们将回顾最初只使用Docker来构建的工作流程。随后的几篇文章中,我们将进一步通过Docker-Compose和Rancher来一起构建工作流程。

 

以下事件发生在我们长期服务的一家SaaS服务商,接下来我们称这家公司为Acme Business Company或者ABC。这个项目始于ABC着手将大部分Java微服务从自建机房的服务器迁移到AWS的Docker环境中。项目目标主要是降低未来上线周期和拥有更高的应用部署服务可靠性。

 

实现应用部署的计划如下所示:


 

整个流程从代码更改、提交、推送到Git仓库开始,这将通知我们的持续集成(CI)系统运行单元测试,单元测试通过的话,编译代码并作为构件保存。如果这步成功通过的话,将触发新任务,通过代码构件来生成Docker镜像并推送到Docker私有镜像仓库。最后,我们将镜像部署到环境中。

 

 

必需组件如下:

  • 源代码仓库- ABC将代码保存在私有的GitHub仓库中。
  • 持续集成和部署工具- ABC使用本地安装的Jenkins。
  • 私有镜像仓库-基于S3的Docker镜像仓库为容器提供服务。
  • 运行Docker的主机环境 -ABC 有多个目标环境,且每个目标均包含预发布环境和生产环境。 

乍看起来,整个流程还是挺简单的,而实际上并没那么简单。和大多数其他公司一样,ABC以前(现在也是)开发和运维是分开的。当代码准备好部署时,一个包含应用详细信息和目标环境的工单(ticket)将被创建。这个工单将指派给运维部门,并安排在当周的部署维护窗口进行上线。这种情况下,持续部署和交付的方式是不清晰的。

 

起初,应用部署的工单可能如下:

 

 

部署过程:

 

  • 打开部署窗口,工程师在Jenkins中对相关项目点击“立即构建”,传递分支名字作为参数。输出显示一个被标记tag的Docker镜像被自动推送到镜像仓库。工程师在环境中选择一个当前不在负载均衡设备中激活的Docker主机,登陆并且从镜像仓库拉取新版本镜像。

 

 

  • 找到当前运行的容器

 

 

  • 停止运行的容器.

 

 

配置必须的标识并正确启动新的容器。具体容器配置可以从之前运行的容器,主机shell历史记录,或者从配置文档中获取。

 

 

  • 启动服务,通过手工执行测试来确认服务正常。

 

 

  • 在生产环境维护窗口,更新负载均衡配置,指向更新的主机。
  • 验证通过后,更新将被应用到环境中需要故障恢复的其他主机上。

这种部署过程并不引人注目,却是持续部署中的第一步,还有很多地方需要改进,先权衡一下收益:

  • 运维工程师可以用相同的步骤来部署所有的应用。Docker run 步骤中参数针对不同的服务或应用需要定制,但步骤始终一致:Docker pull,Docker stop,Docker run。这些流程足够简单,很小概率会遗漏步骤。

 

  • 最小2台主机组成的环境中,我们进行了可管理的蓝绿部署(灰度发布)。通过负载均衡设备配置在生产窗口进行简单版本切换和回滚。随着部署变得更灵活、支持快速升级、支持快速回滚,后端的服务发现机制也变得更复杂。由于是手动进行,蓝绿部署的成本微乎其微,但带来了直接升级(in-place upgrade)的收益。

痛点:

  • 重复输入相同的命令。 更准确的说,在bash框口中重复输入。这个解决办法很简单,自动化! 有很多工具来解决Docker容器的启动。对运维工程师来说最简单的方式是将重复的逻辑写成bash脚本,这样就有了单一的入口。如果你是一个DevOps工程师,你也可以通过Ansible、Puppet、Chef或者 SaltStack来实现。写脚本逻辑很容易,但有一些问题亟待解决:部署逻辑脚本存放在哪?多个服务使用不同的参数如何追踪和管理?

 

  • 降低出错率及日志需求-即使运维工程师拥有超人类的能力避免了打字输入错误,在工作一整天后的午夜还能保持头脑清醒,但他也不会知道某个应用服务的端口需要更改为其他端口。Docker端口参数需要更改,原因在于应用程序的具体工作原理只是开发人员比较清楚,需要将这些信息传递给运维团队。多数情况下,运维脚本逻辑记录在一个单独的资源库,亦或者根本没有记录下来。随着应用的不断变化而时时更新应用部署步骤是比较困难的,所以将部署脚本逻辑用Dockerfile提交到代码仓库是一个不错的选择。即使某些情况下不能这样做,也有其他的解决方式(详见后文)。重点是具体的部署步骤被记录并存储了。代码实现要比一个部署工单能更好的完成上述工作,但至少工单记录下来的部署步骤要比没有任何文字记录或者只放在脑子里要好很多。

 

  • 可见性低  对容器进行故障排查需要登录主机并执行命令。这意味着需要登录一群主机,并重复输入”docker ps” 和”docker logs -tail=100”。有许多很不错的集中日志处理的解决方案,很值得花时间去搭建一套。这个过程中,我们发现比较困难的是,如何定位某个容器运行在哪台主机上。在灰度发布的场景中,开发比较关心当前环境中各个版本的上线情况和扩展水平。对于运维团队来说找到对应的容器来进行升级和故障排查是比较麻烦的。

鉴于此,我们着手对这个问题进行解决。

第一步,将通用的部署过程写成bash脚本,例如:

 

 

这个例子只对最简单的容器生效:用户不需要和容器进行直接交互。我们需要添加特定的应用逻辑来启用主机端口映射(host port mapping)和卷挂载(volume mount)。如下:

 

 

这个脚本放在所有的Docker主机上来优化部署,运维工程师登录主机,传递参数给脚本,然后脚本负责剩下的工作。部署时间因此缩短,但还是需要手动编写部署脚本的逻辑。此外,有一个新问题,当对通用脚本进行更新后,如何将更新的脚本应用到所有的Host上。通常来说,利用仓库来进行代码和脚本管理有以下优势:代码审查、测试、变更记录、复用。需要人为干预的越少越好。

 

理论上,部署应用的脚本可以与应用存储在同一个代码仓库中,但实际中很难,最重要的一点就是开发会反对将运维相关的东西放在他们的Java代码仓库中。特别是一些bash部署脚本也包括Dockerfile本身。

 

这可以归结为一个不同部门的文化问题,如果可能的话,还是值得去尝试的。虽然也可以把部署代码或脚本单独存放在其他仓库中进行维护,但需要额外搞定应用代码和部署代码的同步问题。我们在此将以后面这种方式作为例子。在ABC,每一个项目会单独创建一个文件夹在独立的代码仓库中来存放Dockerfile,部署脚本放在自己的目录下。

 

Dockerfiles仓库会迁出一份副本到Jenkins主机的公共目录下(如:”/opt/abc/Dockerfiles”)。在构建Docker镜像之前,Jenkins会检查本地“docker”文件夹下是否存在Dockerfile。如果不存在,Jenkins会搜索Dockerfiles路径,拷贝Dockerfile和脚本,再通过“docker build”构建镜像。由于Dockerfile放在Master分支,有可能Dockerfile在某些情况下会超前(或落后)于应用程序配置,实际生产中,这在大多数情况下,不会造成问题。下面是从Jenkins 构建逻辑摘录来的:

 

 

渐渐的,Dockerfile和对应的支持脚本被整合到应用的源代码仓库中。由于Jenkins优先在本地仓库中进行查找,所以在整合过程中不需要进行额外的其他配置更改。在迁移完第一个Java应用后,仓库拓扑如下:

 

 

我们在将Dockerfile和脚本存储在独立的仓库时,遇到的一个问题是,当应用源代码或者部署脚本发生变更时,都会触发Jenkins重新构建镜像。由于Dockerfiles仓库包含了多个项目的代码,所以在对某一个Dockerfile进行更新时,不希望触发所有项目重新构建。解决方法:一个比较隐蔽的Jenkins配置项 Git plugin - Included Regions。 当启用这个选项后,只有在仓库中特定的文件夹发生变化时,才会触发镜像构建。这使得我们可以将所有的Dockerfile保存在一个仓库,只有当特定的内容在代码库中更新后触发特定的Docker 镜像构建。(相比之前触发所有的项目重新构建)。

 

 

另一个问题是运维工程师必须在部署之前进行应用的镜像构建,这会导致上线周期的延长,特别是在镜像构建中出错,需要开发介入。为了减少这种情况,更好的支持持续部署,我们定义每一个提交的大分支版本都触发镜像构建。这要求每一个镜像拥有唯一的版本标识而不只是依靠应用程序的版本标识字符串。我们最终采用应用版本字符-提交次数-提交SHA值


 

最终显示的版本字符串如:1.0.1-22-7e56158。

在结束我们这次Dockerfile内容的讨论之前,还有一些关键决策值得我们注意。在大规模使用容器之前,我们基本对此一无所知,这些关键点对容器集群的稳定运行有着重要的作用。

 

  • 重启策略  – 重启策略允许用户定义容器异常终止后的容器动作。容器重启策略可以实现应用程序的故障恢复,一旦应用依赖的子服务从故障中恢复,应用容器即可恢复正常,这得益于应用容器对依赖容器服务可用性的持续监听。长远看来,用户应该配置合适的规则使异常的容器能够在新的主机上启动,这将会节省一部分工作。目前,ABC默认容器重启策略是“-restart always”,故障容器将会不断重启,直到容器恢复正常。一个简单的重启策略将会大大降低计划内(和计划外)主机重启带来的负面影响。
  • 资源配额  – 用户可以通过运行时资源来限制配置容器最大可消耗的内存和CPU。资源配额不能防止主机容器的超额分配,但是可以防止容器内存泄漏和失控容器造成的影响。我们对内存比较敏感的容器分配比较大的内存配额(如,内存=8G)。资源配额虽然可能会造成单个应用的内存溢出、无法响应,但保证了主机和其它容器的正常工作。

容器策略和资源配置的协同配合将会带来更高的容器集群稳定性,同时降低故障影响,缩短故障恢复的时间。实践过程中,有了这种保护机制,以往被用来故障恢复的时间,现在用在和开发团队一起进行故障根本原因排查。

 

小结

 

我们最初从源代码库构建包含标识符的容器镜像,接下来通过Docker 命令行工具写成的脚本和参数来部署容器。同时梳理了我们环境中的容器部署代码逻辑,强调了那些对有助于运维部门保持服务稳定运行的一些关键决策。

 

当前这个阶段,镜像构建和部署之间依然存在欠缺。部署维护工程师需要手动登录到服务器执行部署脚本。我们已经比最初的时候前进了一大步,但还有很多内容可以自动化。当前所有的部署逻辑集中在一个脚本中,当开发人员需要安装调试脚本,理清脚本逻辑变得很复杂。目前我们的脚本中也定义了许多环境参数来引入环境参数,查找某个环境变量对应的服务和添加新的环境变量是相对繁琐并容易混淆。

 

在 下一篇文章,我们将通过解构通用的封装脚本,对我们是如何解决这些痛处一探究竟,介绍如何使用Docker Compose让部署逻辑更贴合应用。

目前可以通过以下链接访问这个系列4篇文章:


Part 1: CI/CD 和 Docker入门
Part 2: 使用Docker Compose
Part 3: Rancher助力容器编排
Part 4: 服务发现

原文地址:Lessons Learned Building a Deployment Pipeline with Docker, Docker-Compose, and Rancher (Part 1)

 

分享:实战总结之 利用Docker、Docker Compose &Rancher构建持续部署

有容云-构筑企业容器云 www.youruncloud.com

温馨提示

对Docker容器技术或容器生产实施感兴趣的朋友欢迎加群讨论。我们汇集了Docker容器技术落地实施团队精英及业内技术派高人,在线为您分享Docker技术干货。我们的宗旨是为了大家拥有更专业的平台交流Docker实战技术,我们将定期邀请嘉宾做各类话题分享及回顾,共同实践研究Docker容器生态圈。

加微信群方法:

1.关注【有容云】公众号

2.留言”我要加群”

QQ群号:454565480

有容云微信二维码