Serverless 风格微服务的持续交付

?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

在这个架构中,咱们采用了先后端分离的技术。咱们把 HTML,JS, CSS 等静态内容部署在 S3 上,并经过 CloudFront 做为 CDN 构成了整个架构的前端部分。html

咱们把 Amazon API Gateway 做为后端的总体接口链接后端的各类风格的微服务,不管是运行在 Lambda 上的函数,仍是运行在 EC2 上的 Java 微服务,他们总体构成了这个应用的后端部分。前端

从这个架构图上咱们能够明显的看到 前端(Frontend)和后端(Backend)的区分。数据库

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

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

因为咱们的架构是先后端分离的,所以咱们为前端和后端分别构造了两条流水线,使得先后端开发能够独立。以下图所示:api

?wx_fmt=png

在这种状况下,前端团队和后端团队是两个不一样的团队,能够独立开发和部署,但在发布的时候则有些不一样。因为用户是最后感知功能变化的。服务器

所以,为了不界面报错找不到接口,在新增功能的场景下,后端先发布,前端后发布。在删除功能的场景下,前端先发布,后端后发布。架构

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

前端流水线

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

?wx_fmt=png

前端流水线的各步骤过程以下:前后端分离

  1. 咱们采用 BDD/ATDD 的方式进行前端开发。用 NightWatch.JS 框架作 端到端的测试,mocha 和 chai 用于作某些逻辑的验证;

  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 的对应实现。

后端流水线

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

?wx_fmt=png

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

  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 的生产环境配置就能够了。

部署的效果以下所示:

?wx_fmt=png无服务器微服务的持续交付新挑战

在实现以上的持续交付流水线的时候,咱们踩了不少坑。但通过咱们的反思,咱们发现是云计算颠覆了咱们不少的认识,当云计算把某些成本下降到趋近于 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% 左右的时间开发和测试。

一开始咱们觉得咱们走入了” 蛋筒冰激凌反模式 “或者” 纸杯蛋糕反模式 “(请见 http://www.51testing.com/html/57/n-3714757.html)但实际上:

  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 所指向的函数版本就可完成部署和发布。


----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------