JavaScript-云原生开发秘籍-全-
JavaScript 云原生开发秘籍(全)
原文:
zh.annas-archive.org/md5/96e50944f4c1f57a68d39f9d5c19a2b2
译者:飞龙
前言
欢迎来到 JavaScript 云原生开发烹饪书。这本烹饪书充满了帮助你沿着云原生之旅的食谱。它旨在成为我的另一本书 云原生开发模式和最佳实践 的伴侣。我个人发现交付云原生解决方案是迄今为止最有乐趣和满足的开发实践。这是因为云原生不仅仅是优化云。它是一种完全不同的思考软件系统的方式和推理方式。
简而言之,云原生是精简且自主的。由可丢弃的基础设施提供动力,利用全面管理的云服务,并采用可丢弃的架构,云原生赋予日常自给自足的全栈团队快速且持续实验创新的能力,同时以比以往任何时候都少的努力构建全球规模的系统。遵循这种无服务器优先的方法允许团队快速行动,但这种快速步伐也打开了诚实的错误在系统中造成破坏的机会。为了防止这种情况,云原生系统由自主服务组成,这为服务之间创建了防波堤,以减少在中断期间的影响范围。
在这本烹饪书中,你将学习如何通过消除所有同步服务间通信来构建自主服务。你将把数据库内外翻转,最终通过实现事件源和 CQRS 模式,利用事件流和物化视图将云转变为数据库。你的团队将对其交付能力充满信心,因为异步服务间通信和数据复制消除了使系统脆弱的上下游依赖。你还将学习如何在多个区域持续部署、测试、观察、优化和保障你的自主服务。
为了充分利用本书,请准备好以开放的心态去发现为什么云原生与众不同。云原生迫使我们重新思考我们对系统的思考方式。它考验了我们所有先入为主的软件架构观念。因此,请准备好在构建云原生系统时有很多乐趣。
本书面向的对象
本书旨在帮助创建自给自足、全栈、云原生开发团队。一些云经验是有帮助的,但不是必需的。假设读者具备 JavaScript 语言的基本知识。本书可作为经验丰富的云原生开发者的参考,并为入门级云原生开发者提供快速入门。最重要的是,本书是为那些准备好重新配置他们的工程大脑以进行云原生开发的人而写的。
本书涵盖的内容
第一章,开始使用云原生,展示了定义和部署无服务器、云原生资源(如函数、流和数据库)的简便性,这使自给自足的全栈团队能够有信心地持续交付。
第二章,应用事件源和 CQRS 模式,展示了如何通过使用事件流和物化视图来消除服务间的同步通信,从而创建完全自主的服务。
第三章,实现自主服务,探讨了创建自主服务的边界和控制模式,例如前端后端、外部服务网关和事件编排。
第四章,利用云边缘,提供了使用云提供商的内容分发网络来提高自主服务性能和安全的具体示例。
第五章,保护云原生系统,探讨了利用云原生安全中的共享责任模型,以便您可以将精力集中在保护云原生系统的特定领域层。
第六章,构建持续部署管道,展示了诸如任务分支工作流、传递式端到端测试和功能标志等技术,这些技术帮助团队通过将部署和测试左移、控制批量大小以及解耦部署与发布,有信心地将更改持续部署到生产环境中。
第七章,优化可观察性,展示了如何通过在生产环境中持续测试以断言云原生系统的健康状态,并关注恢复平均时间来增强团队信心。
第八章,设计故障处理,讨论了诸如背压、幂等性和流电路断器模式等技术,用于提高自主服务的鲁棒性和弹性。
第九章,优化性能,探讨了诸如异步非阻塞 IO、会话一致性和函数调优等技术,以提升自主服务的响应速度。
第十章,部署到多个区域,展示了如何通过在区域间创建完全复制、活动-活动部署并实施区域故障转移来部署全球自主服务,以最大化可用性并最小化延迟。
第十一章,欢迎使用 Polycloud,探讨了每次选择正确的云服务提供商所提供的自由度,同时保持一致的开发管道体验。
要充分利用本书
要跟随本书中的食谱,您需要根据以下步骤配置您的开发环境:
-
安装 Node Version Manager(
github.com/creationix/nvm
或github.com/coreybutler/nvm-windows
)。 -
使用
nvm install 8
安装 Node.js。 -
使用
npm install serverless -g
安装 Serverless Framework。 -
使用
export MY_STAGE=<your-name>
创建MY_STAGE
环境变量。 -
创建 AWS 账户(
aws.amazon.com/free
)并为 Serverless Framework(serverless.com/framework/docs/providers/aws/guide/credentials
)配置您的凭据。
下载示例代码文件
您可以从www.packt.com
的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并遵循屏幕上的说明。
下载文件后,请确保使用最新版本的以下工具解压或提取文件夹:
-
Windows 上的 WinRAR/7-Zip
-
Mac 上的 Zipeg/iZip/UnRarX
-
Linux 上的 7-Zip/PeaZip
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/JavaScript-Cloud-Native-Development-Cookbook
。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/
找到。查看它们!
下载彩色图像
我们还提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781788470414_ColorImages.pdf
。
使用的约定
本书使用了多种文本约定。
CodeInText
:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“首先要注意的是,我们必须在serverless.yml
文件中定义runtime: nodejs8.10
。”
代码块设置如下:
module.exports.hello = (event, context, callback) => {
console.log('event: %j', event);
console.log('env: %j', process.env);
callback(null, 'success');
};
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
service: cncb-create-function
...
functions:
hello:
handler: handler.hello
任何命令行输入或输出都应如下编写:
$ npm run dp:lcl -- -s $MY_STAGE
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“选择监视器 | 新监视器 | 事件”
警告或重要提示看起来像这样。
技巧和窍门看起来像这样。
章节
在本书中,您将找到一些频繁出现的标题(准备工作,如何操作…,它是如何工作的…,更多内容…,和参见)。
要清楚地说明如何完成食谱,请按以下方式使用这些部分:
准备工作
本节告诉您在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。
如何操作…
本节包含遵循食谱所需的步骤。
它是如何工作的…
本节通常包含对上一节发生情况的详细说明。
更多内容…
本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。
参见
本节提供了指向其他有用信息的链接,这些信息对食谱很有帮助。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件联系我们 customercare@packtpub.com
。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误表提交表单链接,并输入详细信息。
盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件联系 copyright@packt.com
并附上材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用过本书,为什么不在您购买书籍的网站上留下评论?潜在的读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者可以查看他们对书籍的反馈。谢谢!
如需了解有关 Packt 的更多信息,请访问packt.com.
第一章:开始使用云原生
本章将涵盖以下食谱:
-
创建一个栈
-
创建一个函数并处理指标和日志
-
创建一个事件流并发布一个事件
-
创建一个流处理器
-
创建一个 API Gateway
-
部署单页应用程序
简介
云原生是精简的。今天的公司必须不断尝试新的产品想法,以便能够适应不断变化的市场需求;否则,它们可能会落后于竞争对手。为了以这种速度运营,它们必须利用完全托管的云服务和完全自动化的部署,以最小化上市时间,降低运营风险,并赋予自给自足的全栈团队以更少的努力完成更多的工作。
本食谱中的食谱展示了如何使用完全托管的无服务器云服务来开发和部署精简且自主的服务。本章包含简化的食谱,没有杂乱,以便专注于部署云原生组件的核心方面,并为本书的其余部分建立一个坚实的基础。
创建一个栈
每个自主的云原生服务和其所有资源都作为一个统一且自包含的组进行配置,称为栈。在 AWS 上,这些是CloudFormation栈。在本食谱中,我们将使用 Serverless Framework 来创建和管理一个基础栈,以突出部署云原生服务所涉及的步骤。
准备工作
在开始本食谱之前,您需要按照前言中的说明配置您的开发环境,包括 Node.js、Serverless Framework 和 AWS 账户凭证。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch1/create-stack --path cncb-create-stack
-
使用
cd cncb-create-stack
切换到cncb-create-stack
目录。 -
查看名为
serverless.yml
的文件,内容如下:
service: cncb-create-stack
provider:
name: aws
- 查看名为
package.json
的文件,内容如下:
{
"name": "cncb-create-stack",
"version": "1.0.0",
"private": true,
"scripts": {
"test": "sls package -r us-east-1 -s test",
"dp:lcl": "sls deploy -r us-east-1",
"rm:lcl": "sls remove -r us-east-1"
},
"devDependencies": {
"serverless": "1.26.0"
}
}
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-create-stack@1.0.0 dp:lcl <path-to-your-workspace>/cncb-create-stack
> sls deploy -r us-east-1 "-s" "john"
Serverless: Packaging service...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Validating template...
Serverless: Updating Stack...
Service Information
service: cncb-create-stack
stage: john
region: us-east-1
stack: cncb-create-stack-john
api keys:
None
endpoints:
None
functions:
None
- 在 AWS 控制台中查看栈:
- 使用
npm run rm:lcl -- -s $MY_STAGE
删除栈。
它是如何工作的...
Serverless Framework(SLS)(serverless.com/framework/docs
)是我部署云资源的首选工具,无论是否部署无服务器资源,如函数。SLS 实质上是建立在基础设施即代码工具(如 AWS CloudFormation)之上的抽象层,具有插件和动态变量等可扩展功能。我们将使用 SLS 在所有食谱中。每个食谱都从使用 SLS 功能通过克隆模板来 创建
一个新项目开始。您最终可能希望创建自己的模板,以便快速启动自己的项目。
这个第一个项目尽可能简单。它本质上创建了一个空的 CloudFormation 堆栈。在 serverless.yml
文件中,我们定义了 service
名称和 provider
。service
名称将与即将讨论的 stage
结合,以在您的账户和 region
内创建一个唯一的堆栈名称。我已将所有堆栈的前缀设置为 cncb
,以便在您使用共享账户(如工作中的开发或沙盒账户)时,在 AWS 控制台中轻松筛选这些堆栈。
我们下一个最重要的工具是 Node 包管理器(NPM)(docs.npmjs.com/
)。我们不会打包任何 Node 模块(也称为库),但我们将利用 NPM 的依赖关系管理和脚本功能。在 package.json
文件中,我们声明了对 Serverless Framework 的开发依赖,以及三个自定义脚本,用于测试、部署和删除我们的堆栈。我们执行的第一条命令是 npm install
,它将安装所有声明的依赖项到项目的 node_modules
目录中。
接下来,我们执行 npm test
脚本。这是 NPM 提供的几个标准脚本之一,用于提供快捷别名。我们定义了 test
脚本来调用 sls package
命令,以断言一切配置是否正确,并帮助我们了解底层发生了什么。此命令处理 serverless.yml
文件,并在 .serverless
目录中生成 CloudFormation 模板。Serverless Framework 的一个优点是它体现了最佳实践,并采用 异常配置 方法,在 serverless.yml
文件中只声明少量内容,并将其扩展为更详细的 CloudFormation 模板。
现在,我们已经准备好部署堆栈。作为开发者,我们需要能够独立于其他开发者和其他环境(如生产环境)部署堆栈并进行工作。为了支持这一需求,SLS 使用了 阶段 的概念。阶段 (-s $MY_STAGE
) 和区域 (-r us-east-1
) 是调用 SLS 命令时的两个必需的命令行选项。堆栈被部署到特定的区域,阶段用作堆栈名称的前缀,以使其在账户和区域内部独特。使用此功能,每个开发者都可以使用他们的名字作为阶段,通过 npm run dp:lcl -- -s $MY_STAGE
来 部署(dp
)我所说的 本地(lcl
)堆栈。在示例中,我使用我的名字作为阶段。我们在 准备就绪 部分中声明了 $MY_STAGE
环境变量。双横线(--
)是 NPM 让我们向自定义脚本传递额外选项的方式。在 第六章,构建持续部署管道 中,我们将讨论将堆栈部署到共享环境,如 预发布 和 生产。
CloudFormation 在 API 请求中对模板体的大小有限制。典型的模板很容易超过这个限制,必须上传到 S3。Serverless Framework 为我们处理了这个复杂性。在.serverless
目录中,您会注意到有一个cloudformation-template-create-stack.json
文件,它声明了一个ServerlessDeploymentBucket
。在sls deploy
输出中,您可以看到 SLS 首先使用这个模板,然后它将cloudformation-template-update-stack.json
文件上传到桶中并更新堆栈。这个问题已经为我们解决是很令人高兴的,因为通常是通过困难的方式才了解到这个限制。
初看起来,创建一个空堆栈可能似乎是一个愚蠢的想法,但实际上它非常有用。从某种意义上说,您可以将 CloudFormation 视为云资源的 CRUD 工具。CloudFormation 跟踪堆栈中所有资源的状态。它知道何时一个资源是新添加到堆栈中并且必须被创建,何时一个资源已从堆栈中移除并且必须被删除,以及何时资源已更改并且必须被更新。它还管理资源之间的依赖关系和顺序。此外,当堆栈的更新失败时,它会回滚所有更改。
不幸的是,当部署大量更改时,如果在最后更改的资源中发生错误,这些回滚操作可能会非常耗时且痛苦。因此,最好以小批量逐步进行更改。在第六章《构建持续部署管道》中,我们将讨论小批量大小、任务分支工作流程以及将部署与发布解耦的实践。目前,如果您是从经过验证的模板创建新服务,那么请初始化新项目,并使用所有模板默认设置通过第一次拉取请求将堆栈部署到生产环境。然后,为每个增量更改创建一个新的分支。然而,如果您正在开发一个没有经过验证的起始点的实验性服务,那么对于您的第一次生产部署,一个空堆栈是完全合理的。
在您的日常开发工作中,当您完成一个任务或故事的工作后,清理您的本地堆栈是很重要的。当孤儿堆栈积累且很少被移除时,开发账户的成本可能会意外地很高。npm run rm:lcl -- -s $MY_STAGE
脚本就是为了这个目的。
创建函数以及与指标和日志一起工作
函数即服务是云原生架构的基石。函数使自给自足的全栈团队能够专注于交付精简的业务解决方案,而无需被运行云基础设施的复杂性所拖累。没有要管理的服务器,并且函数隐式地扩展以满足需求。它们与其他增值云服务(如流、数据库、API 网关、日志和指标)集成,以进一步加速开发。函数是可丢弃的架构,赋予团队能够尝试不同解决方案的能力。这个配方演示了部署函数是多么简单。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch1/create-function --path cncb-create-function
-
使用
cd cncb-create-function
命令导航到cncb-create-function
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-create-function
provider:
name: aws
runtime: nodejs8.10
environment:
V1: value1
functions:
hello:
handler: handler.hello
- 查看以下内容的
handler.js
文件:
module.exports.hello = (event, context, callback) => {
console.log('event: %j', event);
console.log('context: %j', context);
console.log('env: %j', process.env);
callback(null, 'success');
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-create-function@1.0.0 dp:lcl <path-to-your-workspace>/cncb-create-function
> sls deploy -r us-east-1 "-s" "john"
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (881 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.................
Serverless: Stack update finished...
Service Information
service: cncb-create-function
stage: john
region: us-east-1
stack: cncb-create-function-john
api keys:
None
endpoints:
None
functions:
hello: cncb-create-function-john-hello
-
在 AWS 控制台中查看堆栈和函数。
-
使用以下命令调用函数:
$ sls invoke -r us-east-1 -f hello -s $MY_STAGE -d '{"hello":"world"}'
"success"
- 在 AWS 控制台中查看函数指标:
- 在 AWS 控制台中查看函数日志:
- 在本地查看日志:
$ sls logs -f hello -r us-east-1 -s $MY_STAGE
START RequestId: ... Version: $LATEST
2018-03-24 15:48:45 ... event: {"hello":"world"}
2018-03-24 15:48:45 ... context: {"functionName":"cncb-create-function-john-hello","memoryLimitInMB":"1024", ...}
2018-03-24 15:48:45 ... env: {"V1":"value1","TZ":":UTC","AWS_REGION":"us-east-1", "AWS_ACCESS_KEY_ID":"...", ...}
END RequestId: ...
REPORT ... Duration: 3.64 ms Billed Duration: 100 ms ... Max Memory Used: 20 MB
- 完成操作后,使用
npm run rm:lcl -- -s $MY_STAGE
命令删除堆栈。
它是如何工作的...
Serverless 框架处理繁重的工作,这使我们能够专注于编写实际的函数代码。首先要注意的是,我们必须在 serverless.yml
文件中定义 runtime: nodejs8.10
。接下来,我们在 functions
部分定义一个函数,包括一个名称和处理程序。所有其他设置都默认,遵循异常配置方法。当你查看生成的 CloudFormation 模板时,你会看到仅从 serverless.yml
文件中声明的几行代码就生成了超过 100 行。生成模板的大部分内容都用于定义样板化的安全策略。深入查看 .serverless/cloudformation-template-update-stack.json
文件以查看详细信息。
我们还在 serverless.yml
中定义了 environment
变量。这允许函数根据部署阶段进行参数化。我们将在第六章构建持续部署管道中更详细地介绍这一点。这也允许设置,如调试级别,可以临时调整而不需要重新部署函数。
当我们部署项目时,Serverless Framework 会将函数及其在package.json
文件中指定的运行时依赖项打包到一个 ZIP 文件中。然后,它将 ZIP 文件上传到ServerlessDeploymentBucket
,以便 CloudFormation 可以访问。部署命令的输出显示了这一过程。您可以在.serverless
目录中查看 ZIP 文件的内容或从部署存储桶中下载它。我们将在第九章《优化性能》中介绍高级打包选项。
AWS Lambda 函数的签名很简单。它必须导出一个接受三个参数的函数:一个事件对象、一个上下文对象和一个回调函数。我们的第一个函数将仅记录事件、内容和环境变量,以便我们可以稍微了解执行环境。最后,我们必须调用回调。这是一个标准的 JavaScript 回调。我们将错误传递给第一个参数或成功的结果传递给第二个参数。
日志记录是函数即服务(FaaS)的一个重要标准功能。由于云资源的短暂性,轻描淡写地说,云中的日志记录可能会很繁琐。在 AWS Lambda 中,控制台日志是异步执行的,并记录在 CloudWatch 日志中。这是一个内置的完全托管日志解决方案。花点时间查看此函数写入的日志语句的详细信息。环境变量尤其有趣。例如,我们可以看到每个函数调用都会获得一个新的临时访问密钥。
函数还提供了一套标准的指标,例如调用次数、持续时间、错误、节流等。我们将在第七章《优化可观察性》中详细介绍这一点。
创建事件流并发布事件
云原生服务是自治的。每个服务都是完全自给自足的,并且独立运行以最小化任何给定服务出现故障时的影响范围。为了实现这种隔离,必须在服务之间建立防波堤。事件流是创建这些防波堤的一种机制。自治的云原生服务通过流异步执行所有服务间通信,从而解耦上游服务与下游服务。在第二章,《应用事件源和 CQRS 模式》中,我们将更深入地探讨我们如何创建有界、隔离和自治的云原生服务。此配方创建了我们将在整个食谱中使用的的事件流,并提供了一个将事件发布到流的函数。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch1/event-stream --path cncb-event-stream
-
使用
cd cncb-event-stream
命令进入cncb-event-stream
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-event-stream
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
- Effect: Allow
Action:
- kinesis:PutRecord
Resource:
Fn::GetAtt: [ Stream, Arn ]
functions:
publish:
handler: handler.publish
environment:
STREAM_NAME:
Ref: Stream
resources:
Resources:
Stream:
Type: AWS::Kinesis::Stream
Properties:
Name: ${opt:stage}-${self:service}-s1
RetentionPeriodHours: 24
ShardCount: 1
Outputs:
streamName:
Value:
Ref: Stream
streamArn:
Value:
Fn::GetAtt: [ Stream, Arn ]
- 查看以下内容的
handler.js
文件:
const aws = require('aws-sdk');
const uuid = require('uuid');
module.exports.publish = (event, context, callback) => {
const e = {
id: uuid.v1(),
partitionKey: event.partitionKey || uuid.v4(),
timestamp: Date.now(),
tags: {
region: process.env.AWS_REGION,
},
...event,
}
const params = {
StreamName: process.env.STREAM_NAME,
PartitionKey: e.partitionKey,
Data: Buffer.from(JSON.stringify(e)),
};
const kinesis = new aws.Kinesis();
kinesis.putRecord(params, callback);
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-create-stream@1.0.0 dp:lcl <path-to-your-workspace>/cncb-create-stream
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
Service Information
service: cncb-event-stream
stage: john
region: us-east-1
stack: cncb-event-stream-john
...
functions:
publish: cncb-event-stream-john-publish
Stack Outputs
PublishLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:999999999999:function:cncb-event-stream-john-publish:3
streamArn: arn:aws:kinesis:us-east-1:999999999999:stream/john-cncb-event-stream-s1
streamName: john-cncb-event-stream-s1
...
-
在 AWS 控制台中查看堆栈、流和函数。
-
使用以下命令调用函数:
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"thing-created"}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49582906351415672136958521359460531895314381358803976194"
}
- 查看日志:
$ sls logs -f publish -r us-east-1 -s $MY_STAGE
START ...
2018-03-24 23:20:46 ... event: {"type":"thing-created"}
2018-03-24 23:20:46 ... event:
{
"type":"thing-created",
"id":"81fd8920-2fdb-11e8-b749-0d2c43ec73d0",
"partitionKey":"6f4f9a38-61f7-41c9-a3ad-b8c16e42db7c",
"timestamp":1521948046003,
"tags":{
"region":"us-east-1"
}
}
2018-03-24 23:20:46 ... params: {"StreamName":"john-cncb-event-stream-s1","PartitionKey":"6f4f9a38-61f7-41c9-a3ad-b8c16e42db7c","Data":{"type":"Buffer","data":[...]}}
END ...
REPORT ... Duration: 153.47 ms Billed Duration: 200 ms ... Max Memory Used: 39 MB
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
此堆栈是其他菜谱的先决条件,如每个菜谱的准备就绪部分所示。如果你正在继续相关菜谱,那么你可以让这个堆栈运行,直到你完成相关菜谱。然而,此堆栈中的流不包括在 AWS 免费层中,因此你可能想先删除此堆栈,并在需要时重新创建它。
它是如何工作的...
serverless.yml
文件的resources
部分用于创建由服务使用的云资源。这些资源使用标准的 AWS CloudFormation 资源类型定义。在这个菜谱中,我们创建了一个 AWS Kinesis 流。我们给流起了一个名字,定义了保留期,并指定了分片数量。Serverless Framework 提供了一个强大的机制来动态替换变量。
在这里,我们使用命令行中传入的${opt:stage}
选项和serverless.yml
文件中定义的${self:service}
名称来创建一个唯一的流名称。标准保留期是 24 小时,最大是 7 天。对于我们的菜谱,一个分片将绰绰有余。我们将在不久的将来讨论分片,并在第七章,优化可观察性和第九章,优化性能中再次讨论。
serverless.yml
文件的Outputs
部分是我们定义要在外部堆栈中使用的值的区域,例如生成的 ID 和名称。我们输出Amazon Resource Names(ARNs)streamName
和streamArn
,这样我们就可以在其他项目中使用 Serverless Framework 变量来引用它们。这些值在部署完成后也会在终端上显示。
在serverless.yml
文件中定义的publish
函数用于演示如何将事件发布到流中。我们将STREAM_NAME
作为环境变量传递给函数。在iamRoleStatements
部分,我们授予函数kinesis: PutRecord
权限,允许它将事件发布到这个特定的流。
handler.js
函数文件依赖于两个外部库——aws-sdk
和 uuid
。Serverless 框架将自动包含在 package.json
文件中定义的运行时依赖项。查看生成的 .serverless/cncb-event-stream.zip
文件。aws-sdk
是一个特殊情况。它已经在 AWS Lambda Node.js
运行时中可用,因此不包括在内。这很重要,因为 aws-sdk
是一个大型库,ZIP 文件的大小会影响冷启动时间。我们将在第九章 优化性能 中更详细地讨论这个问题。
publish
函数期望接收一个事件对象作为输入,例如 {"type":"thing-created"}
。然后我们为事件添加额外的信息,以符合我们的标准事件格式,我们将在稍后讨论。最后,该函数创建所需的 params
对象,然后从 aws-sdk
调用 kinesis.putRecord
。我们将在本食谱和其他食谱中使用此函数来模拟事件流量。
我们云原生系统中的所有事件都将符合以下 事件结构,以便在所有服务中进行一致的处理。额外的字段是事件类型特定的:
interface Event {
id: string;
type: string;
timestamp: number;
partitionKey: string;
tags: { [key: string]: string };
}
-
type
描述了事件,例如thing-created
-
timestamp
是一个纪元值,由Date.now()
返回 -
id
应该是一个 V1 UUID,基于时间 -
partitionKey
应该是一个 V4 UUID,基于随机数 -
tags
是一个包含有用数据值的哈希表,这些值被用于基于内容的路由和聚合事件度量
使用一个 V4 UUID 作为 partitionKey
非常重要,以避免热点分片并最大化并发性。如果使用 V1 UUID,则同一时间产生的所有事件都会进入同一个分片。partitionKey
通常将是产生事件的域实体的 ID,这也应该使用 V4 UUID,原因相同。这还有一个额外的优点,即确保所有同一域实体的所有事件都按接收顺序通过同一个分片进行处理。
创建流处理器
流处理器 在云原生服务中承担大部分繁重的工作。自主云原生服务通过事件流异步执行所有服务间通信,从而解耦上游服务与下游服务。上游服务将事件发布到流中,无需了解最终将消费这些事件的特定下游服务。下游服务部署流处理函数以消费感兴趣的事件。流处理器将在整个烹饪书中进行详细说明。本食谱演示了如何创建一个监听来自 AWS Kinesis 流的事件的函数,并快速介绍了使用函数式响应式编程范式实现流处理。
准备工作
在开始此配方之前,您需要一个 AWS Kinesis Stream,例如在 创建事件流 配方中创建的那个。
如何做...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch1/create-stream-processor --path cncb-create-stream-processor
-
使用
cd cncb-create-stream-processor
命令进入cncb-create-stream-processor
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-create-stream-processor
provider:
name: aws
runtime: nodejs8.10
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
batchSize: 100
startingPosition: TRIM_HORIZON
- 查看名为
handler.js
的文件,其内容如下:
const _ = require('highland');
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.tap(printEvent)
.filter(forThingCreated)
.collect()
.tap(printCount)
.toCallback(cb);
};
const recordToEvent = r => JSON.parse(Buffer.from(r.kinesis.data, 'base64'));
const forThingCreated = e => e.type === 'thing-created';
const printEvent = e => console.log('event: %j', e);
const printCount = events => console.log('count: %d', events.length);
-
使用
npm install
命令安装依赖项。 -
使用
npm test -- -s $MY_STAGE
命令运行测试。 -
查看
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-create-stream-processor@1.0.0 dp:lcl <path-to-your-workspace>/cncb-create-stream-processor
> sls deploy -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
Service Information
service: cncb-create-stream-processor
stage: john
region: us-east-1
stack: cncb-create-stream-processor-john
...
functions:
listener: cncb-create-stream-processor-john-listener
-
在 AWS 控制台中查看堆栈和函数。
-
使用以下命令从另一个终端发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"thing-created"}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49582906351415672136958521360120605392824155736450793474"
}
- 查看原始终端的日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
START ...
2018-03-25 00:16:32 ... event:
{
"type":"thing-created",
"id":"81fd8920-2fdb-11e8-b749-0d2c43ec73d0",
"partitionKey":"6f4f9a38-61f7-41c9-a3ad-b8c16e42db7c",
"timestamp":1521948046003,
"tags":{
"region":"us-east-1"
}
}
2018-03-25 00:16:32 ... event:
{
"type":"thing-created",
"id":"c6f60550-2fdd-11e8-b749-0d2c43ec73d0",
...
}
2018-03-25 00:16:32 ... count: 2
END ...
REPORT ... Duration: 7.73 ms Billed Duration: 100 ms ... Max Memory Used: 22 MB
START ...
2018-03-25 00:22:22 ... event:
{
"type":"thing-created",
"id":"1c2b5150-2fe4-11e8-b749-0d2c43ec73d0",
...
}
2018-03-25 00:22:22 ... count: 1
END ...
REPORT ... Duration: 1.34 ms Billed Duration: 100 ms ... Max Memory Used: 22 MB
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
命令删除堆栈。
它是如何工作的...
流处理器监听来自流服务(如 Kinesis 或 DynamoDB Streams)的数据。部署流处理器是完全声明式的。我们使用 stream
事件类型和相关设置(如 type
、arn
、batchSize
和 startingPosition
)配置一个函数。arn
使用 CloudFormation 变量 ${cf:cncb-event-stream-${opt:stage}.streamArn}
动态设置,该变量引用 cnbc-event-stream
堆栈的输出值。
流是唯一在自主云原生服务之间共享的资源。
我们将在第八章 设计故障和第九章 优化性能中详细讨论批量大小和起始位置。目前,您可能已经注意到新的流处理器记录了在过去 24 小时内发布到流中的所有事件。这是因为 startingPosition
被设置为 TRIM_HORIZON
。如果它被设置为 LATEST
,那么它只会接收在函数创建后发布的事件。
流处理与 Node.js 流的函数式响应式编程非常匹配。术语可能有点令人困惑,因为“流”这个词被过度使用了。我喜欢将流想象成“宏观”或“微观”。例如,Kinesis 是“宏观”流,而我们流处理器函数中的代码是“微观”流。我最喜欢的实现“微观”流的库是 Highland.js (highlandjs.org
)。一个流行的替代方案是 RxJS (rxjs-dev.firebaseapp.com
)。正如您在这个配方中可以看到的,函数式响应式编程非常直观且易于阅读。其中一个原因是没有任何循环。如果您尝试使用 命令式编程 实现流处理器,您会发现它很快就会变得非常混乱。您还会丢失背压,我们将在第八章 设计故障中讨论这一点。
listener
函数中的代码创建了一系列步骤,Kinesis 流中的数据最终将通过这些步骤流动。第一步,_(event.Records)
,将 Kinesis 记录的数组转换为 Highland.js 流对象,允许数组中的每个元素在下游步骤准备好接收下一个元素时依次通过流。.map(recordToEvent)
步骤解码 Kinesis 记录中的 Base64 编码数据,并将 JSON 解析为事件对象。下一步,.tap(printEvent)
,简单地记录事件,以便我们可以看到配方中的情况。
Kinesis 和事件流,通常来说,是高性能、哑管道-智能端点生成消息中间件的一员。这意味着 Kinesis,这个哑管道,不会浪费其处理能力来过滤端点数据。相反,所有这些逻辑都分散在智能端点的处理能力上。我们的流处理器函数是智能端点。为此,.filter(forThingCreated)
步骤负责过滤掉处理器不感兴趣的事件。所有剩余的步骤都可以假设它们正在接收预期的事件类型。
我们这个简单的流处理器需要做一些有趣但简单的事情。因此,我们统计并打印批次中 thing-created
事件的数量。我们已经过滤掉了所有其他事件类型,所以 .collect()
步骤将所有剩余的事件收集到一个数组中。然后,.tap(printCount)
步骤记录数组的长度。最后,.toCallback(cb)
步骤将在批次中的所有数据都处理完毕后调用回调函数。此时,Kinesis 检查点将前进,并处理下一批事件。我们将在第八章 设计故障处理中介绍错误处理及其与批次和检查点的关联。
创建 API 网关
API 网关是云原生架构的一个基本元素。它为我们云原生系统的边界提供了一个安全且高效的防护。边界是系统与系统外部的一切(包括人类和其他系统)交互的地方。我们将利用 API 网关在创建边界组件(如 Backend For Frontend(BFF)或外部服务网关)的配方中。本配方演示了部署 API 网关是多么简单。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch1/create-api-gateway --path cncb-create-api-gateway
-
使用
cd cncb-create-api-gateway
切换到cncb-create-api-gateway
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-create-api-gateway
provider:
name: aws
runtime: nodejs8.10
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
cors: true
- 查看名为
handler.js
的文件,其内容如下:
module.exports.hello = (event, context, callback) => {
console.log('event: %j', event);
const response = {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
message: 'JavaScript Cloud Native Development Cookbook! Your function executed successfully!',
input: event,
}),
};
callback(null, response);
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-create-api-gateway@1.0.0 dp:lcl <path-to-your-workspace>/cncb-create-api-gateway
> sls deploy -r us-east-1 "-s" "john"
Serverless: Packaging service...
.....
Serverless: Stack update finished...
Service Information
service: cncb-create-api-gateway
stage: john
region: us-east-1
stack: cncb-create-api-gateway-john
api keys:
None
endpoints:
GET - https://k1ro5oasm6.execute-api.us-east-1.amazonaws.com/john/hello
functions:
hello: cncb-create-api-gateway-john-hello
-
在 AWS 控制台中查看堆栈、API 和函数。
-
在以下命令中调用堆栈输出中显示的
endpoint
:
$ curl -v https://k1ro5oasm6.execute-api.us-east-1.amazonaws.com/john/hello | json_pp
{
"input" : {
"body" : null,
"pathParameters" : null,
"requestContext" : { ... },
"resource" : "/hello",
"headers" : { ... },
"queryStringParameters" : null,
"httpMethod" : "GET",
"stageVariables" : null,
"isBase64Encoded" : false,
"path" : "/hello"
},
"message" : "JavaScript Cloud Native Development Cookbook! Your function executed successfully!"
}
- 查看日志:
$ sls logs -f hello -r us-east-1 -s $MY_STAGE
START ...
2018-03-25 01:04:47 ... event: {"resource":"/hello","path":"/hello","httpMethod":"GET","headers":{ ... },"requestContext":{ ... },"body":null,"isBase64Encoded":false}
END
REPORT ... Duration: 2.82 ms Billed Duration: 100 ms ... Max Memory Used: 20 MB
- 完成操作后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
创建 API 网关是完全声明式的。我们只需配置一个具有 http
事件类型和相关设置的函数,例如 path
和 method
。所有其他设置都遵循 异常配置 方法默认设置。当你查看生成的 .serverless/cloudformation-template-update-stack.json
文件时,你会看到仅从 serverless.yml
文件中声明的几行代码就生成了超过 100 行。API 名称是根据 serverless.yml
文件顶部的服务名称和指定的阶段组合计算得出的。无服务器项目和 API 网关之间存在一对一的映射。项目中所有使用 http
事件声明的函数都包含在 API 中。
函数的签名与其他函数相同;然而,事件的内容和预期的响应格式是针对 API 网关服务的。event
包含 HTTP 请求的全部内容,包括路径、参数、头部、主体等。response
需要一个 statusCode
和选项头部和主体。主体必须是一个字符串,头部必须是一个对象。我使用 cors: true
设置声明了函数,以便食谱可以包括一组合法的响应头部。我们将在 第五章,保护云原生系统 中详细讨论安全性。目前,知道 SSL、节流和 DDoS 保护是 AWS API 网关的默认功能。
API 网关的 endpoint
被声明为堆栈输出,并在部署堆栈后显示。我们将在 第四章,利用云的边缘 和 第十章,部署到多个区域 中看到自定义端点的方法。一旦调用服务,你将能够看到输入和输出的详细信息,包括编码的 HTTP 响应以及函数的日志。同时查看 AWS 控制台中的 API 网关。然而,自动化和 Serverless Framework 的目标是消除在控制台中进行更改的需要。我在写这本书时查看控制台中的 API,但除此之外,我不记得上一次我真正需要进入 API 网关控制台是什么时候了。
部署单页应用程序
当我意识到可以将单页应用,如 Angular,部署到 S3 存储桶,并无需服务器和负载均衡器即可在全球范围内提供时,云原生灯泡首先在我的脑海中亮起。这是我第一次云原生 Wow!时刻。这是我开始理解云原生完全遵循一套不同规则的时刻。S3 和基于 JavaScript 的 UI 的组合提供了一种几乎无限可扩展、几乎无成本和几乎没有操作烦恼的 Web 表示层。这个配方展示了部署单页应用是多么简单。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch1/deploy-spa --path cncb-deploy-spa
-
使用
cd cncb-deploy-spa
导航到cncb-deploy-spa
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-deploy-spa
provider:
name: aws
plugins:
- serverless-spa-deploy
custom:
spa:
files:
- source: ./build
globs: '**/*'
headers:
CacheControl: max-age=300
resources:
Resources:
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: index.html
Outputs:
WebsiteBucketName:
Value:
Ref: WebsiteBucket
WebsiteURL:
Value:
Fn::GetAtt: [ WebsiteBucket, WebsiteURL ]
- 检查名为
package.json
的文件,其内容如下:
{
"name": "cncb-deploy-spa",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "sls package -r us-east-1 -s test",
"dp:lcl": "sls deploy -v -r us-east-1",
"rm:lcl": "sls remove -r us-east-1"
},
"dependencies": {
"react": "16.2.0",
"react-dom": "16.2.0"
},
"devDependencies": {
"react-scripts": "1.1.1",
"serverless": "1.26.0",
"serverless-spa-deploy": "¹.0.0"
}
}
-
使用
npm install
安装依赖。 -
使用
npm start
在本地运行应用。 -
使用
npm test
运行测试。 -
检查
.serverless
目录中生成的内容。 -
使用
npm run build
构建应用。 -
检查
build
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-deploy-spa@1.0.0 dp:lcl <path-to-your-workspace>/cncb-deploy-spa
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
WebsiteBucketName: cncb-deploy-spa-john-websitebucket-1s8hgqtof7la7
WebsiteURL: http://cncb-deploy-spa-john-websitebucket-1s8hgqtof7la7.s3-website-us-east-1.amazonaws.com
...
Serverless: Path: ./build
Serverless: File: asset-manifest.json (application/json)
Serverless: File: favicon.ico (image/x-icon)
Serverless: File: index.html (text/html)
Serverless: File: manifest.json (application/json)
Serverless: File: service-worker.js (application/javascript)
Serverless: File: static/css/main.c17080f1.css (text/css)
Serverless: File: static/css/main.c17080f1.css.map (application/json)
Serverless: File: static/js/main.ee7b2412.js (application/javascript)
Serverless: File: static/js/main.ee7b2412.js.map (application/json)
Serverless: File: static/media/logo.5d5d9eef.svg (image/svg+xml)
-
在 AWS 控制台中检查栈和存储桶。
-
浏览栈输出中显示的
WebsiteURL
:
- 完成操作后,使用
npm run rm:lcl -- -s $MY_STAGE
删除栈。
它是如何工作的...
首先要注意的是,我们正在使用全套相同的开发工具。这是使用 JavaScript 进行后端开发的优势之一。一个单一、自给自足的全栈团队能够使用相同的编程语言开发前端应用和 BFF 服务。这可以允许更有效地利用团队资源。
有两个新的标准脚本——start
和build
。npm start
将使用 Node.js 作为网络服务器在本地运行前端应用。npm run build
将为部署准备应用。我使用了react-scripts
库,以便不使示例因详细的 ReactJS 构建过程而变得杂乱。这个配方使用了一个小型、预制的 ReactJS 示例,原因相同。我想创建一个足够大的应用,以便有东西可以部署。ReactJS 不是这个配方或食谱的重点。已经有大量关于 ReactJS 和类似框架的书籍。
我们正在创建一个 S3 存储桶,名为WebsiteBucket
,并将其配置为网站。栈输出显示了用于访问 SPA 的WebsiteUrl
。SPA 将从无需服务器的存储桶中提供。在这种情况下,我们可以将 S3 视为一个全球性的网络服务器。
我们在这个菜谱中首次使用无服务器插件。serverless-spa-deploy
插件将在栈部署后从 ./build
目录上传 SPA 文件。请注意,我们没有明确命名存储桶。CloudFormation 将使用随机后缀生成名称。这很重要,因为存储桶名称必须是全局唯一的。插件推断生成的存储桶名称。该插件有合理的默认设置,可以进行自定义,例如更改不同文件的 CacheControl
头部。插件还会在栈移除前清空存储桶。
我们将在 第四章 中构建这个架构,利用云的边缘。
第二章:应用事件源和 CQRS 模式
本章将涵盖以下示例:
-
创建数据湖
-
应用事件优先的事件源模式变体
-
创建微事件存储
-
使用 DynamoDB 应用数据库优先的事件源模式变体
-
使用 Cognito 数据集应用数据库优先的事件源模式变体
-
在 DynamoDB 中创建物化视图
-
在 S3 中创建物化视图
-
在 Elasticsearch 中创建物化视图
-
在 Cognito 数据集中创建物化视图
-
重放事件
-
索引数据湖
-
实现双向同步
简介
云原生是自治的。它赋予自给自足的全栈团队能够快速进行精益实验,并自信地持续交付创新。这里的关键词是自信。我们利用完全托管的云服务,如函数即服务、云原生数据库和事件流,以降低运行这些先进技术的风险。然而,在这种快速变化的速度下,我们无法完全消除人为错误的可能性。为了在变化的速度下保持稳定,云原生系统由边界明确、隔离和自治的服务组成,这些服务通过防波堤隔开,以最小化任何给定服务出现故障时的破坏范围。每个服务都是完全自给自足的,即使相关服务不可用,也能独立运行。
遵循响应式原则,这些自治服务利用事件流进行所有服务间通信。事件流通过在云原生数据库中存储以物化视图形式复制的跨服务数据,将数据库内部结构颠倒。这种云原生数据在服务之间形成一道防波堤,有效地将云转变为数据库,以最大化响应性、弹性和容错性。事件源和命令查询责任分离(CQRS)模式对于创建自治服务至关重要。本章包含了一些示例,展示了如何使用完全托管、无服务器的云服务来应用这些模式。
创建数据湖
事件溯源模式的主要好处之一是它记录了系统内所有状态变化的历史记录。这些事件存储库也可以用来重放事件以修复损坏的服务和初始化新组件。例如,AWS Kinesis这样的云原生事件流,仅存储事件很短的时间,从 24 小时到 7 天不等。事件流可以被视为一个临时或时间性的事件存储,用于常规的、接近实时的操作。在创建一个微事件存储配方中,我们将讨论如何创建专门针对单个服务的事件存储。在这个配方中,我们将在 S3 中创建一个数据湖。数据湖是一个永久性的事件存储,以原始格式永久收集和存储所有事件,具有完整的保真度和高度的耐用性,以支持审计和重放。
准备工作
在开始此配方之前,您需要一个AWS Kinesis Stream,例如在创建事件流配方中创建的那个。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/data-lake-s3 --path cncb-data-lake-s3
-
使用
cd cncb-data-lake-s3
命令导航到cncb-data-lake-s3
目录。 -
检查以下内容的
serverless.yml
文件:
service: cncb-data-lake-s3
provider:
name: aws
runtime: nodejs8.10
functions:
transformer:
handler: handler.transform
timeout: 120
resources:
Resources:
Bucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
DeliveryStream:
Type: AWS::KinesisFirehose::DeliveryStream
Properties:
DeliveryStreamType: KinesisStreamAsSource
KinesisStreamSourceConfiguration:
KinesisStreamARN: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
ExtendedS3DestinationConfiguration:
BucketARN:
Fn::GetAtt: [ Bucket, Arn ]
Prefix: ${cf:cncb-event-stream-${opt:stage}.streamName}/
...
Outputs:
DataLakeBucketName:
Value:
Ref: Bucket
- 检查以下内容的
handler.js
文件:
exports.transform = (event, context, callback) => {
const output = event.records.map((record, i) => {
// store all available data
const uow = {
event: JSON.parse((Buffer.from(record.data, 'base64')).toString('utf8')),
kinesisRecordMetadata: record.kinesisRecordMetadata,
firehoseRecordMetadata: {
deliveryStreamArn: event.deliveryStreamArn,
region: event.region,
invocationId: event.invocationId,
recordId: record.recordId,
approximateArrivalTimestamp: record.approximateArrivalTimestamp,
}
};
return {
recordId: record.recordId,
result: 'Ok',
data: Buffer.from(JSON.stringify(uow) + '\n', 'utf-8').toString('base64'),
};
});
callback(null, { records: output });
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-data-lake-s3@1.0.0 dp:lcl <path-to-your-workspace>/cncb-data-lake-s3
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
DataLakeBucketName: cncb-data-lake-s3-john-bucket-1851i1c16lnha
...
-
在 AWS 控制台中检查堆栈、数据湖存储桶和 Firehose 传输流。
-
使用以下命令从单独的终端发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"thing-created"}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49582906351415672136958521360120605392824155736450793474"
}
-
允许 Firehose 缓冲时间处理,然后检查 S3 存储桶中创建的数据湖内容。
-
完成以下命令后,请移除堆栈:
npm run rm:lcl -- -s $MY_STAGE
。
在处理完所有其他配方后,请移除数据湖堆栈。这将允许您观察数据湖累积所有其他事件。
它是如何工作的...
数据湖最重要的特征是它永久存储数据。真正满足这一要求的方法是使用对象存储,例如 AWS S3。S3 提供了 11 个 9 的耐用性。换句话说,S3 在给定一年内提供了 99.999999999%的对象耐用性。它也是完全管理的,并提供生命周期管理功能,将对象老化到冷存储。请注意,存储桶使用DeletionPolicy
设置为Retain
定义,这表明即使堆栈被删除,我们仍然想确保我们不会不当地删除这些宝贵的数据。
我们使用 Kinesis Firehose 是因为它执行将事件写入存储桶的重型工作。它基于时间和大小提供缓冲,压缩,加密和错误处理。为了简化此配方,我没有使用压缩或加密,但建议您使用这些功能。
此配方定义了一个交付流,因为在这本烹饪书中,我们的流拓扑只包含一个流,即 ${cf:cncb-event-stream-${opt:stage}.streamArn}
。在实践中,您的拓扑将包含多个流,并且您将为每个 Kinesis 流定义一个 Firehose 交付流,以确保数据湖能够捕获所有事件。我们将 prefix
设置为 ${cf:cncb-event-stream-${opt:stage}.streamName}/
,这样我们就可以通过流轻松区分数据湖中的事件。
数据湖的另一个重要特征是数据以原始格式存储,未经修改。为此,transformer
函数装饰了有关特定 Kinesis 流和 Firehose 交付流的全部可用元数据,以确保收集所有可用信息。在 重放事件 配方中,我们将看到如何利用这些元数据。此外,请注意,transformer
添加了换行符 (\n
) 以便于未来数据处理。
应用事件溯源模式的事件首先变体
事件溯源是设计最终一致云原生系统的一个关键模式。上游服务在其状态变化时产生事件,下游服务消费这些事件并根据需要产生自己的事件。这导致一系列事件,其中服务协作以产生一个业务流程,该流程最终实现一致性解决方案。链中的每一步都必须作为一个原子工作单元实现。云原生系统不支持分布式事务,因为它们无法以经济高效的方式水平扩展。因此,每一步必须更新一个,并且只有一个系统。如果必须更新多个系统,则每个系统都按一系列步骤更新。在这个配方中,我们利用事件溯源模式的事件首先变体,其中原子工作单元是写入事件流。数据的最终持久性委托给下游组件。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/event-first --path cncb-event-first
-
使用
cd cncb-event-first
命令导航到cncb-event-first
目录。 -
检查包含以下内容的
serverless.yml
文件:
service: cncb-event-first
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
- Effect: Allow
Action:
- kinesis:PutRecord
Resource: ${cf:cncb-event-stream-${opt:stage}.streamArn}
functions:
submit:
handler: handler.submit
environment:
STREAM_NAME: ${cf:cncb-event-stream-${opt:stage}.streamName}
- 检查名为
handler.js
的文件,其内容如下:
module.exports.submit = (thing, context, callback) => {
thing.id = thing.id || uuid.v4();
const event = {
type: 'thing-submitted',
id: uuid.v1(),
partitionKey: thing.id,
timestamp: Date.now(),
tags: {
region: process.env.AWS_REGION,
kind: thing.kind,
},
thing: thing,
};
const params = {
StreamName: process.env.STREAM_NAME,
PartitionKey: event.partitionKey,
Data: Buffer.from(JSON.stringify(event)),
};
const kinesis = new aws.Kinesis();
kinesis.putRecord(params, (err, resp) => {
callback(err, event);
});
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-event-first@1.0.0 dp:lcl <path-to-your-workspace>/cncb-event-first
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
submit: cncb-event-first-john-submit
...
-
在 AWS 控制台中检查堆栈和函数。
-
使用以下命令调用
submit
函数:
$ sls invoke -f submit -r us-east-1 -s $MY_STAGE -d '{"id":"11111111-1111-1111-1111-111111111111","name":"thing one","kind":"other"}'
{
"type": "thing-submitted",
"id": "2a1f5290-42c0-11e8-a06b-33908b837f8c",
"partitionKey": "11111111-1111-1111-1111-111111111111",
"timestamp": 1524025374265,
"tags": {
"region": "us-east-1",
"kind": "other"
},
"thing": {
"id": "11111111-1111-1111-1111-111111111111",
"name": "thing one",
"kind": "other"
}
}
- 查看日志:
$ sls logs -f submit -r us-east-1 -s $MY_STAGE
START ...
2018-04-18 00:22:54 ... params: {"StreamName":"john-cncb-event-stream-s1","PartitionKey":"11111111-1111-1111-1111-111111111111","Data":{"type":"Buffer","data":[...]}}
2018-04-18 00:22:54 ... response: {"ShardId":"shardId-000000000000","SequenceNumber":"4958...2466"}
END ...
REPORT ... Duration: 381.21 ms Billed Duration: 400 ms ... Max Memory Used: 34 MB
- 完成使用
npm run rm:lcl -- -s $MY_STAGE
后,请删除堆栈。
它是如何工作的...
在这个菜谱中,我们实现了一个名为 submit
的命令函数,它将是后端前端服务的一部分。遵循事件溯源模式,我们通过只写入单个资源来使这个命令原子化。在某些场景中,例如启动长期业务流程或跟踪用户点击,我们只需要 fire-and-forget。在这些情况下,事件优先变体最为合适。命令只需要快速执行,并尽可能减少偶然性。我们将事件写入高度可用、完全管理的云原生事件流,并相信下游服务最终会消费该事件。
逻辑将域对象包装在标准事件格式中,正如在 第一章 的 创建事件流和发布事件 菜谱中讨论的那样,开始使用云原生。指定了事件 type
,使用域对象 ID 作为 partitionKey
,并装饰了有用的 tags
。最后,将事件写入由 STREAM_NAME
环境变量指定的流。
创建一个微事件存储库
在 创建数据湖 菜谱中,我们将讨论事件溯源模式如何为系统提供所有状态变更事件的审计跟踪。事件流本质上提供了一个近似实时的事件存储,为下游的事件处理器提供数据。数据湖提供了一个高度耐用的永久事件存储,是官方记录的来源。然而,我们有一个中间需求。单个流处理器需要能够获取支持其处理需求的具体事件。在这个菜谱中,我们将实现一个由 AWS DynamoDB 拥有并针对特定服务需求定制的微事件存储库。
如何做...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/micro-event-store --path cncb-micro-event-store
-
使用
cd cncb-micro-event-store
命令进入cncb-micro-event-store
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-micro-event-store
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:Query
Resource:
Fn::GetAtt: [ Table, Arn ]
environment:
TABLE_NAME:
Ref: Table
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
...
trigger:
handler: handler.trigger
events:
- stream:
type: dynamodb
arn:
Fn::GetAtt: [ Table, StreamArn ]
batchSize: 100
startingPosition: TRIM_HORIZON
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${opt:stage}-${self:service}-events
AttributeDefinitions:
...
KeySchema:
- AttributeName: partitionKey
KeyType: HASH
- AttributeName: eventId
KeyType: RANGE
...
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
- 查看以下内容的
handler.js
文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(byType)
.flatMap(put)
.collect()
.toCallback(cb);
};
...
const byType = event => event.type.matches(/thing-.+/);
const put = event => {
const params = {
TableName: process.env.TABLE_NAME,
Item: {
partitionKey: event.partitionKey,
eventId: event.id,
event: event,
}
};
const db = new aws.DynamoDB.DocumentClient();
return _(db.put(params).promise());
};
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.flatMap(getMicroEventStore)
.tap(events => console.log('events: %j', events))
.collect().toCallback(cb);
};
const getMicroEventStore = (record) => {
const params = {
TableName: process.env.TABLE_NAME,
KeyConditionExpression: '#partitionKey = :partitionKey',
ExpressionAttributeNames: {
'#partitionKey': 'partitionKey'
},
ExpressionAttributeValues: {
':partitionKey': record.dynamodb.Keys.partitionKey.S
}
};
const db = new aws.DynamoDB.DocumentClient();
return _(db.query(params).promise());
}
-
使用
npm install
安装依赖。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-micro-event-store@1.0.0 dp:lcl <path-to-your-workspace>/cncb-micro-event-store
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
listener: cncb-micro-event-store-john-listener
trigger: cncb-micro-event-store-john-trigger
...
-
在 AWS 控制台中查看堆栈、函数和表。
-
使用以下命令从单独的终端发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"thing-updated","partitionKey":"11111111-1111-1111-1111-111111111111","thing":{"new":{"name":"thing one","id":"11111111-1111-1111-1111-111111111111"}}}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583553996455686705785668952922460091805481438885707778"
}
- 查看监听函数的日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
START ...
2018-04-18 01:18:55... {"type":"thing-updated","partitionKey":"11111111-1111-1111-1111-111111111111","thing":{"new":{"name":"thing one","id":"11111111-1111-1111-1111-111111111111"}},"id":"fcc03460-42c7-11e8-8756-f75e650b2731","timestamp":1524028734118,"tags":{"region":"us-east-1"}}
2018-04-18 01:18:55.394 (-04:00) b42aaa92-8a9a-418d-8e22-ecc54e9966f6 params: {"TableName":"john-cncb-micro-event-store-events","Item":{"partitionKey":"11111111-1111-1111-1111-111111111111","eventId":"fcc03460-42c7-11e8-8756-f75e650b2731","event":{"type":"thing-updated","partitionKey":"11111111-1111-1111-1111-111111111111","thing":{"new":{"name":"thing one","id":"11111111-1111-1111-1111-111111111111"}},"id":"fcc03460-42c7-11e8-8756-f75e650b2731","timestamp":1524028734118,"tags":{"region":"us-east-1"}}}}
END ...
REPORT ... Duration: 149.24 ms Billed Duration: 200 ms ... Max Memory Used: 35 MB
- 查看触发函数的日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
START ...
2018-04-18 01:18:56 ... event: {"Records":[{"eventID":"b8dbee2d9f49ee05609a7e930ac204e7","eventName":"INSERT",...,"Keys":{"eventId":{"S":"fcc03460-42c7-11e8-8756-f75e650b2731"},"partitionKey":{"S":"11111111-1111-1111-1111-111111111111"}},...}]}
2018-04-18 01:18:56 ... params: {"TableName":"john-cncb-micro-event-store-events",...,"ExpressionAttributeValues":{":partitionKey":"11111111-1111-1111-1111-111111111111"}}
2018-04-18 01:18:56 ... events: {"Items":[{"eventId":"2a1f5290-42c0-11e8-a06b-33908b837f8c","partitionKey":"11111111-1111-1111-1111-111111111111","event":{"id":"2a1f5290-42c0-11e8-a06b-33908b837f8c","type":"thing-submitted","partitionKey":"11111111-1111-1111-1111-111111111111","thing":{"name":"thing one","kind":"other","id":"11111111-1111-1111-1111-111111111111"},"timestamp":1524025374265,"tags":{"region":"us-east-1","kind":"other"}}},{"eventId":"fcc03460-42c7-11e8-8756-f75e650b2731","partitionKey":"11111111-1111-1111-1111-111111111111","event":{"id":"fcc03460-42c7-11e8-8756-f75e650b2731","type":"thing-updated","partitionKey":"11111111-1111-1111-1111-111111111111","thing":{"new":{"name":"thing one","id":"11111111-1111-1111-1111-111111111111"}},"timestamp":1524028734118,"tags":{"region":"us-east-1"}}}],"Count":2,"ScannedCount":2}
END ...
REPORT ... Duration: 70.88 ms Billed Duration: 100 ms Memory Size: 1024 MB Max Memory Used: 42 MB
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
在实现流处理器函数时,我们通常需要比当前事件对象中可用的信息更多。在发布事件时包含所有在发布上下文中可用的相关数据是一种最佳实践,这样每个事件就代表系统在发布时的一个微快照。当这些数据不足时,我们需要检索更多数据;然而,在云原生系统中,我们努力消除所有同步的跨服务通信,因为这会降低服务的自主性。相反,我们创建一个针对特定服务需求的微事件存储。
首先,我们实现一个用于从流中获取所需事件的listener
函数和filter
。每个事件都存储在 DynamoDB 表中。你可以存储整个事件或仅存储所需的信息。在存储这些事件时,我们需要通过仔细定义HASH
和RANGE
键来整理相关事件。例如,我们可能希望整理特定域对象 ID 的所有事件或特定用户 ID 的所有事件。在这个例子中,我们使用event.partitionKey
作为哈希键,但你也可以从任何可用数据中计算哈希键。对于范围键,我们需要一个在哈希键内唯一的值。如果event.id
实现了基于 V1 UUID,那么它是一个不错的选择,因为它们是基于时间的。Kinesis 序列号也是一个不错的选择。event.timestamp
是另一个替代方案,但可能存在在哈希键内事件创建时间完全相同的情况。
与 DynamoDB 流关联的trigger
函数在listener
保存事件后接管。触发器调用getMicroEventStore
来根据为当前事件计算的哈希键检索微事件存储。此时,流处理器在内存中拥有所有相关的数据。微事件存储中的事件按历史顺序排列,基于用于范围键的值。流处理器可以使用这些数据来实现其业务逻辑。
使用 DynamoDB 的 TTL 功能来防止微事件存储无限制地增长。
在 DynamoDB 上应用事件溯源模式的数据库优先变体
在上一个食谱中,应用事件溯源模式的“事件优先”变体,我们讨论了事件溯源模式如何使我们能够设计由一系列原子步骤组成的最终一致性的系统。在云原生系统中不支持分布式事务,因为它们无法有效地扩展。因此,每个步骤必须更新一个,并且只有一个系统。在本食谱中,我们将利用事件溯源模式的 数据库优先 变体,其中原子工作单元是写入单个云原生数据库。云原生数据库提供了一种 变更数据捕获 机制,允许进一步逻辑原子触发,将适当的 领域事件 发布到事件流以进行进一步的下层处理。在本食谱中,我们将使用 AWS DynamoDB 和 DynamoDB Streams 来演示实现此模式。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/db-first-dynamodb --path cncb-db-first-dynamodb
-
使用
cd cncb-db-first-dynamodb
命令进入cncb-db-first-dynamodb
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-db-first-dynamodb
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource:
Fn::GetAtt: [ Table, Arn ]
- Effect: Allow
Action:
- kinesis:PutRecord
Resource: ${cf:cncb-event-stream-${opt:stage}.streamArn}
functions:
command:
handler: handler.command
environment:
TABLE_NAME:
Ref: Table
trigger:
handler: handler.trigger
events:
- stream:
type: dynamodb
arn:
Fn::GetAtt: [ Table, StreamArn ]
...
environment:
STREAM_NAME: ${cf:cncb-event-stream-${opt:stage}.streamName}
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${opt:stage}-${self:service}-things
AttributeDefinitions:
...
KeySchema:
- AttributeName: id
KeyType: HASH
...
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
- 检查名为
handler.js
的文件,其内容如下:
module.exports.command = (request, context, callback) => {
const thing = {
id: uuid.v4(),
...request,
};
const params = {
TableName: process.env.TABLE_NAME,
Item: thing,
};
const db = new aws.DynamoDB.DocumentClient();
db.put(params, callback);
};
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.map(toEvent)
.flatMap(publish)
.collect()
.toCallback(cb);
};
const toEvent = record => ({
id: record.eventID,
type: `thing-${EVENT_NAME_MAPPING[record.eventName]}`,
timestamp: record.dynamodb.ApproximateCreationDateTime * 1000,
partitionKey: record.dynamodb.Keys.id.S,
tags: {
region: record.awsRegion,
},
thing: {
old: record.dynamodb.OldImage ?
aws.DynamoDB.Converter.unmarshall(record.dynamodb.OldImage) :
undefined,
new: record.dynamodb.NewImage ?
aws.DynamoDB.Converter.unmarshall(record.dynamodb.NewImage) :
undefined,
},
});
const EVENT_NAME_MAPPING = {
INSERT: 'created',
MODIFY: 'updated',
REMOVE: 'deleted',
};
const publish = event => {
const params = {
StreamName: process.env.STREAM_NAME,
PartitionKey: event.partitionKey,
Data: Buffer.from(JSON.stringify(event)),
};
const kinesis = new aws.Kinesis();
return _(kinesis.putRecord(params).promise());
}
-
使用
npm install
命令安装依赖项。 -
使用
npm test -- -s $MY_STAGE
命令运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-db-first-dynamodb@1.0.0 dp:lcl <path-to-your-workspace>/cncb-db-first-dynamodb
> sls deploy -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
command: cncb-db-first-dynamodb-john-command
trigger: cncb-db-first-dynamodb-john-trigger
-
在 AWS 控制台中检查栈、函数和表。
-
使用以下命令调用函数:
$ sls invoke -r us-east-1 -f command -s $MY_STAGE -d '{"name":"thing one"}'
- 查看命令函数的日志:
$ sls logs -f command -r us-east-1 -s $MY_STAGE
START ...
2018-04-17 00:29:13 ... request: {"name":"thing one"}
2018-04-17 00:29:13 ... params: {"TableName":"john-cncb-db-first-dynamodb-things","Item":{"id":"4297c253-f512-443d-baaf-65f0a36aaaa3","name":"thing one"}}
END ...
REPORT ... Duration: 136.99 ms Billed Duration: 200 ms Memory Size: 1024 MB Max Memory Used: 35 MB
- 查看触发函数的日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
START ...
2018-04-17 00:29:15 ... event: {"Records":[{"eventID":"39070dc13de0eb76548506a977d4134c","eventName":"INSERT",...,"dynamodb":{"ApproximateCreationDateTime":1523939340,"Keys":{"id":{"S":"4297c253-f512-443d-baaf-65f0a36aaaa3"}},"NewImage":{"name":{"S":"thing one"},"id":{"S":"4297c253-f512-443d-baaf-65f0a36aaaa3"}},"SequenceNumber":"100000000006513931753",...},...}]}
2018-04-17 00:29:15 ... {"id":"39070dc13de0eb76548506a977d4134c","type":"thing-created","timestamp":1523939340000,"partitionKey":"4297c253-f512-443d-baaf-65f0a36aaaa3","tags":{"region":"us-east-1"},"thing":{"new":{"name":"thing one","id":"4297c253-f512-443d-baaf-65f0a36aaaa3"}}}
2018-04-17 00:29:15 ... params: {"StreamName":"john-cncb-event-stream-s1","PartitionKey":"4297c253-f512-443d-baaf-65f0a36aaaa3","Data":{"type":"Buffer","data":[...]}}
2018-04-17 00:29:15 ... {"ShardId":"shardId-000000000000","SequenceNumber":"4958...3778"}
END ...
REPORT ... Duration: 326.99 ms Billed Duration: 400 ms ... Max Memory Used: 40 MB
-
检查数据湖存储桶中收集的事件。
-
完成操作后,使用
npm run rm:lcl -- -s $MY_STAGE
命令删除栈。
它是如何工作的...
在本食谱中,我们实现了一个 command
函数,该函数将是后端前端服务的一部分。遵循事件溯源模式,我们通过只写入单个资源来使此命令原子化。在许多场景中,例如数据的编写,我们需要写入数据并确保它立即可供读取。在这些情况下,数据库优先 变体最为合适。命令只需快速执行,并尽可能减少偶然性。我们将 领域对象 写入高可用、完全管理的云原生数据库,并相信数据库的 变更数据捕获 机制将处理下一步。
在本食谱中,数据库是 DynamoDB,变更数据捕获机制是 DynamoDB Streams。trigger
函数是一个流处理器,它从指定的 DynamoDB 流中消费事件。我们通过将 StreamSpecification
添加到表的定义中启用流。
流处理器逻辑将领域对象包装在标准事件格式中,如 第一章 中 Getting Started with Cloud-Native 的 创建事件流和发布事件 菜谱所述。DynamoDB 生成的 record.eventID
被重用作领域事件 ID,数据库触发器的 record.eventName
被转换为领域事件类型,领域对象 ID 用作 partitionKey
,并添加了有用的 tags
。领域对象的 old
和 new
值包含在事件中,以便下游服务可以按其认为合适的方式计算增量。
最后,事件被写入由 STREAM_NAME
环境变量指定的流中。请注意,触发函数类似于 event-first 变体。它只需要快速执行,并尽可能减少偶然性。我们将事件写入单个资源,即高度可用、完全管理的云原生事件流,并相信下游服务最终会消费该事件。
使用 Cognito 数据集应用事件源模式的数据库优先变体
在 应用事件源模式的 event-first 变体 菜谱中,我们讨论了事件源模式如何使我们能够设计最终一致的系统,这些系统由一系列原子步骤组成。云原生系统中不支持分布式事务,因为它们无法有效扩展。因此,每个步骤必须更新一个,并且只有一个系统。在这个菜谱中,我们利用事件源模式的 database-first 变体,其中原子工作单元是写入单个云原生数据库。云原生数据库提供了一种更改数据捕获机制,允许进一步逻辑原子触发,将适当的领域事件发布到事件流以供进一步下游处理。在菜谱中,我们使用 AWS Cognito 数据集演示了此模式的 离线优先 实现。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/db-first-cognito --path cncb-db-first-cognito
-
使用
cd cncb-db-first-cognito
命令导航到cncb-db-first-cognito
目录。 -
检查名为
serverless.yml
的文件,内容如下:
service: cncb-db-first-cognito
provider:
name: aws
runtime: nodejs8.10
...
functions:
trigger:
handler: handler.trigger
events:
- stream:
type: kinesis
arn:
Fn::GetAtt: [ CognitoStream, Arn ]
...
environment:
STREAM_NAME: ${cf:cncb-event-stream-${opt:stage}.streamName}
resources:
Resources:
CognitoStream:
Type: AWS::Kinesis::Stream
Properties:
ShardCount: 1
IdentityPool:
Type: AWS::Cognito::IdentityPool
Properties:
CognitoStreams:
StreamName:
Ref: CognitoStream
...
Outputs:
identityPoolId:
Value:
Ref: IdentityPool
identityPoolName:
Value:
Fn::GetAtt: [ IdentityPool, Name ]
- 检查 AWS 控制台中名为
handler.js
的文件,内容如下:
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.flatMap(recordToSync)
.map(toEvent)
.flatMap(publish)
.collect().toCallback(cb);
};
const recordToSync = r => {
const data = JSON.parse(Buffer.from(r.kinesis.data, 'base64'));
return _(data.kinesisSyncRecords.map(sync => ({
record: r,
data: data,
sync: sync,
thing: JSON.parse(sync.value)
})));
}
const toEvent = uow => ({
id: uuid.v1(),
type: `thing-created`,
timestamp: uow.sync.lastModifiedDate,
partitionKey: uow.thing.id,
tags: {
region: uow.record.awsRegion,
identityPoolId: uow.data.identityPoolId,
datasetName: uow.data.datasetName
},
thing: {
identityId: uow.data.identityId, // the end user
...uow.thing,
},
raw: uow.sync
});
...
-
使用
npm install
安装依赖。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-db-first-cognito@1.0.0 dp:lcl <path-to-your-workspace>/cncb-db-first-cognito
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
trigger: cncb-db-first-cognito-john-trigger
Stack Outputs
identityPoolName: IdentityPool_P6awUWzjQH0y
identityPoolId: us-east-1:e51ba12c-75c2-4548-868d-2d023eb9398b
...
-
检查 AWS 控制台中的栈、函数和身份池。
-
更新名为
./index.html
的文件,并使用之前输出的identityPoolId
。 -
在浏览器中打开名为
./index.html
的文件,输入name
和description
,然后点击Save
和Synchronize
,如图所示:
- 查看日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
START ...
2018-04-19 00:50:15 ... {"id":"2714e290-438d-11e8-b3de-2bf7e0b964a2","type":"thing-created","timestamp":1524113413268,"partitionKey":"fd398c3b-8199-fd26-8c3c-156bb7ae8feb","tags":{"region":"us-east-1","identityPoolId":"us-east-1:e51ba12c-75c2-4548-868d-2d023eb9398b","datasetName":"things"},"thing":{"identityId":"us-east-1:28a2c685-2822-472e-b42a-f7bd1f02545a","id":"fd398c3b-8199-fd26-8c3c-156bb7ae8feb","name":"thing six","description":"the sixth thing"},"raw":{"key":"thing","value":"{\"id\":\"fd398c3b-8199-fd26-8c3c-156bb7ae8feb\",\"name\":\"thing six\",\"description\":\"the sixth thing\"}","syncCount":1,"lastModifiedDate":1524113413268,"deviceLastModifiedDate":1524113410528,"op":"replace"}}
2018-04-19 00:50:15 ... params: {"StreamName":"john-cncb-event-stream-s1","PartitionKey":"fd398c3b-8199-fd26-8c3c-156bb7ae8feb","Data":{"type":"Buffer","data":[...]}}
END ...
REPORT ... Duration: 217.22 ms Billed Duration: 300 ms ... Max Memory Used: 40 MB
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除栈。
工作原理...
在这个菜谱中,我们正在实现一个离线优先的解决方案,其中 ReactJS 客户端应用程序将所有更改存储在本地存储中,然后在可连接时将数据同步到云中的 Cognito 数据集。这种场景在移动应用程序中非常常见,因为移动应用程序可能并不总是连接。AWS Cognito 数据集与 AWS Identity Pool 中的特定用户相关联。在这个菜谱中,身份池支持未认证的用户。匿名访问是移动应用程序的另一个常见特征。
纯粹的 ReactJS 应用程序在 ./index.html
文件中实现。它包含一个用户输入数据的表单。保存
按钮通过 Cognito SDK 将表单数据保存到本地存储。同步
按钮使用 SDK 将本地数据发送到云。在典型应用程序中,这种同步通常在应用程序的正常流程中由事件触发,例如在保存、加载和退出之前。在 在 Cognito 数据集中创建物化视图 菜谱中,我们展示了同步也会从云中检索数据。
Cognito 的更改数据捕获功能是通过 AWS Kinesis 实现的。因此,我们创建了一个名为 CognitoStream
的 Kinesis 流,专门用于我们的 Cognito 数据集。trigger
函数是一个流处理器,它从该流中消费同步记录。流处理器的 recordToSync
步骤从每个同步记录中提取域对象,该对象以 JSON 字符串的形式存储。toEvent
步骤将域对象包装在标准事件格式中,正如在 第一章 中“创建事件流并发布事件”菜谱所讨论的那样。最后,事件被写入由 STREAM_NAME
环境变量指定的流。请注意,触发函数类似于事件优先的变体。它只需要快速执行,并尽可能减少偶然性。我们将事件写入单个资源,即高度可用、完全管理的云原生事件流,并相信下游服务最终会消费该事件。
在 DynamoDB 中创建物化视图
命令查询责任分离(CQRS)模式对于设计由边界、隔离和自主服务组成,并具有适当防波堤以限制服务出现故障时的破坏半径的云原生系统至关重要。这些防波堤通过在下游服务中创建物化视图来实现。
上游服务负责使用事件溯源模式写入数据的命令。下游服务通过创建专门针对其需求的物化视图来承担其自身的查询责任。这种 复制 数据增加了可伸缩性,减少了延迟,并允许服务在完全 自主 的情况下运行,即使上游源服务不可用。在这个配方中,我们将实现一个 AWS DynamoDB 中的物化视图。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/materialized-view-dynamodb --path cncb-materialized-view-dynamodb
-
使用
cd cncb-materialized-view-dynamodb
命令导航到cncb-materialized-view-dynamodb
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-materialized-view-dynamodb
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
environment:
TABLE_NAME:
Ref: Table
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
query:
handler: handler.query
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${opt:stage}-${self:service}-things
AttributeDefinitions:
...
KeySchema:
- AttributeName: id
KeyType: HASH
...
- 查看名为
handler.js
的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(forThingCreated)
.map(toThing)
.flatMap(put)
.collect()
.toCallback(cb);
};
...
const forThingCreated = e => e.type === 'thing-created';
const toThing = event => ({
id: event.thing.new.id,
name: event.thing.new.name,
description: event.thing.new.description,
asOf: event.timestamp,
});
const put = thing => {
const params = {
TableName: process.env.TABLE_NAME,
Item: thing,
};
const db = new aws.DynamoDB.DocumentClient();
return _(db.put(params).promise());
};
module.exports.query = (id, context, callback) => {
const params = {
TableName: process.env.TABLE_NAME,
Key: {
id: id,
},
};
const db = new aws.DynamoDB.DocumentClient();
db.get(params, callback);
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-materialized-view-dynamodb@1.0.0 dp:lcl <path-to-your-workspace>/cncb-materialized-view-dynamodb
> sls deploy -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
listener: cncb-materialized-view-dynamodb-john-listener
query: cncb-materialized-view-dynamodb-john-query
-
在 AWS 控制台中查看堆栈、函数和表。
-
从另一个终端使用以下命令发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"thing-created","thing":{"new":{"name":"thing two","id":"22222222-2222-2222-2222-222222222222"}}}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583553996455686705785668952916415462701426537440215042"
}
- 查看日志中的
listener
函数:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
START ...
2018-04-17 00:54:48 ... event: {"Records":[...]}
2018-04-17 00:54:48 ... {"id":"39070dc13de0eb76548506a977d4134c","type":"thing-created","timestamp":1523939340000,"tags":{"region":"us-east-1"},"thing":{"new":{"name":"thing two","id":"22222222-2222-2222-2222-222222222222"}}}
2018-04-17 00:54:48 ... params: {"TableName":"john-cncb-materialized-view-dynamodb-things","Item":{"id":"22222222-2222-2222-2222-222222222222","name":"thing two","asOf":1523939340000}}
END ...
REPORT ... Duration: 306.17 ms Billed Duration: 400 ms ... Max Memory Used: 36 MB
- 调用
query
命令:
$ sls invoke -r us-east-1 -f query -s $MY_STAGE -d 22222222-2222-2222-2222-222222222222
{
"Item": {
"id": "22222222-2222-2222-2222-222222222222",
"name": "thing two",
"asOf": 1523939340000
}
}
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
在这个配方中,我们实现了一个 listener
函数,该函数消费上游事件并填充一个由 Backend For Frontend (BFF) 服务使用的物化视图。这个函数是一个 流处理器,就像我们在 第一章 云原生入门 中讨论的 创建流处理器 配方中提到的那样。该函数对所需事件执行 filter
操作,然后在 map
步骤中将数据转换到所需的物化视图中。物化视图已优化以支持 BFF 所需的查询要求。只存储必要的数据,并使用最优的数据库类型。在这个配方中,数据库类型是 DynamoDB。当数据频繁变化时,DynamoDB 是物化视图的一个好选择。
注意记录中包含了 asOf
时间戳。在最终一致性的系统中,向用户提供 asOf
值非常重要,这样他们就可以访问数据的延迟。最后,数据存储在高度可用、完全管理的云原生数据库中。
在 S3 中创建物化视图
在 在 DynamoDB 中创建物化视图 的配方中,我们讨论了 CQRS 模式如何使我们能够设计边界明确、隔离和自主的服务。这使得服务即使在它们的上游依赖不可用的情况下也能运行,因为我们已经消除了所有同步的跨服务通信,转而将所需数据在专用的物化视图中本地复制和缓存。在这个配方中,我们将在 AWS S3 中实现一个物化视图。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/materialized-view-s3 --path cncb-materialized-view-s3
-
使用
cd cncb-materialized-view-s3
命令导航到cncb-materialized-view-s3
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-materialized-view-s3
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
environment:
BUCKET_NAME:
Ref: Bucket
resources:
Resources:
Bucket:
Type: AWS::S3::Bucket
Outputs:
BucketName:
Value:
Ref: Bucket
BucketDomainName:
Value:
Fn::GetAtt: [ Bucket, DomainName ]
- 查看名为
handler.js
的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(forThingCreated)
.map(toThing)
.flatMap(put)
.collect()
.toCallback(cb);
};
...
const forThingCreated = e => e.type === 'thing-created';
const toThing = event => ({
id: event.thing.new.id,
name: event.thing.new.name,
description: event.thing.new.description,
asOf: event.timestamp,
});
const put = thing => {
const params = {
Bucket: process.env.BUCKET_NAME,
Key: `things/${thing.id}`,
ACL: 'public-read',
ContentType: 'application/json',
CacheControl: 'max-age=300',
Body: JSON.stringify(thing),
};
const s3 = new aws.S3();
return _(s3.putObject(params).promise());
};
-
使用
npm install
安装依赖。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-materialized-view-s3@1.0.0 dp:lcl <path-to-your-workspace>/cncb-materialized-view-s3
> sls deploy -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
listener: cncb-materialized-view-s3-john-listener
Stack Outputs
BucketName: cncb-materialized-view-s3-john-bucket-1pp3d4c2z99kt
BucketDomainName: cncb-materialized-view-s3-john-bucket-1pp3d4c2z99kt.s3.amazonaws.com
...
-
在 AWS 控制台中查看堆栈、函数和桶。
-
使用以下命令从单独的终端发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"thing-created","thing":{"new":{"name":"thing three","id":"33333333-3333-3333-3333-333333333333"}}}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583553996455686705785668952918833314346020725338406914"
}
- 查看日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
START ...
2018-04-17 22:49:20 ... event: {"Records":[...]}
2018-04-17 22:49:20 ... {"type":"thing-created","thing":{"new":{"name":"thing three","id":"33333333-3333-3333-3333-333333333333"}},"id":"16a7b930-42b3-11e8-8700-a918e007d88a","partitionKey":"3de89e9d-c48d-4255-84fc-6c1b7e3f8b90","timestamp":1524019758148,"tags":{"region":"us-east-1"}}
2018-04-17 22:49:20 ... params: {"Bucket":"cncb-materialized-view-s3-john-bucket-1pp3d4c2z99kt","Key":"things/33333333-3333-3333-3333-333333333333","ACL":"public-read","ContentType":"application/json","CacheControl":"max-age=300","Body":"{\"id\":\"33333333-3333-3333-3333-333333333333\",\"name\":\"thing three\",\"asOf\":1524019758148}"}
2018-04-17 22:49:20 ... {"ETag":"\"edfee997659a520994ed18b82255be2a\""}
END ...
REPORT ... Duration: 167.66 ms Billed Duration: 200 ms ... Max Memory Used: 36 MB
- 在更新
bucket-suffix
之后,调用以下命令从 S3 获取数据:
$ curl https://s3.amazonaws.com/cncb-materialized-view-s3-$MY_STAGE-bucket-<bucket-suffix>/things/33333333-3333-3333-3333-333333333333 | json_pp
{
"asOf" : 1524019758148,
"name" : "thing three",
"id" : "33333333-3333-3333-3333-333333333333"
}
-
在删除堆栈之前,使用控制台删除桶中的对象。
-
完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
在这个菜谱中,我们实现了一个 listener
函数,该函数消费上游事件并填充一个由 Backend For Frontend 服务使用的物化视图。这个函数是一个流处理器,就像我们在第一章 Getting Started with Cloud-Native 中讨论的 创建流处理器 菜谱中提到的那样。该函数对所需事件执行 filter
操作,然后在 map
步骤中将数据转换到所需的物化视图中。物化视图已优化以支持 BFF 所需的查询需求。只存储必要的数据,并使用最优的数据库类型。
在这个菜谱中,数据库类型是 S3。当数据变化不频繁时,S3 是物化视图的一个好选择,并且它可以缓存在 CDN 中。请注意,记录中包含了 asOf
时间戳,以便用户可以访问数据的延迟。
在 Elasticsearch 中创建物化视图
在 在 DynamoDB 中创建物化视图 菜谱中,我们讨论了 CQRS 模式如何使我们能够设计出有界、隔离和自主的服务。这使得服务即使在它们的上游依赖不可用时也能运行,因为我们已经消除了所有同步的跨服务通信,转而将所需数据在专用的物化视图中本地复制和缓存。在这个菜谱中,我们将实现一个 AWS Elasticsearch 中的物化视图。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/materialized-view-es --path cncb-materialized-view-es
-
使用
cd cncb-materialized-view-es
命令导航到cncb-materialized-view-es
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-materialized-view-es
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
environment:
DOMAIN_ENDPOINT:
Fn::GetAtt: [ Domain, DomainEndpoint ]
...
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
search:
handler: handler.search
resources:
Resources:
Domain:
Type: AWS::Elasticsearch::Domain
Properties:
...
Outputs:
DomainName:
Value:
Ref: Domain
DomainEndpoint:
Value:
Fn::GetAtt: [ Domain, DomainEndpoint ]
- 查看名为
handler.js
的文件,其内容如下:
const client = require('elasticsearch').Client({
hosts: [`https://${process.env.DOMAIN_ENDPOINT}`],
connectionClass: require('http-aws-es'),
log: 'trace',
});
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(forThingCreated)
.map(toThing)
.flatMap(index)
.collect()
.toCallback(cb);
};
...
const forThingCreated = e => e.type === 'thing-created';
const toThing = event => ({
id: event.thing.new.id,
name: event.thing.new.name,
description: event.thing.new.description,
asOf: event.timestamp,
});
const index = thing => {
const params = {
index: 'things',
type: 'thing',
id: thing.id,
body: thing,
};
return _(client.index(params));
};
module.exports.search = (query, context, callback) => {
const params = {
index: 'things',
q: query,
};
client.search(params, callback);
};
-
使用
npm install
安装依赖。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
部署 Elasticsearch 域可能需要 1-20 分钟以上。
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-materialized-view-es@1.0.0 dp:lcl <path-to-your-workspace>/cncb-materialized-view-es
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
listener: cncb-materialized-view-es-john-listener
search: cncb-materialized-view-es-john-search
Stack Outputs
...
DomainEndpoint: search-cncb-ma-domain-gw419rzj26hz-p2g37av7sdlltosbqhag3qhwnq.us-east-1.es.amazonaws.com
DomainName: cncb-ma-domain-gw419rzj26hz
...
-
在 AWS 控制台中查看堆栈、函数和 Elasticsearch 域。
-
使用以下命令从单独的终端发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ $ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"thing-created","thing":{"new":{"name":"thing four","id":"44444444-4444-4444-4444-444444444444"}}}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583655996852917476267785004768832452002332571160543234"
}
- 查看
listener
函数的日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
START ...
2018-04-19 01:54:33 ... {"type":"thing-created","thing":{"new":{"name":"thing four","id":"44444444-4444-4444-4444-444444444444"}},"id":"0e1a68c0-4395-11e8-b455-8144cebc5972","partitionKey":"8082a69c-00ee-4388-9697-c590c523c061","timestamp":1524116810060,"tags":{"region":"us-east-1"}}
2018-04-19 01:54:33 ... params: {"index":"things","type":"thing","id":"44444444-4444-4444-4444-444444444444","body":{"id":"44444444-4444-4444-4444-444444444444","name":"thing four","asOf":1524116810060}}
2018-04-19 01:54:33 ... {"_index":"things","_type":"thing","_id":"44444444-4444-4444-4444-444444444444","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1}
END ...
REPORT ... Duration: 31.00 ms Billed Duration: 100 ms ... Max Memory Used: 42 MB
- 调用
search
命令:
$ sls invoke -r us-east-1 -f search -s $MY_STAGE -d four
{
...
"hits": {
"total": 1,
"max_score": 0.2876821,
"hits": [
{
"_index": "things",
"_type": "thing",
"_id": "44444444-4444-4444-4444-444444444444",
"_score": 0.2876821,
"_source": {
"id": "44444444-4444-4444-4444-444444444444",
"name": "thing four",
"asOf": 1524116810060
}
}
]
}
}
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除栈。
它是如何工作的...
在本食谱中,我们实现了一个 listener
函数,该函数消费上游事件并填充一个由 Backend For Frontend 服务使用的物化视图。这个函数是一个流处理器,就像我们在第一章 Getting Started with Cloud-Native 中讨论的 创建流处理器 食谱中提到的那样。该函数对所需事件执行 filter
操作,然后在 map
步骤中将数据转换到所需的物化视图中。物化视图已优化以支持 BFF 所需的查询要求。只存储必要的数据,并使用最优的数据库类型。在本食谱中,数据库类型是 Elasticsearch。当数据必须被搜索和过滤时,Elasticsearch 是物化视图的一个好选择。请注意,记录中包含了 asOf
时间戳,以便用户可以访问数据的延迟。
在 Cognito 数据集中创建物化视图
在 在 DynamoDB 中创建物化视图 食谱中,我们讨论了 CQRS 模式如何使我们能够设计边界明确、隔离和自主的服务。这允许服务在它们的上游依赖不可用时运行,因为我们已经消除了所有同步的跨服务通信,转而将所需数据在本地物化视图中复制和缓存。在本食谱中,我们将在 AWS Cognito 数据集中实现一个离线优先的物化视图。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/materialized-view-cognito --path cncb-materialized-view-cognito
-
使用
cd cncb-materialized-view-cognito
命令导航到cncb-materialized-view-cognito
目录。 -
检查以下内容的
serverless.yml
文件:
service: cncb-materialized-view-cognito
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
environment:
IDENTITY_POOL_ID:
Ref: IdentityPool
resources:
Resources:
IdentityPool:
Type: AWS::Cognito::IdentityPool
...
Outputs:
identityPoolId:
Value:
Ref: IdentityPool
identityPoolName:
Value:
Fn::GetAtt: [ IdentityPool, Name ]
- 检查以下内容的
handler.js
文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(forThingCreated)
.map(toThing)
.flatMap(put)
.collect()
.toCallback(cb);
};
...
const forThingCreated = e => e.type === 'thing-created';
const toThing = event => ({
id: event.thing.new.id,
name: event.thing.new.name,
description: event.thing.new.description,
identityId: event.thing.new.identityId, // the end user
asOf: event.timestamp,
});
const put = thing => {
const params = {
IdentityPoolId: process.env.IDENTITY_POOL_ID,
IdentityId: thing.identityId,
DatasetName: 'things',
};
const cognitosync = new aws.CognitoSync();
return _(
cognitosync.listRecords(params).promise()
.then(data => {
params.SyncSessionToken = data.SyncSessionToken;
params.RecordPatches = [{
Key: 'thing',
Value: JSON.stringify(thing),
Op: 'replace',
SyncCount: data.DatasetSyncCount,
}];
return cognitosync.updateRecords(params).promise()
})
);
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-materialized-view-cognito@1.0.0 dp:lcl <path-to-your-workspace>/cncb-materialized-view-cognito
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
listener: cncb-materialized-view-cognito-john-listener
Stack Outputs
identityPoolName: IdentityPool_c0GbzyVSh3Ws
identityPoolId: us-east-1:3a07e120-f1d8-4c85-9c34-0f908f2a21a1
...
-
在 AWS 控制台中检查栈、函数和身份池。
-
使用前一个输出的
identityPoolId
更新名为index.html
的文件。 -
在浏览器中打开名为
index.html
的文件并复制身份 ID 以用于下一步。 -
使用以下命令在单独的终端中发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"thing-created","thing":{"new":{"name":"thing five","id":"55555555-5555-5555-5555-555555555555", "identityId":"<identityId from previous step>"}}}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583655996852917476267784847452524471369889169788633090"
}
- 查看日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
START ...
2018-04-19 00:18:42 ... {"type":"thing-created","thing":{"new":{"name":"thing five","id":"55555555-5555-5555-5555-555555555555","identityId":"us-east-1:ee319396-fec4-424d-aa19-71ee751624d1"}},"id":"bda76e80-4388-11e8-a845-5902692b9264","partitionKey":"c9d4e9e5-d33f-4907-9a7a-af03710fa50f","timestamp":1524111521129,"tags":{"region":"us-east-1"}}
2018-04-19 00:18:42 ... params: {"IdentityPoolId":"us-east-1:3a07e120-f1d8-4c85-9c34-0f908f2a21a1","IdentityId":"us-east-1:ee319396-fec4-424d-aa19-71ee751624d1","DatasetName":"things"}
2018-04-19 00:18:43 ... {"Records":[{"Key":"thing","Value":"{\"id\":\"55555555-5555-5555-5555-555555555555\",\"name\":\"thing five\",\"asOf\":1524111521129,\"identityId\":\"us-east-1:ee319396-fec4-424d-aa19-71ee751624d1\"}","SyncCount":1,"LastModifiedDate":"2018-04-19T04:18:42.978Z","LastModifiedBy":"123456789012","DeviceLastModifiedDate":"2018-04-19T04:18:42.978Z"}]}
END ...
REPORT ... Duration: 340.94 ms Billed Duration: 400 ms ... Max Memory Used: 33 MB
- 在浏览器中打开名为
index.html
的文件并按同步按钮从物化视图中检索数据:
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除栈。
它是如何工作的...
在这个示例中,我们实现了一个listener
函数,该函数消费上游事件并填充一个由 Backend For Frontend 服务使用的物化视图。这个函数是一个流处理器,就像我们在第一章中讨论的创建流处理器示例中提到的。该函数对所需事件执行filter
操作,然后在map
步骤中将数据转换到所需的物化视图中。物化视图已优化以支持 BFF 所需的查询需求。只存储必要的数据,并使用最佳数据库类型。
在这个示例中,数据库类型是 Cognito 数据集。当网络可用性间歇性时,Cognito 数据集是物化视图的一个好选择,因此需要采用离线优先的方法将数据同步到用户的设备。数据还必须针对特定用户,以便可以根据用户的identityId
定位到用户。由于连接的间歇性,记录中包含asOf
时间戳,以便用户可以访问数据的延迟。
回放事件
事件源和数据湖模式的优势之一是它们允许我们在必要时回放事件以修复损坏的服务和初始化新的服务,甚至服务的新版本。在这个示例中,我们将实现一个实用程序,该实用程序从数据湖中读取选定的事件并将它们应用到指定的 Lambda 函数。
准备工作
在开始此示例之前,您需要本章中创建数据湖示例中创建的数据湖。数据湖应包含本章其他示例生成的事件。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/replaying-events --path cncb-replaying-events
-
使用以下命令导航到
cncb-replaying-events
目录:cd cncb-replaying-events
。 -
查看名为
./lib/replay.js
的文件,其内容如下:
exports.command = 'replay [bucket] [prefix]'
exports.desc = 'Replay the events in [bucket] for [prefix]'
const _ = require('highland');
const lodash = require('lodash');
const aws = require('aws-sdk');
aws.config.setPromisesDependency(require('bluebird'));
exports.builder = {
bucket: {
alias: 'b',
},
prefix: {
alias: 'p',
},
function: {
alias: 'f',
},
dry: {
alias: 'd',
default: true,
type: 'boolean'
},
region: {
alias: 'r',
default: 'us-east-1'
},
}
exports.handler = (argv) => {
aws.config.logger = process.stdout;
aws.config.region = argv.region;
const s3 = new aws.S3();
const lambda = new aws.Lambda();
paginate(s3, argv)
.flatMap(obj => get(s3, argv, obj))
.flatMap(event => invoke(lambda, argv, event))
.collect()
.each(list => console.log('count:', list.length));
}
const paginate = (s3, options) => {
let marker = undefined;
return _((push, next) => {
const params = {
Bucket: options.bucket,
Prefix: options.prefix,
Marker: marker // paging indicator
};
s3.listObjects(params).promise()
.then(data => {
if (data.IsTruncated) {
marker = lodash.last(data.Contents)['Key'];
} else {
marker = undefined;
}
data.Contents.forEach(obj => {
push(null, obj);
})
})
.catch(err => {
push(err, null);
})
.finally(() => {
if (marker) { // indicates more pages
next();
} else {
push(null, _.nil);
}
})
});
}
const get = (s3, options, obj) => {
const params = {
Bucket: options.b,
Key: obj.Key
};
return _(
s3.getObject(params).promise()
.then(data => Buffer.from(data.Body).toString())
)
.split() // EOL we added in data lake recipe transformer
.filter(line => line.length != 0)
.map(JSON.parse);
}
const invoke = (lambda, options, event) => {
let payload = {
Records: [
{
kinesis: {
partitionKey: event.kinesisRecordMetadata.partitionKey,
sequenceNumber: event.kinesisRecordMetadata.sequenceNumber,
data: Buffer.from(JSON.stringify(event.event)).toString('base64'),
kinesisSchemaVersion: '1.0',
},
eventID: `${event.kinesisRecordMetadata.shardId}:${event.kinesisRecordMetadata.sequenceNumber}`,
eventName: 'aws:kinesis:record',
eventSourceARN: event.firehoseRecordMetadata.deliveryStreamArn,
eventSource: 'aws:kinesis',
eventVersion: '1.0',
awsRegion: event.firehoseRecordMetadata.region,
}
]
};
payload = Buffer.from(JSON.stringify(payload));
const params = {
FunctionName: options.function,
InvocationType: options.dry ? 'DryRun' :
payload.length <= 100000 ? 'Event' : 'RequestResponse',
Payload: payload,
};
return _(
lambda.invoke(params).promise()
);
}
-
使用以下命令安装依赖项:
npm install
。 -
使用以下命令部署堆栈:
npm run dp:lcl -- -s $MY_STAGE
。 -
使用以下命令回放事件:
$ node index.js replay -b cncb-data-lake-s3-john-bucket-396po814rlai -p john-cncb-event-stream-s1 -f cncb-replaying-events-john-listener -dry false
[AWS s3 200 0.288s 0 retries] listObjects({ Bucket: 'cncb-data-lake-s3-john-bucket-396po814rlai',
Prefix: 'john-cncb-event-stream-s1',
Marker: undefined })
[AWS s3 200 0.199s 0 retries] getObject({ Bucket: 'cncb-data-lake-s3-john-bucket-396po814rlai',
Key: 'john-cncb-event-stream-s1/2018/04/08/03/cncb-data-lake-s3-john-DeliveryStream-13N6LEC9XJ6DZ-3-2018-04-08-03-53-28-d79d6893-aa4c-4845-8964-61256ffc6496' })
[AWS lambda 202 0.199s 0 retries] invoke({ FunctionName: 'cncb-replaying-events-john-listener',
InvocationType: 'Event',
Payload: '***SensitiveInformation***' })
[AWS lambda 202 0.151s 0 retries] invoke({ FunctionName: 'cncb-replaying-events-john-listener',
InvocationType: 'Event',
Payload: '***SensitiveInformation***' })
[AWS lambda 202 0.146s 0 retries] invoke({ FunctionName: 'cncb-replaying-events-john-listener',
InvocationType: 'Event',
Payload: '***SensitiveInformation***' })
count: 3
- 查看日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
START ...
2018-04-17 23:43:14 ... event: {"Records":[{"kinesis":{"partitionKey":"ccfd67c3-a266-4dec-9576-ae5ea228a79c","sequenceNumber":"49583337208235522365774435506752843085880683263405588482","data":"...","kinesisSchemaVersion":"1.0"},"eventID":"shardId-000000000000:49583337208235522365774435506752843085880683263405588482","eventName":"aws:kinesis:record","eventSourceARN":"arn:aws:firehose:us-east-1:123456789012:deliverystream/cncb-data-lake-s3-john-DeliveryStream-13N6LEC9XJ6DZ","eventSource":"aws:kinesis","eventVersion":"1.0","awsRegion":"us-east-1"}]}
END ...
REPORT ... Duration: 10.03 ms Billed Duration: 100 ms ... Max Memory Used: 20 MB
...
- 完成后,使用以下命令删除堆栈:
npm run rm:lcl -- -s $MY_STAGE
。
工作原理...
在这个示例中,我们实现了一个命令行界面(CLI)程序,该程序从数据湖 S3 存储桶读取事件并将它们发送到特定的 AWS Lambda 函数。在回放事件时,我们不重新发布事件,因为这会将事件广播给所有订阅者。相反,我们希望将事件回放到特定的函数中,以修复特定的服务或初始化新的服务。
在执行程序时,我们提供数据湖 bucket
的名称和特定的路径 prefix
作为参数。prefix
允许我们仅回放部分事件,例如特定月份、日期或小时。程序使用 Highland.js
库进行功能反应式编程。我们使用 generator
函数遍历桶中的对象并将每个对象推入
流中。背压是这种编程方法的主要优势,我们将在第八章 设计故障中讨论。如果我们像在命令式编程风格中那样在循环中检索桶中的所有数据,那么我们可能会耗尽内存和/或压倒 Lambda 函数并收到节流错误。
相反,我们通过流式传输提取数据。当下游步骤准备好进行更多工作时会提取下一个
数据块。这触发了生成函数在程序准备好更多数据时从 S3 分页数据。
当在数据湖存储桶中存储事件时,Kinesis Firehose 会将事件缓冲,直到达到最大时间量或最大文件大小。这种缓冲最大化了保存事件时的写入性能。在转换这些文件的数据时,我们使用 EOL 字符分隔事件。因此,当我们获取
一个特定文件时,我们利用 Highland.js 的 split
函数一次流式传输文件中的每一行。split 函数还支持背压。
对于每个事件,我们根据命令行参数指定的函数
进行调用
。这些函数旨在监听来自 Kinesis 流的事件。因此,我们必须将每个事件包装在这些函数期望的 Kinesis 输入格式中。这也是为什么我们在 创建数据湖 配方中将事件保存到数据湖时包含了 Kinesis 元数据的原因之一。为了最大化吞吐量,只要有效负载大小在限制范围内,我们就以 Event
InvocationType 异步调用 Lambda。否则,我们以 RequestReponse
InvocationType 同步调用 Lambda。我们还利用 Lambda 的 DryRun
功能,以便在实际上更改之前查看可能被回放的事件。
数据湖索引
数据湖是为云原生系统提供系统内所有事件的审计跟踪并支持事件回放能力的关键设计模式。在 创建数据湖 的配方中,我们实现了提供高持久性的数据湖的 S3 组件。然而,如果我们可以找到相关数据,数据湖才有用。在这个配方中,我们将索引 Elasticsearch 中的所有事件,以便我们可以搜索事件进行故障排除和业务分析。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/data-lake-es --path cncb-data-lake-es
-
使用
cd cncb-data-lake-es
命令导航到cncb-data-lake-es
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-data-lake-es
provider:
name: aws
runtime: nodejs8.10
plugins:
- elasticsearch
functions:
transformer:
handler: handler.transform
timeout: 120
resources:
Resources:
Domain:
Type: AWS::Elasticsearch::Domain
Properties:
...
DeliveryStream:
Type: AWS::KinesisFirehose::DeliveryStream
Properties:
DeliveryStreamType: KinesisStreamAsSource
KinesisStreamSourceConfiguration:
KinesisStreamARN: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
ElasticsearchDestinationConfiguration:
DomainARN:
Fn::GetAtt: [ Domain, DomainArn ]
IndexName: events
IndexRotationPeriod: OneDay
TypeName: event
BufferingHints:
IntervalInSeconds: 60
SizeInMBs: 50
RetryOptions:
DurationInSeconds: 60
...
ProcessingConfiguration: ${file(includes.yml):ProcessingConfiguration}
Bucket:
Type: AWS::S3::Bucket
...
Outputs:
...
DomainEndpoint:
Value:
Fn::GetAtt: [ Domain, DomainEndpoint ]
KibanaEndpoint:
Value:
Fn::Join:
- ''
- - Fn::GetAtt: [ Domain, DomainEndpoint ]
- '/_plugin/kibana'
...
- 查看名为
handler.js
的文件,其内容如下:
exports.transform = (event, context, callback) => {
const output = event.records.map((record, i) => {
// store all available data
const uow = {
event: JSON.parse((Buffer.from(record.data, 'base64')).toString('utf8')),
kinesisRecordMetadata: record.kinesisRecordMetadata,
firehoseRecordMetadata: {
deliveryStreamArn: event.deliveryStreamArn,
region: event.region,
invocationId: event.invocationId,
recordId: record.recordId,
approximateArrivalTimestamp: record.approximateArrivalTimestamp,
}
};
return {
recordId: record.recordId,
result: 'Ok',
data: Buffer.from(JSON.stringify(uow), 'utf-8').toString('base64'),
};
});
callback(null, { records: output });
};
-
使用
npm install
安装依赖。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署栈:
部署 Elasticsearch 域可能需要超过 20 分钟。
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-data-lake-es@1.0.0 dp:lcl <path-to-your-workspace>/cncb-data-lake-es
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
transformer: cncb-data-lake-es-john-transformer
Stack Outputs
DeliveryStream: cncb-data-lake-es-john-DeliveryStream-1ME9ZI78H3347
DomainEndpoint: search-cncb-da-domain-5qx46izjweyq-oehy3i3euztbnog4juse3cmrs4.us-east-1.es.amazonaws.com
DeliveryStreamArn: arn:aws:firehose:us-east-1:123456789012:deliverystream/cncb-data-lake-es-john-DeliveryStream-1ME9ZI78H3347
KibanaEndpoint: search-cncb-da-domain-5qx46izjweyq-oehy3i3euztbnog4juse3cmrs4.us-east-1.es.amazonaws.com/_plugin/kibana
DomainArn: arn:aws:es:us-east-1:123456789012:domain/cncb-da-domain-5qx46izjweyq
...
-
在 AWS 控制台中查看栈、函数和 Elasticsearch 域。
-
从单独的终端使用以下命令发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"thing-created"}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583655996852917476267785049074754815059037929823272962"
}
允许 Firehose 缓冲时间处理,因为缓冲间隔是 60 秒。
- 查看一下
transformer
函数的日志:
$ sls logs -f transformer -r us-east-1 -s $MY_STAGE
-
使用前面的
KibanaEndpoint
输出和https
协议打开 Kibana。 -
选择
Management
菜单,将索引模式设置为events-*
,然后按Next step
按钮。 -
选择
timestamp
作为Time Filter field name
,然后按Create Index pattern
。 -
选择
Discover
菜单以查看索引中的当前事件。 -
完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除栈。
它是如何工作的...
数据湖是一个宝贵的信息来源。Elasticsearch 特别适合索引这种粗粒度的时间序列信息。Kibana 是 Elasticsearch 的数据可视化插件。Kibana 是一个创建包含数据湖中事件统计信息的仪表板和基于事件内容的即席搜索以排查系统问题的优秀工具。
在这个菜谱中,我们使用 Kinesis Firehose,因为它承担了将事件写入 Elasticsearch 的繁重工作。它基于时间和大小提供缓冲,隐藏了 Elasticsearch 批量索引 API 的复杂性,提供错误处理,并支持索引轮换。在自定义的 elasticsearch
Serverless 插件中,我们创建索引模板,该模板定义了 index_patterns
和用于影响索引轮换的 timestamp
字段。
这个菜谱定义了一个交付流,因为在本书中,我们的流拓扑只包含一个流,即 ${cf:cncb-event-stream-${opt:stage}.streamArn}
。在实际应用中,您的拓扑将包含多个流,并且您将为每个 Kinesis 流定义一个 Firehose 交付流,以确保所有事件都被索引。
实现双向同步
云原生系统被设计为支持系统的持续演进。上游和下游服务被设计为可插拔的。新的服务实现可以添加,而不会影响相关服务。此外,持续部署和交付需要能够并行运行服务的多个版本并同步不同版本之间的数据。当新版本完成并且功能开启后,简单地移除旧版本。在这个配方中,我们将使用 数据库优先 的 Event Sourcing 模式的 锁定 模式来增强双向同步,而不会导致事件的无限循环。
如何操作...
- 从以下模板创建两个项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/bi-directional-sync --path cncb-1-bi-directional-sync
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch2/bi-directional-sync --path cncb-2-bi-directional-sync
- 查看每个项目中名为
serverless.yml
的文件,其内容如下:
service: cncb-1-bi-directional-sync
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
environment:
SERVERLESS_PROJECT: ${self:service}
...
functions:
command:
handler: handler.command
trigger:
handler: handler.trigger
events:
- stream:
type: dynamodb
...
listener:
handler: handler.listener
events:
- stream:
type: kinesis
...
query:
handler: handler.query
resources:
Resources:
Table:
...
- 查看名为
handler.js
的文件,其内容如下:
module.exports.command = (request, context, callback) => {
const thing = {
id: uuid.v4(),
latch: 'open',
...request,
};
...
db.put(params, callback);
};
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.filter(forLatchOpen)
.map(toEvent)
.flatMap(publish)
.collect()
.toCallback(cb);
};
const forLatchOpen = e => e.dynamodb.NewImage.latch.S === 'open';
const toEvent = record => ({
id: record.eventID,
...
tags: {
region: record.awsRegion,
source: process.env.SERVERLESS_PROJECT
},
thing: ...,
});
...
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(forSourceNotSelf)
.filter(forThingCrud)
.map(toThing)
.flatMap(put)
.collect()
.toCallback(cb);
};
...
const forSourceNotSelf = e => e.tags.source != process.env.SERVERLESS_PROJECT;
...
const toThing = event => ({
id: event.thing.new.id,
...
latch: 'closed',
});
...
-
使用
cd cncb-1-bi-directional-sync
命令导航到cncb-1-bi-directional-sync
目录。 -
使用
npm install
命令安装依赖项。 -
使用
npm test -- -s $MY_STAGE
命令运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-1-bi-directional-sync@1.0.0 dp:lcl <path-to-your-workspace>/cncb-1-bi-directional-sync
> sls deploy -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
command: cncb-1-bi-directional-sync-john-command
trigger: cncb-1-bi-directional-sync-john-trigger
listener: cncb-1-bi-directional-sync-john-listener
query: cncb-1-bi-directional-sync-john-query
-
在 AWS 控制台中查看堆栈、函数和表。
-
使用
cd cncb-2-bi-directional-sync
命令导航到cncb-2-bi-directional-sync
目录。 -
对
cncb-2-bi-directional-sync
项目重复步骤 5-9。 -
使用
cd cncb-1-bi-directional-sync
命令导航回cncb-1-bi-directional-sync
目录。 -
调用
command
函数将数据保存到第一个服务:
$ sls invoke -r us-east-1 -f command -s $MY_STAGE -d '{"id":"77777777-7777-7777-7777-777777777777","name":"thing seven"}'
- 查看日志以了解
command
和trigger
函数:
$ sls logs -f command -r us-east-1 -s $MY_STAGE
START ...
2018-04-24 02:02:11 ... event: {"id":"77777777-7777-7777-7777-777777777777","name":"thing seven"}
2018-04-24 02:02:11 ... params: {"TableName":"john-cncb-1-bi-directional-sync-things","Item":{"id":"77777777-7777-7777-7777-777777777777","latch":"open","name":"thing seven"}}
END ...
REPORT ... Duration: 146.90 ms Billed Duration: 200 ms ... Max Memory Used: 40 MB
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
START ...
2018-04-24 02:02:13 ... event: {"Records":[{"eventID":"494ec22686941c0d5ff56dee86df47dd","eventName":"INSERT",...,"Keys":{"id":{"S":"77777777-7777-7777-7777-777777777777"}},"NewImage":{"name":{"S":"thing seven"},"id":{"S":"77777777-7777-7777-7777-777777777777"},"latch":{"S":"open"}},...},...}]}
2018-04-24 02:02:13 ... {"id":"494ec22686941c0d5ff56dee86df47dd","type":"thing-created",...,"tags":{"region":"us-east-1","source":"cncb-1-bi-directional-sync"},"thing":{"new":{"name":"thing seven","id":"77777777-7777-7777-7777-777777777777","latch":"open"}}}
...
END ...
REPORT ... Duration: 140.20 ms Billed Duration: 200 ms ... Max Memory Used: 35 MB
-
使用
cd cncb-2-bi-directional-sync
命令导航到cncb-2-bi-directional-sync
目录。 -
查看日志以了解
listener
和trigger
函数:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
- 调用
query
函数从第二个服务检索同步数据:
$ sls invoke -r us-east-1 -f query -s $MY_STAGE -d 77777777-7777-7777-7777-777777777777
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
命令移除两个堆栈。
它是如何工作的...
云原生系统被设计为可以演进的。随着时间的推移,功能需求将发生变化,技术选项也将得到改善。然而,一些变化不是增量性的,或者不支持立即从一个实现切换到另一个实现。在这些情况下,需要同时运行同一功能的多个版本。如果这些服务产生数据,那么需要在服务之间同步数据变化。如果没有采用适当的锁定机制,这种双向同步将产生无限的消息循环。
此配方基于事件源模式的数据库优先变体。服务一的用户调用命令函数。命令
函数通过将域对象上的latch
设置为open
来打开latch
。触发
函数的forLatchOpen
过滤器只允许在latch
为open
时发布事件,因为打开的latch
表示更改起源于服务一。服务一中的listener
函数的forSourceNotSelf
过滤器忽略事件,因为source
标签表示事件起源于服务一。服务二中的listener
函数在保存数据之前关闭latch
,通过将域对象上的latch
设置为closed
。服务二中的触发
函数不发布事件,因为closed
的latch
表示更改并非起源于服务二。
当命令起源于服务二时,相同的流程会展开。你可以添加第三个、第四个服务以及更多,所有服务都将保持同步。
第三章:实现自治服务
在本章中,将涵盖以下食谱:
-
实现 GraphQL CRUD BFF
-
实现搜索 bff
-
实现分析 bff
-
实现入站外部服务网关
-
实现出站外部服务网关
-
协调服务之间的协作
-
实现一个 Saga
简介
在第一章,云原生入门中,我们开始了了解为什么云原生是精简和自治的旅程。我们专注于展示如何利用完全管理的云服务赋予自给自足的全栈团队能够自信地快速和持续地交付创新的食谱。在第二章,应用事件源和 CQRS 模式中,我们通过食谱展示了这些模式如何建立防波堤,从而能够创建自治服务。
在本章中,我们将所有这些基础组件通过实现自治服务模式的食谱汇集在一起。在我的书中,云原生开发模式和最佳实践,我讨论了将云原生系统分解为有界、隔离和自治服务的各种方法。
每个服务当然应该有一个边界上下文和单一职责,但我们也可以根据其他维度分解服务。数据生命周期是定义服务时的重要考虑因素,因为随着数据变老,用户、需求和持久化机制可能会发生变化。我们还可以根据边界和控制模式分解服务。边界服务,如前端后端(BFF)或外部服务网关(ESG),与系统外的事物交互,如人类和其他系统。控制服务协调这些解耦边界服务之间的交互。本章中的食谱展示了这些分解策略的常见排列组合。
实现 GraphQL CRUD BFF
BFF 模式加速了创新,因为实现前端服务的团队也拥有并实现了支持前端的后端服务。这使得团队能够自给自足,不受对共享后端服务的竞争需求的束缚。在本食谱中,我们将创建一个支持其生命周期初期的数据的 CRUD BFF 服务。该服务的单一职责是为特定边界上下文编写数据。它利用数据库优先的事件源将领域事件发布到下游服务。该服务公开了一个基于 GraphQL 的 API。
准备工作
在开始此食谱之前,您需要一个 AWS Kinesis 流,例如在创建事件流食谱中创建的流。
如何做...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch3/bff-graphql-crud --path cncb-bff-graphql-crud
-
使用
cd cncb-bff-graphql-crud
命令导航到cncb-bff-graphql-crud
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-bff-graphql-crud
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
functions:
graphql:
handler: handler.graphql
events:
- http:
path: graphql
method: post
cors: true
environment:
TABLE_NAME:
Ref: Table
graphiql:
handler: handler.graphiql
...
trigger:
handler: handler.trigger
events:
- stream:
type: dynamodb
...
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
...
- 检查名为
./schema/thing/typedefs.js
的文件,其内容如下:
module.exports = `
type Thing {
id: String!
name: String
description: String
}
type ThingConnection {
items: [Thing!]!
cursor: String
}
extend type Query {
thing(id: String!): Thing
things(name: String, limit: Int, cursor: String): ThingConnection
}
input ThingInput {
id: String
name: String!
description: String
}
extend type Mutation {
saveThing(
input: ThingInput
): Thing
deleteThing(
id: ID!
): Thing
}
`;
- 检查名为
./schema/thing/resolvers.js
的文件,其内容如下:
module.exports = {
Query: {
thing(_, { id }, ctx) {
return ctx.models.Thing.getById(id);
},
things(_, { name, limit, cursor }, ctx) {
return ctx.models.Thing.queryByName(name, limit, cursor);
},
},
Mutation: {
saveThing: (_, { input }, ctx) => {
return ctx.models.Thing.save(input.id, input);
},
deleteThing: (_, args, ctx) => {
return ctx.models.Thing.delete(args.id);
},
},
};
- 检查名为
handler.js
的文件,其内容如下:
...
const { graphqlLambda, graphiqlLambda } = require('apollo-server-lambda');
const schema = require('./schema');
const Connector = require('./lib/connector');
const { Thing } = require('./schema/thing');
module.exports.graphql = (event, context, cb) => {
graphqlLambda(
(event, context) => {
return {
schema, context: { models: {
Thing: new Thing( new Connector(process.env.TABLE_NAME) )
} }
};
}
)(event, context, (error, output) => {
cb(error, ...);
});
};
. . .
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-bff-graphql-crud@1.0.0 dp:lcl <path-to-your-workspace>/cncb-bff-graphql-crud
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
POST - https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/john/graphql
GET - https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/john/graphiql
functions:
graphql: cncb-bff-graphql-crud-john-graphql
graphiql: cncb-bff-graphql-crud-john-graphiql
trigger: cncb-bff-graphql-crud-john-trigger
Stack Outputs
...
ServiceEndpoint: https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/john
...
-
在 AWS 控制台中检查堆栈。
-
使用以下
curl
命令调用函数:
确保将端点中的 API 网关 ID(即 ac0n4oyzm6
)替换为部署期间输出的值。
$ curl -X POST -H 'Content-Type: application/json' -d '{"query":"mutation { saveThing(input: { id: \"33333333-1111-1111-1111-000000000000\", name: \"thing1\", description: \"This is thing one of two.\" }) { id } }"}' https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"saveThing" : {
"id" : "33333333-1111-1111-1111-000000000000"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -d '{"query":"mutation { saveThing(input: { id: \"33333333-1111-1111-2222-000000000000\", name: \"thing2\", description: \"This is thing two of two.\" }) { id } }"}' https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"saveThing" : {
"id" : "33333333-1111-1111-2222-000000000000"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -d '{"query":"query { thing(id: \"33333333-1111-1111-1111-000000000000\") { id name description }}"}' https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"thing" : {
"description" : "This is thing one of two.",
"id" : "33333333-1111-1111-1111-000000000000",
"name" : "thing1"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -d '{"query":"query { things(name: \"thing\") { items { id name } cursor }}"}' https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"things" : {
"items" : [
{
"id" : "33333333-1111-1111-1111-000000000000",
"name" : "thing1"
},
{
"name" : "thing2",
"id" : "33333333-1111-1111-2222-000000000000"
}
],
"cursor" : null
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -d '{"query":"query { things(name: \"thing\", limit: 1) { items { id name } cursor }}"}' https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"things" : {
"items" : [
{
"id" : "33333333-1111-1111-1111-000000000000",
"name" : "thing1"
}
],
"cursor" : "eyJpZCI6IjMzMzMzMzMzLTExMTEtMTExMS0xMTExLTAwMDAwMDAwMDAwMCJ9"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -d '{"query":"query { things(name: \"thing\", limit: 1, cursor:\"CURSOR VALUE FROM PREVIOUS RESPONSE\") { items { id name } cursor }}"}' https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"things" : {
"items" : [
{
"id" : "33333333-1111-1111-2222-000000000000",
"name" : "thing2"
}
],
"cursor" : "eyJpZCI6IjMzMzMzMzMzLTExMTEtMTExMS0yMjIyLTAwMDAwMDAwMDAwMCJ9"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -d '{"query":"mutation { deleteThing( id: \"33333333-1111-1111-1111-000000000000\" ) { id } }"}' https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"deleteThing" : {
"id" : "33333333-1111-1111-1111-000000000000"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -d '{"query":"mutation { deleteThing( id: \"33333333-1111-1111-2222-000000000000\" ) { id } }"}' https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"deleteThing" : {
"id" : "33333333-1111-1111-2222-000000000000"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -d '{"query":"query { things(name: \"thing\") { items { id } }}"}' https://ac0n4oyzm6.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"things" : {
"items" : []
}
}
}
- 使用部署期间输出的端点通过 GraphiQL 执行相同的突变和查询。
- 查看触发函数的日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
-
检查数据湖存储桶中收集的事件。
-
完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
此配方基于第二章“应用 DynamoDB 的数据库优先事件源模式”配方,在 第二章 中,应用事件源和 CQRS 模式,通过暴露通过 GraphQL API 在有界上下文中编写数据的能力。GraphQL 由于结果 API 的灵活性和客户端库(如 Apollo 客户端)的强大功能而越来越受欢迎。我们实现了一个单一的 graphql
函数来支持我们的 API,然后通过 schema
、resolvers
、models
和 connectors
添加必要的功能。
GraphQL 模式是我们定义 types
、queries
和 mutations
的地方。在这个配方中,我们可以通过 ID 和名称查询 thing
类型,以及 save
和 delete
。resolvers
将 GraphQL 请求映射到封装业务逻辑的 model
对象。models
然后与封装数据库 API 的 connectors
通信。models
和 connectors
在 handler
函数中使用基于构造函数的依赖注入的非常简单但有效的方式注册到 schema
中。在云原生中,我们并不经常使用依赖注入,因为函数非常小且专注,这可能是过度设计且可能影响性能。使用 GraphQL,这种简单形式对于促进测试非常有效。Graphiql
工具对于暴露 API 的自文档特性非常有用。
此服务的单一职责是编写数据并发布事件,使用数据库优先的事件溯源,针对特定的有界上下文。服务内部的代码遵循非常可重复的编码约定,包括 types
、resolvers
、models
、connectors
和 triggers
。因此,即使服务中的业务域数量增加,也很容易推断代码的正确性。因此,在单个编写 BFF 服务中拥有更多的域是合理的,只要这些域是内聚的,属于同一个有界上下文,并且由一组一致的用户编写。
实现搜索 BFF
在 实现 GraphQL CRUD BFF 的配方中,我们讨论了 BFF 模式如何加速创新。我们还讨论了不同用户群体在不同数据生命周期阶段如何与数据交互,以及在不同阶段哪些持久机制更合适。在这个配方中,我们将创建一个支持数据只读消费的 BFF 服务。此服务的单一职责是针对特定的有界上下文进行 索引 和检索数据。它应用了 CQRS 模式来创建两个协同工作的 物化视图,一个在 Elasticsearch 中,另一个在 S3 中。该服务公开了一个 RESTful API。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch3/bff-rest-search --path cncb-bff-rest-search
-
使用
cd cncb-bff-rest-search
切换到cncb-bff-rest-search
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-bff-rest-search
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
environment:
BUCKET_NAME:
Ref: Bucket
DOMAIN_ENDPOINT:
Fn::GetAtt: [ Domain, DomainEndpoint ]
...
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
trigger:
handler: handler.trigger
events:
- sns:
arn:
Ref: BucketTopic
topicName: ${self:service}-${opt:stage}-trigger
search:
handler: handler.search
events:
- http:
path: search
method: get
cors: true
resources:
Resources:
Bucket:
Type: AWS::S3::Bucket
...
Properties:
NotificationConfiguration:
TopicConfigurations:
- Event: s3:ObjectCreated:Put
Topic:
Ref: BucketTopic
BucketTopic:
Type: AWS::SNS::Topic
...
Domain:
Type: AWS::Elasticsearch::Domain
...
- 查看名为
handler.js
的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(forThingCreated)
.map(toThing)
.flatMap(put)
.collect()
.toCallback(cb);
};
...
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.flatMap(messagesToTriggers)
.flatMap(get)
.map(toSearchRecord)
.flatMap(index)
.collect()
.toCallback(cb);
};
const messagesToTriggers = r => _(JSON.parse(r.Sns.Message).Records);
const get = (trigger) => {
const params = {
Bucket: trigger.s3.bucket.name,
Key: trigger.s3.object.key,
};
const s3 = new aws.S3();
return _(
s3.getObject(params).promise()
.then(data => ({
trigger: trigger,
thing: JSON.parse(Buffer.from(data.Body)),
}))
);
};
const toSearchRecord = uow => ({
id: uow.thing.id,
name: uow.thing.name,
description: uow.thing.description,
url: `https://s3.amazonaws.com/${uow.trigger.s3.bucket.name}/${uow.trigger.s3.object.key}`,
});
...
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-bff-rest-search@1.0.0 dp:lcl <path-to-your-workspace>/cncb-bff-rest-search
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
GET - https://n31t5dsei8.execute-api.us-east-1.amazonaws.com/john/search
functions:
listener: cncb-bff-rest-search-john-listener
trigger: cncb-bff-rest-search-john-trigger
search: cncb-bff-rest-search-john-search
Stack Outputs
...
BucketArn: arn:aws:s3:::cncb-bff-rest-search-john-bucket-1xjkvimbjtfj2
BucketName: cncb-bff-rest-search-john-bucket-1xjkvimbjtfj2
TopicArn: arn:aws:sns:us-east-1:123456789012:cncb-bff-rest-search-john-trigger
DomainEndpoint: search-cncb-bf-domain-xavolfersvjd-uotz6ggdqhhwk7irxhnkjl26ay.us-east-1.es.amazonaws.com
DomainName: cncb-bf-domain-xavolfersvjd
...
ServiceEndpoint: https://n31t5dsei8.execute-api.us-east-1.amazonaws.com/john
...
-
在 AWS 控制台中查看堆栈和资源。
-
从单独的终端使用以下命令发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -f publish -r us-east-1 -s $MY_STAGE -d '{"type":"thing-created","thing":{"new":{"name":"thing three","id":"33333333-2222-0000-1111-111111111111"}}}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583553996455686705785668952918833314346020725338406914"
}
- 在更新
API-ID
和BUCKET-SUFFIX
后,调用以下curl
命令以搜索数据和从S3
获取详细数据:
$ curl https://<API-ID>.execute-api.us-east-1.amazonaws.com/$MY_STAGE/search?q=three | json_pp
[
{
"id" : "33333333-2222-0000-1111-111111111111",
"url" : "https://s3.amazonaws.com/cncb-bff-rest-search-john-bucket-1xjkvimbjtfj2/things/33333333-2222-0000-1111-111111111111",
"name" : "thing three"
}
]
$ curl https://s3.amazonaws.com/cncb-bff-rest-search-$MY_STAGE-bucket-<BUCKET-SUFFIX>/things/33333333-2222-0000-1111-111111111111 | json_pp
{
"asOf" : 1526026359761,
"name" : "thing three",
"id" : "33333333-2222-0000-1111-111111111111"
}
- 查看触发函数的日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
此配方结合并建立在 在 S3 中创建物化视图 和 在 Elasticsearch 中创建物化视图 的配方之上,以创建一个高度可扩展、高效且成本效益高的有界上下文数据只读视图。首先,listener
函数原子性地在 S3 中创建物化视图。S3 的 Bucket
配置为向名为 BucketTopic
的 简单通知服务(SNS)主题发送事件。我们使用 SNS 传递 S3 事件,因为只有一个观察者可以消费 S3 事件,而 SNS 则可以传递给任何数量的观察者。接下来,trigger
函数原子性地索引 Elasticsearch 域
中的数据,并在 S3 中的物化视图中包含 url
。
API 网关公开的 RESTful 搜索服务可以明确地扩展以满足需求,并有效地搜索大量索引数据。然后,可以根据返回的 URL 以成本效益地从 S3 检索详细数据,无需通过 API 网关、函数和数据库。我们首先在 S3 中创建数据,然后在 Elasticsearch 中索引数据,以确保搜索结果不包含尚未成功存储在 S3 中的数据。
实现分析 BFF
在实现 GraphQL CRUD BFF配方中,我们讨论了BFF模式如何加速创新。我们还讨论了不同用户群体如何在数据生命周期的不同阶段与数据交互,以及在不同阶段哪些持久机制更合适。在这个配方中,我们将创建一个 BFF 服务,该服务提供有关数据生命周期的统计信息。该服务的单一职责是累积和聚合特定边界上下文中数据的指标。它应用事件溯源模式创建一个微事件存储库,用于持续计算指标的物化视图。该服务公开 RESTful API。
如何做到这一点...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch3/bff-rest-analytics --path cncb-bff-rest-analytics
-
使用
cd cncb-bff-rest-analytics
导航到cncb-bff-rest-analytics
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-bff-rest-analytics
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
environment:
EVENTS_TABLE_NAME:
Ref: Events
VIEW_TABLE_NAME:
Ref: View
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
trigger:
handler: handler.trigger
events:
- stream:
type: dynamodb
arn:
Fn::GetAtt: [ Events, StreamArn ]
...
query:
handler: handler.query
events:
- http:
...
resources:
Resources:
Events:
Type: AWS::DynamoDB::Table
Properties:
...
KeySchema:
- AttributeName: partitionKey
KeyType: HASH
- AttributeName: timestamp
KeyType: RANGE
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
...
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
View:
Type: AWS::DynamoDB::Table
Properties:
...
KeySchema:
- AttributeName: userId
KeyType: HASH
- AttributeName: yearmonth
KeyType: RANGE
...
- 检查名为
handler.js
的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(byType)
.flatMap(putEvent)
.collect()
.toCallback(cb);
};
...
const byType = event => event.type.match(/.+/); // any
const putEvent = (event) => {
const params = {
TableName: process.env.EVENTS_TABLE_NAME,
Item: {
partitionKey: event.partitionKey,
timestamp: event.timestamp,
event: event,
ttl: moment(event.timestamp).add(1, 'h').unix()
}
};
const db = new aws.DynamoDB.DocumentClient();
return _(db.put(params).promise());
};
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.flatMap(getMicroEventStore)
.flatMap(store => _(store) // sub-stream
.reduce({}, count)
.flatMap(putCounters)
)
.collect()
.toCallback(cb);
};
const getMicroEventStore = (record) => {
...
}
const count = (counters, cur) => {
return Object.assign(
{
userId: cur.partitionKey,
yearmonth: moment(cur.timestamp).format('YYYY-MM'),
},
counters,
{
total: counters.total ? counters.total + 1 : 1,
[cur.event.type]: counters[cur.event.type] ? counters[cur.event.type] + 1 : 1,
}
);
;
}
const putCounters = counters => {
...
};
module.exports.query = (event, context, cb) => {
...
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-bff-rest-analytics@1.0.0 dp:lcl <path-to-your-workspace>/cncb-bff-rest-analytics
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
GET - https://efbildhw0h.execute-api.us-east-1.amazonaws.com/john/query
functions:
listener: cncb-bff-rest-analytics-john-listener
trigger: cncb-bff-rest-analytics-john-trigger
query: cncb-bff-rest-analytics-john-query
Stack Outputs
...
ServiceEndpoint: https://efbildhw0h.execute-api.us-east-1.amazonaws.com/john
...
-
在 AWS 控制台中检查堆栈和资源。
-
从单独的终端使用以下命令从单独的终端发布几个事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"purple","partitionKey":"33333333-3333-1111-1111-111111111111"}'
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"orange","partitionKey":"33333333-3333-1111-1111-111111111111"}'
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"green","partitionKey":"33333333-3333-1111-2222-111111111111"}'
$ sls invoke -r us-east-1 -f publish -s $MY_STAGE -d '{"type":"green","partitionKey":"33333333-3333-1111-2222-111111111111"}'
- 更新
<API-ID>
后,调用以下curl
命令来查询分析:
$ curl https://<API-ID>.execute-api.us-east-1.amazonaws.com/$MY_STAGE/query | json_pp
[
{
"userId" : "33333333-3333-1111-1111-111111111111",
"yearmonth" : "2018-05",
"purple" : 1,
"orange" : 1,
"total" : 2
},
{
"userId" : "33333333-3333-1111-2222-111111111111",
"yearmonth" : "2018-05",
"green" : 2,
"total" : 2
}
]
- 查看触发函数日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
这个配方结合并建立在第二章中创建微事件存储库和在 DynamoDB 中创建物化视图配方的基础上,应用事件溯源和 CQRS 模式以创建一个高级物化视图,该视图按type
、user
、month
和year
计数事件。该服务使用两个 DynamoDB 表、微事件存储库和物化视图。事件存储库的HASH
键是partitionKey
,其中包含userId
,这样我们就可以根据用户关联事件。范围键是timestamp
,这样我们就可以整理事件并按月份查询。视图表的哈希键也是userId
,范围键是monthyear
,这样我们就可以按用户、月份和年份检索统计数据。在这个示例中,我们正在计数所有事件,但在典型解决方案中,您将根据特定的事件类型集过滤byType
。
listener
函数执行了将事件过滤、关联和整理到微事件存储库中的关键任务,但在这个配方中真正有趣的逻辑在于 trigger
函数。该逻辑基于 ACID 2.0 事务模型的概念。ACID 2.0 代表 关联性、交换性、幂等性和分布式。本质上,这个模型允许我们无论事件是否按正确顺序到达,甚至是否多次接收到相同的事件,都能得到相同、正确的答案。微事件存储库中的哈希键和范围键处理幂等性。对于每个新的键,我们根据新事件的上下文查询事件存储库,并基于最新的已知数据进行计算,从而重新计算物化视图。如果事件顺序错误到达,它将简单地触发重新计算。在这个特定示例中,最终用户期望统计信息最终会在月底或之后不久变得一致。
计算可以是任意复杂的。计算是在内存中进行的,微事件存储库查询的结果可以以许多不同的方式切片和切块。对于这个配方,流上的 reduce
方法非常适合计数。需要注意的是,sub-stream
确保计数是通过 userId
进行的,因为这是事件存储库返回的结果的哈希键。结果以 JSON 文档的形式存储在物化视图中,以便可以有效地检索。
TimeToLive (TTL) 功能已设置在事件表上。此功能可以用来防止事件存储库无限制地增长,但也可以用来触发周期性的汇总计算。我将 TTL 设置为一小时,这样如果你等待足够长的时间,就可以看到它执行,但通常你会将此设置为适合你计算值的值,大约为一个月、一个季度或一年。
实现入站外部服务网关
外部服务网关 (ESG) 模式在云原生系统和它所交互的任何外部服务之间提供了一个反腐败层。每个网关充当系统与特定外部系统之间交换事件的桥梁。在这个配方中,我们将创建一个允许事件从外部服务流入的 ESG 服务。这个服务的单一职责是封装外部系统的细节。该服务向外部系统公开了一个 RESTful webhook。外部事件被转换成内部格式,并使用 事件优先 事件溯源进行发布。
如何做到这一点...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch3/esg-inbound --path cncb-esg-inbound
-
使用
cd cncb-esg-inbound
命令导航到cncb-esg-inbound
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-esg-inbound
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
functions:
webhook:
handler: handler.webhook
events:
- http:
path: webhook
method: post
environment:
STREAM_NAME: ${cf:cncb-event-stream-${opt:stage}.streamName}
- 查看名为
handler.js
的文件,其内容如下:
module.exports.webhook = (request, context, callback) => {
const body = JSON.parse(request.body);
const event = {
type: `issue-${body.action}`,
id: request.headers['X-GitHub-Delivery'],
partitionKey: String(body.issue.id),
timestamp: Date.parse(body.issue['updated_at']),
tags: {
region: process.env.AWS_REGION,
repository: body.repository.name,
},
issue: body, // canonical
raw: request
};
...
kinesis.putRecord(params, (err, resp) => {
const response = {
statusCode: err ? 500 : 200,
};
callback(null, response);
});
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-esg-inbound@1.0.0 dp:lcl <path-to-your-workspace>/js-cloud-native-cookbook/workspace/cncb-esg-inbound
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
POST - https://kc880846ve.execute-api.us-east-1.amazonaws.com/john/webhook
functions:
webhook: cncb-esg-inbound-john-webhook
Stack Outputs
...
ServiceEndpoint: https://kc880846ve.execute-api.us-east-1.amazonaws.com/john
...
-
在 AWS 控制台中查看堆栈和资源。
-
在你的 GitHub 项目中设置 webhook:
-
将有效载荷 URL 设置为 webhook 的端点
-
将内容类型设置为
application/json
-
将密钥设置为随机值
-
仅选择
Issues
事件复选框
-
有关创建 GitHub webhook 的详细说明,请参阅 developer.github.com/webhooks/creating.
-
在你的 GitHub 项目中创建和/或更新一个或多个问题以触发 webhook。
-
查看
webhook
函数日志:
$ sls logs -f webhook -r us-east-1 -s $MY_STAGE
-
查看数据湖中的事件。
-
使用
npm run rm:lcl -- -s $MY_STAGE
完成后删除堆栈
它是如何工作的...
我选择在这个食谱中使用 GitHub 作为外部系统,因为它对每个人都是免费可用的,并且代表了典型的需求。在这个食谱中,我们的入站 ESG 服务需要提供一个外部系统将调用的 API,并符合外部系统 webhook 的签名。我们使用 API Gateway 和 webhook
函数来实现这个 webhook。该函数的单一职责是将外部事件转换为内部事件,并使用事件优先的事件溯源原子性地发布它。
注意,外部事件 ID 被用作内部事件 ID 以提供幂等性。外部事件数据以原始格式包含在内部事件中,以便它可以作为数据湖中的审计记录。外部格式也转换为内部规范格式以支持不同外部系统的可插拔性。入站 ESG 服务中的逻辑故意保持简单,以最大限度地减少错误的可能性并帮助确保系统之间事件交换的原子性。
实现出站外部服务网关
在 实现入站外部服务网关 食谱中,我们讨论了 ESG 模式如何在云原生系统和其外部依赖之间提供反腐败层。在这个食谱中,我们将创建一个 ESG 服务,允许事件流出到外部服务。这个服务的单一职责是封装外部系统的细节。该服务应用了 CQRS 模式。内部事件被转换为外部格式并通过其 API 发送到外部系统。
准备工作
在开始这个食谱之前,你需要一个 AWS Kinesis Stream,例如在 创建事件流 食谱中创建的那个。
你需要一个 GitHub 账户和一个存储库。我建议创建一个名为 sandbox
的存储库。使用以下命令创建 GitHub 个人访问令牌,或遵循 GitHub UI 中的说明:
curl https://api.github.com/authorizations \
--user "your-github-id" \
--data '{"scopes":["repo"],"note":"recipe"}'
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch3/esg-outbound --path cncb-esg-outbound
-
使用
cd cncb-esg-outbound
命令进入cncb-esg-outbound
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-esg-outbound
provider:
name: aws
runtime: nodejs8.10
environment:
REPO: enter-your-github-project
OWNER: enter-your-github-id
TOKEN: enter-your-github-token
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
-
在
serverless.yml
文件中更新REPO
、OWNER
和TOKEN
环境变量。 -
查看名为
handler.js
的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(byType)
.flatMap(post)
.collect()
.toCallback(cb);
};
...
const byType = event => event.type === 'issue-created';
const post = event => {
// transform internal to external
const body = {
title: event.issue.new.title,
body: event.issue.new.description,
};
return _(
fetch(`https://api.github.com/repos/${process.env.OWNER}/${process.env.REPO}/issues`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.TOKEN}`
},
body: JSON.stringify(body)
})
);
};
-
使用
npm install
安装依赖。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-esg-outbound@1.0.0 dp:lcl <path-to-your-workspace>/cncb-esg-outbound
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
listener: cncb-esg-outbound-john-listener
...
-
在 AWS 控制台中查看堆栈和资源。
-
从单独的终端使用以下命令发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -f publish -r us-east-1 -s $MY_STAGE -d '{"type":"issue-created","issue":{"new":{"title":"issue one","description":"this is issue one.","id":"33333333-55555-1111-1111-111111111111"}}}'
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583655996852917476267887119095157508589012871374962690"
}
-
确认问题已在您的 GitHub 项目中创建。
-
查看
listener
函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
- 使用
npm run rm:lcl -- -s $MY_STAGE
完成后删除堆栈。
它是如何工作的...
我选择在这个菜谱中使用 GitHub 作为外部系统,因为它对每个人都是免费可用的,并且它的 API 代表了典型需求。ESG 服务封装的一个主要细节是访问外部 API 所需的安全凭证。在这个菜谱中,我们必须创建并保护一个长期有效的 个人访问令牌,并将其作为授权头包含在每个 API 请求中。然而,如何保护令牌的细节超出了这个菜谱的范围,但是通常使用像 AWS Secret Manager 这样的服务。对于这个菜谱,令牌被存储为环境变量。
listener
函数消费所需的事件,将它们转换为外部格式,并原子性地调用外部 API。这就是 ESG 服务的责任极限。这些服务有效地使外部服务看起来像系统中的任何其他服务,同时封装细节,以便将来可以轻松切换这些外部依赖。转换逻辑可能变得复杂。在 实现双向同步 菜谱中讨论的锁定技术可能会发挥作用,以及需要交叉引用外部 ID 到内部 ID。在许多情况下,外部数据可以被视为物化视图,在这种情况下,微事件存储技术可能很有用。在一个作为服务提供的系统中,ESG 服务将提供您的自己的出站 webhook 功能。
协调服务之间的协作。
自主云原生服务通过流异步执行所有服务间通信,以解耦上游服务与下游服务。尽管上游和下游服务之间没有直接耦合,但它们与它们产生和消费的事件类型耦合。事件编排控制模式充当调解者,通过在事件类型之间进行转换,完全解耦事件生产者与事件消费者。
在本菜谱中,我们将创建一个控制服务,该服务协调两个边界服务之间的交互。该服务的单一职责是封装协作的细节。上游事件被转换为下游期望的事件类型,并使用 event-first 事件源进行发布。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch3/event-orchestration --path cncb-event-orchestration
-
使用
cd cncb-event-orchestration
命令进入cncb-event-orchestration
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-event-orchestration
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
functions:
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
environment:
STREAM_NAME: ${cf:cncb-event-stream-${opt:stage}.streamName}
- 查看名为
handler.js
的文件,其内容如下:
const transitions = [
{
filter: 'order-submitted',
emit: (uow) => ({
id: uuid.v1(),
type: 'make-reservation',
timestamp: Date.now(),
partitionKey: uow.event.partitionKey,
reservation: {
sku: uow.event.order.sku,
quantity: uow.event.order.quantity,
},
context: {
order: uow.event.order,
trigger: uow.event.id
}
})
},
{
filter: 'reservation-confirmed',
emit: (uow) => ({
id: uuid.v1(),
type: 'update-order-status',
timestamp: Date.now(),
partitionKey: uow.event.partitionKey,
order: {
status: 'reserved',
},
context: {
reservation: uow.event.reservation,
order: uow.event.context.order,
trigger: uow.event.id
}
})
},
];
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(onTransitions)
.flatMap(toEvents)
.flatMap(publish)
.collect()
.toCallback(cb);
};
const recordToUow = r => ({
record: r,
event: JSON.parse(Buffer.from(r.kinesis.data, 'base64')),
});
const onTransitions = uow => {
// find matching transitions
uow.transitions = transitions.filter(trans => trans.filter === uow.event.type);
// proceed forward if there are any matches
return uow.transitions.length > 0;
};
const toEvents = uow => {
// create the event to emit for each matching transition
return _(uow.transitions.map(t => t.emit(uow)));
};
const publish = event => {
. . .
}
-
使用
npm install
命令安装依赖项。 -
使用
npm test -- -s $MY_STAGE
命令运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-event-orchestration@1.0.0 dp:lcl <path-to-your-workspace>/cncb-event-orchestration
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
listener: cncb-event-orchestration-john-listener
...
-
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令从单独的终端发布这些事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -f publish -r us-east-1 -s $MY_STAGE -p ../cncb-event-orchestration/data/order.json
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583655996852917476267896339723499436825420846818394114"
}
$ sls invoke -f publish -r us-east-1 -s $MY_STAGE -p ../cncb-event-orchestration/data/reservation.json
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49583655996852917476267896340117609254019790713686851586"
}
- 查看以下
listener
函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
-
查看数据湖中的事件。
-
完成后,使用
npm run rm:lcl -- -s $MY_STAGE
命令删除堆栈。
它是如何工作的...
此 控制 服务有一个单一的 流处理器 函数,该函数监听特定事件,并通过使用事件-first 事件源发出更多事件来做出反应。它监听的事件在 transitions
元数据中描述,这实际上定义了长期业务流程的状态机。每个业务流程都作为实现为自主控制服务的独立实例来实施,该服务协调一组完全解耦的边界服务之间的协作。每个参与协作的 边界 服务独立定义其产生和消费的事件集。控制服务提供粘合剂,将这些服务聚集在一起,以提供更高的价值结果。
由发出的事件触发的 下游 服务确实有一个要求,即它们在定义其传入和传出事件类型时必须支持。传入的事件类型必须接受 context
元素作为不透明的数据集,并在传出事件中传递上下文数据。下游服务可以利用上下文数据,但不应显式更改上下文数据。上下文数据允许控制服务在不显式存储和检索数据的情况下关联特定协作实例中的事件。然而,控制服务可以维护自己的 微事件存储 以便于复杂的转换逻辑,例如在前进之前将多个并行流程合并在一起。
实施一个 Saga
Saga 模式是基于最终一致性和补偿事务的长期事务解决方案。它首次在 Hector Garcia-Molina 和 Kenneth Salem 的论文中讨论(www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf
)。长期事务中的每一步都是原子的。当下游步骤失败时,会产生一个违规事件。上游服务通过执行补偿操作来响应违规事件。在这个菜谱中,我们将创建一个提交数据以供下游处理的服务。该服务还监听违规事件并采取纠正措施。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch3/saga --path cncb-saga
-
使用
cd cncb-sage
命令导航到cncb-saga
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-saga
provider:
name: aws
runtime: nodejs8.10
iamRoleStatements:
...
environment:
TABLE_NAME:
Ref: Table
functions:
submit:
handler: handler.submit
trigger:
handler: handler.trigger
events:
- stream:
type: dynamodb
arn:
Fn::GetAtt: [ Table, StreamArn ]
...
environment:
STREAM_NAME: ${cf:cncb-event-stream-${opt:stage}.streamName}
listener:
handler: handler.listener
events:
- stream:
type: kinesis
arn: ${cf:cncb-event-stream-${opt:stage}.streamArn}
...
query:
handler: handler.query
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${opt:stage}-${self:service}-orders
...
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
- 查看名为
handler.js
的文件,其内容如下:
...
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToEvent)
.filter(forReservationViolation)
.flatMap(compensate)
.collect()
.toCallback(cb);
};
const forReservationViolation = e => e.type === 'reservation-violation';
const compensate = event => {
const params = {
TableName: process.env.TABLE_NAME,
Key: {
id: event.context.order.id
},
AttributeUpdates: {
status: { Action: 'PUT', Value: 'cancelled' }
},
};
const db = new aws.DynamoDB.DocumentClient();
return _(db.update(params).promise());
};
...
-
使用
npm install
安装依赖。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-saga@1.0.0 dp:lcl <path-to-your-workspace>/cncb-saga
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
submit: cncb-saga-john-submit
trigger: cncb-saga-john-trigger
listener: cncb-saga-john-listener
query: cncb-saga-john-query
...
-
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用
submit
和query
函数:
$ sls invoke -f submit -r us-east-1 -s $MY_STAGE -p data/order.json
$ sls invoke -f query -r us-east-1 -s $MY_STAGE -d 33333333-7777-1111-1111-111111111111
{
"Item": {
"quantity": 1,
"id": "33333333-7777-1111-1111-111111111111",
"sku": "1",
"status": "submitted"
}
}
- 从单独的终端使用以下命令发布违规事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -f publish -r us-east-1 -s $MY_STAGE -p ../cncb-saga/data/reservation.json
{
"ShardId": "shardId-000000000000",
"SequenceNumber": "49584174522005480245492626573048465901488330636951289858"
}
- 再次使用以下命令调用
query
函数,并注意更新的状态
:
$ sls invoke -r us-east-1 -f query -s $MY_STAGE -d 33333333-7777-1111-1111-111111111111
{
"Item": {
"quantity": 1,
"id": "33333333-7777-1111-1111-111111111111",
"sku": "1",
"status": "cancelled"
}
}
- 查看以下
trigger
函数日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
- 查看以下
listener
函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
此菜谱基于我们已覆盖的菜谱。我们使用 数据库优先 事件源提交订单域对象,并且有一个 query
函数来检索订单的当前状态。listener
函数是这个菜谱中最有趣的部分。它监听 reservation-violation
事件并执行补偿操作。在这种情况下,补偿
简单地是将 状态
改为 已取消
。补偿操作可以是任意复杂的,并且特定于给定的服务。例如,一个服务可能需要反转一个复杂的计算,同时考虑到中间变化并触发级联变化。事件源和 微事件存储 提供的审计跟踪可能有助于重新计算新状态。
在这个例子中,需要注意的是,协作是通过 事件 编排 而不是 事件编排 实现的。换句话说,此服务明确耦合到 reservation-violation
事件类型。事件编排通常用于较小和/或较新的系统,或者在高相关服务之间使用。随着系统的成熟和增长,事件编排 的灵活性变得更加有价值。我们在 编排服务之间的协作 菜谱中使用了事件编排。
第四章:利用云端的边缘优势
本章将涵盖以下食谱:
-
从 CDN 提供单页应用程序
-
将自定义域名与 CDN 关联
-
从 CDN 提供网站
-
在 CDN 后部署服务
-
从 CDN 提供静态 JSON
-
触发 CDN 中内容的无效化
-
在云端的边缘执行代码
简介
云端的边缘可能是云原生系统中最被低估的一层。然而,正如我之前提到的,我的第一个云原生“哇”时刻是当我意识到我可以在不运行弹性负载均衡器和多个 EC2 实例的复杂度、风险和成本的情况下,从边缘运行 单页应用程序(SPA)表示层。此外,利用边缘可以将云原生系统的一些方面扩展到全球规模,而无需在多区域部署上付出额外的努力。我们的最终用户享受较低的延迟,而我们减少了系统内部负载,降低了成本,并提高了安全性。本章中的食谱涵盖了多种我们可以利用云端的边缘来提升云原生系统质量的方法,而所需的工作量最小。
从 CDN 提供单页应用程序
在 部署单页应用程序 食谱中,我们介绍了从 S3 存储桶提供单页应用程序所需的步骤。当一位架构师意识到这样一个简单的部署过程可以提供如此多的可扩展性时,这真是一件令人愉快的事情。以下是来自我的客户的一个最近的引用——“这就完了?真的吗?这就完了?”在本食谱中,我们将此过程进一步推进,并展示我们如何轻松地在 S3 前添加 CloudFront 内容分发网络(CDN)层,以利用更多有价值的特性。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch4/cdn-spa --path cncb-cdn-spa
-
使用
cd cncb-cdn-spa
切换到cncb-cdn-spa
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-cdn-spa
provider:
name: aws
plugins:
- serverless-spa-deploy
- serverless-spa-config
custom:
spa:
files:
- source: ./build
globs: '**/*'
headers:
CacheControl: max-age=31536000 # 1 year
- source: ./build
globs: 'index.html'
headers:
CacheControl: max-age=300 # 5 minutes
-
使用
npm install
安装依赖。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容:
{
"Resources": {
...
"WebsiteBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"AccessControl": "PublicRead",
"WebsiteConfiguration": {
"IndexDocument": "index.html",
"ErrorDocument": "index.html"
}
}
},
"WebsiteDistribution": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"Comment": "Website: test-cncb-cdn-spa (us-east-1)",
"Origins": [
{
"Id": "S3Origin",
"DomainName": {
"Fn::Select": [
1,
{
"Fn::Split": [
"//",
{
"Fn::GetAtt": [
"WebsiteBucket",
"WebsiteURL"
]
}
]
}
]
},
"CustomOriginConfig": {
"OriginProtocolPolicy": "http-only"
}
}
],
"Enabled": true,
"PriceClass": "PriceClass_100",
"DefaultRootObject": "index.html",
"CustomErrorResponses": [
{
"ErrorCachingMinTTL": 0,
"ErrorCode": 404,
"ResponseCode": 200,
"ResponsePagePath": "/index.html"
}
],
"DefaultCacheBehavior": {
"TargetOriginId": "S3Origin",
"AllowedMethods": [
"GET",
"HEAD"
],
"CachedMethods": [
"HEAD",
"GET"
],
"Compress": true,
"ForwardedValues": {
"QueryString": false,
"Cookies": {
"Forward": "none"
}
},
"MinTTL": 0,
"DefaultTTL": 0,
"ViewerProtocolPolicy": "allow-all"
}
}
}
}
},
...
}
-
使用
npm run build
构建应用。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-cdn-spa@1.0.0 dp:lcl <path-to-your-workspace>/cncb-cdn-spa
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
WebsiteDistributionURL: https://dqvo8ga8z7ao3.cloudfront.net
WebsiteS3URL: http://cncb-cdn-spa-john-websitebucket-1huxcgjseaili.s3-website-us-east-1.amazonaws.com
WebsiteBucketName: cncb-cdn-spa-john-websitebucket-1huxcgjseaili
WebsiteDistributionId: E3JF634XQF4PE9
...
Serverless: Path: ./build
Serverless: File: asset-manifest.json (application/json)
...
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中查看堆栈和 CloudFront 分发。
-
浏览到堆栈输出中显示的
WebsiteS3URL
和WebsiteDistributionURL
,并在浏览器检查工具的网络标签中注意启用和禁用缓存时两者的性能差异:
http://<see WebsiteS3URL output>.s3-website-us-east-1.amazonaws.com
http://<see WebsiteDistributionURL output>.cloudfront.net
- 使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈,一旦你完成操作。
它是如何工作的...
CloudFront 分发的配置既冗长又模板化。serverless-spa-config
插件简化了这项工作,封装了最佳实践,并允许通过异常进行配置。在这个菜谱中,我们使用所有默认设置。在生成的.serverless/cloudformation-template-update-stack.json
模板中,我们可以看到WebsiteBucket
被定义并配置为默认的S3Origin
,其中index.html
文件作为DefaultRootObject
。PriceClass
默认为北美和欧洲,以最小化分配分发所需的时间。桶的错误处理(ErrorDocument
)和分发的错误处理(CustomErrorResponses
)被配置为委托错误处理给 SPA 的逻辑。
分发的核心目的是在边缘缓存 SPA 资源。这个逻辑由两部分处理。首先,DefaultCacheBehavior
被设置为DefaultTTL
为零,以确保使用桶中个别资源的缓存控制头来控制 TTL。其次,serverless-spa-deploy
插件配置了两个不同的CacheControl
设置。除了index.html
文件之外的所有内容都部署了一个最大存活时间(max-age)为一年,因为 Webpack 使用为每个构建生成的哈希值来命名这些资源,从而隐式地清除缓存。index.html
文件必须有一个恒定的名称,因为它作为DefaultRootObject
,所以我们将其 max-age 设置为 5 分钟。这意味着在部署后的大约五分钟内,我们可以预期最终用户开始接收最新的代码更改。五分钟后,浏览器将向 CDN 请求index.html
文件,如果 ETag 未更改,CDN 将返回错误 304。这在不减少数据传输的同时允许快速传播更改。您可以根据需要增加或减少 max-age。
在这个阶段,我们通过将资源推送到边缘以减少延迟,并压缩资源以减少带宽,使用 CDN 来提高用户的下载性能。仅此一项就足以利用云的边缘。其他功能包括自定义域名、SSL 证书、日志记录以及与Web 应用防火墙(WAF)的集成。我们将在后续的菜谱中介绍这些主题。
将自定义域名与 CDN 关联
在“从 CDN 中提供单页应用程序”菜谱中,我们介绍了在 S3 前面添加 CloudFront CDN 层并利用更多有价值功能的步骤。这些功能之一是能够将自定义域名与 CDN 提供的资源关联起来,例如单页应用程序或云原生服务。在这个菜谱中,我们将部署过程再向前推进一步,并演示如何添加 Route53 记录集和 CloudFront 别名。
准备工作
您需要一个已注册的域名和一个 Route53 托管区域,您可以使用这些信息在本配方中创建一个用于部署的 SPA 子域名。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch4/cdn-dns --path cncb-cdn-dns
-
使用
cd cncb-cdn-dns
命令进入cncb-cdn-dns
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-cdn-dns
provider:
name: aws
plugins:
- serverless-spa-deploy
- serverless-spa-config
custom:
spa:
files:
...
dns:
hostedZoneId: Z1234567890123
domainName: example.com
endpoint: app.${self:custom.dns.domainName}
-
使用您的
hostedZoneId
和domainName
更新serverless.yml
文件。 -
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容:
{
"Resources": {
...
"WebsiteDistribution": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
...
"Aliases": [
"app.example.com"
],
...
}
}
}
},
"WebsiteEndpointRecord": {
"Type": "AWS::Route53::RecordSet",
"Properties": {
"HostedZoneId": "Z1234567890123",
"Name": "app.example.com.",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": {
"Fn::GetAtt": [
"WebsiteDistribution",
"DomainName"
]
}
}
}
}
},
...
}
-
使用
npm run build
构建应用程序。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-cdn-dns@1.0.0 dp:lcl <path-to-your-workspace>/cncb-cdn-dns
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
WebsiteDistributionURL: https://dwrdvnqsnetay.cloudfront.net
WebsiteS3URL: http://cncb-cdn-dns-john-websitebucket-1dwzb5bfkv34s.s3-website-us-east-1.amazonaws.com
WebsiteBucketName: cncb-cdn-dns-john-websitebucket-1dwzb5bfkv34s
WebsiteURL: http://app.example.com
WebsiteDistributionId: ED6YKAFJDF2ND
...
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中查看堆栈、Route53 和 CloudFront 分发。
-
浏览到堆栈输出中显示的
WebsiteURL
:
http://app.example.com <see WebsiteURL output>
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
在本配方中,我们通过为 SPA 添加自定义域名来改进 从 CDN 提供单页应用程序 配方。在 serverless.yml
中,我们添加了 hostedZoneId
、domainName
和 endpoint
值。这些值触发 serverless-spa-config
插件配置 Route53 中的 WebsiteEndpointRecord
,并在 CloudFront 分发上设置 Aliases
。
从 CDN 提供网站服务
作为行业,我们倾向于创建非常动态的网站,即使内容不经常变化。一些 内容管理系统 (CMS) 即使内容没有变化,也会为每个请求重新计算内容。这些请求通过多个层级,从数据库读取,然后计算并返回响应。平均响应时间在五秒左右并不罕见。据说,重复做同样的事情并期望不同的结果,这就是疯狂的定义。
云原生系统在创建网站方面采取了一种完全不同的方法。JAMstack (jamstack.org
) 是一种基于客户端 JavaScript、可重用 API 和标记的现代云原生方法。这些静态网站由 Git 工作流程管理,由 CI/CD 管道生成,并且每天部署到云的边缘多次。这是云原生挑战我们重新配置软件工程大脑的另一个例子。本配方演示了这些静态网站的生成和部署。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch4/cdn-site --path cncb-cdn-site
-
使用
cd cncb-cdn-site
命令进入cncb-cdn-site
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-cdn-site
provider:
name: aws
plugins:
- serverless-spa-deploy
- serverless-spa-config
custom:
spa:
files:
- source: ./dist
globs: '**/*'
headers:
CacheControl: max-age=31536000 # 1 year
redirect: true
dns:
hostedZoneId: Z1234567890123
domainName: example.com
endpoint: www.${self:custom.dns.domainName}
-
使用您的
hostedZoneId
和domainName
更新serverless.yml
文件。 -
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容:
{
"Resources": {
...
"RedirectBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": "example.com",
"WebsiteConfiguration": {
"RedirectAllRequestsTo": {
"HostName": "www.example.com"
}
}
}
},
"WebsiteDistribution": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"Comment": "Website: test-cncb-cdn-site (us-east-1)",
...
"Aliases": [
"www.example.com"
],
...
}
}
},
"RedirectDistribution": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"Comment": "Redirect: test-cncb-cdn-site (us-east-1)",
...
"Aliases": [
"example.com"
],
...
}
}
},
"WebsiteEndpointRecord": {
"Type": "AWS::Route53::RecordSet",
"Properties": {
"HostedZoneId": "Z1234567890123",
"Name": "www.example.com.",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": {
"Fn::GetAtt": [
"WebsiteDistribution",
"DomainName"
]
}
}
}
},
"RedirectEndpointRecord": {
"Type": "AWS::Route53::RecordSet",
"Properties": {
"HostedZoneId": "Z1234567890123",
"Name": "example.com.",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": {
"Fn::GetAtt": [
"RedirectDistribution",
"DomainName"
]
}
}
}
}
},
...
}
- 部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-cdn-site@1.0.0 dp:lcl <path-to-your-workspace>/cncb-cdn-site
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
WebsiteDistributionURL: https://d2ooxtd49ayyfd.cloudfront.net
WebsiteS3URL: http://cncb-cdn-site-john-websitebucket-mrn9ntyltxim.s3-website-us-east-1.amazonaws.com
WebsiteBucketName: cncb-cdn-site-john-websitebucket-mrn9ntyltxim
WebsiteURL: http://www.example.com
WebsiteDistributionId: E10ZLN9USZTDSO
...
Serverless: Path: ./dist
Serverless: File: after/c2Vjb25kLXBvc3Q=/index.html (text/html)
Serverless: File: after/dGhpcmQtcG9zdA==/index.html (text/html)
Serverless: File: after/Zm91cnRoLXBvc3Q=/index.html (text/html)
Serverless: File: after/ZmlmdGgtcG9zdA==/index.html (text/html)
Serverless: File: after/Zmlyc3QtcG9zdA==/index.html (text/html)
Serverless: File: blog/fifth-post/index.html (text/html)
Serverless: File: blog/first-post/index.html (text/html)
Serverless: File: blog/fourth-post/index.html (text/html)
Serverless: File: blog/second-post/index.html (text/html)
Serverless: File: blog/third-post/index.html (text/html)
Serverless: File: favicon.ico (image/x-icon)
Serverless: File: index.html (text/html)
Serverless: File: phenomic/content/posts/by-default/1/desc/date/limit-2.json (application/json)
Serverless: File: phenomic/content/posts/by-default/1/desc/date/limit-2/after-c2Vjb25kLXBvc3Q=.json (application/json)
Serverless: File: phenomic/content/posts/by-default/1/desc/date/limit-2/after-dGhpcmQtcG9zdA==.json (application/json)
Serverless: File: phenomic/content/posts/by-default/1/desc/date/limit-2/after-Zm91cnRoLXBvc3Q=.json (application/json)
Serverless: File: phenomic/content/posts/by-default/1/desc/date/limit-2/after-ZmlmdGgtcG9zdA==.json (application/json)
Serverless: File: phenomic/content/posts/by-default/1/desc/date/limit-2/after-Zmlyc3QtcG9zdA==.json (application/json)
Serverless: File: phenomic/content/posts/item/fifth-post.json (application/json)
Serverless: File: phenomic/content/posts/item/first-post.json (application/json)
Serverless: File: phenomic/content/posts/item/fourth-post.json (application/json)
Serverless: File: phenomic/content/posts/item/second-post.json (application/json)
Serverless: File: phenomic/content/posts/item/third-post.json (application/json)
Serverless: File: phenomic/phenomic.main.9a7f8f5f.js (application/javascript)
Serverless: File: robots.txt (text/plain)
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中查看堆栈、Route53 和 CloudFront 分发。
-
浏览到堆栈输出中显示的
WebsiteURL
,然后再不带www.
的情况下浏览,并观察重定向到www
:
http://www.example.com <see WebsiteURL output>
http://example.com <see WebsiteURL output>
-
浏览网站页面。
-
完成使用
npm run rm:lcl -- -s $MY_STAGE
后,删除堆栈。
它是如何工作的...
在这个菜谱中,我们使用了一个名为Phenomic的静态站点生成器(www.staticgen.com/phenomic
)。在StaticGen网站上列出了看似无穷无尽的工具(www.staticgen.com
)。使用 Phenomic,我们用 ReactJS 和 Markdown 的组合编写网站。然后,我们将 JavaScript 和 Markdown 同构生成一组静态资源,并将其部署到 S3,如部署输出所示。我已经预先构建了网站,并将生成的资源包含在存储库中。一个典型的 SPA 会在浏览器中下载 JavaScript 并生成网站。根据网站的不同,这个过程可能很耗时且明显。基于运行时的同构生成在将网站返回浏览器之前在服务器端执行此过程。另一方面,JAMstack 网站在部署时静态生成,这导致了最佳的运行时性能。尽管网站是静态的,但页面包含像任何 ReactJS SPA 一样编写的动态 JavaScript。这些网站经常更新并每天多次重新部署,这在不要求每次请求都询问服务器是否有所变化的情况下,又增加了一个动态维度。
这些网站通常部署到www
子域名,并支持将根域名重定向到该子域名。基于从 CDN 提供单页应用程序和将自定义域名与 CDN 关联的菜谱,这个菜谱利用了serverless-spa-deploy
和serverless-spa-config
插件,并添加了一些额外的设置。我们指定endpoint
为www.${self:custom.dns.domainName}
,并将重定向标志设置为true
。这反过来创建了RedirectBucket
,它执行重定向,以及额外的RedirectDistribution
和RedirectEndpointRecord
以支持根域名。
在 CDN 后面部署服务
当我们想到 CDN 时,通常会认为它们仅用于提供静态内容。然而,将 CloudFront 放置在所有资源之前,即使是动态服务,也是 AWS 的最佳实践,以提高安全性和性能。从安全角度来看,CloudFront 最小化了攻击面,并在边缘处理 DDoS 攻击,以减轻内部组件的负载。至于性能,CloudFront 优化了边缘和可用区之间的管道,即使对于 POST
和 PUT
操作也能提高性能。此外,即使只有几秒钟的缓存控制头也能对 GET
操作产生重大影响。在本菜谱中,我们将演示如何将 CloudFront 添加到 AWS API Gateway 之前。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch4/cdn-service --path cncb-cdn-service
-
使用
cd cncb-cdn-service
命令进入cncb-cdn-service
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-cdn-service
provider:
name: aws
runtime: nodejs8.10
endpointType: REGIONAL
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
cors: true
resources:
Resources:
ApiDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
...
Origins:
- Id: ApiGateway
DomainName:
Fn::Join:
- "."
- - Ref: ApiGatewayRestApi
- execute-api.${opt:region}.amazonaws.com
OriginPath: /${opt:stage}
CustomOriginConfig:
OriginProtocolPolicy: https-only
OriginSSLProtocols: [ TLSv1.2 ]
...
DefaultCacheBehavior:
TargetOriginId: ApiGateway
AllowedMethods: [ DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT ]
CachedMethods: [ GET, HEAD, OPTIONS ]
Compress: true
ForwardedValues:
QueryString: true
Headers: [ Accept, Authorization ]
Cookies:
Forward: all
MinTTL: 0
DefaultTTL: 0
ViewerProtocolPolicy: https-only
Outputs:
ApiDistributionId:
Value:
Ref: ApiDistribution
ApiDistributionEndpoint:
Value:
Fn::Join:
- ''
- - https://
- Fn::GetAtt: [ ApiDistribution, DomainName ]
- 查看名为
handler.js
的文件,其内容如下:
module.exports.hello = (request, context, callback) => {
const response = {
statusCode: 200,
headers: {
'Cache-Control': 'max-age=5',
},
body: ...,
};
callback(null, response);
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-cdn-service@1.0.0 dp:lcl <path-to-your-workspace>/cncb-cdn-service
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
GET - https://5clnzj3knc.execute-api.us-east-1.amazonaws.com/john/hello
functions:
hello: cncb-cdn-service-john-hello
Stack Outputs
ApiDistributionEndpoint: https://d1vrpoljefctug.cloudfront.net
ServiceEndpoint: https://5clnzj3knc.execute-api.us-east-1.amazonaws.com/john
...
ApiDistributionId: E2X1H9ZQ1B9U0R
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中查看堆栈和 CloudFront 分发。
-
使用以下
curl
命令多次调用堆栈输出中显示的端点,以查看缓存结果的性能差异:
$ curl -s -D - -w "Time: %{time_total}" -o /dev/null https://<see stack output>.cloudfront.net/hello | egrep -i 'X-Cache|Time'
X-Cache: Miss from cloudfront
Time: 0.324
$ curl ...
X-Cache: Hit from cloudfront
Time: 0.168
$ curl ...
X-Cache: Hit from cloudfront
Time: 0.170
$ curl ...
X-Cache: Miss from cloudfront
Time: 0.319
$ curl ...
X-Cache: Hit from cloudfront
Time: 0.167
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
在 serverless.yml
文件中要注意的第一件事是 endpointType
设置为 REGIONAL
。默认情况下,AWS API Gateway 将提供 CloudFront 分发。然而,我们无法访问此分发,也无法利用其所有功能。此外,默认设置不支持多区域部署。因此,我们指定 REGIONAL
以便我们可以自行管理 CDN。接下来,我们需要配置 Origins
。我们指定 DomainName
指向区域 API Gateway 端点,并指定阶段为 OriginPath
,这样我们就不需要在 URL 中包含它了。
接下来,我们配置 DefaultCacheBehavior
。我们允许读取和写入方法,并缓存读取方法。我们将 DefaultTTL
设置为零以确保在服务代码中设置的 Cache-Control
头部用于控制 TTL。在本配方中,代码将 max-age
设置为 5 秒,我们可以看到我们的缓存命中响应速度大约是两倍。我们还设置 Compress
为 true
以最小化数据传输,从而提高性能并帮助降低成本。向前转发所有后端期望的 Headers
非常重要。例如,授权头部对于使用 OAuth 2.0 保护服务至关重要,正如我们将在第五章 使用 OAuth 2.0 保护 API 网关 配方中讨论的那样,保护云原生系统。
注意,ApiGatewayRestApi
是由 Serverless Framework 创建和控制的逻辑资源 ID。
从 CDN 提供静态 JSON
在 实现搜索 BFF 配方中,我们创建了一个服务,通过 API Gateway 从 Elasticsearch 中的物化视图中提供一些内容,并直接从 S3 中的物化视图中提供其他内容。这是一个非常棒的方法,可以在极端高负载下以成本效益的方式交付。在本配方中,我们将演示如何为 API Gateway 和 S3 前面添加单个 CloudFront 分发,以将这些内部设计决策封装在单个域名之后。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch4/cdn-json --path cncb-cdn-json
-
使用
cd cncb-cdn-json
切换到cncb-cdn-json
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-cdn-json
provider:
name: aws
runtime: nodejs8.10
endpointType: REGIONAL
...
functions:
search:
handler: handler.search
...
resources:
Resources:
ApiDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
...
Origins:
- Id: S3Origin
DomainName:
Fn::Join:
- "."
- - Ref: Bucket
- s3.amazonaws.com
...
- Id: ApiGateway
DomainName:
Fn::Join:
- "."
- - Ref: ApiGatewayRestApi
- execute-api.${opt:region}.amazonaws.com
OriginPath: /${opt:stage}
...
...
DefaultCacheBehavior:
TargetOriginId: S3Origin
AllowedMethods:
- GET
- HEAD
CachedMethods:
- HEAD
- GET
...
MinTTL: 0
DefaultTTL: 0
...
CacheBehaviors:
- TargetOriginId: ApiGateway
PathPattern: /search
AllowedMethods: [ GET, HEAD, OPTIONS ]
CachedMethods: [ GET, HEAD, OPTIONS ]
...
MinTTL: 0
DefaultTTL: 0
...
Bucket:
Type: AWS::S3::Bucket
...
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-cdn-json@1.0.0 dp:lcl <path-to-your-workspace>/cncb-cdn-json
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
GET - https://hvk3o94wij.execute-api.us-east-1.amazonaws.com/john/search
functions:
search: cncb-cdn-json-john-search
load: cncb-cdn-json-john-load
Stack Outputs
...
ApiDistributionEndpoint: https://dfktdq2w7ht2p.cloudfront.net
BucketName: cncb-cdn-json-john-bucket-ls21fzjp2qs2
ServiceEndpoint: https://hvk3o94wij.execute-api.us-east-1.amazonaws.com/john
ApiDistributionId: E3JJREB1B4TIGL
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中查看堆栈和 CloudFront 分发。
-
使用以下命令加载数据:
$ sls invoke -f load -r us-east-1 -s $MY_STAGE -d '{"id":"44444444-5555-1111-1111-000000000000","name":"thing one"}'
{
"ETag": "\"3f4ca01316d6f88052a940ab198b2dc7\""
}
$ sls invoke -f load -r us-east-1 -s $MY_STAGE -d '{"id":"44444444-5555-1111-2222-000000000000","name":"thing two"}'
{
"ETag": "\"926f41091e2f47208f90f9b9848dffd0\""
}
$ sls invoke -f load -r us-east-1 -s $MY_STAGE -d '{"id":"44444444-5555-1111-3333-000000000000","name":"thing three"}'
{
"ETag": "\"2763ac570ff0969bd42182506ba24dfa\""
}
- 使用以下
curl
命令调用堆栈输出中显示的端点:
$ curl https://<see stack output>.cloudfront.net/search | json_pp
[
"https://dfktdq2w7ht2p.cloudfront.net/things/44444444-5555-1111-1111-000000000000",
"https://dfktdq2w7ht2p.cloudfront.net/things/44444444-5555-1111-2222-000000000000",
"https://dfktdq2w7ht2p.cloudfront.net/things/44444444-5555-1111-3333-000000000000"
] $ curl -s -D - -w "Time: %{time_total}" -o /dev/null https://<see stack output>.cloudfront.net/things/44444444-5555-1111-1111-000000000000 | egrep -i 'X-Cache|Time'
X-Cache: Miss from cloudfront
Time: 0.576
$ curl ...
X-Cache: Hit from cloudfront
Time: 0.248
- 使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
此配方基于 在 CDN 后部署服务 配方。本配方中的主要区别在于我们有多达多个 Origins
和多个 CacheBehaviors
,每个分别对应我们的 Bucket
和 ApiGatewayRestApi
。我们为我们的 S3Origin
使用 DefaultCacheBehavior
,因为我们可以在桶中存储许多不同的业务域,路径不同。相反,有一个单一的 PathPattern
(/search
) 需要指向我们的 APIGateway
原点,因此我们在 CacheBehaviors
下定义它。再次强调,在所有情况下,我们将 DefaultTTL
设置为零以确保我们的缓存控制头控制 TTL。最终结果是,我们的多个原点现在从外部看起来像一个。
注意,ApiGatewayRestApi
是由 Serverless Framework 创建和控制的逻辑资源 ID。
触发 CDN 中内容的失效
在 实现搜索 BFF 配方中,我们从 S3 静态服务动态 JSON 内容,在 从 CDN 服务器静态 JSON 配方中,我们添加了一个具有长最大年龄的缓存控制头,以进一步提高性能。这种技术对于动态且变化缓慢且不可预测的内容非常有效。在此配方中,我们将展示如何通过响应数据更改事件并使缓存失效,以便检索最新数据。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch4/cdn-invalidate --path cncb-cdn-invalidate
-
使用
cd cncb-cdn-invalidate
命令导航到cncb-cdn-invalidate
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-cdn-invalidate
provider:
name: aws
runtime: nodejs8.10
...
functions:
load:
...
trigger:
handler: handler.trigger
events:
- sns:
arn:
Ref: BucketTopic
topicName: ${self:service}-${opt:stage}-trigger
environment:
DISABLED: false
DISTRIBUTION_ID:
Ref: ApiDistribution
resources:
Resources:
Bucket:
Type: AWS::S3::Bucket
DependsOn: [ BucketTopic, BucketTopicPolicy ]
Properties:
NotificationConfiguration:
TopicConfigurations:
- Event: s3:ObjectCreated:Put
Topic:
Ref: BucketTopic
BucketTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: ${self:service}-${opt:stage}-trigger
BucketTopicPolicy: ${file(includes.yml):BucketTopicPolicy}
ApiDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
...
- 查看名为
handler.js
的文件,其内容如下:
module.exports.trigger = (event, context, cb) => {
_(process.env.DISABLED === 'true' ? [] : event.Records)
.flatMap(messagesToTriggers)
.flatMap(invalidate)
.collect()
.toCallback(cb);
};
const messagesToTriggers = r => _(JSON.parse(r.Sns.Message).Records);
const invalidate = (trigger) => {
const params = {
DistributionId: process.env.DISTRIBUTION_ID,
InvalidationBatch: {
CallerReference: uuid.v1(),
Paths: {
Quantity: 1,
Items: [`/${trigger.s3.object.key}`]
}
}
};
const cf = new aws.CloudFront();
return _(cf.createInvalidation(params).promise());
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-cdn-invalidate@1.0.0 dp:lcl <path-to-your-workspace>/cncb-cdn-invalidate
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
load: cncb-cdn-invalidate-john-load
trigger: cncb-cdn-invalidate-john-trigger
Stack Outputs
BucketArn: arn:aws:s3:::cncb-cdn-invalidate-john-bucket-z3u7jc60piub
BucketName: cncb-cdn-invalidate-john-bucket-z3u7jc60piub
ApiDistributionEndpoint: https://dgmob5bgqpnpi.cloudfront.net
TopicArn: arn:aws:sns:us-east-1:123456789012:cncb-cdn-invalidate-john-trigger
...
ApiDistributionId: EV1QUKWEQV6XN
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中查看堆栈。
-
使用以下命令加载数据:
$ sls invoke -f load -r us-east-1 -s $MY_STAGE -d '{"id":"44444444-6666-1111-1111-000000000000","name":"thing one"}'
{
"ETag": "\"3f4ca01316d6f88052a940ab198b2dc7\""
}
- 使用以下
curl
命令调用堆栈输出中的端点:
$ curl https://<see stack output>.cloudfront.net/things/44444444-6666-1111-1111-000000000000 | json_pp
{
"name" : "thing one",
"id" : "44444444-6666-1111-1111-000000000000"
}
- 使用以下命令加载数据:
$ sls invoke -f load -r us-east-1 -s $MY_STAGE -d '{"id":"44444444-6666-1111-1111-000000000000","name":"thing one again"}'
{
"ETag": "\"edf9676ddcc150f722f0f74b7a41bd7f\""
}
- 查看
trigger
函数的日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
2018-05-20 02:43:16 ... params: {"DistributionId":"EV1QUKWEQV6XN","InvalidationBatch":{"CallerReference":"13a8e420-5bf9-11e8-818d-9de0acd70c96","Paths":{"Quantity":1,"Items":["/things/44444444-6666-1111-1111-000000000000"]}}}
2018-05-20 02:43:17 ... {"Location":"https://cloudfront.amazonaws.com/2017-10-30/distribution/EV1QUKWEQV6XN/invalidation/I33URVVAO02X7I","Invalidation":{"Id":"I33URVVAO02X7I","Status":"InProgress","CreateTime":"2018-05-20T06:43:17.102Z","InvalidationBatch":{"Paths":{"Quantity":1,"Items":["/things/44444444-6666-1111-1111-000000000000"]},"CallerReference":"13a8e420-5bf9-11e8-818d-9de0acd70c96"}}}
- 使用以下
curl
命令调用堆栈输出中的端点,直到失效完成且输出更改:
$ curl https://<see stack output>.cloudfront.net/things/44444444-6666-1111-1111-000000000000 | json_pp
{
"name" : "thing one again",
"id" : "44444444-6666-1111-1111-000000000000"
}
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
此配方基于 在 S3 中实现物化视图 和 实现搜索 BFF 配方。我们定义存储桶的 NotificationConfiguration
以将事件发送到 SNS BucketTopic
,以便我们可以触发多个消费者。在 实现搜索 BFF 配方中,我们在 Elasticsearch 中触发了索引。在此配方中,我们展示了如何触发 CloudFront 分发中缓存的失效。trigger
函数为每个 trigger.s3.object.key
创建一个失效请求。这些失效将迫使 CDN 在下一次请求时从源检索这些资源。
缓存失效的缓慢流程不会产生显著成本。然而,大量单个失效批次可能会产生费用。这可能在增强服务时的数据转换过程中发生。在这些情况下,在执行转换之前应将 DISABLED
环境变量设置为 true
。然后,在转换完成后,使用通配符手动失效分发。
在云边缘执行代码
AWS Lambda@Edge 是一个功能,允许函数在云边缘响应 CloudFront 事件时执行。这种能力为在云边缘以极低延迟控制、修改和生成内容提供了有趣的机会。我认为我们才刚刚开始挖掘可以使用这个新功能实现的应用场景。这个配方演示了将裸骨函数与 CloudFront 分发关联起来。当请求中不存在授权头时,该函数会以未授权(403)状态码响应。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch4/cdn-lambda --path cncb-cdn-lambda
-
使用
cd cncb-cdn-lambda
命令导航到cncb-cdn-lambda
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-cdn-lambda
plugins:
- serverless-plugin-cloudfront-lambda-edge
provider:
name: aws
runtime: nodejs8.10
...
functions:
authorize:
handler: handler.authorize
memorySize: 128
timeout: 1
lambdaAtEdge:
distribution: 'ApiDistribution'
eventType: 'viewer-request'
...
resources:
Resources:
Bucket:
Type: AWS::S3::Bucket
ApiDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
...
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容:
...
"LambdaFunctionAssociations": [
{
"EventType": "viewer-request",
"LambdaFunctionARN": {
"Ref": "AuthorizeLambdaVersionSticfT7s2DCStJsDgzhTCrXZB1CjlEXXc3bR4YS1owM"
}
}
]
...
- 部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-cdn-lambda@1.0.0 dp:lcl <path-to-your-workspace>/cncb-cdn-lambda
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
functions:
authorize: cncb-cdn-lambda-john-authorize
load: cncb-cdn-lambda-john-load
Stack Outputs
ApiDistributionEndpoint: https://d1kktmeew2xtn2.cloudfront.net
BucketName: cncb-cdn-lambda-john-bucket-olzhip1qvzqi
...
ApiDistributionId: E2VL0VWTW5IXUA
部署 CloudFront 分发可能需要 20 分钟或更长时间。对这些函数的任何更改都将导致分发的重新部署,因为分发与函数版本相关联。
-
在 AWS 控制台中查看栈、函数和 CloudFront 分发。
-
使用以下命令加载数据:
$ sls invoke -f load -r us-east-1 -s $MY_STAGE -d '{"id":"44444444-7777-1111-1111-000000000000","name":"thing one"}'
{
"ETag": "\"3f4ca01316d6f88052a940ab198b2dc7\""
}
- 使用以下
curl
命令调用栈输出中显示的端点:
$ curl -v -H "Authorization: Bearer 1234567890" https://<see stack output>.cloudfront.net/things/44444444-7777-1111-1111-000000000000 | json_pp
...
> Authorization: Bearer 1234567890
>
< HTTP/1.1 200 OK
...
{
"name" : "thing one",
"id" : "44444444-7777-1111-1111-000000000000"
}$ curl -v https://<see stack output>.cloudfront.net/things/44444444-7777-1111-1111-000000000000 | json_pp
...
< HTTP/1.1 401 Unauthorized
...
[
{
"Records" : [
{
"cf" : {
"config" : {
"eventType" : "viewer-request",
"requestId" : "zDpj1UckJLTIln8dwak2ZL1SX0LtWfGPhg3mC1EGIgfRd6gzJFVeqg==",
"distributionId" : "E2VL0VWTW5IXUA",
"distributionDomainName" : "d1kktmeew2xtn2.cloudfront.net"
},
...
}
}
]
},
{
...
"logGroupName" : "/aws/lambda/us-east-1.cncb-cdn-lambda-john-authorize",
...
},
{
"AWS_REGION" : "us-east-1",
...
}
]
-
在 AWS 控制台中查看日志。
-
使用
npm run rm:lcl -- -s $MY_STAGE
命令完成后删除栈。
Lambda@Edge 根据分发的 PriceClass
将函数复制到多个区域。在所有副本被删除之前,无法删除函数。这应该在删除分发后的几小时内发生。因此,删除栈最初会失败,可以在副本被删除后重复执行。
它是如何工作的...
在这个配方中,我们使用 serverless-plugin-cloudfront-lambda-edge
插件将 authorize
函数与 CloudFront 的 ApiDistribution
关联起来。我们在 lambdaAtEdge
元素下指定了 distribution
和 eventType
。然后插件使用这些信息在生成的 CloudFormation 模板中的分发上创建 LambdaFunctionAssociations
元素。eventType
可以设置为 viewer-request
、origin-request
、origin-response
或 view-response
。这个配方使用 viewer-request
,因为它需要访问由观众发送的授权头。我们明确设置了函数的 memorySize
和 timeout
,因为 Lambda@Edge 对这些值有限制。
Lambda@Edge 在与特定边缘位置关联的区域记录 console.log
语句。这个配方在响应中返回 logGroupName
和 AWS_REGION
,以帮助演示如何找到日志。
第五章:保护云原生系统
在本章中,将介绍以下菜谱:
-
保护您的云账户
-
创建联合身份池
-
实现注册、登录和注销
-
使用 OpenID Connect 保护 API 网关
-
实现自定义授权器
-
授权基于 GraphQL 的服务
-
实现 JWT 过滤器
-
使用信封加密
-
为传输加密创建 SSL 证书
-
配置 Web 应用程序防火墙
-
复制数据湖以进行灾难恢复
简介
云中的安全基于共享责任模型。在栈的某个以下线是云提供商的责任,以上线是云消费者的责任。云原生和无服务器计算将这条线推得越来越高。这使得团队能够专注于他们最擅长的领域——他们的业务领域。借助云提供的安全机制,团队能够实践 设计安全 并专注于 深度防御 技术来保护他们的数据。到目前为止,在每一个菜谱中,我们都看到了无服务器计算如何要求我们在栈的每一层定义组件之间的安全策略。本章中的菜谱将涵盖保护我们的云账户、使用 OAuth 2.0/Open ID Connect 保护我们的应用程序、保护我们的静态数据以及通过将安全的一些方面委托给云边缘来在我们的云原生系统周围创建一个边界。
保护您的云账户
如果我们不努力保护我们的云账户,那么我们所做的一切来保护我们的云原生系统都是徒劳的。我们必须为每个创建的云账户实施一系列最佳实践。随着我们努力创建自主服务,我们应该利用云账户之间的自然隔板,通过将相关服务分组到更多、更细粒度的账户中,而不是更少、更粗粒度的账户中。在这个菜谱中,我们将看到将账户视为代码如何使我们能够通过应用我们用于管理许多自主服务的相同基础设施即代码实践,轻松地管理许多账户。
如何做到这一点...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/account-as-code --path cncb-account-as-code
-
使用
cd cncb-account-as-code
切换到cncb-account-as-code
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-account-as-code
provider:
name: aws
# cfnRole: arn:aws:iam::${self:custom.accountNumber}:role/${opt:stage}-cfnRole
custom:
accountNumber: 123456789012
resources:
Resources:
AuditBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
...
CloudTrail:
Type: AWS::CloudTrail::Trail
...
CloudFormationServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: ${opt:stage}-cfnRole
...
ExecuteCloudFormationPolicy:
Type: AWS::IAM::ManagedPolicy
...
CiCdUser:
Type: AWS::IAM::User
Properties:
ManagedPolicyArns:
- Ref: ExecuteCloudFormationPolicy
AdminUserGroup:
Type: AWS::IAM::Group
...
ReadOnlyUserGroup:
Type: AWS::IAM::Group
...
PowerUserGroup:
Type: AWS::IAM::Group
...
ManageAccessKey:
Condition: IsDev
Type: AWS::IAM::ManagedPolicy
...
MfaOrHqRequired:
Condition: Exclude
Type: AWS::IAM::ManagedPolicy
...
...
-
在
serverless.yml
中更新accountNumber
。 -
使用
npm install
安装依赖 -
使用
npm test
运行测试 -
查看在
.serverless
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-account-as-code@1.0.0 dp:lcl <path-to-your-workspace>/cncb-account-as-code
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
-
在 AWS 控制台中查看栈和资源。
-
取消注释
cfnRole
,然后使用带有force
标志的npm run dp:lcl -- -s $MY_STAGE --force
再次部署。 -
使用
npm run rm:lcl -- -s $MY_STAGE
删除栈
它是如何工作的...
我创建的每个账户都使用serverless.yml
文件开始,例如本食谱中的文件。在创建此账户范围堆栈之前,我不会在账户中创建其他堆栈。除创建用户之外的所有进一步更改都作为对此堆栈的更改交付。此堆栈的首要责任是启用CloudTrail
。在第七章优化可观察性中,我们将看到如何使用此审计跟踪来监控和警报关于安全策略意外更改的情况。"AuditBucket"也是复制到恢复账户的候选者,如复制数据湖以进行灾难恢复食谱中讨论的那样。
接下来,堆栈创建用于授予所有账户用户权限的用户组。AdminUserGroup
、PowerUserGroup
和ReadOnlyUserGroup
组是一个良好的起点,同时使用 AWS 提供的托管策略。随着账户使用的成熟,这些组将使用第六章中讨论的相同方法进行演变,即构建持续部署管道。然而,只有安全策略被编码化。用户到组的分配是一个手动过程,应该遵循适当的审批流程。堆栈包括MfaOrHqRequired
策略,要求多因素认证(MFA)并允许企业 IP 地址白名单,但最初是禁用的。对于所有生产账户,肯定应该启用它。在开发账户中,大多数开发者被分配到高级用户组,这样他们可以自由地实验云服务。高级用户组没有 IAM 权限,因此包含一个可选的ManageAccessKey
策略,允许高级用户管理他们的访问密钥。请注意,控制访问密钥的使用并频繁轮换它们非常重要。
当执行serverless.yml
文件时,我们需要一个访问密钥。作为额外的安全措施,CloudFormation 支持使用服务角色,允许 CloudFormation 假定具有临时凭证的特定角色。使用serverless.yml
文件中的cfnRole
属性启用此功能。此堆栈创建一个初始的CloudFormationServiceRole
,所有堆栈都应使用此角色。随着账户的成熟,此角色应调整到尽可能少的权限。包含的ExecuteCloudFormationPolicy
只有足够的权限来执行serverless.yml
文件。此策略将由CiCdUser
使用,我们将在第六章构建持续部署管道中使用它。
创建联合身份池
管理用户是几乎所有系统的要求。在漫长的职业生涯中,我确实可以证明反复创建身份管理功能。幸运的是,我们现在可以从许多提供商那里获得此功能,包括我们的云提供商。由于有这么多选项可用,我们需要一个联邦解决方案,它将委托给许多其他身份管理系统,同时向我们的云原生系统呈现一个单一、统一的模型。在此配方中,我们将展示如何创建一个 AWS Cognito 用户池,然后我们将在其他配方中使用它来保护我们的服务。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/cognito-pool --path cncb-cognito-pool
-
使用
cd cncb-cognito-pool
命令进入cncb-cognito-pool
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-cognito-pool
provider:
name: aws
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
resources:
Resources:
CognitoUserPoolCncb:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: cncb-${opt:stage}
...
Schema:
- AttributeDataType: 'String'
DeveloperOnlyAttribute: false
Mutable: true
Name: 'email'
Required: true
...
CognitoUserPoolCncbClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId:
Ref: CognitoUserPoolCncb
Outputs:
...
plugins:
- cognito-plugin
custom:
pool:
domain: cncb-${opt:stage}
allowedOAuthFlows: ['implicit']
allowedOAuthFlowsUserPoolClient: true
allowedOAuthScopes: ...
callbackURLs: ['http://localhost:3000/implicit/callback']
logoutURLs: ['http://localhost:3000']
refreshTokenValidity: 30
supportedIdentityProviders: ['COGNITO']
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-cognito-pool@1.0.0 dp:lcl <path-to-your-workspace>/cncb-cognito-pool
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
userPoolArn: arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_tDXH8JAky
userPoolClientId: 4p5p08njgmq9sph130l3du2b7q
loginURL: https://cncb-john.auth.us-east-1.amazoncognito.com/login?redirect_uri=http://localhost:3000/implicit/callback&response_type=token&client_id=4p5p08njgmq9sph130l3du2b7q
userPoolProviderName: cognito-idp.us-east-1.amazonaws.com/us-east-1_tDXH8JAky
userPoolId: us-east-1_tDXH8JAky
userPoolProviderURL: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_tDXH8JAky
-
在 AWS 控制台中检查堆栈和资源。
-
从堆栈输出访问
loginURL
以验证用户池是否已成功创建。 -
完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
我们将在本章的其他配方中引用此用户池,因此您完成本章后请删除此堆栈。
它是如何工作的...
此配方仅将 AWS Cognito 用户池的创建过程规范化。像任何其他基础设施资源一样,我们希望将其视为代码并在持续交付管道中管理。对于此配方,我们定义了最基本的内容。您可能需要定义任何希望池管理的附加属性。对于此配方,我们仅指定了 email
属性。
重要的是要理解,这些属性不能通过 CloudFormation 进行更改,除非创建一个新的用户池,这将对其依赖的用户池的任何资源产生连锁反应。因此,请预计您需要投入一些精力来提前实验正确的属性组合。
对于将依赖于此用户池的每个应用程序,我们需要定义一个 UserPoolClient
。每个应用程序通常会将其定义在它管理的堆栈中。然而,过度使用单个用户池是很重要的。这再次是一个关于自主性的问题。如果用户池和那些用户使用的应用程序确实是独立的,那么它们应该分别管理在不同的 Cognito 用户池中,即使这需要一些重复的工作。例如,如果您发现自己正在使用 Cognito 用户组编写复杂的逻辑来不自然地隔离用户,那么您可能更适合使用多个用户池。一个误用的例子是将员工和客户混合在同一个用户池中。
截至撰写本章时,CloudFormation 对 Cognito API 的支持并不完整。因此,使用插件来处理 UserPoolClient
的附加设置,例如 domain
、callbackUrls
和 allowedOAuthFlows
。
实现注册、登录和注销
重复实现注册、登录和注销并不高效。在单页应用程序中实现此逻辑也不理想。在这个菜谱中,我们将了解如何在单页应用程序中实现OpenID Connect 隐式流以使用AWS Cognito 托管 UI对用户进行认证。
如何实现...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/cognito-signin --path cncb-cognito-signin
-
使用
cd cncb-cognito-signin
导航到cncb-cognito-signin
目录。 -
查看名为
src/App.js
的文件,其内容如下,并使用上一个菜谱中userPool
堆栈输出的值更新clientId
和domain
字段:
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { CognitoSecurity, ImplicitCallback, SecureRoute } from './authenticate';
import Home from './Home';
class App extends Component {
render() {
return (
<Router>
<CognitoSecurity
domain='cncb-<stage>.auth.us-east-1.amazoncognito.com'
clientId='a1b2c3d4e5f6g7h8i9j0k1l2m3'
...
redirectSignIn={`${window.location.origin}/implicit/callback`}
redirectSignOut={window.location.origin}
>
<SecureRoute path='/' exact component={Home} />
<Route path='/implicit/callback' component={ImplicitCallback} />
</CognitoSecurity>
</Router>
);
}
}
export default App;
- 查看名为
src/Home.js
的文件,其内容如下:
import React from 'react';
import { withAuth } from './authenticate';
...
const Home = ({ auth }) => (
<div ...>
...
<button onClick={auth.logout}>Logout</button>
<pre ...>{JSON.stringify(auth.getSession(), null, 2)}</pre>
</div>
);
export default withAuth(Home);
-
使用
npm install
安装依赖项。 -
使用
npm start
在本地运行应用程序。 -
浏览到
http://localhost:3000
。 -
点击
注册
链接并按照说明操作。 -
点击
注销
链接,然后再次登录。 -
查看页面上的
idToken
。
我们将使用这些屏幕在授权菜谱中创建有效的idToken
。
工作原理...
将注册、登录和注销功能添加到单页应用中非常简单。我们包括一些额外的库,用适当的配置初始化它们,并装饰现有的代码。在这个菜谱中,我们对我们的简单 React 应用程序进行了这些更改。AWS 提供了amazon-cognito-auth-js
库来简化这项任务,我们在src/authenticate
文件夹中用一些 React 组件包装它。首先,我们在src/App.js
中初始化CognitoSecurity
组件。接下来,我们为Home
组件设置SecureRoute
,如果用户未认证,将重定向到Cognito 托管 UI。ImpicitCallback
组件将在用户登录后处理重定向。最后,我们将withAuth
装饰器添加到Home
组件上。在更复杂的应用程序中,我们只需装饰更多的路由和组件。框架处理其他所有事情,例如将JSON Web Token(JWT)保存到本地存储,并在auth
属性中使用它。例如,Home
组件显示令牌(auth.getSession()
)并提供注销按钮(auth.logout
)。
使用 OpenID Connect 保护 API 网关
使用 API 网关的一个优点是将安全关注点,如授权,推到我们系统的外围,远离我们的内部资源。这简化了内部代码并提高了可扩展性。在这个菜谱中,我们将配置一个AWS API 网关以对AWS Cognito 用户池进行授权。
准备工作
您需要根据创建联合身份池菜谱中创建的 Cognito 用户池和实现注册、登录和注销菜谱中创建的示例应用程序来创建本菜谱中使用的身份令牌。
如何实现...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/cognito-authorizer --path cncb-cognito-authorizer
-
使用
cd cncb-cognito-authorizer
进入cncb-cognito-authorizer
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-cognito-authorizer
provider:
name: aws
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
...
functions:
hello:
handler: handler.hello
events:
- http:
...
authorizer:
arn: ${cf:cncb-cognito-pool-${opt:stage}.userPoolArn}
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-cognito-authorizer@1.0.0 dp:lcl <path-to-your-workspace>/cncb-cognito-authorizer
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
GET - https://ff70szvc44.execute-api.us-east-1.amazonaws.com/john/hello
functions:
hello: cncb-cognito-authorizer-john-hello
-
在 AWS 控制台中检查栈和资源。
-
在更新
<API-ID>
后,调用以下curl
命令,尝试在不使用Authorization
令牌的情况下访问服务:
$ curl https://<API-ID>.execute-api.us-east-1.amazonaws.com/$MY_STAGE/hello | json_pp
{
"message" : "Unauthorized"
}
- 使用前一个菜谱中的应用程序登录并生成
idToken
,然后使用以下命令导出idToken
:
$ export CNCB_TOKEN=<idToken value>
- 在更新
<API-ID>
后,调用以下curl
命令,以使用Authorization
令牌成功访问服务:
$ curl -v -H "Authorization: Bearer $CNCB_TOKEN" https://<API-ID>.execute-api.us-east-1.amazonaws.com/$MY_STAGE/hello | json_pp
{
"message" : "JavaScript Cloud Native Development Cookbook! Your function executed successfully!",
"input" : {
"headers" : {
"Authorization" : "...",
...
},
...
"requestContext" : {
...
"authorizer" : {
"claims" : {
"email_verified" : "true",
"auth_time" : "1528264383",
"cognito:username" : "john",
"event_id" : "e091cd96-694d-11e8-897c-e3fe55ba3d67",
"iss" : "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_tDXH8JAky",
"exp" : "Wed Jun 06 06:53:03 UTC 2018",
"sub" : "e4bdd021-a160-4aff-bce2-e652f9469e3e",
"aud" : "4p5p08njgmq9sph130l3du2b7q",
"email" : "john@example.com",
"iat" : "Wed Jun 06 05:53:03 UTC 2018",
"token_use" : "id"
}
}
},
...
}
}
- 使用
npm run rm:lcl -- -s $MY_STAGE
删除栈,一旦您完成操作。
它是如何工作的...
AWS API 网关、AWS Cognito 和 Serverless Framework 的组合使得使用 OpenID Connect 保护服务变得极其简单。AWS API 网关可以使用授权器函数来控制对服务的访问。这些函数验证Authorization
头中传递的 JWT,并返回 IAM 策略。我们将在实现自定义授权器菜谱中深入了解这些细节。AWS Cognito 提供了一个授权器函数,用于验证特定用户池生成的 JWT。在serverless.yml
文件中,我们只需将authorizer
设置为特定 Cognito 用户池的userPoolArn
。一旦授权,API 网关将 JWT 的解码claims
传递到requestContext
中的 lambda 函数,以便在业务逻辑中(如果需要)使用这些数据。
实现自定义授权器
在使用 OpenID Connect 保护 API 网关的菜谱中,我们利用了 AWS 提供的 Cognito 授权器。这是使用 Cognito 的一个优点。然而,这并不是唯一的选择。有时我们可能希望对返回的策略有更多的控制。在其他情况下,我们可能需要使用第三方工具,如Auth0或Okta。在这个菜谱中,我们将通过实现自定义授权器来展示如何支持这些场景。
准备工作
您需要使用在创建联合身份验证池菜谱中创建的 Cognito 用户池和在实现注册、登录和注销菜谱中创建的示例应用程序来创建本菜谱中使用的身份令牌。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/custom-authorizer --path cncb-custom-authorizer
-
使用
cd cncb-custom-authorizer
进入cncb-custom-authorizer
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-custom-authorizer
provider:
name: aws
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
...
functions:
authorizer:
handler: handler.authorize
environment:
AUD: ${cf:cncb-cognito-pool-${opt:stage}.userPoolClientId}
ISS: ${cf:cncb-cognito-pool-${opt:stage}.userPoolProviderURL}
JWKS: ${self:functions.authorizer.environment.ISS}/.well-known/jwks.json
DEBUG: '*'
hello:
handler: handler.hello
events:
- http:
...
authorizer: authorizer
- 检查名为
handler.js
的文件,其内容如下:
...
module.exports.authorize = (event, context, cb) => {
decode(event)
.then(fetchKey)
.then(verify)
.then(generatePolicy)
...
};
const decode = ({ authorizationToken, methodArn }) => {
...
return Promise.resolve({
...
token: match[1],
decoded: jwt.decode(match[1], { complete: true }),
});
};
const fetchKey = (uow) => {
...
return client.getSigningKeyAsync(kid)
.then(key => ({
key: key.publicKey || key.rsaPublicKey,
...uow,
}));
};
const verify = (uow) => {
...
return verifyAsync(token, key, {
audience: process.env.AUD,
issuer: process.env.ISS
})
...
};
const generatePolicy = (uow) => {
...
return {
policy: {
principalId: claims.sub,
policyDocument: {
Statement: [{ Action: 'execute-api:Invoke', Effect, Resource }],
},
context: claims,
},
...uow,
};
};
...
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-custom-authorizer@1.0.0 dp:lcl <path-to-your-workspace>/cncb-custom-authorizer
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
GET - https://8iznazkhr0.execute-api.us-east-1.amazonaws.com/john/hello
functions:
authorizer: cncb-custom-authorizer-john-authorizer
hello: cncb-custom-authorizer-john-hello
Stack Outputs
...
ServiceEndpoint: https://8iznazkhr0.execute-api.us-east-1.amazonaws.com/john
-
在 AWS 控制台中检查栈和资源。
-
更新
<API-ID>
后,调用以下curl
命令,尝试在不使用Authorization
令牌的情况下访问服务:
$ curl https://<API-ID>.execute-api.us-east-1.amazonaws.com/$MY_STAGE/hello | json_pp
{
"message" : "Unauthorized"
}
- 使用实现注册、登录和注销配方中的应用程序进行登录并生成
idToken
,然后使用以下命令导出idToken
:
$ export CNCB_TOKEN=<idToken value>
- 更新
<API-ID>
后,调用以下curl
命令,使用Authorization
令牌成功访问服务:
$ curl -v -H "Authorization: Bearer $CNCB_TOKEN" https://<API-ID>.execute-api.us-east-1.amazonaws.com/$MY_STAGE/hello | json_pp
{
"message" : "JavaScript Cloud Native Development Cookbook! Your function executed successfully!",
"input" : {
"headers" : {
"Authorization" : "...",
...
},
...
"requestContext" : {
...
"authorizer" : {
"exp" : "1528342765",
"cognito:username" : "john",
"iss" : "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_tDXH8JAky",
"sub" : "e4bdd021-a160-4aff-bce2-e652f9469e3e",
"iat" : "1528339165",
"email_verified" : "true",
"auth_time" : "1528339165",
"email" : "john@example.com",
"aud" : "4p5p08njgmq9sph130l3du2b7q",
"event_id" : "fdcdf125-69fb-11e8-a6ef-ab31871bed60",
"token_use" : "id",
"principalId" : "e4bdd021-a160-4aff-bce2-e652f9469e3e"
}
},
...
}
}
- 完成使用
npm run rm:lcl -- -s $MY_STAGE
后,删除堆栈。
它是如何工作的...
将自定义授权器连接到 API 网关的过程与 Cognito 授权器相同。我们可以在与配方相同的项目中实现authorizer
函数,或者它可以在另一个堆栈中实现,以便可以在服务之间共享。jwks-rsa
和jsonwebtoken
开源库实现了大部分逻辑。首先,我们断言令牌的存在并对其进行解码。然后,我们使用解码令牌中存在的键 ID(kid
)检索发行者的.well-known/jwks.json
公钥。然后,我们验证令牌的签名与密钥,并断言受众(aud
)和发行者(iss
)符合预期。最后,函数返回一个基于路径授予服务访问权限的 IAM 策略。令牌的claims
也返回在context
字段中,以便可以将其转发到后端函数。如果任何验证失败,则返回Unauthorized
错误。
声明对象必须被展平,否则会抛出AuthorizerConfigurationException
异常。
授权基于 GraphQL 的服务
我们已经看到了如何使用JWT来授权对服务的访问。除了这种粗粒度的访问控制之外,我们还可以利用 JWT 中的声明来执行细粒度、基于角色的访问控制。在此配方中,我们将展示如何使用指令创建用于在 GraphQL 模式中声明性定义基于角色的权限的注释。
准备工作
您需要根据创建联合身份池配方中创建的 Cognito 用户池和实现注册、登录和注销配方中创建的示例应用程序来创建在此配方中使用的身份令牌。您需要通过 Cognito 控制台将Author
组分配给您在此配方中使用的用户。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/graphql-jwt --path cncb-graphql-jwt
-
使用
cd cncb-graphql-jwt
命令导航到cncb-graphql-jwt
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-graphql-jwt
provider:
name: aws
runtime: nodejs8.10
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
functions:
graphql:
handler: handler.graphql
events:
- http:
...
authorizer:
arn: ${cf:cncb-cognito-pool-${opt:stage}.userPoolArn}
- 检查以下内容的
index.js
、schema/thing/typedefs.js
和directives.js
文件:
index.js
...
const { directiveResolvers } = require('./directives');
const directives = `
directive @hasRole(roles: [String]) on QUERY | FIELD | MUTATION
`;
...
module.exports = {
typeDefs: [directives, schemaDefinition, query, mutation, thingTypeDefs],
resolvers: merge({}, thingResolvers),
directiveResolvers,
};
schema/thing/typedefs.js
module.exports = `
...
extend type Mutation {
saveThing(
input: ThingInput
): Thing @hasRole(roles: ["Author"])
deleteThing(
id: ID!
): Thing @hasRole(roles: ["Manager"])
}
`;
directives.js
const getGroups = ctx => get(ctx.event, 'requestContext.authorizer.claims.cognito:groups', '');
const directiveResolvers = {
hasRole: (next, source, { roles }, ctx) => {
const groups = getGroups(ctx).split(',');
if (intersection(groups, roles).length > 0) {
return next();
}
throw new UserError('Access Denied');
},
}
module.exports = { directiveResolvers };
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-graphql-jwt@1.0.0 dp:lcl <path-to-your-workspace>/cncb-graphql-jwt
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
POST - https://b33zxnw20b.execute-api.us-east-1.amazonaws.com/john/graphql
functions:
graphql: cncb-graphql-jwt-john-graphql
Stack Outputs
...
ServiceEndpoint: https://b33zxnw20b.execute-api.us-east-1.amazonaws.com/john
-
在 AWS 控制台中检查堆栈和资源。
-
使用实现注册、登录和注销菜谱中的应用程序进行登录并生成
idToken
,然后使用以下命令导出idToken
:
$ export CNCB_TOKEN=<idToken value>
- 在更新
<API-ID>
后,调用以下curl
命令以使用Authorization
令牌成功访问服务:
$ curl -v -X POST -H "Authorization: Bearer $CNCB_TOKEN" -H 'Content-Type: application/json' -d '{"query":"mutation { saveThing(input: { id: \"55555555-6666-1111-1111-000000000000\", name: \"thing1\", description: \"This is thing one of two.\" }) { id } }"}' https://b33zxnw20b.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"saveThing" : {
"id" : "55555555-6666-1111-1111-000000000000"
}
}
}
$ curl -v -X POST -H "Authorization: Bearer $CNCB_TOKEN" -H 'Content-Type: application/json' -d '{"query":"query { thing(id: \"55555555-6666-1111-1111-000000000000\") { id name description }}"}' https://b33zxnw20b.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"thing" : {
"name" : "thing1",
"id" : "55555555-6666-1111-1111-000000000000",
"description" : "This is thing one of two."
}
}
}
$ curl -v -X POST -H "Authorization: Bearer $CNCB_TOKEN" -H 'Content-Type: application/json' -d '{"query":"mutation { deleteThing( id: \"55555555-6666-1111-1111-000000000000\" ) { id } }"}' https://b33zxnw20b.execute-api.us-east-1.amazonaws.com/$MY_STAGE/graphql | json_pp
{
"data" : {
"deleteThing" : null
},
"errors" : [
{
"message" : "Access Denied"
}
]
}
- 使用
npm run rm:lcl -- -s $MY_STAGE
命令完成操作后删除堆栈。
它是如何工作的...
服务配置了一个 Cognito authorizer
,该authorizer
验证令牌并转发claims
。这些claims
包括用户是成员的groups
。在设计时,我们希望声明性地定义访问特权操作的所需角色。在 GraphQL 中,我们可以使用directives
来注释schema
。在这个菜谱中,我们定义了一个hasRole
指令并实现了一个resolver
,该resolver
将注释中定义的允许roles
与声明中存在的groups
进行比较,然后允许或拒绝访问。resolver
逻辑与schema
和schema
中的注释解耦,并且注释简单且清晰。
实现 JWT 过滤器
我们已经看到了如何使用 JWT 来授权访问服务,以及我们如何使用令牌中的声明在服务内部执行细粒度、基于角色的授权。我们通常还需要在数据实例级别上控制访问。例如,客户只能访问他或她的数据,或者员工只能访问特定部门的资料。为了实现这一点,我们通常根据用户的权限装饰查询。在 RESTful API 中,这些信息通常包含在 URL 中作为路径参数。使用路径参数进行查询是典型的做法。
然而,我们希望使用 JWT 中的声明来执行过滤,因为令牌中的值是由令牌签名的真实性断言的。在这个菜谱中,我们将演示如何使用 JWT 中的声明来创建查询过滤器。
准备工作
您需要使用在创建联合身份池菜谱中创建的 Cognito 用户池和在实现注册、登录和注销菜谱中创建的示例应用程序来创建本菜谱中使用的身份令牌。
如何做到...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/jwt-filter --path cncb-jwt-filter
-
使用
cd cncb-jwt-filter
命令导航到cncb-jwt-filter
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-jwt-filter
provider:
name: aws
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
...
functions:
save:
handler: handler.save
events:
- http:
...
authorizer:
arn: ${cf:cncb-cognito-pool-${opt:stage}.userPoolArn}
get:
handler: handler.get
events:
- http:
path: things/{sub}/{id}
...
authorizer:
arn: ${cf:cncb-cognito-pool-${opt:stage}.userPoolArn}
resources:
Resources:
Table:
...
KeySchema:
- AttributeName: sub
KeyType: HASH
- AttributeName: id
KeyType: RANG
- 查看名为
handler.js
的文件,其内容如下:
module.exports.save = (request, context, callback) => {
const body = JSON.parse(request.body);
const sub = request.requestContext.authorizer.claims.sub;
const id = body.id || uuid.v4();
const params = {
TableName: process.env.TABLE_NAME,
Item: {
sub,
id,
...body
}
};
...
db.put(params, (err, resp) => {
...
});
};
module.exports.get = (request, context, callback) => {
const sub = request.requestContext.authorizer.claims.sub;
const id = request.pathParameters.id;
if (sub !== request.pathParameters.sub) {
callback(null, { statusCode: 401 });
return;
}
const params = {
TableName: process.env.TABLE_NAME,
Key: {
sub,
id,
},
};
...
db.get(params, (err, resp) => {
...
});
};
-
使用
npm install
命令安装依赖项。 -
使用
npm test -- -s $MY_STAGE
命令运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-jwt-filter@1.0.0 dp:lcl <path-to-your-workspace>/cncb-jwt-filter
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
POST - https://ho1q4u5hp6.execute-api.us-east-1.amazonaws.com/john/things
GET - https://ho1q4u5hp6.execute-api.us-east-1.amazonaws.com/john/things/{sub}/{id}
functions:
save: cncb-jwt-filter-john-save
get: cncb-jwt-filter-john-get
-
在 AWS 控制台中查看堆栈和资源。
-
使用实现注册、登录和注销菜谱中的应用程序进行登录并生成
idToken
,然后使用以下命令导出idToken
:
$ export CNCB_TOKEN=<idToken value>
- 更新
<API-ID>
后,调用以下curl
命令,以在Authorization
令牌的限制内成功访问数据:
$ curl -v -X POST -H "Authorization: Bearer $CNCB_TOKEN" -H 'Content-Type: application/json' -d '{ "id": "55555555-7777-1111-1111-000000000000", "name": "thing1", "description": "This is thing one of two." }' https://<API-ID>.execute-api.us-east-1.amazonaws.com/$MY_STAGE/things
< HTTP/1.1 201 Created
< location: https://ho1q4u5hp6.execute-api.us-east-1.amazonaws.com/john/things/e4bdd021-a160-4aff-bce2-e652f9469e3e/55555555-7777-1111-1111-000000000000
$ curl -v -H "Authorization: Bearer $CNCB_TOKEN" <Location response header from POST> | json_pp
{
"description" : "This is thing one of two.",
"id" : "55555555-7777-1111-1111-000000000000",
"name" : "thing1",
"sub" : "e4bdd021-a160-4aff-bce2-e652f9469e3e"
}
$ curl -v -H "Authorization: Bearer $CNCB_TOKEN" https://<API-ID>.execute-api.us-east-1.amazonaws.com/$MY_STAGE/things/<An invalid value>/55555555-7777-1111-1111-000000000000 | json_pp
< HTTP/1.1 401 Unauthorized
- 查看日志:
$ sls logs -f save -r us-east-1 -s $MY_STAGE
$ sls logs -f get -r us-east-1 -s $MY_STAGE
- 使用
npm run rm:lcl -- -s $MY_STAGE
完成后,删除堆栈。
它是如何工作的...
API 的客户端可以使用任何值来构建 URL,但不能篡改 JWT 令牌的内容,因为发行者已经签发了令牌。因此,我们需要用令牌中的值覆盖任何请求值。在这个菜谱中,我们根据用户的令牌中的主题或子声明保存和检索特定用户的数据。服务配置了一个 authorizer
,用于验证令牌并转发 claims
。
为了简化示例,主题被用作 HASH
键,数据 uuid
作为 RANGE
键。当数据被检索时,我们将查询参数与令牌中的值进行比较,如果它们不匹配,则返回 401
statusCode
。如果它们匹配,我们使用令牌中的值在实际查询中,以防止断言逻辑中的任何错误意外返回未经授权的数据。
使用信封加密
对于大多数系统来说,在静态数据上加密数据至关重要。我们必须确保我们客户的隐私和公司数据的隐私。不幸的是,我们经常打开基于磁盘的加密,然后勾选要求已完成。然而,这仅在磁盘从系统中断开时保护数据。当磁盘连接时,数据在从磁盘读取时自动解密。例如,创建一个启用服务器端加密的 DynamoDB 表,然后创建一些数据并在控制台中查看它。只要你有权限,你将能够以明文形式看到数据。为了真正确保静态数据的隐私,我们必须在应用程序级别加密数据,并有效地删除所有敏感信息。在这个菜谱中,我们使用 AWS 密钥管理服务(KMS)和一种称为 信封加密 的技术来保护 DynamoDB 表中的静态数据。
如何做到这一点...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/envelope-encryption --path cncb-envelope-encryption
-
使用
cd cncb-envelope-encryption
命令导航到cncb-envelope-encryption
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-envelope-encryption
provider:
name: aws
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
runtime: nodejs8.10
endpointType: REGIONAL
iamRoleStatements:
...
environment:
TABLE_NAME:
Ref: Table
MASTER_KEY_ALIAS:
Ref: MasterKeyAlias
functions:
save:
...
get:
...
resources:
Resources:
Table:
...
MasterKey:
Type: AWS::KMS::Key
Properties:
KeyPolicy:
Version: '2012-10-17'
...
MasterKeyAlias:
Type: AWS::KMS::Alias
Properties:
AliasName: alias/${self:service}-${opt:stage}
TargetKeyId:
Ref: MasterKey
...
- 查看名为
handler.js
的文件,其内容如下:
const encrypt = (thing) => {
const params = {
KeyId: process.env.MASTER_KEY_ALIAS,
KeySpec: 'AES_256',
};
...
return kms.generateDataKey(params).promise()
.then((dataKey) => {
const encryptedThing = Object.keys(thing).reduce((encryptedThing, key) => {
if (key !== 'id')
encryptedThing[key] =
CryptoJS.AES.encrypt(thing[key], dataKey.Plaintext);
return encryptedThing;
}, {});
return {
id: thing.id,
dataKey: dataKey.CiphertextBlob.toString('base64'),
...encryptedThing,
};
});
};
const decrypt = (thing) => {
const params = {
CiphertextBlob: Buffer.from(thing.dataKey, 'base64'),
};
...
return kms.decrypt(params).promise()
.then((dataKey) => {
const decryptedThing = Object.keys(thing).reduce((decryptedThing, key) => {
if (key !== 'id' && key !== 'dataKey')
decryptedThing[key] =
CryptoJS.AES.decrypt(thing[key], dataKey.Plaintext);
return decryptedThing;
}, {});
return {
id: thing.id,
...decryptedThing,
};
});
};
module.exports.save = (request, context, callback) => {
const thing = JSON.parse(request.body);
const id = thing.id || uuid.v4();
encrypt(thing)
.then((encryptedThing) => {
const params = {
TableName: process.env.TABLE_NAME,
Item: {
id,
...encryptedThing,
}
};
...
return db.put(params).promise();
})
...
};
module.exports.get = (request, context, callback) => {
const id = request.pathParameters.id;
...
db.get(params).promise()
.then((resp) => {
return resp.Item ? decrypt(resp.Item) : null;
})
...
};
-
使用
npm install
安装依赖。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-envelope-encryption@1.0.0 dp:lcl <path-to-your-workspace>/cncb-envelope-encryption
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
endpoints:
POST - https://7wpqdcsoad.execute-api.us-east-1.amazonaws.com/john/things
GET - https://7wpqdcsoad.execute-api.us-east-1.amazonaws.com/john/things/{id}
functions:
save: cncb-envelope-encryption-john-save
get: cncb-envelope-encryption-john-get
Stack Outputs
...
MasterKeyId: c83de811-bda8-4bdc-83e1-32e8491849e5
MasterKeyArn: arn:aws:kms:us-east-1:123456789012:key/c83de811-bda8-4bdc-83e1-32e8491849e5
ServiceEndpoint: https://7wpqdcsoad.execute-api.us-east-1.amazonaws.com/john
MasterKeyAlias: alias/cncb-envelope-encryption-john
-
在 AWS 控制台中查看堆栈和资源。
-
更新
<API-ID>
后,调用以下curl
命令以加密和解密数据:
$ curl -v -X POST -d '{ "id": "55555555-8888-1111-1111-000000000000", "name": "thing1", "description": "This is thing one of two." }' https://7wpqdcsoad.execute-api.us-east-1.amazonaws.com/$MY_STAGE/things
< HTTP/1.1 201 Created
< location: https://7wpqdcsoad.execute-api.us-east-1.amazonaws.com/john/things/55555555-8888-1111-1111-000000000000
$ curl -v https://7wpqdcsoad.execute-api.us-east-1.amazonaws.com/$MY_STAGE/things/55555555-8888-1111-1111-000000000000 | json_pp
{
"name" : "thing1",
"id" : "55555555-8888-1111-1111-000000000000",
"description" : "This is thing one of two."
}
-
查看在 DynamoDB 控制台中加密的数据。
-
查看日志:
$ sls logs -f save -r us-east-1 -s $MY_STAGE
$ sls logs -f get -r us-east-1 -s $MY_STAGE
- 使用
npm run rm:lcl -- -s $MY_STAGE
完成后,删除堆栈。
KMS 不包含在 AWS 免费层中。
它是如何工作的...
信封加密本质上是一种使用一个密钥加密另一个密钥的实践;敏感数据首先使用数据密钥进行加密,然后数据密钥再使用主密钥进行加密。在这个配方中,save
函数在将数据保存到 DynamoDB 之前对其进行加密,而get
函数在从 DynamoDB 检索数据并在将其返回给调用者之前对其进行解密。在serverless.yml
文件中,我们定义了一个 KMS MasterKey
和一个MasterKeyAlias
。别名便于主密钥的轮换。save
函数调用kms.generateDataKey
来为对象创建一个数据密钥。每个对象都有自己的数据密钥,每次对象被保存时都会生成一个新的密钥。再次强调,这种做法便于密钥轮换。遵循设计安全实践,我们在设计和开发服务时确定哪些字段是敏感的。在这个配方中,我们单独加密所有字段。数据密钥用于使用AES
加密库在本地加密每个字段。当生成数据密钥时,数据密钥也被主密钥加密并返回到CyphertextBlob
字段。加密后的数据密钥与数据一起存储,以便get
函数可以对其进行解密。get
函数在从数据库检索数据后可以直接访问加密的数据密钥。get
函数已被授权调用kms.decrypt
来解密数据密钥。这个环节至关重要。必须根据最小权限原则限制对主密钥的访问。字段使用 AES 加密库在本地解密,以便可以返回给调用者。
创建用于传输加密的 SSL 证书
对于包含敏感数据的系统,传输中的数据加密至关重要,这占到了当今大多数系统。完全管理的云服务,如函数即服务、云原生数据库和 API 网关,将数据传输加密作为一项常规操作。这有助于确保我们的数据在运动过程中在整个堆栈中得到保护,而我们几乎不需要付出任何努力。然而,我们最终希望通过自定义域名公开我们的云原生资源。为了做到这一点并支持 SSL,我们必须提供自己的 SSL 证书。这个过程可能很繁琐,我们必须确保在证书到期并导致系统故障之前及时轮换证书。幸运的是,越来越多的云服务提供商正在提供自动轮换的完全管理证书。在这个配方中,我们将使用AWS 证书管理器来创建证书并将其与CloudFront分发关联。
准备工作
您需要一个已注册的域名和一个可以用于此配方创建将要部署的站点的子域的 Route 53 托管区域,并且您需要有权批准证书的创建或有权访问具有该权限的人。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/ssl-cert --path cncb-ssl-cert
-
使用
cd cncb-ssl-cert
导航到cncb-ssl-cert
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-ssl-cert
provider:
name: aws
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
plugins:
- serverless-spa-deploy
- serverless-spa-config
custom:
spa:
...
dns:
hostedZoneId: ZXXXXXXXXXXXXX
validationDomain: example.com
domainName: ${self:custom.dns.validationDomain}
wildcard: '*.${self:custom.dns.domainName}'
endpoint: ${opt:stage}-${self:service}.${self:custom.dns.domainName}
cdn:
acmCertificateArn:
Ref: WildcardCertificate
resources:
Resources:
WildcardCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: ${self:custom.dns.wildcard}
DomainValidationOptions:
- DomainName: ${self:custom.dns.wildcard}
ValidationDomain: ${self:custom.dns.validationDomain}
SubjectAlternativeNames:
- ${self:custom.dns.domainName}
...
-
使用您的
hostedZoneId
和validationDomain
更新serverless.yml
文件。 -
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-ssl-cert@1.0.0 dp:lcl <path-to-your-workspace>/cncb-ssl-cert
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
...
WebsiteDistributionURL: https://d19i44112h4l3r.cloudfront.net
...
WebsiteURL: https://john-cncb-ssl-cert.example.com
WebsiteDistributionId: EQSJSWLD0F1JI
WildcardCertificateArn: arn:aws:acm:us-east-1:870671212434:certificate/72807b5b-fe37-4d5c-8f92-25ffcccb6f79
Serverless: Path: ./build
Serverless: File: index.html (text/html)
要完成此步骤,需要授权人员接收证书批准请求电子邮件并批准证书的创建。CloudFormation 堆栈将暂停,直到证书获得批准。
-
在 AWS 控制台中查看堆栈和资源。
-
使用堆栈输出中的
WebsiteURL
调用以下curl
命令:
$ curl -v -https://john-cncb-ssl-cert.example.com
- 完成操作后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
在serverless.yml
文件的资源部分,我们定义了WildcardCertificate
资源,以指示 AWS 证书管理器创建所需的证书。我们创建了一个通配符证书,以便它可以由许多服务使用。指定正确的validationDomain
非常重要,因为这将用于在创建证书之前请求批准。您还必须为顶级域名提供hostedZoneId
。其余一切由serverless-spa-config
插件处理。该配方部署了一个由index.html
页面组成的简单网站。该网站的endpoint
用于在托管区域中创建 Route 53 记录集,并将其分配为网站的 CloudFront 分发的别名。通配符证书与端点匹配,其acmCertificateArn
分配给分发。最终,我们可以使用https
协议和自定义域名访问WebsiteURL
。
配置 Web 应用防火墙
Web 应用防火墙(WAF)是控制云原生系统流量的重要工具。WAF 通过阻止来自常见漏洞,如恶意机器人、SQL 注入、跨站脚本攻击(XSS)、HTTP 洪水和已知攻击者的流量来保护系统。我们有效地在系统周围创建了一个外围,在流量能够影响系统资源之前,在云边缘阻止流量。我们可以使用本书中的许多技术来监控内部和外部资源,并根据流量变化动态更新防火墙规则。市场上也有可用的托管规则,因此我们可以利用第三方广泛的安全专业知识。在本配方中,我们将通过创建一个阻止来自企业网络外部的流量的规则,并将 WAF 与CloudFront分发关联起来,来展示这些组件是如何组合在一起的。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/waf --path cncb-waf
-
使用
cd cncb-waf
导航到cncb-waf
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-waf
provider:
name: aws
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
plugins:
- serverless-spa-deploy
- serverless-spa-config
custom:
spa:
...
cdn:
webACLId:
Ref: WebACL
# logging:
# bucketName: ${cf:cncb-account-as-code-${opt:stage}.AuditBucketName}
resources:
Resources:
WhitelistIPSet:
Type: AWS::WAF::IPSet
Properties:
Name: IPSet for whitelisted IP adresses
IPSetDescriptors:
- Type: IPV4
Value: 0.0.0.1/32
WhitelistRule:
Type: AWS::WAF::Rule
Properties:
Name: WhitelistRule
MetricName: WhitelistRule
Predicates:
- DataId:
Ref: WhitelistIPSet
Negated: false
Type: IPMatch
WebACL:
Type: AWS::WAF::WebACL
Properties:
Name: Master WebACL
DefaultAction:
Type: BLOCK
MetricName: MasterWebACL
Rules:
- Action:
Type: ALLOW
Priority: 1
RuleId:
Ref: WhitelistRule
Outputs:
WebACLId:
Value:
Ref: WebACL
- 通过登录 VPN 并执行以下命令来确定您的企业外部公共 IP 地址:
$ curl ipecho.net/plain ; echo
-
在
serverless.yml
文件中更新WhitelistIPSet
,后跟你的 IP 地址和/32
。 -
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-waf@1.0.0 dp:lcl <path-to-your-workspace>/cncb-waf
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
WebsiteDistributionURL: https://d3a9sc88i7431l.cloudfront.net
WebsiteS3URL: http://cncb-waf-john-websitebucket-1c13hrzslok5s.s3-website-us-east-1.amazonaws.com
WebsiteBucketName: cncb-waf-john-websitebucket-1c13hrzslok5s
WebsiteDistributionId: E3OLRBU9LRZBUE
WebACLId: 68af80b5-8eda-43d0-be25-6c65d5cc691e
Serverless: Path: ./build
Serverless: File: index.html (text/html)
-
在 AWS 控制台中查看堆栈和资源。
-
在连接 VPN 的同时浏览堆栈输出中列出的
WebsiteDistributionURL
,您将能够访问该页面。 -
现在,断开 VPN 连接,一旦缓存清除,您将无法访问页面。
-
完成使用
npm run rm:lcl -- -s $MY_STAGE
后,请移除堆栈。
它是如何工作的...
首先要注意的是,使用 AWS WAF 的前提是使用 CloudFront。serverless-spa-config
插件创建 CloudFront 分布并分配webACLId
。对于这个配方,我们正在保护一个简单的index.html
页面。我们创建一个静态的WebACL
来阻止除单个 IP 地址之外的所有内容。我们为单个地址定义WhitelistIPSet
并将其与WhitelistRule
关联。然后,我们将规则与访问控制列表(ACL)关联,并将默认操作定义为BLOCK
所有访问,然后基于WhitelistRule
定义一个允许访问的操作。
这个示例很有用,但它只是触及了可能性的表面。例如,我们可以取消注释 CloudFront 分布的日志配置,然后根据可疑活动处理访问日志并动态创建规则。在日常生活中,我们还可以检索公共声誉列表并更新规则集。定义和维护有效的规则可能是一项全职工作,因此 AWS 市场上可用的完全管理的规则是自定义规则的有吸引力的替代品或补充。
复制数据湖以进行灾难恢复
当我第一次阅读关于 Code Spaces(www.infoworld.com/article/2608076/data-center/murder-in-the-amazon-cloud.html
)的故事时,我有点害怕,直到我意识到这家公司之所以倒闭,是为了让我们都能从它的经验中学习。Code Spaces 是一家使用 AWS 的公司,他们的账户被黑客入侵并被勒索。他们进行了反击,他们的整个账户内容,包括备份,都被删除了,他们就这样倒闭了。正确使用多因素认证(MFA)和良好的访问密钥卫生习惯对于防止此类攻击至关重要。同样重要的是,要维护一个完全独立且断开的备份账户,这样单个账户的泄露就不会给整个系统和公司带来灾难。在这个菜谱中,我们将使用 S3 复制功能将存储桶复制到专门的恢复账户。至少,这项技术应该用于复制数据湖的内容,因为数据湖中的事件可以被重放以重建系统。
如何操作...
- 从以下模板创建源和恢复项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/dr/recovery-account --path cncb-dr-recovery-account
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch5/dr/src1-account --path cncb-dr-src1-account
-
使用
cd cncb-dr-recovery-account
导航到cncb-dr-recovery-account
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-dr-recovery-account
provider:
name: aws
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
custom:
accounts:
src1:
accountNumber: '#{AWS::AccountId}' # using same account to simplify recipe
...
resources:
Resources:
DrSrc1Bucket1:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName: cncb-${opt:stage}-us-west-1-src1-bucket1-dr
VersioningConfiguration:
Status: Enabled
DrSrc1Bucket1Policy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: DrSrc1Bucket1
PolicyDocument:
Statement:
- Effect: Allow
Principal:
AWS: arn:aws:iam::${self:custom.accounts.src1.accountNumber}:root
Action:
- s3:ReplicateDelete
- s3:ReplicateObject
- s3:ObjectOwnerOverrideToBucketOwner
Resource:
...
...
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-dr@1.0.0 dp:lcl <path-to-your-workspace>/cncb-dr
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
DrSrc1Bucket1Name: cncb-john-us-west-1-src1-bucket1-dr
-
使用
cd cncb-dr-src1-account
导航到cncb-dr-src1-account
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-dr-src1-account
provider:
name: aws
# cfnRole: arn:aws:iam::<account-number>:role/${opt:stage}-cfnRole
...
custom:
replicationBucketArn: arn:aws:s3:::cncb-${opt:stage}-us-west-1-src1-bucket1-dr
recovery:
accountNumber: '#{AWS::AccountId}' # using same account to simplify recipe
...
resources:
Resources:
Src1Bucket1:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName: cncb-${opt:stage}-us-east-1-src1-bucket1
VersioningConfiguration:
Status: Enabled
ReplicationConfiguration:
Role: arn:aws:iam::#{AWS::AccountId}:role/${self:service}-${opt:stage}-${opt:region}-replicate
Rules:
- Destination:
Bucket: ${self:custom.replicationBucketArn}
StorageClass: STANDARD_IA
Account: ${self:custom.recovery.accountNumber}
AccessControlTranslation:
Owner: Destination
Status: Enabled
Prefix: ''
Src1Bucket1ReplicationRole:
DependsOn: Src1Bucket1
Type: AWS::IAM::Role
Properties:
RoleName: ${self:service}-${opt:stage}-${opt:region}-replicate
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service:
- s3.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: replicate
PolicyDocument:
Statement:
...
- Effect: Allow
Action:
- s3:ReplicateObject
- s3:ReplicateDelete
- s3:ObjectOwnerOverrideToBucketOwner
Resource: ${self:custom.replicationBucketArn}/*
...
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-dr@1.0.0 dp:lcl <path-to-your-workspace>/cncb-dr
> sls deploy -v -r us-east-1 "-s" "john"
Serverless: Packaging service...
...
Serverless: Stack update finished...
...
Stack Outputs
Src1Bucket1Name: cncb-john-us-east-1-src1-bucket1
-
在 AWS 控制台中检查栈和存储桶。
-
使用以下命令加载数据:
$ sls invoke -r us-east-1 -f load -s $MY_STAGE
-
检查两个存储桶的内容,以确保数据已复制。
-
完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除每个栈。
它是如何工作的...
AWS S3 负责将一个存储桶的内容复制到另一个存储桶的所有繁重工作。我们只需定义要复制的内容和位置,并正确设置所有权限即可。将数据复制到完全与任何其他账户断开连接的单独账户中非常重要,以确保另一个账户的漏洞不会影响恢复账户。为了简化这个流程,我们将使用单个账户,但这并不会改变除了账户编号值之外的 CloudFormation 模板。将数据复制到您通常不使用的区域也是一个好主意,这样您某个区域的故障就不会导致您的恢复区域也出现故障。接下来,我们需要为每个要复制的源存储桶在恢复账户中创建相应的存储桶。使用前缀和/或后缀的命名约定来轻松区分源存储桶和目标存储桶是一个好主意。所有存储桶都必须开启版本控制以支持复制。源存储桶授予 S3 服务执行复制的权限,而目标存储桶授予源账户向恢复账户写入的权限。我们还把复制的文件内容的所有权转让给恢复账户。最终,如果源存储桶中的内容被完全删除,目标存储桶仍然会包含这些内容以及删除标记。
第六章:构建持续部署管道
在本章中,将涵盖以下食谱:
-
创建 CI/CD 管道
-
编写单元测试
-
编写集成测试
-
为同步 API 编写合同测试
-
为异步 API 编写合同测试
-
组装传递性端到端测试
-
利用功能标志
简介
在前面的章节中,我们看到了云原生是如何精益和自主的。利用完全管理的云服务和建立适当的防波堤,使自给自足的全栈团队能够快速且持续地交付自主服务,并具有信心,即任何单个服务的故障都不会损害依赖于它的上游和下游服务。这种架构是一个重大进步,因为这些保障措施保护我们免受不可避免的人类错误的影响。然而,我们仍然必须努力减少人类错误并提高我们对系统的信心。
为了最小化和控制潜在的错误,我们需要最小化和控制我们的批次大小。我们通过遵循将部署与发布解耦的实践来实现这一点。部署只是将软件部署到环境中的行为,而发布只是将软件提供给一组用户的行为。遵循精益方法,我们通过一系列小而专注的实验向用户发布功能,以确定解决方案是否在正确的轨道上,以便及时进行纠正。每个实验由一系列故事组成,每个故事由一系列小而专注的任务组成。这些任务是我们的部署单元。对于每个故事,我们制定一个路线图,以连续部署这些任务,并考虑到所有相互依赖关系,以确保零停机时间。管理每个单独任务的实践统称为任务分支工作流程。本章中的食谱展示了任务分支工作流程的内部运作方式以及我们如何最终通过功能标志为用户提供这些功能。
创建 CI/CD 管道
小批次大小可以降低部署风险,因为更容易推理它们的正确性,并且在出错时更容易纠正。任务分支工作流程是一种 Git 工作流程,专注于极短生命周期的分支,范围仅为几小时而不是几天。它类似于问题分支工作流程,因为每个任务都作为项目管理工具中的问题进行跟踪。然而,问题的长度是模糊的,因为一个问题可以用来跟踪整个功能。这个食谱展示了在任务分支工作流程中,问题跟踪、Git 分支、拉取请求、测试、代码审查和 CI/CD 管道如何协同工作,以管理小而专注的部署单元。
准备工作
在开始这个食谱之前,您需要在 GitLab 上有一个账户(about.gitlab.com/
)。
如何做...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/pipeline --path cncb-pipeline
-
导航到
cncb-pipeline
目录,cd cncb-pipeline
。 -
在
gitlab.com
上本地和远程初始化 Git 仓库,如下所示:
$ git init
$ git remote add origin git@gitlab.com:<username>/cncb-pipeline.git
$ git add .gitignore
$ git commit -m "initial commit"
$ git push -u origin master
-
确认项目已在您的 Gitlab 账户中创建。
-
在项目中创建一个名为
intialize-project
的新问题。 -
点击创建合并请求按钮并注意分支名称,例如
1-initialize-project
。 -
使用
git pull && git checkout 1-initialize-project
在本地检出分支。 -
查看名为
.gitlab-ci.yml
的文件,其内容如下:
image: node:8
before_script:
- cp .npmrc-conf .npmrc
- npm install --unsafe-perm
test:
stage: test
script:
- npm test
- npm run test:int
stg-east:
stage: deploy
variables:
AWS_ACCESS_KEY_ID: $DEV_AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY: $DEV_AWS_SECRET_ACCESS_KEY
script:
- npm run dp:stg:e
except:
- master
production-east:
stage: deploy
variables:
AWS_ACCESS_KEY_ID: $PROD_AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY: $PROD_AWS_SECRET_ACCESS_KEY
script:
- npm run dp:prd:e
only:
- master
- 查看名为
package.json
的文件,其内容如下:
...
"scripts": {
"test": "echo running unit tests...",
"test:int": "echo running integration tests...",
...
"dp:stg:e": "sls deploy -v -r us-east-1 -s stg --acct dev",
"dp:prd:e": "sls deploy -v -r us-east-1 -s prd --acct prod"
},
...
- 查看名为
serverless.yml
的文件,其内容如下:
service: cncb-pipeline
provider:
name: aws
# cfnRole: arn:aws:iam::${self:custom.accounts.${opt:acct}.accountNumber}:role/${opt:stage}-cfnRole
custom:
accounts:
dev:
accountNumber: 123456789012
prod:
accountNumber: 123456789012
-
在 GitLab 项目(
gitlab.com/
)的设置* | *CI/CD | 变量下配置DEV_AWS_ACCESS_KEY_ID
、DEV_AWS_SECRET_ACCESS_KEY
、PROD_AWS_ACCESS_KEY_ID
、PROD_AWS_SECRET_ACCESS_KEY
和NPM_TOKEN
环境变量。 -
按如下方式将项目文件推送到远程仓库:
$ git pull
$ git add .
$ git commit -m "initialize project"
$ git push origin 1-initialize-project
在推送更改之前,你应该始终执行 git pull
以保持你的任务分支与主分支同步。
-
查看合并请求中的代码。
-
查看分支管道的进度。
-
在 AWS 控制台中查看
cncb-pipeline-stg
堆栈。 -
从合并请求的名称中移除
WIP:
前缀并接受合并请求。
最好尽早开始拉取请求,以便尽早收到反馈。WIP:
前缀向审阅者表明工作仍在进行中。前缀纯粹是程序性的,但 GitLab 不会允许带有 WIP 前缀的合并请求被意外接受。
-
查看主管道的进度。
-
在 AWS 控制台中查看
cncb-pipeline-prd
堆栈。 -
完成后,删除两个堆栈。
它是如何工作的...
我们使用 GitLab.com
简单是因为它是一个免费且托管的工具集,且集成良好。还有其他替代方案,如 Bitbucket Pipelines,虽然需要更多努力才能设置,但它们仍然提供类似的功能。比较的食谱中包含了一个 bitbucket-pipelines.yml
文件。
正如我们在整个食谱中看到的那样,我们的部署单元是一个堆栈,由项目目录根目录中的 serverless.yml
文件定义。正如我们在本食谱中看到的那样,每个项目都在其自己的 Git 仓库中管理,并有自己的 CI/CD 管道。管道由位于项目根目录的配置文件定义,例如 .gitlab-ci.yml
或 bitbucket-pipelines.yml
文件。这些管道与 Git 分支策略集成,并由拉取请求管理。
注意 GitLab 使用术语 merge request,而其他工具使用术语 pull request。这两个术语可以互换使用。
当从待办事项中拉取问题或任务以在仓库中创建分支时,任务分支工作流程开始。创建一个拉取请求来管理分支。管道在分支上执行所有测试,并在拉取请求中显示其进度。一旦测试被认为成功,管道将堆栈部署到开发账户的预发布环境中。在拉取请求中执行代码审查,并通过评论记录讨论。一旦一切就绪,拉取请求可以被接受以合并更改到主分支,并触发将堆栈部署到生产账户的生产环境的部署。
管道定义的第一行表示将使用node:8
Docker 镜像来执行管道。管道定义的其余部分编排了我们在此菜谱中手动执行的步骤。首先,npm install
安装了在package.json
文件中定义的所有依赖项。然后,我们在给定的分支上执行所有测试。最后,我们使用npm
将堆栈部署到特定的环境和区域;在这种情况下,npm run dp:stg:e
或npm run dp:prd:e
。每个步骤的详细信息都封装在npm
脚本中。
注意,在整个菜谱中,我们一直在使用npm run dp:lcl
脚本来执行我们的部署。这些允许每个开发者在一个个人堆栈(即本地或lcl
)中进行开发和测试,以确保预发布(stg
)环境保持稳定,从而生产(prd
)环境也是如此。
环境变量,如AWS_ACCESS_KEY_ID
和AWS_SECRET_ACCESS_KEY
,由管道安全存储且不会被记录。我们为每个账户定义一组变量,由DEV_
和PROD_
前缀标识,然后在管道定义中映射它们。在保护您的云账户菜谱中,我们创建了CiCdUser
来授予管道权限。在这里,我们需要为这些用户手动创建访问密钥并将它们作为管道变量安全存储。密钥和管道变量随后定期轮换和更新。
管道将堆栈部署到开发账户的每个任务分支的预发布环境中,以及生产账户的主分支的生产环境中。访问密钥决定了使用哪个账户,Serverless Framework -s
选项完全限定堆栈的名称。然后我们添加一个额外的选项--acct
,允许我们索引到账户范围的自定义变量,例如${self:custom.accounts.${opt:acct}.accountNumber}
。为了避免在生产阶段和生产账户之间产生混淆,我们需要使用略微不同的缩写,例如prd
和prod
。
编写单元测试
单元测试可能是最重要的测试类型,并且应该当然占据测试金字塔中的大多数测试用例。测试应遵循科学方法,其中我们保持一些变量不变,调整输入,并测量输出。单元测试通过在隔离状态下测试单个单元来实现这一点。这使得单元测试能够专注于功能并最大化覆盖率。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/unit-testing --path cncb-unit-testing
-
使用
cd cncb-unit-testing
命令导航到cncb-unit-testing
目录。 -
使用
npm install
安装依赖项。 -
检查名为
package.json
的文件,其内容如下:
"scripts": {
...
"pretest": "npm run clean && npm run lint",
"test": "nyc mocha ... ./test/unit/**/*.test.js",
...
},
- 按以下方式运行单元测试:
$ npm test
...
14 passing (76ms)
...
===== Coverage summary =====
Statements : 100% ( 57/57 )
Branches : 100% ( 4/4 )
Functions : 100% ( 28/28 )
Lines : 100% ( 51/51 )
- 检查名为
test/unit/connector/db.test.js
的文件,其内容如下:
it('should get by id', async () => {
const spy = sinon.spy((params, cb) => cb(null, {
Item: ...
}));
AWS.mock('DynamoDB.DocumentClient', 'get', spy);
const data = await new Connector('t1').getById(ID);
expect(spy).to.have.been.calledOnce;
...
});
- 检查名为
test/unit/get/index.test.js
的文件,其内容如下:
it('should get by id', async () => {
...
const stub = sinon.stub(Connector.prototype, 'getById')
.returns(Promise.resolve(THING));
const data = await new Handler(TABLE_NAME).handle(REQUEST);
expect(stub).to.have.been.calledWith(ID);
...
});
- (可选) 使用此项目重复从“创建 CI/CD 管道”菜谱中的步骤。
工作原理...
在之前的菜谱中,我们故意将示例简化到合理的程度,以突出特定主题。代码是正确的,但菜谱没有包含任何单元测试,因为该主题尚未被涉及。你在这个章节的菜谱中可能首先注意到的是,我们正在向代码中添加额外的结构;例如,每个函数都有自己的目录和文件,我们还在代码中添加了一些轻量级分层。这种结构旨在通过使隔离测试单元更容易而简化测试过程。因此,现在让我们更深入地探讨已添加的工具和结构。
在 npm test
脚本中调用的第一个工具是 nyc
,它是 istanbul
代码覆盖率工具的命令行界面。.nycrc
文件配置了代码覆盖率过程。在这里,我们要求达到 100% 的覆盖率。对于我们的边界、隔离和自主服务的范围来说,这是完全合理的。这也合理,因为我们正在以增量方式编写单元测试,同时也在一系列任务分支工作流程中增量构建服务。此外,如果不保持覆盖率在 100%,那么在开发过程的后期跳过测试就会变得过于容易,这在持续部署管道中是危险的,并且违背了测试的目的。幸运的是,代码的结构使得识别缺少测试的功能变得更加容易。
npm pretest
脚本运行 linting 过程。eslint
是一个非常宝贵的工具。它强制执行最佳实践,自动修复许多违规行为,并识别常见问题。从本质上讲,linting 有助于教会开发者如何编写更好的代码。可以通过 .eslintignore
和 .eslintrc.js
文件调整 linting 过程。
隔离外部依赖是单元测试的一个基本部分。我们选择的测试工具是 mocha
、chai
、sinon
和 aws-sdk-mock
。有许多工具可用,但这个组合非常受欢迎。mocha 是总的测试框架;chai
是断言库;sinon
提供测试间谍、存根和模拟;aws-sdk-mock
基于 sinon
来简化对 aws-sdk
的测试。为了进一步便于这个过程,我们在连接器类中隔离了我们的 aws-sdk
调用。这确实有代码在整个服务中重用的额外好处,但其主要好处是简化测试。我们专门为使用 aws-sdk-mock
的类编写单元测试。在整个单元测试代码中,我们存根化连接器层,这极大地简化了每个测试的设置,因为我们已经隔离了 aws-sdk
的复杂性。
Handler
类负责大部分的测试。这些类封装和编排业务逻辑,因此将需要最多的测试组合。为了便于这项工作,我们将 Handler
类与 callback
函数解耦。handle
方法要么返回一个 promise,要么返回一个 stream,然后顶层函数将这些适配到回调。这允许测试轻松地接入处理流程以断言输出。
我们的测试本质上是异步的;因此,防止永远不失败的测试(即代码出错时不会失败的测试)非常重要。对于返回 promises 的处理器,最佳做法是使用 async
/await
来防止吞没异常。对于返回 stream 的处理器,最佳做法是使用 collect/tap/done 模式来保护数据没有完全通过流流动的场景。
编写集成测试
集成测试专注于测试依赖服务之间的 API 调用。在我们的云原生系统中,这些集中在与完全托管的云服务之间的服务内交互。它们确保交互被正确编码以发送和接收正确的有效负载。这些调用需要网络,但网络是出了名的不可靠。这是随机和随意失败的测试不稳定的主要原因。不稳定测试反过来又是团队士气低落的主要原因。这个食谱展示了如何使用 VCR 库来创建测试替身,允许在没有网络依赖或外部服务部署的情况下独立执行集成测试。
准备工作
在开始这个食谱之前,你需要一个 AWS Kinesis Stream,例如在 创建事件流 食谱中创建的那个。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/integration-testing --path cncb-integration-testing
-
使用
cd cncb-integration-testing
命令导航到cncb-integration-testing
目录。 -
使用
npm install
安装依赖。 -
查看名为
package.json
的文件,其内容如下:
"scripts": {
...
"test:int": "npm start -- --exec \"mocha ... ./test/int/**/*.test.js\"",
"start": "sls offline start --port 3001 -r us-east-1 -s stg --acct dev",
...
},
-
使用
npm test
运行单元测试。 -
以
replay
模式运行集成测试,如下所示:
$ npm run test:int
...
Serverless: Replay mode = replay
Serverless: Starting Offline: stg/us-east-1.
...
get/index.js
✓ should get by id
...
3 passing (358ms)
...
Serverless: Halting offline server
-
检查
./fixtures
目录中的文件。 -
检查与同步集成测试相关的文件,如下所示:
serverless.yml
...
plugins:
- serverless-webpack
- baton-vcr-serverless-plugin
- serverless-offline
...
test/int/get/index.test.js
...
const supertest = require('supertest');
const client = supertest('http://localhost:3001');
...
describe('get/index.js', () => {
it('should get by id', () => client
.get('/things/00000000-0000-0000-0000-000000000000')
.expect(200)
.expect((res) => {
expect(JSON.parse(res.text)).to.deep.equal(THING);
}));
});
webpack.config.js
...
const injectMocks = (entries) =>
Object.keys(entries).reduce((e, key) => {
e[key] = ['./test/int/mocks.js', entries[key]];
return e;
}, {});
const includeMocks = () => slsw.lib.webpack.isLocal && process.env.REPLAY != 'bloody';
module.exports = {
entry: includeMocks() ? injectMocks(slsw.lib.entries) : slsw.lib.entries,
...
- 检查与异步集成测试相关的文件,如下所示:
test/int/trigger/index.test.js
describe('trigger/index.js', () => {
before(() => {
require('baton-vcr-replay-for-aws-sdk');
process.env.STREAM_NAME = 'stg-cncb-event-stream-s1';
aws.config.update({ region: 'us-east-1' });
});
it('should trigger', (done) => {
new Handler(process.env.STREAM_NAME).handle(TRIGGER)
.collect()
.tap((data) => {
expect(data).to.deep.equal([{
response: RESPONSE,
event: EVENT,
}]);
})
.done(done);
});
})
-
使用
npm run dp:stg:e
部署堆栈。 -
删除
./fixtures
目录,并以record
模式再次运行集成测试,如下所示:
$ DEBUG=replay REPLAY=record npm run test:int
...
Serverless: GET /things/00000000-0000-0000-0000-000000000000 (λ: get)
replay Requesting POST https://dynamodb.us-east-1.amazonaws.com:443/ +0ms
replay Creating ./fixtures/dynamodb.us-east-1.amazonaws.com-443 +203ms
replay Received 200 https://dynamodb.us-east-1.amazonaws.com:443/ +5ms
...
- (可选)使用此项目重复“创建 CI/CD 管道”配方中的步骤。
它是如何工作的...
集成测试使用与单元测试相同的工具,并增加了一些额外的工具。这有一个优点,就是学习曲线是逐步增加的。第一个引起兴趣的新工具是serverless-offline
插件。此插件读取serverless.yml
文件,并在本地模拟 API 网关,以方便同步 API 的测试。接下来,我们使用supertest
向本地运行的服务发送 HTTP 请求并断言响应。不可避免的是,这些服务会使用默认或指定的访问密钥调用 AWS 服务。这些是我们想在 CI/CD 管道中记录和回放的调用。"baton-vcr-serverless-plugin"在serverless-offline
过程中初始化Replay
VCR 库。默认情况下,VCR 以replay
模式运行,如果固定目录下找不到记录,则会失败。当编写新测试时,开发者通过设置REPLAY
环境变量以record
模式运行测试,REPLAY=record npm run test:int
。为了确保找到记录,我们必须保持所有在请求中使用的动态生成的值不变;为此,我们在 webpack 配置中注入模拟。在这个例子中,./test/int/mocks.js
文件使用sinon
来模拟 UUID。
为异步函数,如triggers
和listeners
编写集成测试,大部分是相似的。首先,我们需要手动捕获日志文件中的事件,例如 DynamoDB 流事件,并将它们包含在测试用例中。然后,在测试开始时初始化baton-vcr-replay-for-aws-sdk
库。从这里开始,记录请求的过程是相同的。对于trigger
函数,它将记录向 Kinesis 发布事件的调用,而对于listener
函数,通常记录对服务表的调用。
在编写集成测试时,重要的是要记住测试金字塔。集成测试专注于特定 API 调用内的交互;它们不需要覆盖所有不同的功能场景,因为这是单元测试的工作。集成测试只需要关注消息的结构,以确保它们与外部服务期望和返回的内容兼容。为了支持这些测试的记录创建,外部系统可能需要使用特定的数据集进行初始化。这个初始化应该作为测试的一部分进行编码,并记录下来。
为同步 API 编写合约测试
合约测试和集成测试是同一枚硬币的两面。集成测试确保消费者正确调用提供商服务,而合约测试确保提供商服务继续满足其对消费者的义务,并且任何更改都是向后兼容的。这些测试也是由消费者驱动的。这意味着消费者向提供商的项目提交拉取请求以添加这些额外的测试。提供商不应该更改这些测试。如果合约测试失败,则意味着已经做出了不兼容的更改。提供商必须使更改兼容,然后与消费者团队合作创建升级路线图。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/contract-testing-sync --path cncb-contract-testing-sync
-
使用
cd cncb-contract-testing-sync/bff
命令导航到bff
目录 -
使用
npm install
安装依赖项。 -
使用
npm test
运行单元测试。 -
使用
npm run test:int
在replay
模式下运行集成测试。 -
检查
./fixtures
目录中的文件。 -
检查名为
./test/int/frontend/contract.test.js
的文件,其内容如下:
import 'mocha';
const supertest = require('supertest');
const relay = require('baton-request-relay');
const client = supertest('http://localhost:3001');
describe('contract/frontend', () => {
it('should relay the frontend save request',
() => run('./fixtures/frontend/save'));
it('should relay the frontend get request',
() => run('./fixtures/frontend/get'));
});
const run = fixture => {
const rec = relay(fixture);
return clientrec.request.method
.set(rec.request.headers)
.send(rec.request.body)
.expect(rec.response.statusCode)
.expect(rec.response.body);
};
-
检查
./frontend
下的消费者的test
和fixture
文件。 -
(可选)使用此项目重复 创建 CI/CD 管道 菜单中的步骤。
它是如何工作的...
当涉及到集成和合约测试时,魔鬼藏在细节中。具体来说,是各个字段的细节、它们的数据类型以及它们的有效值。提供商改变看似微不足道的细节,可能会违反消费者对合约的理解。这些变化往往直到最糟糕的时刻才被发现。在这方面,手工制作的合约测试是不可靠的,因为关于合约的相同误解通常也会反映在测试中。相反,我们需要在交互的双方使用相同的录音。
消费者在其项目中创建一个集成测试,记录与提供商服务的交互。然后,将这些相同的录音复制到提供商的项目中,并用于驱动合约测试。为录音创建一个针对消费者的特定 fixtures
子目录。合约测试使用 baton-request-relay
库读取录音,以便它们可以用来驱动 supertest
在提供商的项目中执行相同的请求。
在我们的云原生系统中,这些同步请求通常是在前端和其 Backend for Frontend(BFF)服务之间进行的,该服务由同一团队拥有。同一团队拥有消费者和提供商的事实并不否定这些测试的价值,因为即使是细微的变化,例如将短整数更改为长整数,如果任一方做出了所有错误的假设,都可能产生巨大的影响。
为异步 API 编写合约测试
异步 API 的合同测试目标是确保提供者和消费者之间的向后兼容性——就像同步 API 一样。测试异步通信自然是不可靠的,因为网络不可靠加上异步消息的不可靠延迟。这些测试经常失败,因为消息到达缓慢,测试超时。为了解决这个问题,我们将测试从消息系统中隔离出来;我们在一端记录发送消息请求,然后中继消息并在接收端断言合同。
如何操作...
- 从以下模板创建上游和下游项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/contract-testing-async/upstream --path cncb-contract-testing-async-upstream
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/contract-testing-async/downstream --path cncb-contract-testing-async-downstream
-
使用
cd cncb-contract-testing-async-upstream
命令导航到upstream
目录。 -
使用
npm install
安装依赖。 -
使用
npm test
运行单元测试。 -
使用
npm run test:int
在replay
模式下运行集成测试。 -
检查
./fixtures/downstream-consumer-x
目录中的文件。 -
检查名为
./test/int/downstream-consumer-x/contract.test.js
的文件,内容如下:
...
const EVENT = require('../../../fixtures/downstream-consumer-x/thing-created.json');
describe('contract/downstream-consumer-x', () => {
before(() => {
const replay = require('baton-vcr-replay-for-aws-sdk');
replay.fixtures = './fixtures/downstream-consumer-x';
process.env.STREAM_NAME = 'stg-cncb-event-stream-s1';
aws.config.update({ region: 'us-east-1' });
});
afterEach(() => {
sinon.restore();
});
it('should publish thing-created', (done) => {
sinon.stub(utils, 'uuidv4').returns('00000000-0000-0000-0000-000000000001');
handle(EVENT, {}, done);
});
});
-
使用
cd ../cncb-contract-testing-async-downstream
命令导航到downstream
目录。 -
重复相同的步骤来运行测试。
-
检查下游消费者的
./test/int/upstream-provider-y
和./fixture/upstream-provider-y
文件,如下所示:
...
const relay = require('baton-event-relay');
describe('contract/upstream-provider-y', () => {
before(() => {
...
const replay = require('baton-vcr-replay-for-aws-sdk');
replay.fixtures = './fixtures/upstream-provider-y';
});
it('should process the thing-created event', (done) => {
const rec = relay('./fixtures/upstream-provider-y/thing-created');
handle(rec.event, {}, done);
});
});
- (可选)使用此项目重复 创建 CI/CD 管道 配方中的步骤。
它是如何工作的...
记录和为异步 API 创建合同测试的过程与同步 API 的过程相反。对于同步 API,消费者启动交互,而对于异步 API,提供者启动事件的发布。然而,异步提供者不知道他们的消费者,因此这些测试仍然需要由消费者驱动。
首先,消费者项目在上游项目中创建一个测试,并记录向事件流发布事件的调用。然后,消费者使用 baton-event-relay
库在其自己的项目中中继记录,以断言事件的内容符合预期。再次强调,提供者项目不拥有这些测试,如果由于向后不兼容的更改而测试失败,不应修复测试。
组装传递的端到端测试
传统的端到端测试劳动密集且成本高昂。因此,传统的批量大小很大,端到端测试执行频率低,甚至完全跳过。这与我们持续部署的目标正好相反。我们希望批量大小小,并且希望每次部署都进行全面测试,每天多次,这时我们通常听到异端的呼声。这个配方展示了如何使用本章中已介绍的工具和技术实现这一点。
如何操作...
- 从以下模板创建
author-frontend
、author-bff
、customer-bff
和customer-frontend
项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/transitive-testing/author-frontend --path cncb-transitive-testing-author-frontend
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/transitive-testing/author-bff --path cncb-transitive-testing-author-bff
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/transitive-testing/customer-bff --path cncb-transitive-testing-customer-bff
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/transitive-testing/customer-frontend --path cncb-transitive-testing-customer-frontend
-
使用
npm install
在每个项目中安装依赖。 -
按照以下方式在每个项目中以
replay
模式运行端到端测试:
$ cd ./author-frontend
$ DEBUG=replay npm run test:int
$ cd ../author-bff
$ npm test
$ DEBUG=replay npm run test:int
$ cd ../customer-bff
$ npm test
$ DEBUG=replay npm run test:int
$ cd ../customer-frontend
$ DEBUG=replay npm run test:int
- 审查以下所有固定文件:
./author-frontend/fixtures/0.0.0.0-3001/save-thing0
./author-frontend/fixtures/0.0.0.0-3001/get-thing0
./author-bff/fixtures/author-frontend/save-thing0
./author-bff/fixtures/dynamodb.us-east-1.amazonaws.com-443/save-thing0
./author-bff/fixtures/author-frontend/get-thing0
./author-bff/fixtures/dynamodb.us-east-1.amazonaws.com-443/get-thing0
./author-bff/fixtures/downstream-customer-bff/thing0-INSERT.json
./author-bff/fixtures/kinesis.us-east-1.amazonaws.com-443/thing0-created
./customer-bff/fixtures/upstream-author-bff/thing0-created
./customer-bff/fixtures/dynamodb.us-east-1.amazonaws.com-443/save-thing0
./customer-bff/fixtures/customer-frontend/get-thing0
./customer-bff/fixtures/dynamodb.us-east-1.amazonaws.com-443/get-thing0
./customer-frontend/fixtures/0.0.0.0-3001/get-thing0
- 审查以下所有端到端测试用例文件:
./author-frontend/test/int/e2e.test.js
./author-bff/test/int/author-frontend/e2e.test.js
./author-bff/test/int/downstream-customer-bff/e2e.test.js
./customer-bff/test/int/upstream-author-bff/e2e.test.js
./customer-bff/test/int/customer-frontend/e2e.test.js
./customer-frontend/test/int/e2e.test.js
它是如何工作的...
集成测试和契约测试是同一枚硬币的两面。我们已经看到了如何使用测试双倍体来实现这些测试,这些测试双倍体重新播放之前记录的请求和响应对。这使得每个服务都可以在无需部署任何其他服务的情况下独立测试。服务提供者团队创建集成测试以确保他们的服务按预期工作,而服务消费者团队创建契约测试以确保提供者的服务按预期工作。然后我们在此基础上构建,使得所有团队中的测试工程师一起工作,定义足够的端到端测试场景以确保所有服务协同工作。对于每个场景,我们在所有项目中串联一系列集成和契约测试,其中来自一个项目的记录用于驱动下一个项目的测试,依此类推。借鉴等价传递性质,如果服务 A 产生有效载荷 1,它与服务 B 一起工作产生有效载荷 2,它与服务 C 一起工作产生预期的有效载荷 3,那么我们可以断言当服务 A 产生有效载荷 1 时,最终服务 C 将产生有效载荷 3。
在这个食谱中,我们有一个作者应用和一个客户应用。每个应用都包含一个前端项目和 bff 项目。author-frontend
项目制作了 save-thing0
记录。这个记录被复制到 author-bff
项目,最终产生了 thing0-created
记录。然后,thing0-created
记录被复制到 customer-bff
项目,最终产生了 get-thing0
记录。get-thing0
记录随后被复制到 customer-frontend
项目以支持其测试。
最终结果是完全自主的测试套件集合。每次修改特定服务的测试套件时都会断言该服务的测试套件,无需在每个项目中重新运行测试套件。只有不兼容的更改需要依赖项目更新其测试用例和记录;因此,我们不再需要维护端到端测试环境。不再需要数据库脚本将数据库重置到已知状态,因为数据体现在测试用例和记录中。这些传递性的端到端测试构成了一个全面测试金字塔的尖端,增加了我们对我们持续部署管道中人为错误最小化的信心。
利用功能标志
将部署与发布解耦的实践基于使用功能标志。我们持续部署小批量的变更以减轻每次部署的风险。这些变更一直部署到生产环境,因此我们需要一个功能标志机制来禁用这些功能,直到我们准备好发布它们并使其普遍可用。我们还需要能够为用户子集,如测试用户和内部测试人员,启用这些功能。利用系统的自然功能标志,如权限和偏好,以最小化因在代码中添加自定义功能标志而产生的技术债务也是更好的选择。这个配方将向您展示如何利用 JWT 令牌中的声明来启用和禁用功能。
准备工作
在开始这个配方之前,您需要一个 AWS Cognito 用户池,例如在创建联合身份池配方中创建的那个。用户池应该定义以下两个组:Author
和BetaUser
。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch6/feature-flag --path cncb-feature-flag
-
使用
cd cncb-feature-flag
命令导航到cncb-feature-flag
目录。 -
检查名为
src/Authorize.js
的文件,如下所示:
...
const getGroups = props => get(props,
'auth.cognito.signInUserSession.idToken.payload.cognito:groups', '');
const check = (allowedRoles, props) => {
const groups = getGroups(props);
return intersection(groups, allowedRoles).length > 0;
};
const HasRole = allowedRoles =>
props => check(allowedRoles, props) ?
props.children :
null;
export const HasAuthorRole = HasRole(['Author']);
export const HasBetaUserRole = HasRole(['BetaUser']);
- 检查名为
src/Home.js
的文件,如下所示:
const Home = ({ auth }) => (
<div>
...
<HasAuthorRole>
This is the Author Feature!
</HasAuthorRole>
<HasBetaUserRole>
This is a New Beta Feature...
</HasBetaUserRole>
...
</div>
);
-
检查名为
src/App.js
的文件,并使用用户池堆栈的值更新clientId
和domain
字段。 -
使用
npm install
安装依赖项。 -
使用
npm start
命令在本地运行应用程序。 -
点击
Sign Up
链接并遵循指示。 -
点击
Sign Out
链接,然后为另一个用户Sign Up
。 -
在 AWS 用户池控制台中为每个用户分配不同的组——一个分配到
Author
组,另一个分配到BetaUser
组。 -
以每个用户身份
Sign In
,并注意屏幕对每个用户的渲染方式都不同。
它是如何工作的...
首先最重要的是,零停机部署必须被视为一级要求,必须在任务路线图和系统设计中予以考虑。如果增强功能只是在一个现有的域实体上添加一个新可选字段,那么根本不需要功能标志。如果只是删除一个字段,部署的顺序很重要,从最依赖到最不依赖。如果添加了一个全新的功能,则可以限制对整个功能的访问。最有趣的场景是当对现有且流行的功能进行重大修改时。如果变化很大,那么同时支持两个版本的功能可能是最好的选择,比如两个版本的页面——在这种情况下,场景本质上与全新的功能场景相同。如果没有必要添加新版本,那么应该注意不要使代码变得过于复杂。
在这个简化的 ReactJS 示例中,JWT 令牌在登录后通过auth
属性可访问。HasRole
组件的实例配置了allowedRoles
。该组件会检查 JWT 令牌在cognito:groups
字段中是否有匹配的组。如果找到匹配项,则渲染children
组件;否则,返回null
且不进行渲染。
第七章:优化可观察性
在本章中,将介绍以下配方:
-
监控云原生系统
-
实现自定义指标
-
监控域事件
-
创建警报
-
创建持续的合成事务测试
简介
信心对于最大限度地发挥我们精简和自主的云原生服务的潜力至关重要,因为信心危机会阻碍进步。利用完全管理的云服务和遵循云原生设计模式来创建自主服务显著增加了团队信心。将部署与发布解耦并将测试左移,以创建流线化的持续交付管道,进一步增加了团队信心。然而,这还不够。我们需要将测试右移,一直进入生产环境,这样我们就可以监控和提醒团队关于系统状态的信息。这使团队有信心,他们将在错误发生时及时获得信息,从而最大限度地减少平均恢复时间。本章中的配方演示了如何优化云原生服务的可观察性,对重要事项发出警报,并在生产环境中持续测试以增加团队信心。
监控云原生系统
利用完全管理的云服务是创建精简、云原生服务的关键,因为采用这种可丢弃的架构使自给自足的全栈团队能够基于云服务提供的基础快速自信地交付。团队信心进一步增加,因为这种基础附带良好的可观察性。本配方演示了如何使用云提供商无关的第三方监控服务利用云提供商指标。
准备工作
在开始此配方之前,您需要一个 Datadog 账户 (www.datadoghq.com
)。
如何做到...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch7/datadog-account --path cncb-datadog-account
-
使用
cd cncb-datadog-account
切换到cncb-datadog-account
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-datadog-account
...
resources:
Resources:
DatadogAWSIntegrationPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument: ${file(includes.yml):PolicyDocument}
DatadogAWSIntegrationRole:
Type: AWS::IAM::Role
Properties:
RoleName: DatadogAWSIntegrationRole
AssumeRolePolicyDocument:
Statement:
Effect: Allow
Principal:
AWS: arn:aws:iam::464622532012:root
Action: sts:AssumeRole
Condition:
StringEquals:
sts:ExternalId: <copy value from datadog aws integration dialog>
ManagedPolicyArns:
- Ref: DatadogAWSIntegrationPolicy
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
使用
npm run dp:lcl -- -s $MY_STAGE
部署堆栈。 -
登录到 Datadog 并转到 集成 页面,然后选择 AWS 集成磁贴。
-
选择角色委派,输入您的 AWS 账户 ID 并将 AWS 角色名称设置为
DatadogAWSIntegrationRole
。 -
复制 AWS 外部 ID 值并使用它来更新
serverless.yml
中的sts:ExternalId
。 -
将标签设置为
account:cncb
并按安装集成。 -
使用
npm run dp:lcl -- -s $MY_STAGE
更新堆栈。 -
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令多次调用示例函数:
$ sls invoke -f hello -r us-east-1 -s $MY_STAGE
-
查看预设的 Datadog Lambda 仪表板,在 Dashboards | Dashboard List | All Integrations | AWS Lambda。
-
完成本章内容后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
云服务提供商为他们的云服务收集有价值的指标。然而,他们不一定保留这些数据很长时间,并且对数据的切片和切块能力可能有限。因此,建议使用第三方监控服务来填补空白并提供更全面的监控能力。此外,在多语言云的使用情况下,云提供商无关的监控服务提供统一的监控体验。我选择的监控服务是 Datadog。这个配方展示了我们如何轻松快速地将 Datadog 连接到 AWS 账户并开始汇总指标,以增加我们云原生系统的可观察性。
要允许 Datadog 从 AWS 账户开始收集指标,我们必须授予它这样做的能力。正如 如何操作 部分所示,这需要在 AWS 端和 Datadog 端执行步骤。首先,我们部署一个堆栈来创建具有所有必要权限的 DatadogAWSIntegrationPolicy
,以及 DatadogAWSIntegrationRole
将 AWS 账户与 Datadog 的 AWS 账户连接起来。这一点很重要。Datadog 也在 AWS 中运行。这意味着我们可以使用 角色委托 来连接账户,而不是共享访问密钥。一旦创建了 DatadogAWSIntegrationRole
,我们就可以在 Datadog 端配置 AWS 集成,这需要角色的存在作为先决条件。Datadog 生成 ExternalId
,我们需要将其添加到 DatadogAWSIntegrationRole
中,作为假设角色的条件。一旦集成到位,Datadog 就会从您的 AWS 账户中的 CloudWatch 消费请求的指标,以便它们可以汇总成有意义的仪表板,保留用于历史分析,并监控以发出关于感兴趣条件的警报。
实现自定义指标
价值增加型云服务提供的指标,如 函数即服务,是一个很好的起点。团队只需这些指标就可以有信心地将云原生服务投入生产。然而,更多的可观察性几乎总是更好的。我们需要了解我们函数内部运作的细粒度详细信息。这个配方展示了如何收集额外的指标,例如冷启动、内存、CPU 利用率和 HTTP 资源的延迟。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch7/custom-metrics --path cncb-custom-metrics
-
使用
cd cncb-custom-metrics
命令导航到cncb-custom-metrics
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-custom-metrics
...
functions:
hello:
...
environment:
ACCOUNT_NAME: ${opt:account}
SERVERLESS_STAGE: ${opt:stage}
SERVERLESS_PROJECT: ${self:service}
MONITOR_ADVANCED: false
DEBUG: '*'
- 查看名为
handler.js
的文件,其内容如下:
const { monitor, count } = require('serverless-datadog-metrics');
const debug = require('debug')('handler');
module.exports.hello = monitor((request, context, callback) => {
debug('request: %j', request);
count('hello.count', 1);
const response = { ... };
callback(null, response);
});
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-custom-metrics@1.0.0 dp:lcl <path-to-your-workspace>/cncb-custom-metrics
> sls deploy -r us-east-1 --account cncb "-s" "john"
...
endpoints:
GET - https://h865txqjqj.execute-api.us-east-1.amazonaws.com/john/hello
-
在 AWS 控制台中查看堆栈和资源。
-
在以下命令中调用堆栈输出中显示的端点:
$ curl https://<APP-ID>.execute-api.us-east-1.amazonaws.com/$MY_STAGE/hello | json_pp
{
"message" : "JavaScript Cloud Native Development Cookbook! ..."
}
- 查看日志:
$ sls logs -f hello -r us-east-1 -s $MY_STAGE
MONITORING|1530339912|1|count|aws.lambda.coldstart.count|#account:cncb,...
MONITORING|1530339912|0.259|gauge|node.process.uptime|#account:cncb,...
MONITORING|1530339912|1|count|hello.count|#account:cncb,...
MONITORING|1530339912|0|check|aws.lambda.check|#account:cncb,...
MONITORING|1530339912|0.498...|gauge|node.mem.heap.utilization|#account:cncb,...
MONITORING|1530339912|1.740238|histogram|aws.lambda.handler|#account:cncb,...
-
执行服务几次后,然后在 Datadog 中的仪表板下查看 Lambda 控板:仪表板 | 控板列表 | 所有集成 | AWS Lambda。
-
在 Datadog 中探索自定义指标,使用图形探索:
hello.count
和aws.lambda.handler.avg
。 -
完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
将自定义指标添加到函数中的操作与传统监控不同。传统方法涉及在每个机器上添加代理来收集指标,并定期将数据发送到监控系统。但是,在 Function-as-a-service 中,我们没有机器可以部署代理。一种替代方法是简单地在每个函数调用的末尾发送收集到的指标。然而,这会给每个函数调用增加显著的延迟。Datadog 提供了一种基于结构化日志语句的独特替代方案。计数、仪表、直方图和检查只是按收集时的顺序记录,Datadog 自动从 CloudWatch 日志中消费这些语句。
serverless-datadog-metrics
库 (www.npmjs.com/package/serverless-datadog-metrics
) 简化了此方法的使用。我们只需用 monitor
函数包装处理函数,它将收集有用的指标,例如冷启动、错误、执行时间、内存和 CPU 利用率,以及 HTTP 资源的延迟。HTTP 指标非常有价值。所有对资源(如 DynamoDB、S3 和 Kinesis)的 HTTP 调用都会自动记录,这样我们就可以看到函数在其外部资源上花费了多少时间。
此库还导出低级函数,例如 count
、gauge
和 histogram
,以支持额外的自定义指标。环境变量,如 ACCOUNT_NAME
和 SERVERLESS_PROJECT
,用作仪表板和警报中过滤指标的分标签。
监控领域事件
在传统系统中,我们通常专注于观察同步请求的行为。然而,我们的云原生系统是高度异步和事件驱动的。因此,我们需要对系统中的领域事件流给予同等或更大的关注,以便我们可以确定这些流程何时偏离正常。此配方演示了如何收集领域事件指标。
准备工作
在开始此配方之前,您需要一个 AWS Kinesis Stream,例如在 创建事件流 配方中创建的流。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch7/event-metrics --path cncb-event-metrics
-
使用
cd cncb-event-metrics
命令进入cncb-event-metrics
目录。 -
查看名为
serverless.yml
的文件。 -
查看包含以下内容的
handler.js
文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.tap(count)
...
.collect().toCallback(cb);
};
const count = (uow) => {
const tags = [
`account:${process.env.ACCOUNT_NAME}`,
`region:${uow.record.awsRegion}`,
`stream:${uow.record.eventSourceARN.split('/')[1]}`,
`shard:${uow.record.eventID.split('-')[1].split(':')[0]}`,
`source:${uow.event.tags && uow.event.tags.source || 'not-specified'}`,
`type:${uow.event.type}`,
];
console.log(`MONITORING|${uow.event.timestamp}|1|count|domain.event|#${tags.join()}`);
};
...
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
npm run dp:lcl -- -s $MY_STAGE
-
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用
simulate
函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
[
{
"total": 4500,
"purple": 1151,
"green": 1132,
"blue": 1069,
"orange": 1148
}
]
- 查看日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
...
... MONITORING|1531020609200|1|count|domain.event|#account:cncb,...,type:purple
-
在 Datadog 的指标下探索事件指标,使用图形:
domain.event
,每个type
一个图形。 -
完成后删除堆栈:
npm run rm:lcl -- -s $MY_STAGE
它是如何工作的...
监控事件的工作方式与在数据湖中收集事件类似。单个流处理器观察所有流的所有事件,并简单地按事件type
计数域事件,以及额外的标签,如region
、stream
和source
。同样,这些计数被记录为结构化日志语句,Datadog 从 CloudWatch 日志中消费这些语句。在仪表板上绘制域事件指标可以提供对系统行为的深入了解。我们将在创建警报配方中看到如何对域事件流发出警报。我们还会对fault
事件进行特殊处理。对于这些事件,我们调用 Datadog 事件 API,该 API 提供发送附加上下文信息的功能,例如堆栈跟踪。我们将在第八章,设计失败中讨论fault
事件。
创建警报
为了最大限度地提高我们对云原生服务的信心,我们需要在最终用户之前就收到问题警报,以便我们能够快速响应并最小化平均恢复时间。这也意味着我们需要消除警报疲劳,并且只对真正重要的事情发出警报,否则重要的警报将淹没在噪音中。本配方演示了如何创建关键指标的警报。
如何做...
-
登录到您的 Datadog 账户。
-
使用以下设置创建一个 IAM 警报:
-
选择监控器 | 新监控器 | 事件
-
匹配来自
Amazon Cloudtrail
的aws-service:iam
事件 -
多警报—
aws_account
-
警报阈值—
above 0
-
标题—
aws.iam
-
包含触发标签—
true
-
通知—
yourself
-
-
使用以下设置创建一个年龄迭代器警报:
-
选择监控器 | 新监控器 | 集成 | AWS Lambda
-
指标—
aws.lambda.iterator_age
-
最大值按—
functionname
、region
和account
-
多警报—
functionname
、region
和account
-
警报阈值—
7200000
(2 小时) -
警告阈值—
1800000
(0.5 小时) -
标题—
aws.lambda.iterator_age
-
包含触发标签—
true
-
通知—
yourself
-
-
使用以下设置创建一个请求速率警报:
-
选择监控器 | 新监控器 | 异常
-
指标—
aws.apigateway.count
-
平均值按—
apiname
、region
和account
作为rate
-
多警报—
apiname
、region
和account
-
警报条件—使用默认值并调整到您的数据
-
标题—
aws.apigateway.rate
-
包含触发标签—
true
-
通知—
yourself
-
-
使用以下设置创建一个域事件速率警报:
-
选择监控器 | 新监控器 | 异常
-
指标—
domain.event
-
平均值按—
type
、region
和account
作为rate
-
多警报—
type
、region
和account
-
警报条件—从默认值开始,并根据您的数据进行调整
-
标题—
domain.event.rate
-
包含触发标签—
true
-
通知—
yourself
-
-
使用以下设置创建一个故障事件警报:
-
选择监控器 | 新监控器 | 事件
-
匹配包含—
Fault Event
且状态为error
的事件,来自:My Apps
-
多警报—
functionname
、region
和account
-
警报阈值—
above 0
-
标题—
fault.event
-
包含触发标签—
true
-
通知—
yourself
-
Datadog 自动完成菜单是根据最近收集的指标填充的。
它是如何工作的...
现在我们系统是可观察的,我们需要用所有这些数据进行一些积极主动且有用的事情。手动处理的数据太多,而且如果我们能将这些数据转化为有价值、及时的信息,我们的信心就会增加。我们肯定会使用这些数据进行根本原因和事后分析,但我们的信心是通过关注平均恢复时间来增加的。因此,我们需要创建不断测试数据、将其转化为信息并在重要事项上发出警报的监控器。然而,我们必须小心避免警报疲劳。最佳实践是广泛发出警报,但明智地针对症状而不是原因进行页面。例如,我们应该创建许多仅记录阈值被跨越的监控器,以便在根本原因分析中使用这些附加信息。其他监控器将通过电子邮件向团队发出潜在问题的警告,但少数精选监控器将向团队发送页面,以便立即采取行动。
为了了解症状和原因之间的区别,我们将我们的指标分为工作指标和资源指标。工作指标代表系统对用户可见的输出。资源指标代表系统的内部工作。我们的资源监控器通常会记录并发送警告,而我们的工作监控器会向团队发送页面。RED 方法(dzone.com/articles/red-method-for-prometheus-3-key-metrics-for-micros
)和 USE 方法(dzone.com/articles/red-method-for-prometheus-3-key-metrics-for-micros
)进一步细分了这些类别。RED代表速率、错误和持续时间。当一个关键服务的请求数或事件数显著减少,或错误显著增加,或者延迟显著增加时,这可能需要向团队发送页面。USE代表利用率、饱和度和错误。当资源(如 DynamoDB 或 Kinesis)的利用率达到一定水平时,可能需要向团队发出警告。然而,饱和度和/或错误(如限流)可能只需要记录,因为它们可能会迅速减轻,或者如果持续存在,它们将触发工作监控器。
此配方演示了几种可能的监控器。故障
监控器代表失败的工作,必须解决。流迭代器年龄
监控器处于边缘,因为它可能代表临时资源饱和,也可能代表导致工作积压的错误。因此,它在不同的阈值处都有警告和警报。异常检测
监控器应关注工作指标,例如关键请求的速率或域事件。同时监控 CloudTrail 以检测任何 IAM 更改,例如角色和权限的更改也是一个好主意。
如果您只需要记录条件,则通知
步骤是可选的。为了警告团队,请将通知发送到聊天和/或群组电子邮件。为了唤醒团队,请将通知发送到 SNS 主题。最好使用 Multi Alert 功能,在相关标签上触发,并在通知标题中包含这些信息,以便一目了然地获取这些信息。
最终,为了有价值并避免疲劳,这些监控器需要得到关注和培养。这些监控器是您在生产中的测试。随着您的团队对系统的理解加深,您将发现更好的测试/监控器。当您的监控器产生误报时,它们需要调整或消除。您的信心水平是成功监控的真实衡量标准。
创建合成事务测试
如果森林里有一棵树倒下,没有人听到声音,它还会发出声音吗?或者更贴近话题,如果某个地区的部署出现故障,没有人醒来使用它,这会有所不同吗?当然,答案是肯定的。我们希望被通知关于损坏的部署,以便在正常流量开始之前修复它。为了启用此功能,我们需要通过系统连续泵送合成流量,以便有一个连续的信号进行测试。此配方演示了如何使用云提供商无关的第三方服务生成合成流量。
准备工作
在开始此配方之前,您需要一个 Pingdom 账户(www.pingdom.com
)。您还需要一个 AWS Cognito 用户池,例如在创建联合身份池配方中创建的那个。
如何做到这一点...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch7/synthetics --path cncb-synthetics
-
导航到
cncb-synthetics
目录cd cncb-synthetics
。 -
使用
npm install
安装依赖项。 -
检查名为
src/App.js
的文件,并使用用户池堆栈的值更新clientId
和domain
字段。 -
使用
npm run build
构建应用程序。 -
部署堆栈:
部署 CloudFront 分发可能需要 20 分钟以上。
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-synthetics@1.0.0 dp:lcl <path-to-your-workspace>/cncb-synthetics
> sls deploy -r us-east-1 --account cncb "-s" "john"
...
WebsiteDistributionURL: https://dqvo8ga8z7ao3.cloudfront.net
-
更新用户池堆栈的
callbackURLs
和logoutURLs
以包括WebsiteDistributionURL
,然后重新部署。 -
浏览堆栈输出中提供的
WebsiteDistributionURL
以测试网站配置。 -
登录到您的 Pingdom 账户。
-
使用以下设置创建一个可用性检查:
-
选择“经验监控”|“可用性”|“添加检查”。
-
名称—
cncb-synthetics
。 -
检查间隔—
1 分钟
-
URL—
WebsiteDistributionURL
(从您的部署输出中获取)
-
-
定期审查经验监控 | 服务器正常运行时间仪表板。
-
使用以下设置创建真实用户监控:
-
选择“经验监控 | 访客洞察(RUM) | 添加站点”。
-
名称—
cncb-synthetics
-
URL—
WebsiteDistributionURL
(从您的部署输出中获取)
-
-
审查名为
public/index.html
的文件,取消以下代码的注释,并将 ID 替换为生成的代码片段中的值:
<!--
<script src="img/strong>.js" async></script>
-->
- 构建和重新部署应用程序:
$ npm run build
$ npm run dp:lcl -- -s $MY_STAGE
-
定期审查经验监控 | 访客洞察(RUM)仪表板。
-
使用以下设置创建合成事务测试:
-
选择“经验监控 | 事务 | 添加检查”。
-
名称—
cncb-synthetics
-
测试间隔—
10 分钟
-
前往
WebsiteDistributionURL
URL -
在
username
字段中填写your-username
-
在
password
字段中填写your-password
-
点击“登录”
-
等待元素
.App-title
包含Welcome to React
-
-
定期审查经验监控 | 事务仪表板。
-
完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。 -
在 14 天试用期到期前,在“账户 | 订阅 | 管理计划”下取消 Pingdom 免费试用,以避免费用。
它是如何工作的...
生产环境中的测试与传统的前生产测试不同。正如在“创建警报”配方中所示,我们的生产测试是以监控的形式实现的,这些监控不断测试系统发出的信号。然而,如果没有流量,就没有信号,因此也就没有测试来提醒我们这些期间发生的任何部署中的问题。这反过来又降低了我们对这些部署的信心。解决方案是在没有自然流量时生成稳定的合成流量来填补空白。
服务器正常运行检查是最容易实施的,因为它们只进行单个请求。这些至少应该包括在内,因为它们可以快速实施且几乎不需要努力,并且它们具有最高的频率。真实用户监控(RUM)应该包括在内,因为它只需要简单的代码修改,并且云原生系统中大量的用户体验是由单页应用程序在浏览器中执行的。最后,需要实施一组小型但战略性的合成事务脚本,以持续测试关键功能。这些脚本类似于传统的测试脚本,但它们的重点是持续执行这些关键的成功路径,以确保关键功能不受新功能持续部署的影响。
第八章:为失败而设计
在本章中,将涵盖以下食谱:
-
使用适当的超时和重试
-
实现背压和速率限制
-
处理故障
-
重新提交故障事件
-
使用反向 OpLock 实现幂等性
-
使用事件溯源实现幂等性
简介
处理失败是云原生的基础。我们构建自主服务,当它们失败时限制爆炸半径,并在其他服务失败时继续运行。我们将部署与发布解耦,并控制每次部署的批量大小,以便在部署出错时容易识别问题。我们将测试左移到持续部署管道中,以在部署之前捕获问题,以及一直移到生产环境中,在那里我们持续测试系统并在异常情况下发出警报,以最小化恢复时间。本章中的食谱演示了如何设计一个在失败面前具有弹性和宽容性的服务,以便正确处理瞬态故障,最小化其影响,并使服务能够自我修复。
使用适当的超时和重试
计算机网络的现实是它们不可靠。云计算的现实是它依赖于计算机网络,因此我们必须实施服务来正确处理网络异常。这个食谱演示了如何正确配置函数和 SDK 调用,使用适当的超时和重试。
如何做...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/timeout-retry --path cncb-timeout-retry
-
使用
cd cncb-timeout-retry
切换到cncb-timeout-retry
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-timeout-retry
...
functions:
command:
handler: handler.command
timeout: 6
memorySize: 1024
...
- 查看名为
handler.js
的文件,其内容如下:
module.exports.command = (request, context, callback) => {
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
logger: console,
});
...
db.put(params, callback);
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
使用
npm run dp:lcl -- -s $MY_STAGE
部署堆栈。 -
查看 AWS 控制台中的堆栈和资源。
-
使用以下命令调用函数:
$ sls invoke -f command -r us-east-1 -s $MY_STAGE -d '{"name":"thing one"}'
- 查看以下命令函数日志:
$ sls logs -f command -r us-east-1 -s $MY_STAGE
2018-07-14 23:41:14.229 (-04:00) ... [AWS dynamodb 200 0.063s 0 retries] putItem({ TableName: 'john-cncb-timeout-retry-things',
Item:
{ id: { S: 'c22f3ce3-551a-4999-b750-a20e33c3d53b' },
name: { S: 'thing one' } } })
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
网络中断可能随时发生。一个请求可能无法通过,而下一个请求可能顺利通过;因此,我们应该设置较短的超时时间,以便我们能够尽快重试过程,但又不至于太短,以至于一个良好的请求在正常完成之前就超时了。我们还必须确保我们的超时和重试周期在函数超时之前有足够的时间完成。默认情况下,aws-sdk
配置为两分钟后超时,并执行三次带有递增延迟时间的重试。当然,两分钟太长了。将 timeout
设置为 1000(1 秒)通常足以让请求完成,并允许在 6 秒的函数超时之前完成三次重试。
如果请求频繁超时,这可能表明函数分配的资源过少。例如,函数分配的 memorySize
与使用的机器实例大小之间存在关联。较小的机器实例也有更少的网络 I/O 容量,这可能导致更频繁的网络中断。因此,增加 memorySize
将降低网络波动性。
实现背压和速率限制
云原生系统中的各种服务必须能够处理系统中的流量起伏。上游服务不应过载下游服务,下游服务必须能够处理峰值负载而不会落后或过载更下游的服务。本食谱展示了如何利用流处理器的自然背压并实现额外的速率限制来管理节流。
准备工作
在开始此食谱之前,您需要一个 AWS Kinesis Stream,例如在 创建事件流 食谱中创建的那个。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/backpressure-ratelimit --path cncb-backpressure-ratelimit
-
使用以下命令导航到
cncb-backpressure-ratelimit
目录:cd cncb-backpressure-ratelimit
。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-backpressure-ratelimit
...
functions:
listener:
handler: handler.listener
timeout: 240 # headroom for retries
events:
- stream:
batchSize: 1000 # / (timeout / 2) < write capacity
...
environment:
WRITE_CAPACITY_UNITS: 10
SHARD_COUNT: 1
...
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
Properties:
...
ProvisionedThroughput:
...
WriteCapacityUnits: ${self:functions.listener.environment.WRITE_CAPACITY_UNITS}
- 查看名为
handler.js
的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forPurple)
.ratelimit(Number(process.env.WRITE_CAPACITY) /
Number(process.env.SHARD_COUNT) / 10, 100)
.flatMap(put)
.collect()
.toCallback(cb);
};
const put = uow => {
const params = { ... };
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
// default values:
// maxRetries: 10,
// retryDelayOptions: {
// base: 50,
// },
logger: console,
});
return _(db.put(params).promise()
.then(() => uow)
);
};
-
使用以下命令安装依赖项:
npm install
。 -
使用以下命令运行测试:
npm test -- -s $MY_STAGE
。 -
查看在
.serverless
目录中生成的内容。 -
使用以下命令部署堆栈:
npm run dp:lcl -- -s $MY_STAGE
。 -
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用
simulate
函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
[
{
"total": 4775,
"blue": 1221,
"green": 1190,
"purple": 1202,
"orange": 1162
}
]
- 查看以下
trigger
函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count' 2018-07-15 22:53:29 ... event count: 1000
2018-07-15 22:54:00 ... event count: 1000
2018-07-15 22:54:33 ... event count: 1000
2018-07-15 22:55:05 ... event count: 425
2018-07-15 22:55:19 ... event count: 1000
2018-07-15 22:55:51 ... event count: 350$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration' REPORT ... Duration: 31011.59 ms ...
REPORT ... Duration: 33038.58 ms ...
REPORT ... Duration: 32399.91 ms ...
REPORT ... Duration: 13999.56 ms ...
REPORT ... Duration: 31670.86 ms ...
REPORT ... Duration: 12856.77 ms ...
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'retries' ...
2018-07-15 22:55:49 ... [AWS dynamodb 200 0.026s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '686dc03a-88a3-11e8-829c-67d049599dd2' },
type: { S: 'purple' },
timestamp: { N: '1531709604787' },
partitionKey: { S: '3' },
tags: { M: { region: { S: 'us-east-1' } } } },
ReturnConsumedCapacity: 'TOTAL' })
2018-07-15 22:55:50 ... [AWS dynamodb 200 0.013s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '686d4b14-88a3-11e8-829c-67d049599dd2' },
type: { S: 'purple' },
timestamp: { N: '1531709604784' },
partitionKey: { S: '4' },
tags: { M: { region: { S: 'us-east-1' } } } },
ReturnConsumedCapacity: 'TOTAL' })
...
- 完成后,使用以下命令删除堆栈:
npm run rm:lcl -- -s $MY_STAGE
。
它是如何工作的...
背压 是实现良好的流处理器的关键特性。当使用 命令式编程范式,例如在批处理记录数组中循环时,下游目标系统可能会轻易过载,因为循环会尽可能快地处理记录,而不考虑目标系统的吞吐量能力。另一方面,函数式响应式编程(FRP)范式,例如使用 Highland.js (highlandjs.org
) 或 RxJS (github.com/ReactiveX/rxjs
) 库,提供了自然的背压,因为数据只以下游步骤能够完成任务的速度被拉取。例如,吞吐量低的系统只会以其容量允许的速度快速拉取数据。
另一方面,像 DynamoDB 或 Kinesis 这样高度可扩展的系统,能够以极高的吞吐量处理数据,它们依赖于节流来限制容量。在这种情况下,FRP 的自然背压不足以应对;需要额外使用 ratelimit
功能。例如,当我编写这个菜谱时,我运行了一个没有速率限制的模拟,然后出去吃饭。当我几个小时后回来时,模拟生成的事件仍在尝试处理。这是因为 DynamoDB 节流了请求,而 aws-sdk
内置的指数退避和重试消耗了太多时间,导致函数 timeout
并重新尝试整个批量。这表明,虽然重试,如 Employing proper timeouts and retries 菜谱中讨论的那样,对于同步请求很重要,但不能仅依赖于异步流处理。相反,我们需要积极限制流量速率,以避免节流和指数退避,从而确保批量在函数超时内完成。
在这个菜谱中,我们使用一个简单的算法来计算流量速率——每 100
毫秒 WRITE_CAPACITY / SHARD_COUNT / 10
。这确保了 DynamoDB 每秒接收的请求数量不会超过已分配的数量。我还使用了一个简单的算法来确定批量大小——batchSize / (timeout / 2) < WRITE_CAPACITY
。这确保了在正常情况下应该有足够的时间来完成批量,但在有节流的情况下,将会有两倍于必要的时间。请注意,这只是一个逻辑起点;这是云原生系统中性能调优应该关注的领域。你的数据和目标系统的特性将决定最有效的设置。正如我们将在 Autoscaling DynamoDB 菜谱中看到的那样,自动扩展为背压、速率限制和性能调优增加了另一个维度。无论如何,你可以从这些简单且安全的算法开始,并在一段时间内调整它们。
处理故障
流处理器对瞬时错误具有天然的抗性和宽容性,因为它们使用背压并自动重试失败的批量。然而,如果处理不当,硬错误可能会导致交通堵塞,导致消息丢失。这个菜谱将向你展示如何将这些错误委派为故障事件,以便良好的消息可以继续流动。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/handling-faults --path cncb-handling-faults
-
使用
cd cncb-handling-faults
命令进入cncb-handling-faults
目录。 -
检查名为
serverless.yml
的文件。 -
检查包含以下内容的
handler.js
文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forThingCreated)
.tap(validate)
.tap(randomError)
.flatMap(save)
.errors(errors)
.collect().toCallback(cb);
};
const validate = uow => {
if (uow.event.thing.name === undefined) {
const err = new Error('Validation Error: name is required');
// handled
err.uow = uow;
throw err;
}
};
const randomError = () => {
if (Math.floor((Math.random() * 5) + 1) === 3) {
// unhandled
throw new Error('Random Error');
}
};
const save = uow => {
...
return _(db.put(uow.params).promise()
.catch(err => {
// handled
err.uow = uow;
throw err;
}));
};
const errors = (err, push) => {
if (err.uow) {
// handled exceptions are adorned with the uow in error
push(null, publish({
type: 'fault',
timestamp: Date.now(),
tags: {
functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
},
err: {
name: err.name,
message: err.message,
stack: err.stack,
},
uow: err.uow,
}));
} else {
// rethrow unhandled errors to stop processing
push(err);
}
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
使用
npm run dp:lcl -- -s $MY_STAGE
部署堆栈。 -
在 AWS 控制台中检查堆栈和资源。
-
使用以下命令调用
simulate
函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
- 查看以下
trigger
函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'publishing fault' ... {"type":"fault","timestamp":...,"tags":{"functionName":"cncb-handling-faults-john-listener"},"err":{"name":"Error","message":"Validation Error: name is required" ...
... {"type":"fault","timestamp":...,"tags":{"functionName":"cncb-handling-faults-john-listener"},"err":{"name":"ValidationException","message":"...Missing the key id..." ...
- 完成后,删除堆栈:
npm run rm:lcl -- -s $MY_STAGE
。
它是如何工作的...
这个食谱实现了我在我的书云原生开发模式和最佳实践(www.packtpub.com/application-development/cloud-native-development-patterns-and-best-practices
)中深入讨论的流断路器模式。遇到硬错误的流处理器将继续重新处理这些事件,直到它们从流中过期——除非流处理器被实现为将这些错误放在一边,将它们作为故障事件委托给其他地方处理,如重新提交故障事件食谱中所述。这缓解了交通堵塞,以便其他事件可以继续流动。
这个食谱模拟了在流处理器中不同阶段可能失败的事件。一些事件模拟了上游的 bug,这些 bug 将导致验证逻辑失败,该逻辑断言事件正在上游正确创建。其他事件在它们被插入到 DynamoDB 时将失败。逻辑还会随机失败一些事件来模拟不会产生故障的短暂错误,这些错误将自动重试。在日志中,您将看到发布了两条故障事件。当生成随机错误时,您将在日志中看到函数正在重试批次。如果模拟没有生成随机错误,那么您应该重新运行它,直到它生成错误。
为了隔离流中的事件,我们需要引入工作单元(UOW)的概念,它将一批中的一个或多个事件组合成一个原子单元,这个单元必须一起成功或失败。UOW 包含原始的 Kinesis 记录(uow.record
)、从记录中解析的事件(uow.event
)以及任何附加到 UOW 的中间处理结果(uow.params
),这些结果在它通过流处理器时被附加。UOW 还用于标识错误是否被处理或未处理。当验证逻辑识别出预期的错误或从外部调用中捕获到错误时,UOW 将被装饰到错误上,并且错误将被重新抛出。装饰的工作单元(error.uow
)作为指示器,告知errors
处理器错误已在流处理器中被处理,并且应该发布
错误作为故障事件。意外的错误,如随机生成的错误,不会被流处理器逻辑处理,因此不会有装饰的 UOW。错误处理器将推送
这些错误到下游,导致函数失败并重试。在创建警报食谱中,我们讨论了对故障事件以及迭代器年龄的监控,以便团队可以及时收到关于流处理器错误的通知。
重新提交故障事件
我们已经设计我们的流处理器将错误作为故障事件进行委派,以便有效的事件可以继续流动。我们监控并警告故障事件,以便可以采取适当的行动来解决问题。一旦问题得到解决,可能需要重新处理失败的事件。本食谱演示了如何将故障事件重新提交给引发故障的流处理器。
如何操作...
- 从以下模板创建
monitor
、simulator
和cli
项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/resubmitting-faults/monitor --path cncb-resubmitting-faults-monitor
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/resubmitting-faults/cli --path cncb-resubmitting-faults-cli
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/resubmitting-faults/simulator --path cncb-resubmitting-faults-simulator
-
检查
cncb-resubmitting-faults-monitor
和cncb-resubmitting-faults-simulator
目录中的serverless.yml
文件。 -
按照以下方式部署
monitor
和simulator
堆栈:
$ cd ../cncb-resubmitting-faults-monitor
$ npm install
$ npm test -- -s $MY_STAGE
$ npm run dp:lcl -- -s $MY_STAGE
Stack Outputs
BucketName: cncb-resubmitting-faults-monitor-john-bucket-1llq835xdczd8
$ cd ../cncb-resubmitting-faults-simulator
$ npm install
$ npm test -- -s $MY_STAGE
$ npm run dp:lcl -- -s $MY_STAGE
-
在 AWS 控制台中检查堆栈和资源。
-
按照以下方式从
cncb-resubmitting-faults-simulator
目录运行模拟器:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
-
确认故障文件已写入监控堆栈输出中指定的存储桶
cncb-resubmitting-faults-monitor-*-bucket-*
并注意路径。如果没有生成故障,请再次运行模拟器,直到生成故障。 -
检查名为
./cli/lib/resubmit.js
的文件,其内容如下:
exports.command = 'resubmit [bucket] [prefix]'
exports.desc = 'Resubmit the faults in [bucket] for [prefix]'
...
const invoke = (lambda, options, event) => {
const Payload = JSON.stringify({
Records: [event.uow.record],
});
const params = {
FunctionName: event.tags.functionName,
...
Payload: Buffer.from(Payload),
};
return _(lambda.invoke(params).promise());
}
- 使用以下命令重新提交故障:
$ cd ../cncb-resubmitting-faults-cli
$ npm install
$ node index.js resubmit -b cncb-resubmitting-faults-monitor-$MY_STAGE-bucket-<suffix> -p <s3-path> --dry false
- 手动清空
cncb-resubmitting-faults-monitor-*
存储桶,并在完成npm run rm:lcl -- -s $MY_STAGE
后移除monitor
和simulator
堆栈。
它是如何工作的...
在 处理故障 食谱中,我们看到了流处理器如何通过发布包含失败工作单元所有相关数据的故障事件来委派硬错误。在本食谱中,故障监控器消费这些故障事件并将它们存储在 S3 存储桶中。这使得团队能够审查特定的故障,以帮助确定问题的根本原因。故障包含捕获的具体异常、失败的事件以及附加到工作单元的所有上下文信息。
一旦解决了根本原因,原始事件可以提交回发布故障的流处理器。这是可能的,因为故障事件包含原始 Kinesis 记录 (event.uow.record
) 和要调用的函数名称 (event.tags.functionName
)。命令行实用程序从指定路径的存储桶中读取所有故障事件并调用特定函数。从函数逻辑的角度来看,这种直接调用函数与直接从 Kinesis 流调用没有区别。然而,流处理器必须设计为幂等的,并且能够处理乱序事件,正如我们将在 使用逆 OpLock 实现幂等性 和 使用事件溯源实现幂等性 食谱中讨论的那样。
使用逆 OpLock 实现幂等性
从业务规则的角度来看,事件必须恰好处理一次非常重要;否则,可能会出现问题,例如重复计数或根本不计数。然而,我们的云原生系统必须能够抵御故障并主动重试以确保不丢失任何消息。不幸的是,这意味着消息可能会被多次投递,例如当生产者重新发布事件或流处理器重试可能已部分处理的批次时。解决这个问题的方法是实现所有操作都具有幂等性。这个配方通过我所说的逆 OpLock 来实现幂等性。
如何做到这一点...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/idempotence-inverse-oplock --path cncb-idempotence-inverse-oplock
-
使用
cd cncb-idempotence-inverse-oplock
命令导航到cncb-idempotence-inverse-oplock
目录。 -
检查名为
serverless.yml
的文件。 -
检查名为
handler.js
的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forThingSaved)
.flatMap(save)
.collect().toCallback(cb);
};
const save = uow => {
const params = {
TableName: process.env.TABLE_NAME,
Item: {
...uow.event.thing,
oplock: uow.event.timestamp,
},
ConditionExpression: 'attribute_not_exists(#oplock) OR #oplock < :timestamp',
...
};
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
logger: console,
});
return _(db.put(params).promise()
.catch(handleConditionalCheckFailedException)
.then(() => uow)
);
}
const handleConditionalCheckFailedException = (err) => {
if (err.code !== 'ConditionalCheckFailedException') {
err.uow = uow;
throw err;
}
};
-
使用
npm install
命令安装依赖项。 -
使用
npm test -- -s $MY_STAGE
命令运行测试。 -
检查
.serverless
目录中生成的内容。 -
使用
npm run dp:lcl -- -s $MY_STAGE
命令部署栈。 -
在 AWS 控制台中检查栈和资源。
-
使用以下命令调用模拟函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
- 查看以下
listener
函数的日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
... [AWS dynamodb 200 0.098s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '3022eaeb-45b7-46d5-b0a1-696c0eb9aa25' },
oplock: { N: '1531628180237' } },
...
... [AWS dynamodb 400 0.026s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '3022eaeb-45b7-46d5-b0a1-696c0eb9aa25' },
oplock: { N: '1531628180237' } },
...
... { ConditionalCheckFailedException: The conditionalrequest failed
at Request.extractError ...
...
message: 'The conditional request failed',
code: 'ConditionalCheckFailedException',
time: 2018-07-15T04:16:21.202Z,
requestId: '36BB14IGTPGM8DE8CFQJS0ME3VVV4KQNSO5AEMVJF66Q9ASUAAJG',
statusCode: 400,
retryable: false,
retryDelay: 20.791698133966396 }
... [AWS dynamodb 200 0.025s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '3022eaeb-45b7-46d5-b0a1-696c0eb9aa25' },
oplock: { N: '1531628181237' } },
...
... [AWS dynamodb 400 0.038s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '3022eaeb-45b7-46d5-b0a1-696c0eb9aa25' },
oplock: { N: '1531628180237' } },
...
... { ConditionalCheckFailedException: The conditionalrequest failed
at Request.extractError ...
...
message: 'The conditional request failed',
...
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
命令删除栈。
它是如何工作的...
传统的乐观锁定防止多个用户同时更新相同的记录。只有当用户检索数据后oplock
字段没有变化时,记录才会被更新。如果数据已更改,则会抛出异常,并强制用户在继续更新之前重新检索数据。这迫使更新按顺序执行,并需要人工交互来解决任何潜在冲突。
逆 OpLock旨在为异步处理提供幂等性。我们不是强制事务重试,而是简单地做相反的事情——我们丢弃较旧或重复的事件。在上游 Backend For Frontend 服务中,可以使用传统的 OpLock 来序列化用户事务,其中下游服务实现逆 OpLock 以确保较旧或重复的事件不会覆盖最新的数据。在这个配方中,我们使用uow.event.timestamp
作为oplock
值。在某些场景中,如果多个事件在确切的同一毫秒发生,可能更倾向于使用序列号。ConditionalCheckFailedException
被捕获并忽略。所有其他异常都会重新抛出,并附加工作单元以发布故障事件,如处理故障配方中所述。
这个配方中的模拟会发布一个thing-created
事件,再次发布它,然后发布一个thing-updated
事件,最后第三次发布thing-created
事件。日志显示thing-created
事件只被处理一次,重复的则被忽略。
使用事件溯源实现幂等性
从业务规则的角度来看,事件必须被精确处理一次非常重要;否则,可能会出现问题,例如重复计数或根本不计数。然而,我们的云原生系统必须能够抵御故障并主动重试以确保不丢失任何消息。不幸的是,这意味着消息可能会被多次投递,例如当生产者重新发布事件或流处理器重试可能已部分处理的事务批次时。解决这个问题的方法是实现所有操作的可幂等性。本菜谱演示了如何使用事件溯源和微事件存储来实现幂等性。
如何做到这一点...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/idempotence-es --path cncb-idempotence-es
-
使用
cd cncb-idempotence-es
命令导航到cncb-idempotence-es
目录。 -
查看名为
serverless.yml
的文件。 -
查看以下内容的
handler.js
文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forThingSaved)
.flatMap(saveEvent)
.collect().toCallback(cb);
};
const saveEvent = uow => {
const params = {
TableName: process.env.EVENTS_TABLE_NAME,
Item: {
id: uow.event.thing.id,
sequence: uow.event.id,
event: uow.event,
}
};
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
logger: console,
});
return _(db.put(params).promise()
.then(() => uow)
);
}
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
使用
npm run dp:lcl -- -s $MY_STAGE
部署堆栈。 -
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用模拟函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
- 查看以下
trigger
函数日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
... record: { ... "Keys":{"sequence":{"S":"3fdb8c10-87ea-11e8-9cf5-0b6c5b83bdcb"},"id":{"S":"8c083ef9-d180-48b8-a773-db0f61815f38"}}, ...
... record: { ... "Keys":{"sequence":{"S":"3fdb8c11-87ea-11e8-9cf5-0b6c5b83bdcb"},"id":{"S":"8c083ef9-d180-48b8-a773-db0f61815f38"}}, ...
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
事件溯源通过事件不可变来促进幂等性。具有相同唯一 ID 的相同事件可以被发布或处理多次,并产生相同的结果。微事件存储充当缓冲区,以过滤掉重复项。服务消费所需事件,并将它们存储在具有 hashkey 的微事件存储中,该 hashkey 用于分组相关事件,例如领域对象的 uow.event.thing.id
,以及基于 uow.event.id
的范围键。这个主键也是不可变的。因此,相同的事件可以保存多次,但在数据库流上只产生一个事件。因此,在 创建微事件存储 或 实现分析 BFF 菜谱中讨论的业务逻辑只会触发一次。
本菜谱中的模拟发布了一个 thing-created
事件,再次发布它,然后发布一个 thing-updated
事件,最后第三次发布 thing-created
事件。日志显示,三个 thing-created
事件实例只在 DynamoDB 流上产生一个事件。
第九章:优化性能
在本章中,将涵盖以下食谱:
-
调优函数即服务
-
批量请求
-
利用异步非阻塞 IO
-
在流处理器中分组事件
-
自动扩展 DynamoDB
-
使用缓存控制
-
利用会话一致性
简介
云原生将性能测试、调优和优化颠倒过来。许多利用的完全管理的无服务器云服务具有隐式可伸缩性。这些服务按请求购买,并将自动扩展以满足峰值和意外的需求。对于这些资源,进行前期性能测试的必要性要小得多;相反,我们根据在生产环境中进行的持续测试收集到的信息进行优化,如第七章优化可观察性中所述,并基于收集到的信息持续调优。我们还利用持续部署来推动必要的改进。这种基于价值的开发方法有助于确保我们专注于最高价值的改进。
尽管如此,还有一些资源,如某些流处理器和数据存储,严重依赖于显式定义的批量大小和读写容量。对于关键服务,这些资源必须得到充分分配,以确保峰值数据处理量不会使其过载。因此,本章的食谱将专注于在设计和发展过程中值得提前应用的性能优化技术,以帮助确保服务不会相互对抗。
调优函数即服务
调优函数与传统服务调优非常不同,因为可调整的旋钮很少。函数的短生命周期也带来了许多影响,使得传统的技术和框架成为反模式。以下食谱解释了一个常见的内存错误,讨论了传统语言和库选择对冷启动的影响,并展示如何使用webpack打包 JavaScript 函数以最小化下载时间。
如何做...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/tuning-faas --path cncb-tuning-faas
-
导航到
cncb-tuning-faas
目录,cd cncb-tuning-faas
。 -
查看以下内容的
serverless.yml
文件:
service: cncb-tuning-faas
plugins:
- serverless-webpack
provider:
memorySize: 1024 # default
...
package:
individually: true
custom:
webpack:
includeModules: true
functions:
save:
memorySize: 512 # function specific
...
- 查看以下内容的
webpack.config.js
文件:
const slsw = require('serverless-webpack');
const nodeExternals = require("webpack-node-externals");
const path = require('path');
module.exports = {
entry: slsw.lib.entries,
output: {
libraryTarget: 'commonjs',
path: path.join(__dirname, '.webpack'),
filename: '[name].js'
},
optimization: {
minimize: false
},
target: 'node',
mode: slsw.lib.webpack.isLocal ? "development" : "production",
externals: [
nodeExternals()
],
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
}
],
include: __dirname,
exclude: /node_modules/
}
]
}
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看
.serverless
目录中生成的内容,注意每个函数的 ZIP 文件,然后解压每个文件以查看其内容。 -
部署堆栈,
npm run dp:lcl -- -s $MY_STAGE
。 -
在 AWS 控制台中查看堆栈和资源,并在 Lambda 控制台中注意代码大小。
-
最后,一旦完成
npm run rm:lcl -- -s $MY_STAGE
,请删除堆栈。
它是如何工作的...
我们调整函数即服务(FaaS)的最明显旋钮是memorySize
分配。这个设置也驱动着价格计算。不幸的是,价格的相关性往往具有反直觉性,可能会导致增加成本的同时降低性能的决策。AWS Lambda 的价格机制是这样的:如果你将内存分配加倍,但相应地将执行时间减半,价格保持不变。由此推论,如果你将内存分配减半,相应地将执行时间加倍,你将花费相同数量的钱,但性能却更低。这是因为内存分配实际上与函数执行的机器实例大小相关。更多的内存分配意味着函数也将拥有更快的 CPU 和更多的网络 IO 吞吐量。简而言之,不要在内存分配上节省。
函数即服务的主要担忧和混淆来源是冷启动时间。对于异步函数,例如处理 Kinesis 流,冷启动时间并不那么令人担忧。这是因为冷启动频率较低,因为函数通常在每个分片上重复使用数小时。然而,最小化 API 网关后面同步函数的冷启动时间非常重要,因为需要启动多个函数来适应并发负载。因此,冷启动时间可能会影响最终用户的使用体验。
影响冷启动时间的第一个因素是必须下载到容器中的函数包的大小。这也是为什么分配更多内存和更多的网络 IO 吞吐量可以提高函数性能的一个原因。同时,最小化必须下载的包的大小也很重要。我们将在稍后讨论如何使用 webpack 来优化 JavaScript 函数。
影响冷启动时间的下一个因素是语言或运行时的选择。脚本语言,如 JavaScript 和 Python,在启动时做的工作很少,因此对冷启动时间的影响很小。相反,Java 必须在启动时执行工作来加载类和准备 JVM。随着类数量的增加,对冷启动的影响也增加。这导致下一个影响冷启动时间的因素:库和框架的选择,例如对象关系映射(ORM)和依赖注入框架,以及连接池库。这些库和框架通常在启动时做大量工作,因为它们被设计成在长时间运行的服务器上工作。
在使用 Java 编写的函数中,FaaS 开发者面临的一个常见问题是提高 Spring 和 Hibernate 函数的冷启动时间;然而,这些工具最初并不是为 FaaS 设计的。我使用 Java 编程已经超过 20 年,从它在 1990 年代初出现时开始。起初我对转向 JavaScript 表示怀疑,但这个食谱证明了它与云原生和无服务器架构的契合度。然而,值得注意的是,多语言编程是最好的策略;为特定服务使用合适的编程语言,但了解在使用 Faas 时其带来的影响。
为了最小化 JavaScript 函数的包大小,我们利用 webpack,原因与我们在浏览器中用于最小化下载的原因相同。Webpack 执行摇树优化,移除未使用的代码以减小包大小。在 serverless.yml
文件中,我们包括 serverless-webpack
插件并配置它以单独打包函数。单独打包函数使我们能够最大化摇树优化的好处。webpack.config.js
文件进一步控制打包过程。serverless-webpack
插件提供了 slsw.lib.entries
工具,这样我们就不需要重复函数名称来定义所有的 entry
点。我们还关闭了 minimize
功能,这会使代码变得难以阅读。我们这样做是为了避免包含源映射进行调试,这会显著增加包大小。我们还排除了 node_modules
文件夹中的所有外部库,并配置插件为 includeModules
,这包括那些实际用作运行时的库。一个特殊的例外是 aws-sdk
模块,因为它已经在函数容器中可用,所以永远不会被包含。最终结果是包含仅必要内容的精简函数包。
批处理请求
流处理器的设计必须考虑到它将接收的数据量。数据应实时处理,处理器不应落后。以下菜谱演示了如何使用 DynamoDB 批写入来确保足够的吞吐量。
准备工作
在开始此菜谱之前,您需要一个 AWS Kinesis Stream,例如在 创建事件流 菜谱中创建的那个。
如何做到这一点...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/frp-batching --path cncb-frp-batching
-
导航到
cncb-frp-batching
目录,cd cncb-frp-batching
。 -
查看名为
serverless.yml
的文件。 -
查看名为
handler.js
的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forPurple)
.ratelimit(Number(process.env.WRITE_CAPACITY) /
Number(process.env.SHARD_COUNT) /
Number(process.env.WRITE_BATCH_SIZE) / 10, 100)
.batch(Number(process.env.WRITE_BATCH_SIZE))
.map(batchUow)
.flatMap(batchWrite)
.collect().toCallback(cb);
};
const batchUow = batch => ({ batch });
const batchWrite = batchUow => {
batchUow.params = {
RequestItems: {
[process.env.TABLE_NAME]: batchUow.batch.map(uow =>
({
PutRequest: {
Item: uow.event
}
})
)
},
};
...
return _(db.batchWrite(batchUow.params).promise()
.then(data => (
Object.keys(data.UnprocessedItems).length > 0 ?
Promise.reject(data) :
batchUow
))
.catch(err => {
err.uow = batchUow;
throw err;
})
);
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
使用以下命令部署堆栈,
npm run dp:lcl -- -s $MY_STAGE
。 -
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用
simulate
函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
[
{
"total": 3850,
"orange": 942,
"purple": 952,
"blue": 1008,
"green": 948
}
]
- 查看以下
listener
函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count' 2018-08-04 23:46:53 ... event count: 100
2018-08-04 23:46:57 ... event count: 1000
2018-08-04 23:47:23 ... event count: 250
2018-08-04 23:47:30 ... event count: 1000
2018-08-04 23:47:54 ... event count: 1000
2018-08-04 23:48:18 ... event count: 500$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration' REPORT ... Duration: 3688.65 ms ...
REPORT ... Duration: 25869.08 ms ...
REPORT ... Duration: 7293.39 ms ...
REPORT ... Duration: 23662.65 ms ...
REPORT ... Duration: 24752.11 ms ...
REPORT ... Duration: 13983.72 ms ...
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'retries' ...
2018-08-04 23:48:20 ... [AWS dynamodb 200 0.031s 0 retries] batchWriteItem({ RequestItems:
{ 'john-cncb-frp-batching-things':
[ { PutRequest:
{ Item:
{ id: { S: '320e6023-9862-11e8-b0f6-01e9feb460f5' },
type: { S: 'purple' },
timestamp: { N: '1533440814882' },
partitionKey: { S: '5' },
tags: { M: { region: { S: 'us-east-1' } } } } } },
{ PutRequest:
{ Item:
{ id: { S: '320e6025-9862-11e8-b0f6-01e9feb460f5' },
type: { S: 'purple' },
timestamp: { N: '1533440814882' },
partitionKey: { S: '1' },
tags: { M: { region: { S: 'us-east-1' } } } } } },
...
[length]: 10 ] },
ReturnConsumedCapacity: 'TOTAL',
ReturnItemCollectionMetrics: 'SIZE' })
...
- 最后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
如果流处理器接收一批 1,000 个事件,它是否需要向数据库发出 1,000 次请求或只需 100 次请求才能执行得更快?答案当然取决于许多变量,但一般来说,通过网络进行更少的调用更好,因为它最小化了网络延迟的影响。为此,像 DynamoDB 和 Elasticsearch 这样的服务提供了 API,允许在单个请求中提交命令批处理。在这个配方中,我们使用 DynamoDB 的batchWrite
操作。为了准备一个批处理,我们只需在管道中添加一个batch
步骤并指定WRITE_BATCH_SIZE
。这种性能提升非常简单添加,但重要的是要记住,批处理请求会增加 DynamoDB 写入容量消耗的速度。因此,有必要在ratelimit
计算中包含WRITE_BATCH_SIZE
并相应地增加写入容量,正如在实现背压和速率限制配方中讨论的那样。
另一个需要注意的重要事项是,这些批处理请求不被视为单个事务。某些命令可能在单个请求中成功,而其他命令可能失败;因此,有必要检查响应中的UnprocessedItems
,这些项目需要重新提交。在这个配方中,我们将每个批处理视为一个工作单元(uow)并引发整个批次的故障,正如在处理故障配方中讨论的那样。在调整逻辑以仅重试失败的命令之前,这是一个良好的、安全的地方。请注意,最终,你只有在尝试了最大重试次数后才会引发故障。
利用异步非阻塞 IO
流处理器的设计必须考虑到它将接收的数据量。数据应实时处理,处理器不应落后。以下配方演示了如何利用异步、非阻塞 IO 并行处理数据,以确保足够的吞吐量。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/frp-async-non-blocking-io --path cncb-frp-async-non-blocking-io
-
导航到
cncb-frp-async-non-blocking-io
目录,cd cncb-frp-async-non-blocking-io
。 -
检查名为
serverless.yml
的文件。 -
检查以下内容的
handler.js
文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forPurple)
.ratelimit(Number(process.env.WRITE_CAPACITY) /
Number(process.env.SHARD_COUNT) /
Number(process.env.PARALLEL) / 10, 100)
.map(put)
.parallel(Number(process.env.PARALLEL))
.collect().toCallback(cb);
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
部署堆栈
npm run dp:lcl -- -s $MY_STAGE
。 -
在 AWS 控制台中检查堆栈和资源。
-
使用以下命令调用
simulate
函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
[
{
"total": 4675,
"blue": 1136,
"green": 1201,
"purple": 1167,
"orange": 1171
}
]
- 查看以下
listener
函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count' 2018-08-05 00:03:05 ... event count: 1675
2018-08-05 00:03:46 ... event count: 1751
2018-08-05 00:04:34 ... event count: 1249$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration' REPORT ... Duration: 41104.28 ms ...
REPORT ... Duration: 48312.47 ms ...
REPORT ... Duration: 31450.13 ms ...
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'retries' ...
2018-08-05 00:04:58.034 ... [AWS dynamodb 200 0.024s 0 retries] ...
2018-08-05 00:04:58.136 ... [AWS dynamodb 200 0.022s 0 retries] ...
2018-08-05 00:04:58.254 ... [AWS dynamodb 200 0.034s 0 retries] ...
2018-08-05 00:04:58.329 ... [AWS dynamodb 200 0.007s 0 retries] ...
2018-08-05 00:04:58.430 ... [AWS dynamodb 200 0.007s 0 retries] ...
2018-08-05 00:04:58.540 ... [AWS dynamodb 200 0.015s 0 retries] ...
2018-08-05 00:04:58.661 ... [AWS dynamodb 200 0.035s 0 retries] ...
2018-08-05 00:04:58.744 ... [AWS dynamodb 200 0.016s 0 retries] ...
2018-08-05 00:04:58.843 ... [AWS dynamodb 200 0.014s 0 retries] ...
2018-08-05 00:04:58.953 ... [AWS dynamodb 200 0.023s 0 retries] ...
...
- 最后,一旦你完成了
npm run rm:lcl -- -s $MY_STAGE
,请移除堆栈。
它是如何工作的...
异步非阻塞 I/O 对于最大化吞吐量至关重要。没有它,流处理器将阻塞并无所事事,直到外部调用完成。这个配方演示了如何使用 parallel
步骤来控制可以执行的并发调用数量。作为一个例子,我曾经有一个从 S3 读取的脚本,处理时间超过一个小时,但一旦我添加了一个设置为 16 的 parallel
步骤,脚本只用了五分钟就执行完毕。改进如此显著,Datadog 几乎立即联系我,看看我们是否有失控的进程。
为了允许并发调用,我们只需在外部调用步骤之后将 parallel
步骤添加到管道中,并指定 PARALLEL
数量。这种性能改进非常简单添加,但重要的是要记住,并行请求会增加 DynamoDB 写入容量消耗的速度。因此,有必要在 ratelimit
计算中包含 PARALLEL
数量,并相应地增加写入容量,正如在 实现背压和速率限制 配方中讨论的那样。通过结合并行执行、分组和批量,可以实现进一步的性能改进。
流处理器中的事件分组
流处理器的设计必须考虑到它将接收的数据量。数据应实时处理,处理器不应落后。以下配方演示了如何通过在流中将相关数据分组来确保足够的吞吐量。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/frp-grouping --path cncb-frp-grouping
-
导航到
cncb-frp-grouping
目录,cd cncb-frp-grouping
。 -
查看名为
serverless.yml
的文件。 -
查看以下内容的
handler.js
文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forPurple)
.group(uow => uow.event.partitionKey)
.flatMap(groupUow)
.ratelimit(Number(process.env.WRITE_CAPACITY) /
Number(process.env.SHARD_COUNT) / 10, 100)
.flatMap(put)
.collect().toCallback(cb);
};
const groupUow = groups => _(Object.keys(groups).map(key => ({ batch: groups[key]})));
const put = groupUow => {
const params = {
TableName: process.env.TABLE_NAME,
Item: groupUow.batch[groupUow.batch.length - 1].event, // last
};
...
return _(db.put(params).promise()
.then(() => groupUow)
);
};
-
使用
npm install
安装依赖。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈
npm run dp:lcl -- -s $MY_STAGE
。 -
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用
simulate
函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
[
{
"total": 4500,
"blue": 1134,
"green": 1114,
"purple": 1144,
"orange": 1108
}
]
- 查看以下
listener
函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count' 2018-08-05 00:28:19 ... event count: 1000
2018-08-05 00:28:20 ... event count: 1000
2018-08-05 00:28:21 ... event count: 650
2018-08-05 00:28:22 ... event count: 1000
2018-08-05 00:28:23 ... event count: 850$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration' REPORT ... Duration: 759.50 ms ...
REPORT ... Duration: 611.70 ms ...
REPORT ... Duration: 629.91 ms ...
REPORT ... Duration: 612.90 ms ...
REPORT ... Duration: 623.11 ms ...
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'retries' 2018-08-05 00:28:20.197 ... [AWS dynamodb 200 0.112s 0 retries] ...
2018-08-05 00:28:20.320 ... [AWS dynamodb 200 0.018s 0 retries] ...
...
2018-08-05 00:28:23.537 ... [AWS dynamodb 200 0.008s 0 retries] ...
2018-08-05 00:28:23.657 ... [AWS dynamodb 200 0.019s 0 retries] ...
- 最后,一旦完成,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
批量请求 配方演示了如何通过将多个不相关的命令批量到一个请求中,以最小化网络 I/O 的开销。另一种减少网络 I/O 的方法是简单地减少需要执行的命令数量,通过分组相关事件,并且每组只执行一个命令。例如,我们可能对每个组进行计算,或者只是采样一些数据。在这个配方中,我们根据 partitionKey
对事件进行分组。我们可以根据事件中的任何数据进行分组,但最佳结果是在分组相对于分区键时实现;这是因为分区键确保相关事件被发送到同一个分片。
group
步骤使得根据事件的内容将相关事件归组变得简单。对于更复杂的逻辑,可以直接使用 reduce
步骤。接下来,我们将每个组映射到一个必须成功或失败一起的工作单元(groupUow
),正如在“处理故障”配方中讨论的那样。最后,如前例所示,我们写入每个组的最后一个事件。从日志中注意,分组导致写入次数显著减少;对于这次特定的运行,模拟了 4,500 个事件,但只有 25 次写入。通过结合分组、批处理和并行调用,可以进一步提高性能。
自动扩展 DynamoDB
云原生,使用 FaaS 和无服务器架构,可以最小化支持服务层的基础设施扩展所需的努力。然而,我们现在需要关注调整流处理器并最小化对目标数据存储的任何限制。以下配方演示了如何使用 DynamoDB 自动扩展来确保分配足够的容量以提供足够的吞吐量并避免限制。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/dynamodb-autoscaling --path cncb-dynamodb-autoscaling
-
导航到
cncb-dynamodb-autoscaling
目录,cd cncb-dynamodb-autoscaling
。 -
使用以下内容审查名为
serverless.yml
的文件:
service: cncb-dynamodb-autoscaling
...
plugins:
- serverless-dynamodb-autoscaling-plugin
custom:
autoscaling:
- table: Table
write:
minimum: 5
maximum: 50
usage: 0.6
actions:
- name: morning
minimum: 5
maximum: 50
schedule: cron(0 6 * * ? *)
- name: night
minimum: 1
maximum: 1
schedule: cron(0 0 * * ? *)
read:
...
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
...
-
审查名为
handler.js
的文件。 -
使用以下命令安装依赖项:
npm install
。 -
使用以下命令运行测试:
npm test -- -s $MY_STAGE
。 -
审查
.serverless
目录中生成的内容。 -
使用以下命令部署堆栈:
npm run dp:lcl -- -s $MY_STAGE
。 -
在 AWS 控制台中审查堆栈和资源。
-
使用以下命令多次调用
simulate
函数以触发自动扩展:sls invoke -f simulate -r us-east-1 -s $MY_STAGE
。 -
使用以下命令查看以下“监听器”函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"1 retries"'$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"2 retries"'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"3 retries"'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"4 retries"'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"5 retries"'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"6 retries"'
-
在 AWS 控制台的 DynamoDB 指标选项卡上查看“写入容量”和“受限写入请求数量”指标,以查看自动扩展增量是否满足需求,然后在夜间缩小规模,并在早晨再次扩大。
-
最后,一旦完成,使用以下命令删除堆栈:
npm run rm:lcl -- -s $MY_STAGE
。
它是如何工作的...
在“实现背压和速率限制”配方中,我们看到流处理器最小化限制以最大化吞吐量的重要性。在本章中,我们讨论了优化吞吐量的技术,例如批处理、分组和异步非阻塞请求,这些都增加了必须分配的数据存储容量。然而,虽然我们需要确保有足够的容量,但我们还希望最小化浪费的容量,自动扩展帮助我们实现这一点。自动扩展可以解决随着时间的推移而增长到预期峰值的需求,如已知事件的可预测需求,以及不可预测的需求。
在本配方中,我们使用 serverless-dynamodb-autoscaling-plugin
在每个表的基础上定义 autoscaling
策略。对于 read
和 write
容量,我们指定 minimum
和 maximum
容量和期望的 usage
百分比。这个 usage
百分比定义了我们希望拥有的额外空间量,这样我们就可以提前增加容量,以确保在达到 100% 利用率并开始节流之前分配额外的容量。我们还可以在特定时间安排自动扩展 actions
。在本配方中,我们在夜间缩小规模以最小化浪费,然后在典型需求到来之前在早上恢复规模。
利用缓存控制
自主、云原生服务维护自己的物化视图,并将复制的数据存储在高度可用且性能极优的云原生数据库中。当与 API 网关和 FaaS 的性能结合时,通常没有必要添加传统的缓存机制来实现面向用户的 前端后端(BFF)服务的预期性能。但话虽如此,这并不意味着我们不应该利用已经包装服务的 CDN,例如 CloudFront。因此,以下配方将向您展示如何利用缓存控制头和利用 CDN 来提高最终用户性能,同时减少对服务的负载。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/cache-control --path cncb-cache-control
-
导航到
cncb-cache-control
目录,cd cncb-cache-control
。 -
检查名为
serverless.yml
的文件。 -
检查名为
handler.js
的文件,其内容如下:
module.exports.get = (request, context, callback) => {
const response = {
statusCode: 200,
headers: {
'Cache-Control': 'max-age=5',
},
body: ...,
};
callback(null, response);
};
module.exports.save = (request, context, callback) => {
const response = {
statusCode: 200,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
};
callback(null, response);
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
检查
.serverless
目录中生成的内容。 -
按照以下步骤部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
...
Stack Outputs
ApiDistributionEndpoint: https://d2thj6o092tkou.cloudfront.net
...
部署 CloudFront 分发通常需要超过 20 分钟。
-
在 AWS 控制台中检查堆栈和资源。
-
使用以下
curl
命令多次调用堆栈输出中显示的端点,以查看缓存结果的性能差异:
$ curl -s -D - -w "Time: %{time_total}" -o /dev/null https://<see stack output>.cloudfront.net/things/123 | egrep -i 'X-Cache|Time'
X-Cache: Miss from cloudfront
Time: 0.712
$ curl ...
X-Cache: Hit from cloudfront
Time: 0.145
$ curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"thing 1"}' https://<see stack output>.cloudfront.net/things ...
< HTTP/1.1 200 OK
< Cache-Control: no-cache, no-store, must-revalidate
< X-Cache: Miss from cloudfront
...
- 最后,使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的...
在这个相关的配方中,我们关注等式的服务端。云原生数据库,如 DynamoDB,响应时间在低 10 毫秒,AWS API 网关和 AWS Lambda 对于 BFF 服务的整体延迟通常在低 100 毫秒。只要数据库容量设置得当且节流最小化,就很难从最终用户的角度观察到明显的性能提升。真正改善这一点的唯一方法就是根本不需要发出请求。
这是一个云原生可能真正反直觉的案例。传统上,为了提高性能,我们需要增加基础设施的数量,并在代码和数据库之间添加一个昂贵的缓存层。换句话说,我们需要花费更多的钱来提高性能。然而,在这个菜谱中,我们利用了一个极低成本的边缘缓存,既提高了性能,又降低了成本。通过向我们的响应添加 Cache-Control
头部,例如 max-age
,我们可以告诉浏览器不要重复请求,并告诉 CDN 为其他用户重用响应。因此,我们减少了 API 网关和函数的负载,并减少了数据库所需的容量,从而降低了所有这些服务的成本。明确控制哪些操作应该存储 no-cache
,例如 PUT
、POST
和 DELETE
方法,也是一种良好的实践。
利用会话一致性
云原生前端应用程序的设计必须考虑到系统最终应该是一致的。例如,在一个传统的前端应用程序中,保存数据然后立即执行查询以检索相同的数据并不罕见。然而,在一个最终一致性的系统中,查询第一次尝试很可能找不到数据。相反,云原生前端利用了单页应用程序至少可以缓存数据的事实,以用户会话的持续时间为限。这种方法被称为会话一致性。以下菜谱演示了如何使用流行的 Apollo Client (www.apollographql.com/client
) 与 ReactJS 结合使用,通过实现 会话一致性 来提高感知性能并减少系统负载。
如何做到这一点...
- 从以下模板创建
service
和spa
项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/session-consistency/spa --path cncb-session-consistency-spa
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/session-consistency/service --path cncb-session-consistency-service
- 使用以下命令部署
service
:
$ cd ./cncb-session-consistency-service
$ npm install
$ npm run dp:lcl -- -s $MY_STAGE
-
导航到
cncb-session-consistency-spa
目录,cd ../cncb-session-consistency-spa
。 -
查看以下内容的
src/index.js
文件,并根据service
堆栈输出的值更新uri
,如下所示:
...
const client = new ApolloClient({
link: new HttpLink({
// CHANGE ME
uri: 'https://*<API_ID>*.execute-api.us-east-1.amazonaws.com/*<STAGE>*/graphql',
}),
cache: new InMemoryCache(),
});
...
- 查看名为
src/App.js
的文件,其内容如下:
...
const AddThing = () => {
...
return (
<Mutation
mutation={SAVE_THING}
update={(cache, { data: { saveThing } }) => {
const { things } = cache.readQuery({ query: GET_THINGS });
cache.writeQuery({
query: GET_THINGS,
data: { things: { items: things.items.concat([saveThing]) } }
});
}}
>
...
</Mutation>
);
};
...
-
使用
npm install
安装依赖项。 -
使用
npm start
在本地运行应用程序。 -
浏览到
http://localhost:3000
。 -
添加和更新一些内容,注意查询结果是如何更新的。
-
最后,一旦完成,按照以下方式删除服务堆栈:
$ cd ../cncb-session-consistency-service
$ npm run rm:lcl -- -s $MY_STAGE
它是如何工作的...
在这个菜谱中,我们使用了一个类似于我们在 实现一个 GraphQL CRUD BFF 菜谱中创建的 GraphQL BFF。这里的重点是前端应用程序,我们使用 ReactJS 和 Apollo Client 创建它,并且特别关注如何缓存我们与服务的交互。首先,我们在 src/index.js
文件中创建 ApolloClient
并用服务的端点和最重要的 InMemoryCache
对象初始化它。
接下来,我们在src/App.js
文件中实现用户界面。屏幕显示从things
查询返回的项目列表。Apollo 客户端将自动缓存查询的结果。更新单个对象的突变将自动更新缓存并触发屏幕重新渲染。请注意,添加新数据需要更多努力。《AddThing》函数使用突变的update
功能来保持缓存同步并触发重新渲染。update
函数接收对缓存的引用和突变返回的对象。然后我们调用readQuery
从缓存中读取查询,将新对象追加到查询结果中,并最终通过调用writeQuery
来更新缓存。
最终结果是用户体验非常低延迟,因为我们正在优化执行请求的数量、传输的数据量以及使用的内存量。最重要的是,对于新数据和更新数据,我们不会丢弃客户端创建的任何内容,并用相同的检索值来替换它——毕竟,这只是不必要的劳动。我们已经有数据了,为什么还要丢弃它并再次检索呢?我们也无法确定服务端的数据是一致的——除非我们执行一个更慢、成本更高的连续读取,正如所述,这是不必要的。在区域故障的情况下,会话一致性对于多区域部署变得更加有价值。正如我们将在第十章“部署到多个区域”中讨论的,最终一致性的云原生系统对区域故障非常宽容,因为它们已经对最终一致性有容忍度,这在故障转移期间可能会更加持久。因此,会话一致性有助于使区域故障对最终用户透明。对于在区域故障期间必须保持可用的任何数据,我们可以将会话一致性进一步推进,并将用户会话持久化到本地存储。
第十章:部署到多个区域
在本章中,将涵盖以下食谱:
-
实现基于延迟的路由
-
创建区域健康检查
-
触发区域故障转移
-
使用 DynamoDB 实现区域复制
-
实现轮询复制
简介
并不是某个云提供商是否会经历一个值得报道的区域中断的问题,而是何时会发生。这是不可避免的。根据我的经验,这种情况大约每两年发生一次。当这种事件发生时,许多系统没有其他选择,在发生中断期间变得不可用,因为它们只设计为在单个区域内的多个可用区工作。与此同时,其他系统几乎不会在可用性方面出现任何波动,因为它们已经被设计为跨多个区域运行。底线是,真正的云原生系统利用区域隔舱,并在多个区域运行。幸运的是,我们利用了已经跨可用区运行的完全托管、增值云服务。这使团队能够将精力重新集中在创建一个活动-活动、多区域系统上。本章中的食谱从三个相互关联的视角涵盖了多区域主题——同步请求、数据库复制和异步事件流。
实现基于延迟的路由
许多系统会做出有意识的决策,不跨多个区域运行,因为这根本不值得额外的努力和成本。当以活动-被动模式运行时,这一点完全可以理解,因为额外的努力不会产生容易看到的效益,直到发生区域中断。当以活动-活动模式运行时,它将每月运行成本加倍,这也是可以理解的。相反,无服务器云原生系统可以轻松部署到多个区域,并且由于交易量成本在各个区域之间分散,成本的增加是微不足道的。本食谱演示了如何在多个区域运行 AWS API Gateway 和基于 Lambda 的服务,并利用 Route53 在这些活动-活动区域之间路由流量以最小化延迟。
准备工作
您需要一个已注册的域名和一个Route53 托管区域,您可以在本食谱中使用它来为将要部署的服务创建子域名,例如我们在将自定义域名与 CDN 关联食谱中讨论的那样。您还需要在us-east-1
和us-west-2
区域为您的域名获取通配符证书,正如我们在创建用于传输加密的 SSL 证书食谱中讨论的那样。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/latency-based-routing --path cncb-latency-based-routing
-
使用
cd cncb-latency-based-routing
导航到cncb-latency-based-routing
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-latency-based-routing
plugins:
- serverless-multi-regional-plugin
provider:
...
endpointType: REGIONAL
custom:
dns:
hostedZoneId: ZXXXXXXXXXXXXX
domainName: ${self:service}.example.com
regionalDomainName: ${opt:stage}-${self:custom.dns.domainName}
us-east-1:
acmCertificateArn: arn:aws:acm:us-east-1:xxxxxxxxxxxx:certificate/...
us-west-2:
acmCertificateArn: arn:aws:acm:us-west-2:xxxxxxxxxxxx:certificate/...
cdn:
region: us-east-1
aliases:
- ${self:custom.dns.domainName}
acmCertificateArn: ${self:custom.dns.us-east-1.acmCertificateArn}
functions:
hello:
...
-
更新
serverless.yml
文件中的以下字段:-
custom.dns.hostedZoneId
-
custom.dns.domainName
-
custom.dns.us-east-1.acmCertificateArn
-
custom.dns.us-west-2.acmCertificateArn
-
-
审查名为
handler.js
的文件。 -
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
审查
.serverless
目录中生成的内容。 -
使用
npm run dp:lcl:**w** -- -s $MY_STAGE
在us-west-2
区域部署堆栈。 -
在 AWS 控制台中审查
us-west-2
区域的堆栈和资源。 -
测试服务的
regional
端点并注意负载中返回的区域:
$ curl -v https://$MY_STAGE-cncb-latency-based-routing.example.com/$MY_STAGE/hello
{"message":"Your function executed successfully in us-west-2!"}
- 使用
npm run dp:lcl:**e** -- -s $MY_STAGE
在us-east-1
区域部署堆栈。
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中审查
us-east-1
区域的堆栈和资源。 -
测试服务的
global
端点并注意负载中返回的区域,如果你不是最接近us-west-2
区域,则应与上述不同:
$ curl -v https://cncb-regional-failover-service.example.com/hello
{"message":"Your function executed successfully in us-east-1!"}
- 完成后删除堆栈:
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAG
它是如何工作的...
在这个菜谱中,我们正在将单个服务部署到两个区域——us-east-1
和us-west-2
。从 API 网关和 Lambda 函数的角度来看,我们只是在每个区域创建两个不同的 CloudFormation 堆栈。我们有两个脚本——dp:lcl:**e**
和dp:lcl:**w**
,这两个脚本之间的唯一区别是它们指定了不同的区域。因此,部署到两个区域的努力微乎其微,而且没有额外的成本,因为我们只按交易付费。在serverless.yml
文件中需要注意的一点是,我们正在为 API 网关定义endpointType
为REGIONAL
,这将使我们能够利用 Route53 的区域路由功能:
如前图所示,我们需要配置 Route53 在两个区域之间执行基于延迟的路由。这意味着 Route53 将请求路由到请求者最近的位置。serverless-multi-regional-plugin
封装了这些配置的大部分细节,因此我们只需要在custom.dns
下指定变量。首先,我们提供托管顶级域名(例如example.com
)的区域的hostedZoneId
。接下来,我们定义将用作通过CloudFront访问服务的全局alias
的domainName
。为此,我们使用服务名称(即${self:service}
)作为顶级域名的子域名来唯一标识一个服务。
我们还需要定义一个 regionalDomainName
,以便在所有区域提供一个通用名称,这样 CloudFront 就可以依赖 Route53 来选择最佳的访问区域。为此,我们使用阶段(即 ${opt:stage}-${self:custom.dns.domainName}
)作为前缀,并请注意,我们将其与破折号连接,以便与简单的通配符证书(如 *.example.com
)一起使用。区域 acmCertificateArn
变量指向每个区域中你的通配符证书的副本,如 准备就绪 部分所述。API Gateway 要求证书位于与服务相同的区域。CloudFront 要求证书位于 us-east-1
区域。CloudFront 是一项全球服务,因此我们只需要从 us-east-1
区域部署 CloudFront 分发。
所有对全局端点(service.example.com
)的请求都将路由到最近的 CloudFront 边缘位置。CloudFront 然后将请求转发到区域端点(stage-service.example.com
),Route53 将请求路由到最近的区域。一旦请求进入一个区域,对服务(如 Lambda、DynamoDB 和 Kinesis)的所有请求都将保持在区域内,以最小化延迟。所有状态更改都将复制到其他区域,正如我们在 使用 DynamoDB 实现区域复制 和 实现轮询复制 配方中讨论的那样。
我强烈建议查看生成的 CloudFormation 模板,以了解所有创建的资源细节。
创建区域健康检查
云原生系统中的健康检查与传统健康检查的关注点不同。传统的健康检查在实例级别运行,以确定集群中何时需要替换特定的实例。然而,云原生系统使用完全托管、增值的云服务,因此没有实例需要管理。这些无服务器功能提供了特定区域内可用区的高可用性。因此,云原生系统可以专注于在区域之间提供高可用性。本配方演示了如何断言给定区域内使用的增值云服务的状态。
准备就绪
要完整完成此配方,您需要一个 Pingdom (www.pingdom.com
) 账户。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/regional-health-check --path cncb-regional-health-check
-
使用
cd cncb-regional-health-check
命令导航到cncb-regional-health-check
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-regional-health-check
provider:
name: aws
runtime: nodejs8.10
endpointType: REGIONAL
iamRoleStatements:
...
functions:
check:
handler: handler.check
events:
- http:
path: check
method: get
environment:
UNHEALTHY: false
TABLE_NAME:
Ref: Table
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
...
- 查看名为
handler.js
的文件,其内容如下:
module.exports.check = (request, context, callback) => {
Promise.all([readCheck, writeCheck])
.catch(handleError)
.then(response(callback));
};
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
logger: console,
});
const readCheck = () => db.get({
TableName: process.env.TABLE_NAME,
Key: {
id: '1',
},
}).promise();
const writeCheck = () => db.put({
TableName: process.env.TABLE_NAME,
Item: {
id: '1',
},
}).promise();
const handleError = (err) => {
console.error(err);
return true; // unhealthy
};
const response = callback => (unhealthy) => {
callback(null, {
statusCode: unhealthy || process.env.UNHEALTHY === 'true' ? 503 : 200,
body: JSON.stringify({
timestamp: Date.now(),
region: process.env.AWS_REGION,
}),
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
});
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
在
us-east-1
和us-west-2
区域部署堆栈:
$ npm run dp:lcl:e -- -s $MY_STAGE
...
GET - https://0987654321.execute-api.us-east-1.amazonaws.com/john/check
$ npm run dp:lcl:w -- -s $MY_STAGE
...
GET - https://1234567890.execute-api.us-west-2.amazonaws.com/john/check
-
在 AWS 控制台中查看两个区域的堆栈和资源。
-
对于每个区域,调用堆栈输出中显示的端点,如下所示:
$ curl -v https://0987654321.execute-api.us-east-1.amazonaws.com/$MY_STAGE/check
$ curl -v https://1234567890.execute-api.us-west-2.amazonaws.com/$MY_STAGE/check
-
在您的 Pingdom 账户中为每个区域端点创建一个 Uptime 检查,间隔为
1 分钟
。 -
完成后,请删除堆栈:
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
它是如何工作的...
传统的健康检查通常断言实例能够访问它操作所需的全部资源。区域健康检查做的是同样的事情,但是从整个区域的角度来看。它断言系统使用的所有增值云服务(即资源)在给定区域内都运行正常。如果任何一个资源不可用,我们将根据 触发区域故障转移 菜谱中的讨论,将整个区域故障转移。
健康检查服务被实现为一个基于 REGIONAL
API 网关的服务,并部署到每个区域。然后我们需要定期在每个区域调用健康检查,以确认该区域是健康的。我们可以让 Route53 ping 这些区域端点,但它会频繁地 ping,以至于健康检查服务可能会成为你整个系统中最昂贵的服务。作为替代方案,我们可以使用外部服务,例如 Pingdom,在每个区域每分钟调用一次健康检查。对于许多系统来说,每分钟一次已经足够,但对于流量极高的系统,可能从 Route53 提供的更高频率中受益。
健康检查需要确认所需资源可用。健康检查本身隐含地确认 API 网关和 Lambda 服务可用,因为它建立在那些服务之上。对于所有其他资源,它将需要执行某种 ping 操作。在这个菜谱中,我们假设 DynamoDB 是所需资源。健康检查服务定义了自己的 DynamoDB 表,并在每次调用时执行 readCheck
和 writeCheck
,以确认服务仍然可用。如果任一请求失败,则健康检查服务将失败并返回 503
状态码。为了测试,该服务提供了一个 UNHEALTHY
环境变量,可以用来模拟失败,我们将在下一个菜谱中使用它。
触发区域故障转移
正如我们讨论的,在 创建区域健康检查 菜谱中,我们的区域健康检查断言系统使用的所有完全管理、增值云服务都正常运行。当这些服务中的任何一个出现故障或错误率足够高时,最好是整个区域故障转移到下一个最佳活动区域。这个菜谱演示了如何使用 CloudWatch Alarms 将区域健康检查连接到 Route53,以便 Route53 可以将流量导向健康的区域。
准备工作
您需要一个已注册的域名和一个Route53 托管区域,您可以使用此配方为要部署的服务创建子域名,例如我们在将自定义域名与 CDN 关联配方中讨论的那样。您还需要在us-east-1
和us-west-2
区域为您的域名获取通配符证书,例如我们在创建用于传输加密的 SSL 证书配方中讨论的那样。
如何做到这一点...
- 从以下模板创建服务并检查项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/regional-failover/check --path cncb-regional-failover-check
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/regional-failover/service --path cncb-regional-failover-service
-
使用
cd cncb-regional-failover-check
进入cncb-regional-failover-check
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-regional-failover-check
...
functions:
check:
...
resources:
Resources:
Api5xxAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
Namespace: AWS/ApiGateway
MetricName: 5XXError
Dimensions:
- Name: ApiName
Value: ${opt:stage}-${self:service}
Statistic: Minimum
ComparisonOperator: GreaterThanThreshold
Threshold: 0
Period: 60
EvaluationPeriods: 1
ApiHealthCheck:
DependsOn: Api5xxAlarm
Type: AWS::Route53::HealthCheck
Properties:
HealthCheckConfig:
Type: CLOUDWATCH_METRIC
AlarmIdentifier:
Name:
Ref: Api5xxAlarm
Region: ${opt:region}
InsufficientDataHealthStatus: LastKnownStatus
Outputs:
ApiHealthCheckId:
Value:
Ref: ApiHealthCheck
-
查看名为
handler.js
的文件。 -
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
在
us-east-1
和us-west-2
区域部署堆栈:
$ npm run dp:lcl:e -- -s $MY_STAGE
...
GET - https://0987654321.execute-api.us-east-1.amazonaws.com/john/check
$ npm run dp:lcl:w -- -s $MY_STAGE
...
GET - https://1234567890.execute-api.us-west-2.amazonaws.com/john/check
-
在 AWS 控制台中查看两个区域的堆栈和资源。
-
使用
cd cncb-regional-failover-service
进入cncb-regional-failover-service
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-regional-failover-service
plugins:
- serverless-multi-regional-plugin
...
custom:
dns:
...
us-east-1:
...
healthCheckId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
us-west-2:
...
healthCheckId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
...
-
在
serverless.yml
文件中更新以下字段:-
custom.dns.hostedZoneId
-
custom.dns.domainName
-
custom.dns.us-east-1.acmCertificateArn
-
从
east
健康检查堆栈的输出中获取custom.dns.us-east-1.healthCheckId
-
custom.dns.us-west-2.acmCertificateArn
-
从
west
健康检查堆栈的输出中获取custom.dns.us-west-2.healthCheckId
-
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
在
us-west-2
和us-east-1
区域部署堆栈:
$ npm run dp:lcl:w -- -s $MY_STAGE
$ npm run dp:lcl:e -- -s $MY_STAGE
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中查看两个区域的堆栈和资源。
-
测试服务的
global
端点并注意负载中返回的区域:
$ curl -v https://cncb-regional-failover-service.example.com/hello
{"message":"Your function executed successfully in us-east-1!"}
-
前往 AWS 控制台中路由到您请求的区域对应的
cncb-regional-failover-check-<stage>-check
Lambda 函数,将UNHEALTHY
环境变量更改为true
并保存函数。例如,前面的输出显示us-east-1
,因此更新us-east-1
中的函数。 -
在几分钟内多次调用该区域的健康检查端点以触发故障转移:
$ curl -v https://0987654321.execute-api.us-east-1.amazonaws.com/$MY_STAGE/check
- 测试服务的
global
端点并注意区域已更改:
$ curl -v https://cncb-regional-failover-service.example.com/hello
{"message":"Your function executed successfully in us-west-2!"
-
在 AWS 控制台中查看 Route53 健康检查的状态。
-
完成后删除堆栈:
$ cd cncb-regional-failover-service
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
$ cd ../cncb-regional-failover-check
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
它是如何工作的...
我们的区域健康检查服务设计为当所需的一个或多个服务返回错误时返回5xx
状态码。我们在健康检查服务中添加了一个名为Api5xxAlarm
的 CloudWatch 警报,该警报监控给定区域的 API Gateway 5xxError
指标,并在一分钟内至少有一个5xx
时发出警报。您可能需要根据具体需求调整警报的灵敏度。接下来,我们向服务中添加了一个名为ApiHealthCheck
的 Route53 健康检查,它依赖于Api5xxAlarm
并输出ApiHealthCheckId
供其他服务使用。最后,我们将healthCheckId
与每个区域中每个服务的 Route53 RecordSet 关联起来,例如cncb-regional-failover-service
。当警报状态为Unhealthy
时,Route53 将停止将流量路由到该区域,直到状态再次变为Healthy
。
在这个配方中,我们使用了UNHEALTHY
环境变量来模拟区域故障,并手动调用服务以触发警报。正如我们在创建区域健康检查配方中讨论的那样,健康检查通常由另一个服务(如 Pingdom)定期调用,以确保区域健康状态有持续的流量。为了增加覆盖率,我们还可以通过从警报中删除ApiName
维度来检查区域中所有服务的5xx
指标,但仍依赖于 ping 健康检查服务来断言状态,当没有其他流量时。
在 DynamoDB 中实现区域复制
在区域故障发生时,及时跨区域复制数据对于提供无缝的用户体验非常重要。在正常执行期间,区域复制将在近实时发生。在区域故障期间,应预期数据复制会变慢。我们可以将其视为延期的最终一致性。幸运的是,我们的云原生系统被设计为最终一致性。这意味着它们可以容忍过时的数据,无论数据何时变得一致。本配方展示了如何创建全局表以跨区域复制 DynamoDB 表,并讨论了为什么我们不复制事件流。
准备工作
在开始此配方之前,您需要在us-east-1
和us-west-2
区域拥有一个 AWS Kinesis Stream,例如在创建事件流配方中创建的那个。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/dynamodb-global-table --path cncb-dynamodb-global-table
-
使用
cd cncb-dynamodb-global-table
命令导航到cncb-dynamodb-global-table
目录。 -
查看名为
serverless.yml
的文件,其内容如下:
service: cncb-dynamodb-global-table
...
plugins:
- serverless-dynamodb-autoscaling-plugin
- serverless-dynamodb-global-table-plugin
custom:
autoscaling:
- table: Table
global: true
read:
...
write:
...
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${opt:stage}-${self:service}-things
...
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
- 查看名为
handler.js
的文件,其内容如下:
...
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.filter(forOrigin)
.map(toEvent)
.flatMap(publish)
.collect()
.toCallback(cb);
};
const forOrigin = e => e.dynamodb.NewImage['aws:rep:updateregion'] &&
e.dynamodb.NewImage['aws:rep:updateregion'].S === process.env.AWS_REGION;
...
-
使用
npm install
命令安装依赖项。 -
使用
npm test -- -s $MY_STAGE
命令运行测试。 -
查看
.serverless
目录中生成的内容。 -
在
us-east-1
和us-west-2
区域部署堆栈:
$ npm run dp:lcl:e -- -s $MY_STAGE
...
Serverless: Created global table: john-cncb-dynamodb-global-table-things with region: us-east-1
...
$ npm run dp:lcl:w -- -s $MY_STAGE
...
Serverless: Updated global table: john-cncb-dynamodb-global-table-things with region: us-west-2
...
-
在两个区域的 AWS 控制台中查看栈和资源。
-
调用
command
函数将数据保存到us-east-1
区域:
$ sls invoke -f command -r us-east-1 -s $MY_STAGE -d '{"id":"77777777-4444-1111-1111-111111111111","name":"thing one"}'
- 调用
query
函数从us-west-2
区域检索数据:
$ sls invoke -f query -r us-west-2 -s $MY_STAGE -d 77777777-4444-1111-1111-111111111111
{
"Item": {
"aws:rep:deleting": false,
"aws:rep:updateregion": "us-east-1",
"aws:rep:updatetime": 1534819304.087001,
"id": "77777777-4444-1111-1111-111111111111",
"name": "thing one",
"latch": "open"
}
}
- 查看两个区域中
trigger
和listener
函数的日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
$ sls logs -f trigger -r us-west-2 -s $MY_STAGE
$ sls logs -f listener -r us-west-2 -s $MY_STAGE
- 完成后,删除两个栈:
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
它是如何工作的...
DynamoDB 全局表负责在所有与全局表关联的区域表中复制数据,并保持数据同步,这一切都在近乎实时完成。serverless-dynamodb-global-table-plugin
将创建全局表,并设计用于与serverless-dynamodb-autoscaling-plugin
一起工作。对于每个将global
标志设置为 true 的表,当服务部署到第一个区域时,插件将创建全局表。对于后续的区域部署,插件将区域表添加到全局表中。每个区域表必须具有相同的名称,启用流,并具有相同的自动扩展策略,这由插件处理。插件不处理的一件事是,当全局表最初部署时,所有表都必须为空。
我们将从快乐的路径场景开始,其中没有区域中断,一切运行顺利,并逐步通过以下图表。当数据写入某个区域的表,例如us-east-1
时,数据将被复制到us-west-2
区域。us-east-1
区域的trigger
也会被调用。该触发器有一个forOrigin
过滤器,它会忽略所有aws:rep:updateregion
字段不等于当前AWS_REGION
的事件。否则,触发器将向当前区域的 Kinesis Stream 发布事件,并且所有订阅者将在当前区域执行并复制自己的数据到其他区域。当前服务的listener
将忽略它自己产生的任何事件。在us-west-2
区域,复制完成后,trigger
也会被调用,但forOrigin
过滤器将短路逻辑,因此不会向 Kinesis 发布重复事件,也不会在该区域的所有订阅者中重新处理。重复事件处理的不效率和可能的无穷复制循环是最好不复制事件流,而是依赖于叶数据存储复制的原因:
注意,这个配方基于实现双向同步配方中的代码,因此您可以查看该配方以获取有关代码的更多详细信息。
在区域故障转移期间,在最佳情况下,用户的数据已经复制,故障转移过程将完全无缝。用户的下一个命令将在新区域执行,事件链将在新区域处理,最终结果将复制回故障区域。当存在一些复制延迟时,会话一致性有助于使故障转移过程看起来无缝,正如我们在利用会话一致性配方中所讨论的那样。然而,在区域故障转移期间,失败的区域中的一些订阅者可能会落后于处理区域流中剩余的事件。幸运的是,区域中断通常意味着失败区域中的吞吐量较低,而不是没有吞吐量。这意味着将事件处理的结果复制到其他区域会有更高的延迟,但它们最终会变得一致。为最终一致性设计的用户体验,例如移动设备上的电子邮件应用程序,将轻松处理这种拖延的最终一致性。
尝试跟踪哪些事件已处理以及哪些事件卡在失败区域中的复杂性是最好不复制事件流的另一个原因。在无法容忍这种拖延的最终一致性情况下,新区域中的最新事件可以依赖会话一致性以获取更准确的信息,并使用使用逆 oplock 实现幂等性和使用事件溯源实现幂等性配方中讨论的技术来处理从缓慢恢复的区域接收到的顺序错误的旧事件。
实现轮询复制
在我们的多语言持久性架构中,并非所有数据库都支持像 AWS DynamoDB 那样的即插即用区域复制,但我们仍然需要将它们的数据复制到多个区域以提高延迟并支持区域故障转移。本配方演示了如何使用 AWS S3 作为代理为任何数据库添加区域复制。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/round-robin-replication --path cncb-round-robin-replication
-
使用
cd cncb-round-robin-replication
导航到cncb-round-robin-replication
目录。 -
检查名为
serverless.yml
的文件,其内容如下:
service: cncb-round-robin-replication
...
functions:
listener:
...
trigger:
...
search:
...
replicator:
handler: replicator.trigger
events:
- sns:
arn:
Ref: BucketTopic
topicName: ${self:service}-${opt:stage}-trigger
environment:
REPLICATION_BUCKET_NAME: ${self:custom.regions.${opt:region}.replicationBucketName}
custom:
regions:
us-east-1:
replicationBucketName: cncb-round-robin-replication-${opt:stage}-bucket-WWWWWWWWWWWWW
us-west-2:
replicationBucketName: cncb-round-robin-replication-${opt:stage}-bucket-EEEEEEEEEEEEE
...
- 检查名为
replicator.js
的文件,其内容如下:
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.flatMap(messagesToTriggers)
.flatMap(get)
.filter(forOrigin)
.flatMap(replicate)
.collect()
.toCallback(cb);
};
...
const forOrigin = uow => uow.object.Metadata.origin !== process.env.REPLICATION_BUCKET_NAME;
...
const replicate = uow => {
const { ContentType, CacheControl, Metadata, Body } = uow.object;
const params = {
Bucket: process.env.REPLICATION_BUCKET_NAME,
Key: uow.trigger.s3.object.key,
Metadata: {
'origin': uow.trigger.s3.bucket.name,
...Metadata,
},
ACL: 'public-read',
ContentType,
CacheControl,
Body,
};
const s3 = new aws.S3(...);
return _(
s3.putObject(params).promise()
...
);
};
-
使用
npm install
安装依赖项。 -
使用
npm test -- -s $MY_STAGE
运行测试。 -
检查
.serverless
目录中生成的内容。 -
在
us-east-1
和us-west-2
区域部署堆栈:
$ npm run dp:lcl:e -- -s $MY_STAGE
$ npm run dp:lcl:w -- -s $MY_STAGE
-
在
serverless.yml
中更新replicationBucketName
变量,以便us-east-1
复制到us-west-2
,反之亦然,然后重新部署堆栈。 -
在 AWS 控制台中检查堆栈和资源。
-
使用以下命令从单独的终端发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -f publish -r us-east-1 -s $MY_STAGE -d '{"type":"thing-created","thing":{"new":{"name":"thing two","id":"77777777-5555-1111-1111-111111111111"}}}'
- 使用以下 curl 命令在
us-west-2
区域搜索数据:
$ curl https://<API-ID>.execute-api.us-west-2.amazonaws.com/$MY_STAGE/search?q=two | json_pp
[
{
"id" : "77777777-5555-1111-1111-111111111111",
"url" : "https://s3.amazonaws.com/cncb-round-robin-replication-john-bucket-1cqxst40pvog4/things/77777777-5555-1111-1111-111111111111",
"name" : "thing two"
}
]
- 查看两个区域中
trigger
和listener
函数的日志:
$ sls logs -f replicator -r us-east-1 -s $MY_STAGE
...
2018-08-19 17:00:05 ... [AWS s3 200 0.04s 0 retries] getObject({ Bucket: 'cncb-round-robin-replication-john-bucket-1a3rh4v9tfedw',
Key: 'things/77777777-5555-1111-1111-111111111111' })
2018-08-19 17:00:06 ... [AWS s3 200 0.33s 0 retries] putObject({ Bucket: 'cncb-round-robin-replication-john-bucket-1cqxst40pvog4',
Key: 'things/77777777-5555-1111-1111-111111111111',
Metadata:
{ origin: 'cncb-round-robin-replication-john-bucket-1a3rh4v9tfedw' },
ACL: 'public-read',
ContentType: 'application/json',
CacheControl: 'max-age=300',
Body: <Buffer ... > })
...
$ sls logs -f replicator -r us-west-2 -s $MY_STAGE
...
2018-08-19 17:00:06 ... [AWS s3 200 0.055s 0 retries] getObject({ Bucket: 'cncb-round-robin-replication-john-bucket-1cqxst40pvog4',
Key: 'things/77777777-5555-1111-1111-111111111111' })
2018-08-19 17:00:06 ... {... "object":{..."Metadata":{"origin":"cncb-round-robin-replication-john-bucket-1a3rh4v9tfedw"}, ...}}
...
-
在移除堆叠之前,请先在每个区域清空水桶
-
完成后,请移除两个堆叠。
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
它是如何工作的...
在这个配方中,我们在 Elasticsearch 中创建了一个物化视图,并希望允许用户搜索他们最近区域的数据库。然而,Elasticsearch 不支持区域复制。正如我们在 使用 DynamoDB 实现区域复制 配方中讨论的那样,我们不希望复制事件流,因为该解决方案复杂且难以理解。相反,如图所示,我们将在每个区域将一个 S3 桶放在 Elasticsearch 前面,并利用 S3 触发器来更新 Elasticsearch 以及实现循环复制方案:
注意,这个配方基于 实现搜索 BFF 配方中的代码,因此您可以查看该配方以获取有关代码的更多详细信息。
该服务监听当前区域的 Kinesis 流,并将数据写入当前区域的 S3 桶,这会生成一个路由到 SNS 主题的 S3 触发器。一个函数响应该主题并在当前区域创建 Elasticsearch 中的物化视图。同时,一个 replicator
函数也响应相同的主题。复制器将 S3 桶中对象的全部内容复制到由 REPLICATION_BUCKET_NAME
环境变量指定的下一个区域的匹配桶。这反过来在该区域生成一个触发器。再次,一个函数响应该主题并在该区域创建 Elasticsearch 中的物化视图。该区域的 replicator
也响应并试图将对象复制到下一个区域。这个过程将 trigger
和 replicate
循环进行,直到 forOrigin
过滤器看到原始桶(即 uow.object.Metadata.origin
)等于当前复制器的目标(即 process.env.REPLICATION_BUCKET_NAME
)。在这个配方中,我们有两个区域——us-east-1
和 us-west-2
。数据起源于东部区域,因此东部复制器将数据复制到西部桶(1cqxst40pvog4
)。西部复制器不会将数据复制到东部桶(1a3rh4v9tfedw
),因为原始桶是东部桶。
这种循环复制技术是一种简单且成本效益高的方法,它建立在现有的事件架构之上。请注意,我们无法利用内置的 S3 复制功能来实现此目的,因为它只复制到单个区域,并且不会产生连锁反应。然而,我们可以像在 为灾难恢复复制数据湖 配方中讨论的那样,将这些桶添加 S3 复制以进行备份和灾难恢复。
第十一章:欢迎多云
在本章中,将介绍以下食谱:
-
在 Google Cloud Functions 中创建服务
-
在 Azure Functions 中创建服务
简介
供应商锁定是服务器无服务器、云原生开发中一个常见的担忧。然而,这种担忧是源于必须从一家供应商整体改变到另一家供应商的单一思维模式的遗物。另一方面,自主服务可以逐个改变。尽管如此,一次编写;到处运行的诱人承诺仍然是多云方法的战斗口号。然而,这种方法忽略了只编写一次的部分只是一个非常大的冰山的一角,而水下部分则蕴含着最大的风险,并且不能直接在云服务提供商之间转换。这不可避免地导致使用一套最少共同分母的工具和技术,这些工具和技术可以更容易地从一家提供商转移到另一家提供商。
我们选择拥抱完全托管、增值云服务的可丢弃架构。这种无服务器优先的方法赋予自给自足的全栈团队以精益和实验新想法的能力,快速失败,学习,并快速调整方向。这自然地导致了一种多语言云或多云的方法,其中团队根据每个服务选择最佳的云服务提供商。最终,公司确实有一个首选的云服务提供商。但是,对于多元化,其中一部分服务是在不同的云服务提供商上实施的,以获得经验和利用,这是一个有争议的观点。目标是在具有类似甚至相同的工具、技术和模式进行开发、测试、部署和监控的服务之间实现一致的管道体验。许多前几章中的食谱专注于 AWS 完全托管、增值云服务。本章中的食谱展示了如何通过添加额外的云服务提供商,如 Google 和 Azure,实现一致的云原生开发管道体验。
使用 Google Cloud Functions 创建服务
无服务器框架在许多不同的云服务提供商之上提供了一个抽象层,这有助于实现一致的开发和部署体验。本食谱演示了如何使用 Google Cloud Functions 创建服务。
准备工作
在开始此食谱之前,您需要一个配置了 Serverless Framework(serverless.com/framework/docs/providers/google/guide/credentials
)的 Google Cloud Billing Account、项目和凭证。
如何操作...
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch11/gcp --path cncb-gcp
-
使用
cd cncb-gcp
命令导航到cncb-gcp
目录。 -
查看名为
serverless.yml
的文件,内容如下:
service: cncb-gcp
provider:
name: google
runtime: nodejs8
project: cncb-project
region: ${opt:region}
credentials: ~/.gcloud/keyfile.json
plugins:
- serverless-google-cloudfunctions
functions:
hello:
handler: hello
events:
- http: path
...
#resources:
# resources:
# - type: storage.v1.bucket
# name: my-serverless-service-bucket
- 查看名为
index.js
的文件,内容如下:
exports.hello = (request, response) => {
console.log('env: %j', process.env);
response.status(200).send('... Your function executed successfully!');
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-gcp@1.0.0 dp:lcl <path-to-your-workspace>/cncb-gcp
> sls deploy -v --r us-east1 "-s" "john"
...
Serverless: Done...
Service Information
service: cncb-gcp
project: cncb-project
stage: john
region: us-east1
Deployed functions
hello
https://us-east1-cncb-project.cloudfunctions.net/hello
-
在 Google Cloud 控制台中查看部署和资源。
-
在以下命令中调用堆栈输出中显示的端点:
$ curl -v https://us-east1-cncb-project.cloudfunctions.net/hello
...
JavaScript Cloud Native Development Cookbook! Your function executed successfully!
- 查看日志:
$ sls logs -f hello -r us-east1 -s $MY_STAGE
...
2018-08-24T05:10:20...: Function execution took 12 ms, finished with status code: 200
...
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
工作原理...
首先要注意的是,如何操作… 部分的步骤几乎与所有之前的菜谱相同。这是因为 Serverless Framework 抽象了部署细节,我们进一步将所有命令包装在 NPM 脚本中,以封装依赖项管理。从这里,我们可以使用所有相同的工具和技术进行开发和测试,如第六章构建持续部署管道中概述的。这使得团队成员在跨不同云服务提供商实现的服务工作时可以顺利过渡。
serverless-google-cloudfunctions
插件处理与 Google Cloud APIs(如 Cloud Functions 和 Deployment Manager)交互的细节,以提供服务。serverless.yml
文件看起来应该非常熟悉。我们指定 provider.name
为 google
并设置 plugins
,然后我们专注于定义 functions
和 resources
。具体细节取决于云服务提供商,但通常我们选择特定提供商的原因是特定服务增值服务的细节。index.js
文件中的 Node.js 代码也很熟悉,尽管函数签名不同。最终,对于实现本烹饪书各菜谱中列举的云原生模式和技术的 Google Cloud 服务有一个清晰的映射。
使用 Azure Functions 创建服务
Serverless Framework 在许多不同的云服务提供商之上提供了一个抽象层,这有助于实现一致的开发和部署体验。本菜谱演示了如何使用 Azure Functions 创建服务。
准备工作
在开始此菜谱之前,您需要一个配置了 Serverless Framework 凭据的 Azure 账户(serverless.com/framework/docs/providers/azure/guide/credentials
)。
如何操作...
- 根据以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch11/azure --path cncb-azure
-
使用
cd cncb-azure
命令进入cncb-azure
目录。 -
查看以下内容的
serverless.yml
文件:
service: cncb-azure-${opt:stage}
provider:
name: azure
location: ${opt:region}
plugins:
- serverless-azure-functions
functions:
hello:
handler: handler.hello
events:
- http: true
x-azure-settings:
authLevel : anonymous
- http: true
x-azure-settings:
direction: out
name: res
- 查看名为
handler.js
的文件,其内容如下:
module.exports.hello = function (context) {
context.log('context: %j', context);
context.log('env: %j', process.env);
context.res = {
status: 200,
body: '... Your function executed successfully!',
};
context.done();
};
-
使用
npm install
安装依赖项。 -
使用
npm test
运行测试。 -
查看在
.serverless
目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-azure@1.0.0 dp:lcl <path-to-your-workspace>/cncb-azure
> sls deploy -v -r 'East US' "-s" "john"
...
Serverless: Creating resource group: cncb-azure-john-rg
Serverless: Creating function app: cncb-azure-john
...
Serverless: Successfully created Function App
-
在 Azure 控制台中查看部署和资源。
-
在另一个终端中跟踪日志:
$ sls logs -f hello -r 'East US' -s $MY_STAGE
Serverless: Logging in to Azure
Serverless: Pinging host status...
2018-08-25T04:02:34 Welcome, you are now connected to log-streaming service.
2018-08-25T04:05:00.843 [Info] Function started (Id=...)
2018-08-25T04:05:00.856 [Info] context: {...}
2018-08-25T04:05:00.856 [Info] env: {...}
2018-08-25T04:05:00.856 [Info] Function completed (Success, Id=..., Duration=19ms)
- 在以下命令中调用堆栈输出中显示的端点:
$ curl -v https://cncb-azure-$MY_STAGE.azurewebsites.net/api/hello
...
JavaScript Cloud Native Development Cookbook! Your function executed successfully!
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE
删除堆栈。
它是如何工作的…
首先要注意的是,“如何做…”(How to do it…)部分的步骤几乎与所有之前的食谱完全相同。这是因为 Serverless Framework 抽象化了部署细节,我们进一步使用NPM scripts将所有命令封装起来以实现依赖管理。从这里开始,我们可以使用与第六章,“构建持续部署管道”中概述的相同工具和技术进行开发和测试。这使得团队成员在跨不同云提供商实现的服务工作时能够顺利过渡。
serverless-azure-cloudfunctions
插件处理与 Azure API 交互的细节,例如 Azure Functions 和资源管理器,以提供该服务。serverless.yml
文件看起来应该非常熟悉。我们指定provider.name
为azure
并设置plugins
,然后我们专注于定义functions
。具体细节是云提供商特定的,但云提供商特定的增值服务细节通常是我们选择特定提供商以提供特定服务的原因。handler.js
文件中的 Node.js 代码也很熟悉,尽管函数签名不同。最终,有一个清晰的 Azure 服务映射,用于实现本烹饪书食谱中列举的云原生模式和技术的实现。