随着Docker项目及其相关生态系统逐渐成熟,容器已经开始被更多企业用在了更大规模的项目中。因此,我们需要一套连贯的工作流程和流水线来简化大规模项目的部署。
Rancher Labs准备了此持续集成与持续部署系列文章,共两万余字,希望能供企业参考如何利用诸如Docker和Rancher这类工具来创建属于企业的持续集成和持续部署流水线,并根据自己的实际情况和需求在这CI/CD流水线中也加入自定义的流程。
本文是此系列文章的最后一篇,我们将在本文中完成创建持续部署流水线的最后工作。本文内容包括创建持续部署流水线(发布Docker镜像、部署到集成环境、发布和部署一个新的版本)和部署策略(就地更新、蓝绿部署)。
创建持续部署流水线
我们已经创建好了测试环境,在前文中我们也构建好了CI流水线,由它来创建应用、将应用打包进容器、进行集成测试。现在我们将来到本系列文章的最终章,拓展CI流水线来创建一个持续部署流水线。
发布Docker镜像
我们首先将打包的镜像发布到Docker存储库中。方便起见,我们使用了一个公共DockerHub仓库,不过,对于实际的开发项目,我们还是建议将Docker镜像push到私有库中。下面我们在Jenkins中创建一个新的Free Style Project任务,点击New Item按钮,把任务命名为push-go-auth-image。完成了这些步骤后,你会进入到Jenkins任务配置页面,在这里你可以定义push你的go-auth镜像到Dockerhub所需要的步骤。
因为这是我们对前一章中构建的流水线的后续,因此该作业会有和go-auth-integration-test作业类似的配置。你需要首先设置的是parameterized build并且添加GO_AUTH_VERSION变量。
要push镜像,我们选择Add build step下拉菜单,然后选择Execute shell选项。在结果文本框中添加下面的命令。在这些命令中,我们将登陆到DockerHub,并且push我们之前构建好的镜像。这里我们准备将其push到usman/go-auth仓库中,而你则需要把它推送到自己的DockerHub库中。
上一章提到,我们使用的是git-flow分支模型,其所有的功能分支都合并到“开发”分支中。为了能够不断地将发生的变更部署到集成环境中,我们需要一个简单的机制来生成基于开发分支的最新镜像。在打包过程中,我们使用GO_AUTH_VERSION来标记docker容器(例如,docker build -t usman/go-auth:${GO_AUTH_VERSION} ....)。在默认情况下,这个版本将会成为开发分支,但是本章后面我们将为我们的应用程序创建新发布,使用CI/CD流水线来构建、打包、测试,并把它们部署到我们的集成环境中。要注意的是,在这个方案中,我们总会覆盖我们开发分支的镜像(usman/go-auth:develop) ,这限制了我们引用历史构建以及执行回滚。你可以对流水线做一个简单的更改,把Jenkins构建编号添加到自身的版本名称上,比如usman/go-auth:develop-14。
你需要指定你的DockerHub用户名、密码和电子邮件地址。你可以在每次运行时用参数构建的方式指定它们,也可以使用Jenkins Mask Passwords Plugin在主要的Jenkins配置中安全地定义它们,并将它们添加到构建中。要确保Build Environment中为你的作业启用了“Mask passwords(并且启用全局密码)”。
现在我们需要确保这个作业是在集成测试作业之后才触发的。要做到这一点,我们需要更新集成测试作业,以便使用当前的构建参数触发参数化构建。这意味着在每次成功运行集成测试作业后,我们会把测试好的镜像push到DockerHub上。
最后,我们需要在镜像成功push到DockerHub之后触发部署作业。就像我们为其他作业所做的一样,我们可以通过添加构建后操作来实现这一点。
部署到集成环境
我们将使用Rancher Compose CLI来停止运行的环境,从DockerHub获取最新的镜像,并重新启动环境(提醒一下,Updates API还在发展,可能会发生变化。未来几周或者几个月内肯定会增加新的功能,因此请随时查看文档是否有更新项)。在我们创建Jenkins作业来实现持续部署之前,我们先手动完成这些步骤。
我们可以使用最简单的方法——停止所有服务(auth服务、负载均衡器以及mysql),提取最新的镜像并启动所有服务。然而,对于我们来说,我们只想更新应用程序,而并不想停止长时间运行的环境,这样就不太理想了。要更新我们的应用程序,我们首先要停止auth-service。你可以在Rancher Compose使用stop命令完成操作。
这会停止运行goauth服务的所有容器,你可以在Rancher UI中打开堆栈来验证该服务的状态是否设置为Inactive(不活跃)。接下来,我们要让Rancher拉取我们想要部署的镜像版本。现在,我们已经可以动态指定想要运行的版本,而无需每次都要更新模板了。如果你需要多次push相同的镜像版本,请添加pull开关,确保我们使用的是镜像版本的最新副本。
我们还可以通过下面的命令使用升级功能,零停机地实现环境的滚动更新。下一节中我们将进一步讨论滚动更新。在升级完成后,你可以使--rollbach命令或--confirm-upgrade来确认更改或者回滚到预览状态。
现在我们已经知道该如何运行我们的更新,我们在流水线中创建一个Jenkins作业来执行此操作。和之前一样,创建一个新的freestyle项目,命名为deploy-integration。与其他的作业一样,它也是一个参数化构建,用GO_AUTH_VERSION作为字符串参数。接下来,我们要从上游的build-go-auth作业中复制工件。
最后,我们需要向Execute Sheell构建步骤中添加Rancher Compose up命令,这是我们在之前指定好的。需要注意,你还需要提前在Jenkins上设置Rancher CLI,让它可以用于你在系统路径上的构建。执行shell步骤的内容和下面的代码片段相似。如果你有多个Rancher Compose节点,负载均衡器容器可能会在不同的主机上启动,你的Route 53记录集合就可能需要更新。
有了我们两个新的Jenkins作业,我们从上一章就开始构建的流水线现在看起来就像下图所示。每次对我们示例应用程序的check-in都会被编译,确保其没有出现语法错误并且能够通过自动化测试。然后,将这些变更打包,通过集成测试,最后再部署到手动测试中。下面的五个步骤为构建流水线提供了一个良好的基线模板,并且有助于将代码从开发阶段转移到测试和部署阶段上。拥有一个连续的部署流水线确保了代码不仅可以进过自动化系统的测试,而且还能快速提供给测试人员使用。除此之外它还能够作为生产部署自动化的模型,测试操作工具和代码,持续部署应用程序。
发布和部署一个新的版本
在我们将代码部署到持续的可测试环境中后,我们就会让QA团队测试对这些变更进行一段时间的测试。在他们确定代码已经就绪后,我们可以创建一个发布,随后将它部署到生产环境中。用git-flow发布的方式类似于特征分支(我们在前一章讨论过的工作方式),我们使用gitflow release start [Release Name]命令(如下所示)进行发布。这将创建一个新的名称来发布分支。在这个分支中,我们将执行一些内部的操作,比如增加版本号,做最后的修改。
完成这些后,我们运行releasefinish命令将发布分支合并到主分支中。这样,主分支总能反映出最新发布的代码。另外,每个发布都会加上标签,对每个发布的内容都能够有历史记录。如果我们不需要再进行其他的更改,我们就可以确定最终的发布了。
最后一步就是把发布push到远端仓库中
git push origin master git push --tags //pushes the v1 tag to remote repository
如果你使用的是Github来托管git库,现在应该会发现有一个新的版本了。我们还可以将与发布名称相匹配的版本镜像push到DockerHub,这也是一个不错的选择。我们先运行第一项作业来触发我们的CD流水线。可能你还记得,我们为CI流水线设置了GitParameter插件,以便能够从git中获取与过滤器相匹配的所有标记。不过,这是对于默认的开发分支而言,当我们手动触发流水线时,我们可以从git标签中进行选择。例如,在下面我们为应用程序提供了两个版本。我们选择其中一个,启动集成和部署流水线。
然后,经过以下步骤,我们的应用程序1.1版本将会被部署到长时间运行的集成环境中,只需要点击几下就能实现。
-
从git中获取所选的发布
-
构建应用程序,运行单元测试
-
创建一个带有标签v1.1的新镜像(比如usman/go-auth:v1.1)
-
运行集成测试
-
Push镜像(usman/go-auth:v1.1)到DockerHub
-
将该版本部署到我们的集成环境
部署策略
管理长时间运行的环境时会遇到很多挑战,其中之一就是尽可能在发布期间的停机时间要降至最短,最好为零。为了让这个过程可预测并且安全,需要做相当多的工作。自动化和质量保证确实可以大大提高发布的可预测性和安全性。不过即使是这样,失败也会发生,而且对任何优秀的运维团队来说,他们的目标都是在最小化影响的同时快速恢复。在本节中,我们将介绍一些部署长时间运行环境的策略以及它们的优缺点。
就地更新
第一个策略称为就地更新(In-placeupdates),顾名思义,它的思想就是复用应用程序环境,并且就地更新应用程序。这有时也称作是滚动部署。我们将使用我们目前讨论的示例应用程序(go-auth)。此外,我们假定你有用Rancher运行的服务。如果要就地更新,你可以使用下面的升级命令:
在用户看不到的地方,Rancher agent会在每个运行auth服务容器的主机上获取新的镜像(--pull会重新下载,尽管镜像已经存在)。之后代理会停止旧的容器,批量启动新的容器。你可以使用--batch-size标志来控制批处理的大小。此外,你还可以指定批更新之间的暂停时间间隔(--interval),使用足够大的时间间隔来验证新容器的行为是否按照预期运行,并且总体而言,服务是健康的。在默认情况下,旧的容器终止后,新的容器会在它们的位置上启动。或者你可以在rancher-compose.yml中设置start_first标志,告诉Rancher在启动新容器之前先停止旧容器。
如果对当前的更新不满意想要回滚,可以使用回滚标志来完成回滚。或者你想要继续进行更新,只需告诉rancher指定confirm-update标志完成更新即可。你也可以在原始的up命令中指定confirm-update标志一步完成这些操作。你还可以在RancherUI执行更新,从服务菜单(下图所示)中选择“upgrade”。
就地更新非常简单,不需要额外的资源来管理多个堆栈。然而,这种生产方式是存在缺点的。首先,对回滚更新进行细粒度控制大多很困难,就是说在故障发生的情况下它们往往是难以估计的。例如,处理部分故障和滚动更新会变得非常混乱。你需要知道在哪些节点上部署了更改,哪些没能部署,还有哪些仍在运行之前的版本。其次,你还需要保证所有的更新不仅是向后兼容,还能向前兼容,因为旧版本和新版本都是需要在相同的环境中同时运行的。最后,根据使用的情况,就地更新可能不实用。比如,如果老的客户端需要继续使用旧环境,而新的客户端需要向前滚。在这种情况下,使用我们今天要列出的方法分离客户端会更加容易。
蓝绿部署
就地更新的一个问题是缺少可预测性。为了克服这一问题,另一个部署策略是针对应用程序使用两个并行堆栈:一个处于活跃状态,另一个处于待机状态。要运行新版本,部署应用程序的最新版本到待机堆栈。当验证到新版本需要工作,将会从活动堆栈切换流量到备用堆栈上。这时,先前活跃的堆栈称为备用堆栈,反之亦然。该策略允许验证已经部署的代码、快速回滚(切换备用和重新激活),并还可以在需要时扩展两个堆栈的并发操作。这种策略通常被称为蓝绿部署。用我们的示例应用程序完成这样的部署,可以简单地在Rancher中创建两个堆栈:go-auth-blue和go-auth-green。此外,我们假设数据库不是这些堆栈的一部分,而是独立管理的。每个堆栈都会运行goauth和auth-lb服务。如果假设go-auth-green堆栈式活跃的,要执行更新,我们要做的就是将最新版本部署到蓝色堆栈中,执行验证并将流量切换到它这。
流量切换
有两个方法可用于执行流量切换,更改DNS记录来指向新堆栈,或者使用代理或负载均衡器,将流量路由到活动堆栈。下面我们会详细介绍这两个方法。
记录更新
一个简单的方法是更新DNS记录指向活动堆栈。这种方法的一个优点是,我们可以使用加权DNS记录将流量慢慢转换到新版本中。这也是执行canary releases的简单方法,对安全地在实时环境中进行更新或者做A/B测试非常有用。例如,我们可以将实验性的功能更部署到自己的功能堆栈(或活动堆栈),然后更新DNS,仅将一小部分流量转发到新版本。如果新更新出现问题,我们可以逆转DNS记录回滚回来。此外,他比将所有流量从一个堆栈切换到另一个堆栈要安全得多,因为这样会覆盖掉新的堆栈。虽然简单,如果你希望所有流量能一次切换到新版本,DNS记录更新就把u 事最简洁的方法。根据DNS客户端不同,这些更改可能需要很长时间才能传播,从而导致和旧版本之间的大量通信,而不是直接了断切换。
使用反向代理
使用代理或负载均衡器,只需要将其更新成指向新堆栈就可以一次切换整个流量。这种方法在各种场景下都非常有用,例如非向后兼容的更新。要使用Rancher完成这一操作,我们首先要创建一个仅包含负载均衡器的堆栈。
接着,我们为负载均衡器指定一个端口,配置SSL并从下拉菜单中选择活动堆栈的负载均衡器作为target service来完成创建。本质上来说,我们将负载放到了负载均衡器上,它会在之后将流量路由到实际的服务节点。借助外部负载均衡器,你不需要更新每个版本的DNS记录。相反,你可以简单地更新外部负载均衡器指向已更新的堆栈。
总 结
在本章中,我们介绍了如何创建一个持续部署流水线,可以将我们的示例应用程序放在集成环境中。我们还研究了如何集成DNS和HTTPs支持来创建一个用户能够进行集成的更加安全可用的环境。在后续的工作中,我们着眼于运行中的生产环境。部署到生产环境会带来一些列的挑战,我们希望部署是满足负载,并且有很少停机时间的(理想下为零)。此外,生产环境也面临着挑战,因为它们必须能向外扩展以满足负载,同时还要减少控制成本。最后,为了提供自动故障转移以及高可用性,我们对DNS管理进行了更全面地研究。我们还将研究生产中Docker环境的操作管理以及不同类型的工作负载,比如state-full连接服务。
结 论
这一系列文章介绍了使用容器实现完整的CI/CD流水线的几种方法。我们尝试涵盖常见的使用案例,提供详细的示例,并且分享了我们在Web服务公司工作多年的DevOps中学到的一些最佳实践。未来我们还会继续将内容进一步深化,发布一份子妹篇,深入介绍如何在生产中使用容器运行服务,敬请保持关注!