如何设计一条稳定的应用交付流程?

一次波折的部署

许多个迭代后,面对陪风扇一起嘎吱嘎吱转着的流水线,程序员阿伟会回忆起把系统部署到预发环境、提交最后一轮验收,然后被打回来的那个并不遥远的下午。当时他有一个酷炫的Java SpringBoot应用要上线,实现了酷炫的“在不同部署环境下、发送带环境路由标签的业务消息”的接口:

package com.example.demo.service; import com.example.demo.model.BrilliantInfo; import io.coolmq.sdk.MessageSender; import io.coolmq.sdk.model.TaggedMessage; import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Optional; @Service public class CoolMessageDispatchServiceImpl extends CoolMessageDispatchService {     // 环境路由tag:     //   日常: daily     //   预发:staging     //   杭州区域生产:production-hangzhou     @Value("${routing.env}")     private String environmentIdentifier;     @Autowired     private BrilliantInfoService brilliantInfoService;     @Override     public String saySomething(String brilliantIdentifier) {         // 使用酷炫的入参查询业务信息         BrilliantInfo brilliantInfo = brilliantInfoService.query(brilliantIdentifier);         // 基于业务信息拼装字符串消息体         String messageContent = "Yes, indeed - 'tis called " + Optional.ofNullable(brilliantInfo).map(BrilliantInfo::getName).orElse("oops");         // 构造消息对象并打tag, 保证只有监听对应tag的消费方能收到消息         TaggedMessage messageWrapped = TaggedMessage.builder().content(messageContent).tag(environmentIdentifier);         // 请原谅阿伟,他有点急,调SDK发消息的时候没有捕获异常打日志         MessageSender.send(messageWrapped);     } } 

日常环境的镜像构建、部署和验收测试一路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配置,只留下一份:

# 这里只展示了阿伟关心的路由配置 routing:   env: ${DEPLOY_ENV}

这里使用了占位符`${DEPLOY_ENV}`,要求环境变量提供`routing.env`的值。

Dockerfile则可以去掉所有环境差异化的环境变量定义、统一为一份配置,并假定环境变量都已经正确注入。

Step 2: 编写Helm chart

从创建一份空的helm chart开始:

# 用helm初始化模板目录 helm create cool-service-chart # 把不需要的文件删掉 rm -rf cool-service-chart/templates/* rm -f cool-service-chart/values.yaml

接下来,可以把原先的编排文件按照helm模板的格式简单改写,放置到`cool-service-chart/templates/`目录下。以Deployment为例:

# cool-service-chart/templates/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata:  name: {{ .Release.Name }}-deployment  labels:    app: {{ .Release.Name }} spec:  selector:    matchLabels:      app: {{ .Release.Name }}  template:    metadata:      labels:        app: {{ .Release.Name }}    spec:      containers:      - name: main        image: {{ .Values.image }}        envFrom:        - configMapRef:            name: {{ .Release.Name }}-config

我们使用`.Values.image`这一helm占位符将镜像注入容器。环境变量注入的方式则有多种——变量较少的情况下可以在pod template中直接定义name和value;不过如果考虑到更长远的扩展性,也可以采用关注点分离的方式,单独定义一份ConfigMap用于定义环境变量;这样做的好处,则是添加环境变量的开发者无需理解Deployment的具体结构,甚至只需要理解“往ConfigMap的数据定义里写一个键值对就能实现环境变量注入”就可以了。

基于这些考虑,我们定义容器使用下面的ConfigMap提供键值对、注入环境变量:

# cool-service-chart/templates/envs-configmap.yaml apiVersion: v1 kind: ConfigMap metadata:   name: {{ .Release.Name }}-config data:   # 请大家把环境变量的key和注入占位符统一加到这里   # 加到其他地方的话codereview无条件不通过   DEPLOY_ENV: {{ .Values.deployEnv }}

Chart里的模板编写完成后,记得推送到一个git库里,方便后面使用。

Step 3: 编写Values.yaml生成脚本

在准备好Helm chart的静态模板部分之后,需要为CICD工具编写部署时生成Values.yaml的脚本。我们不妨假设阿伟的团队选择使用Jenkins建设CICD流水线:

pipeline {     agent any     stages {         stage('Unified Build') {             // 在这里做Docker镜像构建和推送,结果保存在环境变量DOCKER_IMAGE_URL中         }         stage('Deploy to Daily') {             environment {                 DEPLOY_ENV_VALUE = "daily"             }             // 生成values, 补全helm chart             sh "chart-complete.sh"             // 部署到环境             sh "deploy-to-env.sh"         }         stage('Staging Approval') {             // 用input插件做一个卡点             input "Deploy to staging?"         }         stage('Deploy to Staging') {             environment {                 DEPLOY_ENV_VALUE = "staging"             }             // 内容和部署日常完全一样,差别主要在于stage的环境变量             sh "chart-complete.sh"             sh "deploy-to-env.sh"         }         // 后面还有生产环境的人工卡点和部署,不再赘述     } } 

这里我们主要关注chart-complete.sh,它需要完成如下的任务:

  • 从git仓库克隆chart库的主干;

  • 从环境变量中,生成values.yaml.

#!/bin/sh # 准备git账户等 # 克隆chart git clone git@gitlab.cool-inc.com:cool-group/cool-service-chart.git # 生成values.yaml touch cool-service-chart/values.yaml echo "image: $DOCKER_IMAGE_URL" >> cool-service-chart/values.yaml echo "deployEnv: $DEPLOY_ENV_VALUE" >> cool-service-chart/values.yaml # chart打包,用于后面的helm本地包部署 tar -cvzf cool-service-chart.tgz ./cool-service-chart 

如果已经搭建了helm仓库,这里也可以选择把生成好的chart推到仓库。

总结

在单一应用逐环境晋级部署的过程中,往往会涉及到针对环境定制的差异化配置;为了避免Dockerfile、配置文件等冗余带来的治理成本及bug隐患,我们可以利用云原生IaC的优势,基于统一的制品和编排定义,将环境的差异化配置项延迟到部署时注入。这样,在各个环境中所部署的代码是完全一致的,提高了集成的可信程度及测试效率。

当然,从头搭建CICD体系往往也需要一定的试错;云效应用交付AppStack提供了符合前述实践的编排管理、环境治理和差异化配置能力,可以通过集成云效流水线Flow,快速搭建出整条晋级流程,欢迎大家尝试。若有收获,就点个赞吧!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值