Serverless的微服务持续交付案例

本文是GitChat《Serverless 微服务的持续交付》部分内容已做修改。文章聊天实录请见:“顾宇:Serverless 微服务的持续交付解析

Serverless 风格微服务的持续交付(上):架构案例”中,我们介绍了一个无服务器风格的微服务的架构案例。这个案例中混合了各种风格的微服务。

架构图如下:

在这个架构中,我们采用了前后端分离的技术。我们把 HTML,JS, CSS 等静态内容部署在 S3 上,并通过 CloudFront 作为 CDN 构成了整个架构的前端部分。我们把 Amazon API Gateway 作为后端的整体接口连接后端的各种风格的微服务,无论是运行在 Lambda 上的函数,还是运行在 EC2 上的 Java 微服务,他们整体构成了这个应用的后端部分。

从这个架构图上我们可以明显的看到 前端(Frontend)和后端(Backend)的区分。

持续部署流水线的设计和实现

任何 DevOps 部署流水线都可以分为三个阶段:待测试,待发布,已发布

由于我们的架构是前后端分离的,因此我们为前端和后端分别构造了两条流水线,使得前后端开发可以独立。如下图所示:

(整体流水线)

在这种情况下,前端团队和后端团队是两个不同的团队,可以独立开发和部署,但在发布的时候则有些不同。由于用户是最后感知功能变化的。因此,为了避免界面报错找不到接口,在新增功能的场景下,后端先发布,前端后发布。在删除功能的场景下,前端先发布,后端后发布。

我们采用 Jenkins 构建我们的流水线,Jenkins 中已经含有足够的 AWS 插件可以帮助我们完成整个端到端的持续交付流水线。

前端流水线

前端持续交付流水线如下所示:

前端流水线的各步骤过程如下:

  1. 我们采用 BDD/ATDD 的方式进行前端开发。用 NightWatch.JS 框架做 端到端的测试,mochachai 用于做某些逻辑的验证。
  2. 我们采用单代码库主干(develop 分支)进行开发,用 master 分支作为生产环境的部署。生产环境的发布则是通过 Pull Request 合并的。在合并前,我们会合并提交。
  3. 前端采用 Webpack 进行构建,形成前端的交付产物。在构建之前,先进行一次全局测试。
  4. 由于 S3 不光可以作为对象存储服务,也可以作为一个高可用、高性能而且成本低廉的静态 Web 服务器。所以我们的前端静态内容存储在 S3 上。每一次部署都会在 S3 上以 build 号形成一个新的目录,然后把 Webpack 构建出来的文件存储进去。
  5. 我们采用 Cloudfront 作为 CDN,这样可以和 S3 相互集成。只需要把 S3 作为 CDN 的源,在发布时修改对应发布的目录就可以了。

由于我们做到了前后端分离。因此前端的数据和业务请求会通过 Ajax 的方式请求后端的 Rest API,而这个 Rest API 是由 Amazon API Gateway 通过 Swagger 配置生成的。前端只需要知道 这个 API Gateway,而无需知道API Gateway 的对应实现。

后端流水线

后端持续交付流水线如下所示:

后端流水线的各步骤过程如下:

  1. 我们采用“消费者驱动的契约测试”进行开发,先根据前端的 API 调用构建出相应的 Swagger API 规范文件和示例数据。然后,把这个规范上传至 AWS API Gateway,AWS API Gateway 会根据这个文件生成对应的 REST API。前端的小伙伴就可以依据这个进行开发了。
  2. 之后我们再根据数据的规范和要求编写后端的 Lambda 函数。我们采用 NodeJS 作为 Lambda 函数的开发语言。并采用 Jest 作为 Lambda 的 TDD 测试框架。
  3. 和前端一样,对于后端我们也采用单代码库主干(develop 分支)进行开发,用 master 分支作为生产环境的部署。
  4. 由于 AWS Lambda 函数需要打包到 S3 上才能进行部署,所以我们先把对应的构建产物存储在 S3 上,然后再部署 Lambda 函数。
  5. 我们采用版本化 Lambda 部署,部署后 Lambda 函数不会覆盖已有的函数,而是生成新版本的函数。然后通过别名(Alias)区分不同前端所对应的函数版本。默认的 $LATEST,表示最新部署的函数。此外我们还创建了 Prod,PreProd, uat 三个别名,用于区分不同的环境。这三个别名分别指向函数某一个发布版本。例如:函数 func 我部署了4次,那么 func 就有 4个版本(从1开始)。然后,函数 func 的 $LATEST 别名指向 4 版本。别名 PreProd 和 UAT 指向 3 版本,别名 Prod 在 2 版本。
  6. 技术而 API 的部署则是修改 API Gateway 的配置,使其绑定到对应版本的函数上去。由于 API Gateway 支持多阶段(Stage)的配置,我们可以采用和别名匹配的阶段绑定不同的函数。
  7. 完成了 API Gateway 和 Lamdba 的绑定之后,还需要进行一轮端到端的测试以保证 API 输入输出正确。
  8. 测试完毕后,再修改 API Gateway 的生产环境配置就可以了。

部署的效果如下所示:

(API Gateway + Lambda 配置)

无服务器微服务的持续交付新挑战

在实现以上的持续交付流水线的时候,我们踩了很多坑。但经过我们的反思,我们发现是云计算颠覆了我们很多的认识,当云计算把某些成本降低到趋近于 0 时。我们发现了以下几个新的挑战:

  1. 如果你要 Stub,有可能你走错了路。
  2. 测试金字塔的倒置。
  3. 你不再需要多个运行环境,你需要一个多阶段的生产环境 (Multi-Stage Production)。
  4. 函数的管理和 Nanoservice 反模式。

Stub ?别逗了

很多开发者最初都想在本地建立一套开发环境。由于 AWS 多半是通过 API 或者 CloudFormation 操作,因此开发者在本地开发的时候对于AWS 的外部依赖进行打桩(Stub) 进行测试,例如集成 DynamoDB(一种 NoSQL 数据库),当然你也可以运行本地版的 DynamoDB,但组织自动化测试的额外代价极高。然而随着微服务和函数规模的增加,这种管理打桩和构造打桩的虚拟云资源的代价会越来越大,但收效却没有提升。另一方面,往往需要修改几行代码立即生效的事情,却要执行很长时间的测试和部署流程,这个性价比并不是很高。

