原文:
zh.annas-archive.org/md5/96e50944f4c1f57a68d39f9d5c19a2b2译者:飞龙
第五章:保护云原生系统
在本章中,将介绍以下菜谱:
-
保护您的云账户
-
创建联合身份池
-
实现注册、登录和注销
-
使用 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 分钟 -
前往
WebsiteDistributionURLURL -
在
username字段中填写your-username -
在
password字段中填写your-password -
点击“登录”
-
等待元素
.App-title包含Welcome to React
-
-
定期审查经验监控 | 事务仪表板。
-
完成后,使用
npm run rm:lcl -- -s $MY_STAGE删除堆栈。 -
在 14 天试用期到期前,在“账户 | 订阅 | 管理计划”下取消 Pingdom 免费试用,以避免费用。
它是如何工作的…
生产环境中的测试与传统的前生产测试不同。正如在“创建警报”配方中所示,我们的生产测试是以监控的形式实现的,这些监控不断测试系统发出的信号。然而,如果没有流量,就没有信号,因此也就没有测试来提醒我们这些期间发生的任何部署中的问题。这反过来又降低了我们对这些部署的信心。解决方案是在没有自然流量时生成稳定的合成流量来填补空白。
服务器正常运行检查是最容易实施的,因为它们只进行单个请求。这些至少应该包括在内,因为它们可以快速实施且几乎不需要努力,并且它们具有最高的频率。真实用户监控(RUM)应该包括在内,因为它只需要简单的代码修改,并且云原生系统中大量的用户体验是由单页应用程序在浏览器中执行的。最后,需要实施一组小型但战略性的合成事务脚本,以持续测试关键功能。这些脚本类似于传统的测试脚本,但它们的重点是持续执行这些关键的成功路径,以确保关键功能不受新功能持续部署的影响。
1291



被折叠的 条评论
为什么被折叠?



