大家好,我叫王泊,负责云效应用交付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

我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚
Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack
在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/