这时我们意识到一件事:如果某一个环节代价过大而价值不大,你就需要思考一下这个环节存在的必要性。

由于 AWS 提供了很好的配置隔离机制,于是为了得到更快速的反馈,我们放弃了 Stub 或构建本地 DynamoDB,而是直接部署在 AWS 上进行集成测试。只在本地执行单元测试,由于单元测试是 NodeJS 的函数,所以非常好测试。

另外一方面,我们发现了一个有趣的事实,那就是:

测试金字塔的倒置

由于我们采用 ATDD 进行开发,然后不断向下进行分解。在统计最后的测试代码和测试工作量的的时候,我们有了很有趣的发现:

  • End-2-End (UI)的测试代码占30%左右,占用了开发人员 30% 的时间(以小时作为单位)开发和测试。
  • 集成测试(函数、服务和 API Gateway 的集成)代码占 45%左右,占用了开发人员60% 的时间(以小时作为单位)开发和测试。
  • 单元测试的测试代码占 25%左右,占用了10%左右的时间开发和测试。

一开始我们以为我们走入了“蛋筒冰激凌反模式”或者“纸杯蛋糕反模式”但实际上:

  1. 我们并没有太多的手动测试,绝大部分自动化。除了验证手机端的部署以外,几乎没有手工测试工作量。
  2. 我们的自动化测试都是必要的,且没有重复。
  3. 我们的单元测试足够,且不需要增加单元测试。

但为什么会造成这样的结果呢,经过我们分析。是由于 AWS 供了很多功能组件,而这些组件你无需在单元测试中验证(减少了很多 Stub 或者 Mock),只有通过集成测试的方式才能进行验证。因此,Serverless 基础设施大大降低了单元测试的投入,但把这些不同的组件组合起来则劳时费力 。如果你有多套不一致的环境,那你的持续交付流水线配置则是很困难的。因此我们意识到:

你不再需要多个运行环境,你只需要一个多阶段的生产环境 (Multi-Stage Production)

通常情况下,我们会有多个运行环境,分别面对不同的人群:

  1. 面向开发者的本地开发环境
  2. 面向测试者的集成环境或测试环境(Test,QA 或 SIT)
  3. 面向业务部门的测试环境(UAT 环境)
  4. 面向最终用户的生产环境(Production 环境)

然而多个环境带来的最大问题是环境基础配置的不一致性。加之应用部署的不一致性。带来了很多不可重现问题。在 DevOps 运动,特别是基础设施即代码实践的推广下,这一问题得到了暂时的缓解。然而无服务器架构则把基础设施即代码推向了极致:只要能做到配置隔离和部署权限隔离,资源也可以做到同样的隔离效果。

我们通过 DNS 配置指向了同一个的 API Gateway,这个 API Gateway 有着不同的 Stage:我们只有开发(Dev)和生产(Prod)两套配置,只需修改配置以及对应 API 所指向的函数版本就可完成部署和发布。

然而,多个函数的多版本管理增加了操作复杂性和配置性,使得整个持续交付流水线多了很多认为操作导致持续交付并不高效。于是我们在思考:

对函数的管理和“ Nanoservices 反模式 ”

根据微服务的定义,AWS API Gateway 和 Lambda 的组合确实满足 微服务的特征,这看起来很美好。就像下图一样:

但当Lambda 函数多了,管理众多的函数的发布就成为了很高的一件事。而且, 可能会变成“Nanoservice 反模式”:

Nanoservice is an antipattern where a service is too fine-grained. A nanoservice is a service whose overhead (communications, maintenance, and so on) outweighs its utility.

如何把握微服务的粒度和函数的数量,就变成了一个新的问题。而 Serverless Framework ,就是解决这样的问题的。它认为微服务是由一个多个函数和相关的资源所组成。因此,才满足了微服务可独立部署可独立服务的属性。它把微服务当做一个用于管理 Lambda 的单元。所有的 Lambda 要按照微服务的要求来组织。Serverless Framework 包含了三个部分:

  1. 一个 CLI 工具,用于创建和部署微服务。
  2. 一个配置文件,用于管理和配置 AWS 微服务所需要的所有资源。
  3. 一套函数模板,用于让你快速启动微服务的开发。

此外,这个工具由 AWS 自身推广,所以兼容性很好。但是,我们得到了 Serverless 的众多好处,却难以摆脱对 AWS 的依赖。因为 AWS 的这一套架构是和别的云平台不兼容的。

所以,这就又是一个“自由的代价”的问题。

CloudNative 的持续交付

在实施 Serverless 的微服务期间,发生了一件我认为十分有意义的事情。我们客户想增加一个很小的需求。我和两个客户方的开发人员,客户的开发经理,以及客户业务部门的两个人要实现一个需求。当时我们 6 个人在会议室里面讨论了两个小时。讨论两个小时之后我们不光和业务部门定下来了需求(这点多么不容易),与此同时我们的前后端代码已经写完了,而且发布到了生产环境并通过了业务部门的测试。由于客户内部流程的关系,我们仅需要一个生产环境发布的批准,就可以完成新需求的对外发布!

在这个过程中,由于我们没有太多的环境要准备,并且和业务部门共同制定了验收标准并完成了自动化测试的编写。这全得益于 Serverless 相关技术带来的便利性。

我相信在未来的环境,如果这个架构,如果在线 IDE 技术成熟的话(由于 Lambda 控制了代码的规模,因此在线 IDE 足够),那我们可以大量缩短我们需求确定之后到我功能上线的整体时间。

通过以上的经历,我发现了 CloudNative 持续交付的几个重点:

  1. 优先采用 SaaS 化的服务而不是自己搭建持续交付流水线。
  2. 开发是离不开基础设施配置工作的。
  3. 状态和过程分离,把状态通过版本化的方式保存到配置管理工具中。

而在这种环境下,Ops工作就只剩下三件事:

  1. 设计整体的架构,除了基础设施的架构以外,还要关注应用架构。以及优先采用的 SaaS 服务解决问题。
  2. 严格管理配置和权限并构建一个快速交付的持续交付流程。
  3. 监控生产环境。

剩下的事情,就全部交给云平台去做。


更多精彩洞见,请关注微信公众号:思特沃克

Share

持续交付模式下的安全活动

