如何设计一条稳定的应用交付流程?|云效工程师指北
大家好,我叫王泊,负责云效应用交付 AppStack 的开发。把应用部署到各个环境、一步步进行集成测试,最终发布到生产环境,是程序员工作中必不可少的组成部分;而云原生技术引入的容器化、IaC(基础设施即代码,Infrastructure as Code)等等技术与理念,为持续交付的过程提供了规范化的可能,但也引入了让人不时埋首于配置文件的小山里的麻烦。我们不妨从一次略有波折、稍显隐患的集成部署案例开始,看看如何着手设计一条更为稳定的应用交付流程。
一次波折的部署
许多个迭代后,面对陪风扇一起嘎吱嘎吱转着的流水线,程序员阿伟会回忆起把系统部署到预发环境、提交最后一轮验收,然后被打回来的那个并不遥远的下午。当时他有一个酷炫的 Java SpringBoot 应用要上线,实现了酷炫的“在不同部署环境下、发送带环境路由标签的业务消息”的接口:
日常环境的镜像构建、部署和验收测试一路 OK,但是在再次构建部署到预发环境后,阿伟发现消息丢了:预发环境的消费方并没有消费到消息。经过一系列不管黑屏白屏康到 bug 就是好屏的排查,发现问题起源于在预发环境使用的 SpringBoot 配置文件 application-staging.yaml 中漏配了 routing.env 属性,应用启动时使用了缺省配置 application.yaml 中的兜底值,导致消息 tag 打错。
具体的问题倒是解决了,不过多少会留下点顾虑:以后写配置项的时候,免不了翻来覆去 diff 一下,是不是漏了什么,会不会导致各个环境里的产物有微妙的结构差异引发 bug……
旧交付方式的潜在问题
仍然以 SpringBoot 应用为例,一部分开发者将应用从传统的虚机部署迁移到 Kubernetes 上的容器化部署时,会使用类似下面的思路:
框架提供了为不同环境编写不同 application.yaml 配置文件的机制,用以达到环境差异化部署的效果。我们不难构陷出小故事的主人公阿伟也使用了类似的思路:
● 使用 application.yaml 提供所有环境的共性(和一部分兜底)配置;● 各环境的差异化配置由单独的 application-xxx.yaml 给出,覆盖兜底配置;各差异化配置不作特别的规范要求,允许属性取值不同,也允许引入某个环境特有的属性值;● 为不同环境的镜像编写不同的 Dockerfiles, 环境配置方面的差异主要在于启动应用时指定的参数不同。
一个典型的工程目录看起来像是这样:
看起来很规整,但其实也引入了一些问题:
● 环境差异化配置需要靠人工核对来减少错漏,编写 application.yaml 这类基准配置的时候也需要慎重考虑提供什么样的兜底值,一旦有差错则排查成本相对高;● Dockerfile 往往没有很大的差异,但构建出来的产物是和具体环境强绑定的,没办法复用;多次编译可能因为某些隐患(最典型的比如依赖版本不严格)导致不同环境下的交付内容并不一致,有引入 bug、招致线上问题的风险。 ○ 比如在日常环境下完成构建后,某个(可能是间接)依赖的快照包被更新了(可能是不规范的快照包更新,也可能是安全包之类选择倾向于让接入方无感升级而使用快照版本当作 release);此后部署到预发环境时,构建引用了新版本的依赖包,导致日常环境下的测试验收结论可信度下降。
单应用逐环境晋级方案的考量
吃一堑长一智,我们不妨帮阿伟的应用发布方案列出下面的考量:
● 产物对环境中立:环境差异化配置在部署时注入,一份镜像可以用于所有环境的部署。● 环境配置统一:所有环境使用同样格式的配置模板和差异化的值注入,避免“兜底+覆盖”引入的配置模板差别。
具体来说,在“日常-预发-生产”的整条集成发布流程中,使用的镜像和编排只有一份;镜像中的 SpringBoot 应用里,也只使用 application.yaml,不再引入其他差异化配置。
这样做看起来限制了一些灵活性,但核心考虑在于:通常情况下很难规范配置文件和编排的具体格式;一旦存在“一份配置兜底+多份差异化调整”的情况,理解应用代码逻辑和部署细节的成本会变高,维护、验证应用逻辑所需理解的内容也随配置文件的增加而线性增长。即使是应用的设计者或是 owner,也难免随着时过境迁而忘记一些细节(“我当时为什么要加这个环境变量来着”),更不用提中途加入进行功能迭代的其他开发人员了。
实际部署到 Kubernetes 集群中时,环境变量通过编排中容器的环境变量注入。接下来需要统一 Deployment 编排——如果为不同的环境使用多份编排文件,仍然会引入无意义的重复。这里我们可以使用 Helm chart 的形式,诸如镜像、环境变量等等在构建部署时才能决定的差异化配置,都可以通过 values 配置进行注入:
需要定制化的部分,则是 CICD 系统中动态生成 Values.yaml 配置的脚本。这部分的复杂性相对容易控制,具体的实现则根据使用的 CICD 工具不同而略有差异,我们将会在后文中看到一个概略的示例。
方案改造例
现在可以回到阿伟的服务上进行改造了。
Step 1: 统一 application.yaml 和 Dockerfile
首先我们要压缩服务中的 SpringBoot application yaml 配置,只留下一份:
这里使用了占位符${DEPLOY_ENV}
,要求环境变量提供routing.env
的值。
Dockerfile 则可以去掉所有环境差异化的环境变量定义、统一为一份配置,并假定环境变量都已经正确注入。
Step 2: 编写 Helm chart
从创建一份空的 helm chart 开始:
接下来,可以把原先的编排文件按照 helm 模板的格式简单改写,放置到cool-service-chart/templates/
目录下。以 Deployment 为例:
我们使用.Values.image
这一 helm 占位符将镜像注入容器。环境变量注入的方式则有多种——变量较少的情况下可以在 pod template 中直接定义 name 和 value;不过如果考虑到更长远的扩展性,也可以采用关注点分离的方式,单独定义一份 ConfigMap 用于定义环境变量;这样做的好处,则是添加环境变量的开发者无需理解 Deployment 的具体结构,甚至只需要理解“往 ConfigMap 的数据定义里写一个键值对就能实现环境变量注入”就可以了。
基于这些考虑,我们定义容器使用下面的 ConfigMap 提供键值对、注入环境变量:
Chart 里的模板编写完成后,记得推送到一个 git 库里,方便后面使用。
Step 3: 编写 Values.yaml 生成脚本
在准备好 Helm chart 的静态模板部分之后,需要为 CICD 工具编写部署时生成 Values.yaml 的脚本。我们不妨假设阿伟的团队选择使用 Jenkins 建设 CICD 流水线:
这里我们主要关注 chart-complete.sh,它需要完成如下的任务:
● 从 git 仓库克隆 chart 库的主干;● 从环境变量中,生成 values.yaml.
如果已经搭建了 helm 仓库,这里也可以选择把生成好的 chart 推到仓库。
总结
在单一应用逐环境晋级部署的过程中,往往会涉及到针对环境定制的差异化配置;为了避免 Dockerfile、配置文件等冗余带来的治理成本及 bug 隐患,我们可以利用云原生 IaC 的优势,基于统一的制品和编排定义,将环境的差异化配置项延迟到部署时注入。这样,在各个环境中所部署的代码是完全一致的,提高了集成的可信程度及测试效率。
当然,从头搭建 CICD 体系往往也需要一定的试错;云效应用交付 AppStack 提供了符合前述实践的编排管理、环境治理和差异化配置能力,可以通过集成云效流水线 Flow,快速搭建出整条晋级流程,欢迎大家尝试。若有收获,就点个赞吧!
点击下方链接,免费体验应用交付平台 AppStack。
https://www.aliyun.com/product/yunxiao/appstack?channel=yy_practice
版权声明: 本文为 InfoQ 作者【阿里云云效】的原创文章。
原文链接:【http://xie.infoq.cn/article/1b06d871733131036ff79883f】。文章转载请联系作者。
评论