在上一篇文章《开发团队面临的三大安全挑战》中,我们对现如今敏捷精益团队所面临的安全挑战进行了总结和分析,这三大挑战分别是:

  1. 一次性的安全检查无法匹配持续性的交付模式
  2. 缺乏自动化、自助化的支持,安全实践落地难
  3. 高耸的部门墙让开发和安全团队难以进行高效的协作

在接下来的几篇文章中,我们将逐一为你介绍团队、组织应该如何应对这些挑战。本篇文章先来讲讲如何解决第一个挑战。

采用持续性的、轻量级的,能够融入到持续交付模式的安全活动

对于绝大多数团队而言,为了确保开发出来的应用具有足够的安全性,渗透测试是一个被广泛采用的手段,也可以说是唯一依赖的手段。然而由于渗透测试比较重量级,通常只能提供一次性的安全反馈,而这在追求快速开发、迅速响应市场变化的敏捷精益开发方式下,它的不足被放大了。

团队需要的是一个高效的安全质量反馈机制。所谓“高效”是指,这个机制必须能以更快的速度、更高的频率提供应用的安全质量反馈,并且要足够轻量级以便于无缝融入到迭代交付过程中。

那么“更快”是多快?多频繁才算“更高的频率”?“轻量级”要轻到什么程度?

快到“立等可取”,比如只需几分钟甚至更少时间,就能知道应用的安全质量如何;频繁到任意时刻都可以获取一次安全质量反馈,比如每次代码提交后,都能知道应用安全质量是否被破坏;轻量级到团队不认为这些安全活动会带来多大的额外付出,比如每日代码审查中,顺便从安全的角度对代码进行评审,就能阻止有安全风险的代码被提交到代码仓库,而整个过程可能只多花费了几分钟时间而已。

一些推荐采用的安全活动

在CI中集成自动化安全扫描工具

其实在很早以前大家就已经发现,凭借人工的力量从应用里寻找安全漏洞异常耗时,另外对于某些安全漏洞,完全可以通过特征识别的方式对脚本来自动进行检测,于是从那时起就诞生了一系列自动化安全扫描工具,以下简称“漏扫工具”。

与此同时,在应用开发这件事情上,持续集成、持续交付的理念也在迅速的被广泛接纳,越来越多的团队开始使用CI,也体会到了自动化的威力所带来的好处。

随着CI的广泛普及,团队完全可以把这些漏扫工具集成到CI当中,作为应用构建过程中的一个标准步骤来执行。这样,团队既可以借助漏扫工具以节约人工成本,又可以持续性的对应用安全质量进行监控:一旦某次构建发现安全问题,构建流水线就会失败,引起开发团队的注意,促使团队尽快对有问题的代码进行修复,从而降低漏洞修复成本。

漏扫工具数量众多,可以说是百家争鸣,不过可惜的是,易于集成到CI中的却不多。OWASP ZAP提供了RESTful API,也有Jenkins插件,算是做得比较不错的漏扫工具,而BurpSuite要做到同样的效果还需要额外的配置才行。推荐团队可以先尝试把ZAP集成到CI中。关于具体的集成细节,我们会通过后续文章进行介绍。

编写自动化安全功能性测试用例

漏扫工具不是全能的,有些类型的安全漏洞,比如身份认证、访问控制以及和业务强相关的漏洞,它就很难甚至根本无法扫出来。然而,这些漏扫工具不容易扫出来的安全漏洞,对于人来讲却正好是小菜一碟。

举个例子,人能够很好的理解下面这个API应当具备的安全行为,并对其进行有针对性的测试,然而漏扫工具则已经哭晕在厕所:

漏扫工具检查不出来的安全问题,人可以很好的进行测试,并且依然通过自动化来提高效率。团队可以像平常编写集成测试,或者端到端功能性测试那样,对于期望应用应当具备的安全行为,编写对应的测试用例进行覆盖。我们把这种做法叫做编写自动化安全功能性测试用例。

随着自动化安全功能性测试用例数量的不断累积,它的威力也将越来越明显,尤其是在对应用进行回归测试的时候,更是显露无疑。通常而言,在团队每次做应用发布之前,都会进行一次回归测试,主要目的是确认新功能工作正常,与此同时已有功能未被破坏。安全作为应用质量中的重要组成部分,也应该进行一次回归测试。相比于传统的渗透测试,自动化安全功能性测试用例再配合上CI中的自动化漏扫工具,在很短的时间里就能对应用进行比较全面的安全检查,为应用发布提供决策支持。

识别安全需求,并将其作为验收标准写入到用户故事卡

如果说把漏扫工具集成到CI,以及编写自动化安全功能性测试用例是在“把事情做对”,那么识别安全需求,并把它作为验收标准写入到用户故事卡中则是在“做正确的事情”。

团队其实是很重视安全的,他们愿意付出努力提升应用的安全性,然而这又和我们实际观察到的现状有冲突:安全的事情大家心知肚明,但就是没人主动去做。

为什么?原因是多方面的,其中一个重要的原因是,因为安全需求没有被明确提出来,它既不在故事卡的验收标准里,也没有在给故事卡估点的时候被考虑进去。于是安全需求就仿佛变成了“多出来的”工作量,一旦团队面临交付压力,这部分工作自然就会被无限期的往后推迟,最后的归宿就是不了了之。

因此,团队除了需要借助漏扫工具以及自动化安全性功能测试用例,还需要把安全需求明确出来,纳入到项目交付范围内。团队可以用威胁建模、恶意攻击场景头脑风暴等活动来梳理安全需求。

此外需要注意的是,安全需求的表现形式不是最重要的,最关键的在于把需求在团队内部明确出来,而不是大家心照不宣。比如,安全需求是写入到故事卡的验收标准里,还是单独创建安全故事卡,这本身并不重要,重要的是安全需求通过这些形式能得到明确,让团队能把安全需求和常规的业务需求一起放到Backlog里,统一对它们估点、设置优先级、安排迭代交付计划。

小结

敏捷精益团队面临的第一大安全挑战就是一次性的安全检查无法匹配持续性的交付模式。应对这一挑战,团队需要采用一系列持续性的、轻量级的,能够融入到持续交付模式的安全活动,从而使得团队建立起一个高效获取应用安全质量反馈的机制。

至于敏捷精益团队如何应对另外两大安全挑战,我们将在后续的文章里一一详解,敬请期待。


更多精彩洞见,请关注微信公众号:思特沃克

Share

DevOps实践-打造自服务持续交付-下

本文首发于InfoQ:

http://www.infoq.com/cn/articles/devops–build-self-service-continuous-delivery-part02

上一篇文章中,主要讲了DevOps转型的动机、策略和方法,本文将会为大家带来更多DevOps转型的落地策略和实践。

实践过程

下图是我们为团队设计的持续交付流水线,目的是能让Platform团队和交付团队之间的触点能够被融入到持续交付流水线中,并且以基础设施代码作为协同媒介,通过自动化的方式实现开发与运维(即基础设施与软件系统)的无缝对接。

我们来看看我们给持续交付流水线赋予了哪些能力:

  1. 站在交付团队的视角,我们决定将基础设施构建,流水线构建,部署等活动都代码化,与应用代码放在同一个代码仓库中。
  2. 交付团队通过提交我们的基础设施代码到仓库后,自动触发持续交付工具创建或更新流水线。
  3. 接着会自动触发构建,静态检查,测试覆盖率校测,代码规范验证等任务,最终输出构建产物并将构建产物推送到仓库。
  4. 然后会根据交付团队对基础设施和环境的定义到当前要部署的网络环境中去创建或更改虚拟机、网络、存储方式等
  5. 最后,当基础设施创建成功以后,就会去仓库下载指定版本的构建产物进行最终的部署活动。

但需要注意的是:

  1. 为了持续优化交付流程,我们对开发的许多活动进行的数据收集和分析,以报表的形式去分析展示代码提交频率,系统和代码的质量情况,缺陷和构建情况等,帮助团队找到自己的瓶颈或问题。
  2. 帮助团队能够实时监控自己应用的运行状态,设计和查看不同纬度的日志总汇等。

那我们来看看通过什么技术可以实现这样的持续交付流程:

我们选择了一种轻量级、低耦合的技术组合Ansible+Jenkins+AWS。我认为其核心是Ansible。

下面我们来看看Ansible可以帮助我们做些什么:

  1. 创建和更改AWS中的资源
  2. 自动化部署和基础设施测试
  3. 建立开发与平台团队之间的沟通体系

考虑到基于yaml语法的Ansible配置简洁且易读,所以我们选择直接用它作为提供给交付团队的公有DSL模板,利用Ansible Playbook的模块化思想将开发团队的职责和平台团队的职责很清晰的分离,平台团队关注Ansible提供给交付团队的服务是否满足需求和DSL模板是否易用,而交付团队只用关注如何基于公有DSL去定制自己的基础设施,环境依赖和部署等。

于此同时也满足了很多开发对于Ansible和AWS的兴趣和热情,更使得之后在交付团队落地变得更容易。

接下来通过一个实例来看看:

左边是Platform团队的仓库,这个仓库里面包含了创建基础设施、环境配置和部署的实现。

右边是交付团队的仓库,其中deployment目录下,是公有的DSL模板,其中包含多种环境(开发、测试、预生产环境等的独立配置),以及一套基于DSL的代码模板,其中包含创建基础设施和部署应用这两部分DSL代码模板。

接下来,我们来看看它们配合与集成的方式:

他们会在持续集成流水线中被动态组合到一起:

  1. 在创建基础设施和部署的时候会分别拉取基础设施代码库和应用代码库。
  2. 此时应用代码为调用入库,公有基础设施为功能框架库,两者配合,完成环境的创建和应用部署。

在做微服务的团队,接受度非常高,能够快速上手,而且甚至有团队因为自身的一些需求,自己去写一些Ansible模块,然后向我们发起pull request。

当然,我们在推广这套流程的过程中发现,一些实践能够帮助我们更快速落地:

  1. DevOps团队的成员由各交付团队和原运维团队组成,这样的组成方式,能够保证团队的视角可以关注到整个持续交付过程的每个环节。
  2. 交付团队成员与DevOps团队成员定期轮岗制,DevOps小组中的文化(如自动化优先)可以蔓延开,让交付团队更快适应。
  3. 结对、Showcase和培训,主要目的是知识的传递,让更多地团队逐步采用新的交付模式,得到更多改进中的反馈。
  4. 提供给交付团队的自服务代码仓库对每个人开放,交付团队被授权优化、新增基础设施,让DevOps文化和职责落地到交付流程中。

现在来看,集中式、审批式、被动响应请求的中央运维团队不再是整个交付流程中的依赖和瓶颈,已基本转向带自服务化、审查式、主动优化的去中心化交付团队:

我们通过技术驱动改进,让团队之间的合作方式发生了巨大改变,开发与运维之间的那道墙也渐渐消失,以前被动响应请求中央运维团队逐步被平台团队所替代,平台团队中一部分人会负责基础设施平台的发展,负责公有云与企业内部系统的对接、完善安全、灾备、提供基础设施的自服务机制,另一部分人会为产品团队提供可定制的工作、平台、并为产品团队赋能。这时交付团队开始管理自己的环境、维护流水线、负责生产环境变更。

在推广和落地自服务持续交付流程的过程中,我们也遇到了很多遗留系统和复杂部署应用的交付团队,他们无法直接对接这套交付流程。

例如有一个40-50人的团队,它是基于AEM开发整个公司所有的前端门户,AEM是Adobe公司的CMS系统,其安装和部署很复杂,以前都是通过手工安装和拷贝的方式进行部署,而且他们在开发-》测试-》部署阶段可能会动态扩张多套环境来支持,且每次代码变更的提交都会对已经安装的AEM进行修改、配置、重启等操作。

整个开发和测试流程都很复杂,而且效率很低,出现问题和故障的风险也很大,如果我们直接利用Ansible把AEM的安装和部署过程都自动化,由于AEM本身部署的复杂性,可以预见以后这部分更新和维护的工作还是很难交由交付团队自治。所以我们第一步要做的就是为其设计新的持续交付流水线,然后在这个流程中去定义和识别两个团队的职责和关注重心,最后再通过打造高效的自服务使整个交付流程得到改进。

首先我们根据校服团队提交变更的平率,从低到高依次定义了三条持续集成流水线(如下图):

  1. 创建和测试基础设施资源
  2. 配置基础设施资源和环境
  3. 部署应用程

因为AEM安装和更新很复杂,所以我们引入了镜像技术。基础设施和基础设施配置两条流水线的产物为一个image,应用流水线在部署阶段会去检查是否存在新的环境镜像,如果存在,就会基于快速创建一个新的AEM环境,然后进行应用代码的部署。

通过新的自动化持续交付流水线大大加速了AEM团队的开发和测试速度,也使得整个环境更加可控和易维护。对于交付团队来说,他们可以自己去维护包括基础设施、环境变更和应用部署等全生命周期交付活动。对于Platform团队来说,只用去考虑镜像的生命周期管理,如何去优化镜像的创建速度等,这些可以帮助到更多其它团队解决类似问题的领域。对于这种特殊情况,我们尽管引入很多与大多数团队不同的交付流程和技术,但所有的工作和优化都是基于之前打造的自服务持续交付流程、协议和工具平台之上的,保证了不同的交付团队与Platform的配合方式的一致性。

实践启示

通过在大量交付团队落地基于自服务的持续交付流程,两种团队的职责更加清晰了:

所有好的实践都必须考虑规模化的问题,如果无法大规模的被接受和落地,再好的实践也没用。对于咱们这个转型的过程,我也给出一个套路:(如下图)

有了套路,接下来总结一下应用这个套路进行DevOps转型过程中的一些经验和思考:

  1. 易用的通用DSL模板设计,提供交付与Platform团队统一的DSL模板(build and update anything)。
  2. 构建通用持续交付流水框架,提供给交付团队定制化流水线的能力,使流水线主要关注点始终在产品的成功交付。
  3. 以技术驱动DevOps文化大面积传播,让Platform团队成员走入交付团队,协作改进、知识传递,确保实践落地。
  4. 将一切自动化、自服务化。交付团队应该被授权优化、新增基础设施服务,让DevOps能力和职责在交付团队落地生根。

最后,我提取了5点对我们来说非常重要的策略或是推进方法:

  1. 小步快跑,在有大方向的基础上,需要将每一步改变都设计得足够小,这样才能足够快的去改进。
  2. 交付团队赋能,给每个人都留一扇门,在他意识到要做些事情的时候,可以很快付诸行动。
  3. 逐步用基础设施自服务化替代运维部门的审批流程。 建立持续反馈和改进机制。
  4. 以DevOps团队为杠杆,撬动更大范围自服务交付。

非常感谢你的耐心阅读,希望我的文章能够给你带来哪怕一点点启示。有任何问题或是想与我讨论的点都可以给我发Emial: jxzhong@thoughtworks.com


更多精彩洞见,请关注微信公众号:思特沃克

Share

DevOps实践-打造自服务持续交付-上

本文首发于InfoQ:

http://www.infoq.com/cn/articles/devops–build-self-service-continuous-delivery-part01

DevOps转型的动机

我们的客户是一家海外本土最大的金融保险集团,他们在发展到一定规模以后,意识到自己就像一头笨重的大象,举步维艰,通过对整个交付流程的思考和分析,发现了以下一些严重影响交付速度的问题:

  1. 一些好的关于产品改善和创新的想法很难落地。涉及到一些遗留系统的配合:调整、部署、扩展等,使团队对发布没有信心。新的服务或者应用的构建,很难快速上线,被卡在了生产环境部署阶段。
  2. 各种不同种类的应用、服务的部署方式和流程不一致。运维部门作为一个支持部门,很难为大量团队提供快速反应。运维人员对于需要部署和运营环境之上的产品也不够了 解。
  3. 微服务运营过程中,交付团队难以做到快速集成和部署。运维团队对微服务的部署运维方式不理解,依旧老瓶装新药,很难适配新架构下的交付模式。开发团队大多关注代码和架构,对于产品如何能在生产环境稳定运行、需要考虑哪些安全性和可持续性的因素并不是很了解。

问题分析和挑战

通过对这些问题和各个团队反馈的深入分析,发现其中最大的瓶颈在于交付团队与运维部门之间的各种依赖和沟通浪费,而这个瓶颈又是解决大多数问题的前提。

我们将瓶颈具象化后(如上图),可以看到两种团队之间其实是存在一堵墙的,一是因为传统的部署流程非常繁琐和低效。二是因为两种角色关注点和目标的不一致。

如果在这样的情况下,想实现微服务架构转型,实现更快速和安全的交付,只会更快的暴露出这堵墙引起的各种问题。开发阶段,系统的架构和依赖环境都是Developer说了算,对生产环境的关注度不高。部署、发布阶段,Operations会考虑如何构建一套稳定的基础设施,又如何去部署和运维开发的产物,但是往往对于产物的了解不充分,对于产物的周边生态和与它们关系的了解也不够。

那么引入DevOps文化,消除开发与运维之间的壁垒,逐步打造更高效的交付流程就成为我们破局的关键,那我们应该怎么做呢?

改革之初,我们发现并去尝试了Bimodel(双模IT), 我们看看它是否能解决我们的问题:

先简单介绍一下什么是双模IT:

它将IT系统分成了两种模式:

  1. 一种是新型的数字化、高市场适应性的IT,这部分业务聚焦企业新市场和业务的开拓,创新和发展,强调IT自身对于市场的高适应力。
  2. 另外一种模式下,我们则需要稳固发展,对于传统模式我们倾向于更加严谨和标准的流程去保护现有业务,稳定性比速度更加重要。

我们从采用这样一种模式的实践案例中发现:组织内部会出现连两种速度的交付流程,好的情况可能是采用敏捷开发流程的交付线,有着快速的交付能力,相反,对于继续采用传统开发流程和运维方式的团队,保持着稳定但低效的交付能力。

从业界很多公司的现状和发展趋势来看,双模IT确实是很多组织存在的现状或是必然经历的过程,但不是一个好的模式,从实际的交付过程来看,存在4点问题:

  1. 双模IT的划分方式更多是基于软件系统,而不是从业务活动出发进行的,所有软件系统的交付都应该是面向业务价值的。
  2. 双模IT会让我们误以为速度的提升会引起质量的下降,但是对于我们在ThoughtWorks的很多敏捷实践中学到的:随着交付的推进,质量内建是团队共同负责和持续改进的重点。交付速度的提升,往往都伴随着质量的保障,而不是忽视质量。
  3. 实际生产中,一个新的产品或者功能往往会依赖很多遗留系统提供的服务,如果它们仅仅只能达到稳定和低效的交付,对企业来说对市场整体的响应能力也会越来越低。
  4. 企业的创新不仅仅只是从零创造一个新的产品,还有很多机遇现有的资源。一个新的系统和功能往往不仅会既涉及到新服务、应用的创建,也会涉及到遗留系统的修改,调整和改造。

由此可见双模IT是在以一种试图掩盖问题的方式来逃避目前最重要的问题:开发和运维之间的壁垒。感觉像是一个病人先是放弃治疗,然后又努力的寻找延长寿命的方法,有些隐患终会爆发。

打造自服务持续交付

紧接着,我们开始采用了一种看似不可行的方式开启了DevOps转型,建立公有DevOps团队。有很多人可能会说这是一种反模式,怎么可能会建立一个团队专做DevOps相关的事情,那和以前的运维部门又有什么区别?DevOps提倡的Dev与Ops高频度合作的文化是不是就无法大面积传播了?

因此我们需要很明确的定义我们对这个团队的期望和它的职责是什么,它是怎样和交付团队合作,影响交付团队,最终能让DevOps的文化可以大面积传播。这个团队的目标是要像杠杆一样,翘起更大面积的DevOps变革。

所以我们认为公有DevOps团队应该与其它的端到端交付团队的人员构成是一样的。不同的只是目标和价值,主要体现在帮助更多的团队植入DevOps文化、优化持续交付流程。最终达到的目标是每个团队都可以自治,每个团队都可以进行端到端的开发、测试和部署,并可以自驱动的持续改进。与此同时,这个团队不仅仅只是为交付团队提供更多涉及基础设施、持续交付流水线、部署等活动所需要的自动化能力,还会支撑交付团队根据自身的上下文去定制和规划自己的持续交付流程和部署策略等。

(图片来自:http://t.cn/R9jnzHR)

现在,相比于DevOps团队的叫法,我们更愿意称呼这个团队为Platform团队,一个原因是我之前所说的避免被错误理解,另一个原因是随着各个交付团队逐步实现自服务持续交付,这个专有团队也有了更高的目标:持续打造和优化一个能够支持各交付团队快速交付的平台。

当时,我们首先为团队定义了新的工作方式:以自服务,自动化和协作 作为核心文化,希望团队通过提供便捷的基础服务,让交付团队拥有自动化的交付流水线,并通过更多的沟通协作,尽可能让每个交付团队都能够独立自主的设计、创建和更改自己的基础设施。然后再根据各个交付团队的实施情况和结果来对流程和服务持续改进。

所以第一件事,我们首先设计了一个高效的持续交付流水线,让Platform团队和交付团队建立触点:

如下图所示,蓝色的基因链为交付团队的持续交付环,红色的基因链为平台团队的持续交付环。两种团队以某种低耦合的弱连接进行全程协作,Platform团队在整个端到端的交付过程中都要能尽量通过构建自动化能力来支撑交付团队能够快速、安全的进行持续集成、部署等活动。这样的合作方式也给我们提供了优化触点的可能性,也能够通过优化和改进,缩小这个持续交付周期,让交付更高效。

请原谅我在这篇文章进入高潮时卖个关子,由于考虑到大家的阅读体验,所以文章分成了上、下两个部分,上半部分主要讲DevOps转型的动机、策略和方法,下半部分会讲我们如何实际应用基础设施即代码来建立Platform团队和交付团队的触点, 又怎样让遗留系统团队和微服务团队的交付速度成倍增加。敬请期待!


更多精彩洞见,请关注微信公众号:思特沃克

Share

遗留系统流水线的改进

持续集成(Continuous Integration)是一种软件开发实践,它倡导开发团队频繁地进行系统集成,每一次的集成都可以通过流水线(Pipeline)快速验证。

(图片来自:http://t.cn/Rajfxek)

和传统的集成方式相比,持续集成可以有效地缩短反馈周期、提高软件质量、降低开发成本。这种开发实践也越来越为更多的开发者所接受。对于一个有七年历史的项目,非常幸运的是我们在项目刚开始就使用了持续集成,这也是我们可以长期、稳定地给客户交付高质量软件的保障之一,但是有时我们在项目中也经常会听到一些这样的声音:

  • 每次想提交代码的时候都没有机会, 我们是不是要考虑引入提交“令牌”机制,拥有“令牌”的人才能提交
  • 这个Pipeline已经挂了这么久了,今天估计是无包可测了
  • 这个是测试“随机挂”,重新触发一下Pipeline就好了
  • 我的提交Break了Pipeline了吗?我确认一下
  • ….

通过分析我们发现,这些声音背后的真相更是残酷:

  • 约20对Pair依赖的核心Pipeline构建时间超过1个小时,开发的反馈周期长,大量的半成品积累在本地开发环境和Pipeline上
  • 代码提交之后大约需要2个小时才能出包,每周平均每天可供QA测试的包数量不足1个,平均每个Story的周期时间长
  • 上线前冻结代码、回归测试的时间大约需要2周左右,冻结期间产生的代码无法集成、验证
  • Pipeline不稳定测试导致某些Pipeline构建至少需要重新触发2到3次左右
  • 依赖关系复杂,牵一发而动全身,优化不知从何做起

有时“正确地做事情”比“做正确的事情”还要困难,在项目一开始便在项目中尝试实施TDD等敏捷开发实践,但是随着项目的规模的增加,功能越来越丰富,单元测试在增加、基于UI的功能性测试也在增加,流水线的构建速度却变得越来越慢。微服务架构具有易扩展,技术选择灵活和部署独立等特性,于是我们把应用拆分为不同的微服务,但是同时也带来了流水线的数量和微服务之间集成测试的增加,Pipeline的依赖关系也变得越来越复杂。

在去年,项目中的DevOps小组,在前人的基础上和团队大刀阔斧地开始了Pipeline的改进工作,希望可以通过一些必要的措施,优化流水线,保证QA每天有包可测、缩短开发期间的反馈周期。

改进什么?

这是我们在改进刚开始就面临的一个问题,本地Pipeline数量众多,每一个Pipeline平均有3-4个构建阶段,每一个阶段又有2-3个并行执行的任务,如此众多的Pipeline和任务,应该从什么地方着手?面对Pipeline构建时间长、测试不稳定、代码冻结时间长,依赖关系复杂等问题,应该如何决定改进的优先级?根据高德拉特的约束理论(ToC),所有在非约束点的改进都是假象,我们可以把整个构建流水线看作是一个完整的系统,如果我们改进的是约束点的上游,就会增加约束点的负担;如果我们改进的是约束点的下游,由于通过下游的工作量主要由约束点决定,所以任何在这个位置的改进都是徒劳无获,无益于整个系统产出的提高还造成了浪费。

现状问题树是寻找约束点的方法之一,借助于这个思维过程(Thinking Process)可以帮助我们梳理“不良效果”(Undesirable Effects )之前的因果关系,最后找到需要解决的核心问题,解决了根本问题之后,由其衍生的各种“不良效果”也大多会消失。通过内部的讨论和演练,我们最终把问题定位在以下几个方面:

  • 核心Pipeline构建时间过长
  • 集成环境测试成功率低
  • 缺少必要的监控预警机制

如何改进?

降低资源的占用时间

在零件制造车间中,每一个零件都需要按照既定工序通过车、镗、铣、磨、刨等车床,每一个阶段都需要占用车床资源进行特定的加工工作。和零件加工类似,来自客户的每一个功能性需求也同样要经过类似处理流程,从需求分析到编码开发,从构建打包到部署测试,每一个环节都需要占用一定的资源。如果在单位时间内,资源占用的比率越高,就会产生比较严重的排队现象。如下图所示,在单位时间内如果资源占用大于百分之七十,队列的长度也会呈指数型增长。

Pipeline作为整个软件交付流程重要的一环,如果每次构建资源占用比例过高,会导致大量的代码积压在开发环境等待构建、验证和打包,“在制品”数量也越来越多,而过多的“在制品”恰恰就是软件交付延期的隐形杀手之一。解决对Pipeline资源占用比例过高的途径只有一个:加速处理“在制品”的流程。在改进的过程中我们总结出了以下几种主要的加速手段:

1.并行化

假设Pipeline单元测试需要30分钟才能运行结束,可以通过切分单元测试多进程并发执行,如下图所示,可以节省近20分钟的运行时间:

同样也可以把并行化用于优化Pipeline结构,如下图所示,通过减少不必要的Pipeline依赖关系,让不同的Pipeline并行执行,可以减少大约五分钟的端到端的构建时间。

优化前: 端到端构建时间20分钟

优化后:端到端构建时间15分钟

2.使用Mock或者Stub,隔离真实服务

对于有数据库依赖的单元测试,如果在运行期间连接真实的数据库,读写速度会比较慢,除了IO操作之外,为了保证不同测试之间的隔离性,往往还需要考虑测试运行之后的数据清理工作,而这也会带来一部分的性能损耗。针对这种情况,可以考虑使用内存数据库替代真实的数据库,在提升IO操作的同时,数据清理工作也变得很简单。

为了保证代码的修改没有破坏现有的功能,一般我们会增加基于UI的回归测试,在测试运行之前部署当前应用以及其所依赖的各个服务。对于应用依赖的服务的部署和API调用,也会消耗部分时间。这是可以考虑使用SinatraMoco等工具隔离部分第三方服务,从而缩短部署时间和API的调用时间。

但是隔离真实服务的同时也掩盖了测试替身和真实组件之间的差异性,比如我们在API测试中使用Sqlite替代SQL Server,但是SQLite并没有datatime字段类型,需要在测试代码中需要做额外的映射配置,这种差异性同样也会导致潜在的产品缺陷。所以我们在选择Mock或者Stub时需要权衡利弊,如果使用则需要额外的手段来验证这种差异性

3.优化基础设施和运行环境

增加硬件配置如CPU,内存、替换固态硬盘等也可以一定程度地降低Pipeline的构建时间。对于由于语言或者框架本身带来的性能约束,也可以通过升级到新版本来解决,比如把Ruby从1.8.7版本升级到2.0版本。

提高构建成功率

团队在改进的过程中发现可以通过下面的公式大致估算出平均每天Pipeline产出的可用包的数量:

根据这个公式,如果要增加平均每天出包的数量,除了降低每次构建的时间之外还需要提高Pipeline的构建成功率,而影响构建成功率最常见的问题就是“非确定性”测试。在项目的Pipeline上曾经出现过下面这些情况:

  • 一些UI测试每次至少需要被重新执行一次才能通过
  • 部分单元测试在特定的时间段会稳定失败
  • 构建结构和测试被执行的顺序有关

Martin Fowler在Eradicating Non-Determinism in Tests中指出了这种非确定性测试存在的两个问题:首先它们属于无用测试,由于测试本身的不确定性,它们已经无法用来描述、验证对应的功能。测试运行的结果也无法给开发人员提供正确的反馈,如果测试失败,开发人员无法直接判断这个测试是由于产品缺陷导致还是由于非确定行为导致。其次这些测试就像“致命的传染病菌”一样,降低正常测试的存在价值。假设一个测试套件中有100个测试,其中10个测试为非确定性测试,这些非确定测试会给开发团队带来很多的“噪音”,团队对于Pipeline失败会觉得司空见惯、习以为常,剩余90个测试的作用也会大打折扣。

1.保证隔离性

在Pipeline的结构方面,由于非确定性测试的“传染性“,在着手解决非确定性测试之前可以考虑从测试套件中隔离这种类型的测试,这种隔离一方面可以保证正常的测试可以继续提供正确的反馈,另一方面也方便开发人员解决非确定性的测试问题,如果被隔离的测试失败,只需要重新执行部分测试而不是整个测试套件,很大程度地缩短了修复过程中的反馈周期。

从测试代码级别也需要保证不同测试之间的隔离性,构建的结果不应该依赖于测试被执行的顺序。在优化过程中我们遇到过这样的情况:基于UI的功能性测试依赖于一部分用户基础数据,其中测试T1在运行过程中需要修改特定用户的角色,在测试T2需要使用该用户完成其他的业务操作。如果T1在T2之前执行可以构建成功,反之则会构建失败。解决这类问题通常有两种做法,测试运行之前创建不同的用户或者测试运行结束之后恢复用户数据。对于第二种方法,如果当前测试没有正确地清理数据会导致下一个执行测试失败,增加了定位问题的难度,所以更推荐使用前者来保证不同测试之前数据隔离。

2.增加必要的等待

在UI测试中很多操作都依赖于页面元素出现的时间、位置等,在不同的网络环境、机器性能不同,页面的加载速度也不一样,测试运行的结果也会有所不同。通常web driver会提供一系列的方法来帮助开发者判断元素是否已经加载完成、是否可见、页面是否已经加载完成等(比如Watir的when_present, wait_until_present等),在测试代码中合适的地方使用这些方法可以让测试代码更加健壮,从而提升Pipeline构建的成功率。

3.正确测试异步行为

系统中的异步操作可以为用户提供更好的使用体验,系统不需要等待当前操作完成就可以继续处理其他操作,但是异步操作也增加了测试的复杂度。在项目的集成测试代码中我们发现类似这样的等待操作:sleep 10, 这种原始的等待策略不够稳定,对于网络状况、机器性能、数据量等外部因素依赖较大。回调(callback)和轮询(loop)是两种推荐的测试异步的方法,回调不会有任何尝试任何多余的等待时间,但是使用场景比较有限;轮询通用性更高但是会产生一定的多余等待时间,对于轮询操作,建议使用更小的等待时间间隔(interval)和重试(retry)上限。

调整测试结构

不合理的测试结构也是影响Pipeline性能的重要因素,根据测试金字塔理论,就测试数量来说,从低层级到高层级的测试需要保证金字塔状的结构。测试的运行时间呈现的却是一个倒金字塔状,测试的层级越高测试运行的时间越长,对应Pipeline的构建时间也越长。所以改进Pipeline也可以从调整测试层级结构开始。

1.梳理业务流程,简化测试结构

新的功能在不断增加,已有的需求也不断在变动,产品本身也不断接受来自最终用户和市场的反馈,现有的测试有可能并没有覆盖那些最有价值的场景而已经覆盖的场景也许在真正的产品环境下使用率很低;有些场景已经在低层级的单元测试覆盖,在高层级测试中出现了很多重复的用例。

调整测试结构可以和领域专家一起,重新梳理业务流程,把测试的重点放在那些最有价值的业务场景上,在高层级增加适当的UI测试保证核心功能没有被破坏。对于出现重复或者价值不大的测试,可以考虑删除高层级的测试,用更多的单元测试来替代,从而降低测试的运行时间。关于自动化测试更多的优化手段可以参考一个遗留系统自动化测试的七年之痒

除此之外还需要构建有效的反馈回路,通过Google Analytics等网站分析平台收集来自于最终用户和市场的数据、用户使用习惯、时区语言等地域性信息对应地调整现有的测试结构,让测试环境下的业务场景更加接近真实的产品环境。

2.用契约测试替代集成测试

“Integrated tests are a scam. A self replicating virus that threatens the very health of your codebase, your sanity, and I’m not exaggerating when I say, your life.” – JB Rainsberger

JB Rainsberger的这个说法一点也不夸张,在项目中总是有关于集成测试的各种“吐槽”,构建时间慢、问题难以复现和定位、修复难以验证、不稳定等。而契约测试就是那个可以拯救你,让你脱离“苦海”的利器。

契约测试是“单元级别”的集成测试,基于消费者驱动的契约测试把契约测试分为了两个阶段:消费者(Consumer)生成契约和提供方(Provider)验证契约,在生成契约时通过Mock隔离真实的服务提供方,运行单元测试生成用JSON描述的契约文件;服务端验证只需要部署自身就可以验证契约文件的正确性。

契约测试有很多优点,首先它不依赖于完整的集成环境,部署成功率高,其实在测试运行期间无真实的API调用和模拟的UI操作,测试运行的速度快,成功率高;而且在本地开发环境就可以验证契约测试,问题容易定位,修复的反馈周期短。引入契约测试不但给Pipeline性能带来大幅度的改善,还可以提升整个团队的开发效率。

如何保护改进成果

Pipeline是软件交付的命脉,为了保证每一个功能需求可以长期稳定、源源不断地通过,在优化过程中我们引入了关于Pipeline性能的监控机制,我们基于ThoughtWorks的开源产品GoCD 提供的API开发了一个监控工具,每一次构建之后可以自动统计该次构建的时间和成功率,如果超过这两个指标超过了阈值,则让该次构建失败,提醒代码提交者检查是否引入了影响Pipeline性能的变更,避免性能的进一步恶化。考虑到网络和机器性能的问题,在设置实际的阈值的时候可以稍大于期望值。

我们还构建了基于邮件通知的预警机制,每个工作日的下午发送出包数量通知,提醒团队解决影响出包的问题,同时我们把Pipeline的性能可视化并纳入每周的周报中。

写在最后

优化Pipeline除了Pipeline结构、测试策略和监控可视化手段之外,还需要关注软件架构和团队组织结构,下面是我们项目架构的局部依赖图:

核心的微服务OrderAPI依赖复杂,和RatingSrv之间甚至出现了双向依赖,领域上下文(Bounded Context)的不合理切分使得业务逻辑散落在不同的服务之间,不管我们增加集成测试还是契约测试,这种依赖关系也同样会体现在Pipeline之间的依赖关系上,增加Pipeline的复杂度。

在ThoughtWorks技术雷达上A single CI instance for all teams目前处于Hold状态,在一个组织中多个团队共享一个臃肿的CI会导致很多的问题,比如上文中提到的构建队列过长,构建时间长等,一旦这个共享的Pipeline出现问题会造成多个团队工作 的中断。技术雷达建议在具有多团队的组织中由各个团队分布式地管理自己独立的CI。这种分布式的CI同样也依赖于整洁的软件架构和与之相契合的团队组织形式。

在项目的DevOps小组解散之后,我们成立的项目内部的DevOps Community,以保证产品交付为目标,同时肩负着项目提高内部DevOps技能的职责。项目内部的成员有跨多个开发团队的不同角色组成,DevOps community产生的相关Task,最后都会分配到不同的开发团队中。DevOps是一种文化而不应该是一个单独的小组,DevOps主旨在于构建整个团队中的责任共享的文化,改进现有的流水线是每一个开发团队都需要具有的技能之一。


更多精彩洞见,请关注微信公众号思特沃克

Share