1. Overview

AWS CDK 是一个软件开发框架,用于在代码中定义云基础架构并通过 AWS CloudFormation 进行配置。

在后面的演练中,首先将写一个 Hello, CDK! Lambda 函数并在其前面配置一个 API Gateway 端点,以便用户可以通过 HTTP 请求进行调用。

然后,将介绍 AWS CDK Constructs 的概念,包括编写自己的 Constructs。AWS CDK Constructs 可以将多个基础架构资源捆绑到可重用的组件中。你可以共享这些组件,以供其他人在其应用程序中使用。

最后,将学习 Testing constructs 并将测试功能添加到应用程序中。

所有的应用程序开发都将在 AWS Cloud9 IDE 中完成。

演练中使用以下服务:

  • AWS CDK
    • 可以让你使用熟悉的编程语言来建模和配置云应用程序的资源。
  • AWS Cloud9
    • 基于云的 IDE,可以使用浏览器即可编写、运行和调式代码。
    • 它包括代码编辑器、调式器和终端。
    • 预先安装了常用编程语言(JS, Python, PHP, Java等)的基本工具。无需安装文件或配置开发计算机即可启动新项目。
  • AWS Lambda
    • 利用它,无需预置或管理服务器即可运行代码。只需为使用的计算时间付费,在代码未运行期间不产生任何费用。
    • 可以为几乎任何类型的应用程序或后端服务运行代码,而无需任何管理。
    • 只需上传您的代码,Lambda 会处理运行和扩展高可用性代码所需的一切工作。
  • Amazon API Gateway
    • 它是一种完全托管的服务,可以帮助开发人员轻松创建、发布、维护、监控和保护任意规模的 API。API 充当应用程序的前门,可从您的后端服务访问数据、业务逻辑或功能。
    • 使用它,您可以创建 RESTful API 和 WebSocket API,以便实现实时双向通信应用程序。API Gateway 支持容器化和无服务器工作负载,以及 Web 应用程序。
  • Amazon DynamoDB
    • 它是一个键值和文档数据库,可以在任何规模上提供单位毫秒的性能。
  • AWS CloudFormation
    • 它提供了一种通过语言,供你在云环境中对 AWS 和第三方应用程序资源进行建模和配置。

2. 实验

创建 Cloud9 IDE 环境

image.pngimage.png

任务1. 指定使用的 CDK 版本

此任务,需要指定要使用的 CDK 版本,并验证是否正确引用了该版本。 :::info 默认情况下,在 AWS Cloud9 环境中 CDK 软件包安装的是最新版本。 ::: 在本实验中,我们指定使用 1.50.0 版本。

运行 npm 命令安装 CDK 软件包, i 代表 install;-g 代表在环境中全局安装软件包,后面跟着 CDK 软件包名称以及要安装的版本; --force 覆盖较新的版本,强制安装。

  1. $ npm i -g aws-cdk@1.50.0 --force

验证版本:

  1. $ cdk --version

任务2. 新建一个工作目录

在 Cloud9 环境中创建一个新目录,作为项目根目录。

创建一个名为 cdk-primer 的目录并将目录改为根目录:

  1. $ mkdir cdk-primer && cd cdk-primer

任务3. 在工作目录中创建一个空的 AWS CDK 项目

创建一个 TypeScript 语言的 AWS CDK 项目,之后将查看所创建的文件和文件内容,熟悉其中的内容。

运行 cdk 命令创建一个新的 AWS CKD 空模板项目。 init 代表初始化; -l 选择编程语言,后跟语言名称:

  1. $ cdk init -l typescript

这将在目录中创建以下文件和子目录:
image.png

  • 一个隐藏的 .git 子目录和一个隐藏的 .gitignore 文件,使该项目与 Git 兼容;
  • bin 目录,其中包括 cdk-primer.ts 文件。该文件包含你的 AWS CDK 应用程序的入口点;
  • lib 目录,其中包括 cdk-primer-stack.ts 文件。该文件包含你的 AWS CDK 堆栈的代码;
  • node_modules 目录,其中包含应用程序和堆栈可能需要使用的代码库;
  • test 目录,包括 cdk-primer-test.ts 文件。用于 Jest 测试;
  • 一个隐藏的 .npmignore 文件,其中列出了在构件代码时 npm* 不需要的子目录和文件类型;
  • cdk.json 文件,其中包含使 cdk 命令的运行更容易的信息。
  • jest.config.js 文件,用于 Jest 测试;
  • package-lock.json 文件,其中包含 npm 可用来减少可能的构件和运行错误的信息;
  • package.json 文件,其中信息能使运行 npm 命令更加容易,并且可能减少构件和运行错误;
  • README.md 文件,其中列表了可与 npm 和 AWS CDK 一起运行的有用命令;
  • tsconfig.json 文件,其中包含的信息使运行 tsc 命令更加容易,并且可能减少构件和运行错误。

查看 bin/ckd-primer.ts

  • 此代码从 lib/cdk-primer-stack.ts 文件加载并实例化 CdkPrimerStack 类。
  • 不用修改。 ```shell

    !/usr/bin/env node

    import ‘source-map-support/register’; import * as cdk from ‘@aws-cdk/core’; import { CdkPrimerStack } from ‘../lib/cdk-primer-stack’;

const app = new cdk.App(); new CdkPrimerStack(app, ‘CdkPrimerStack’);

  1. 查看 `lib/cdk-primer-stack.ts`
  2. - `Stack` `Construct` `StackProps` 类表示 AWS CloudFormation 堆栈和其属性。
  3. - `CdkPrimerStack` 类表示该应用程序的 AWS CloudFormation 堆栈。当前为空,随着实验的进行,将逐步把代码添加到该堆栈中。
  4. ```shell
  5. import * as cdk from '@aws-cdk/core';
  6. export class CdkPrimerStack extends cdk.Stack {
  7. constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
  8. super(scope, id, props);
  9. // The code that defines your stack goes here
  10. }
  11. }

任务4. 导入 Lambda 模块并将 Lambda 函数添加到堆栈中

创建新文件: /cdk-primer/lambda/hello.js ,并添加代码:

  1. exports.handler = async function(event) {
  2. console.log("request:", JSON.stringify(event, undefined, 2));
  3. return {
  4. statusCode: 200,
  5. headers: { "Content-Type": "text/plain" },
  6. body: `Hello, CDK! You've hit ${event.path}\n`
  7. };
  8. };

这是一个 Lambda 函数,返回文本 Hello,CDK! You've hit [URL path]。该函数的输出还包括 HTTP 状态码和 HTTP 标头。API Gateway 使用它们来指定对用户的 HTTP 响应。

任务5. 安装 Lambda construct library

CDK 包含了一个叫 AWS Construct Library 的扩展包。Construct Library 分为多个模块,每个 AWS 服务就有一个模块。例如,如果要定义 Lambda 函数,则需要使用 Lambda Construct Library。

在 Cloud9 终端上,运行 npm i 命令将 Lambda 模块及其依赖项安装到项目中:

  1. $ npm i @aws-cdk/aws-lambda@1.50.0

任务6. 将 Lambda 函数添加到堆栈中

/cdk-primer/lib/cdk-primer-stack.ts 文件添加代码:
TODO: 1 - 导入依赖项;
TODO: 2 - 添加一个 lambda.Function construct;

  1. import * as cdk from '@aws-cdk/core';
  2. //BEGIN TODO:1 - Import the Lambda module
  3. import * as lambda from '@aws-cdk/aws-lambda';
  4. import * as iam from '@aws-cdk/aws-iam';
  5. //END TODO:1
  6. export class CdkPrimerStack extends cdk.Stack {
  7. constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
  8. super(scope, id, props);
  9. //BEGIN TODO:2 - Define a Lambda resource
  10. const PrimerRole = iam.Role.fromRoleArn(this, 'Role', 'arn:aws:iam::${Token[AWS::AccountId.0]}:role/CDKPrimerHelloHandlerLambdaRole', {
  11. mutable: false,
  12. });
  13. const hello = new lambda.Function(this, 'HelloHandler', {
  14. runtime: lambda.Runtime.NODEJS_12_X, // execution environment
  15. code: lambda.Code.fromAsset('lambda'), // code loaded from "Lambda" directory
  16. handler: 'hello.handler', // file is "hello", function is "handler"
  17. role: PrimerRole
  18. });
  19. //END TODO:2
  20. }
  21. }
  • 导入了 Lambda 和 IAM 模块;
  • 创建了一个 Construct 以指定一个预建角色。此角色具有附加的 AWSLambdaBasicExecutionRole 策略,以便 Lambda 函数起作用;
  • 角色 CDKPrimerHelloHandlerLambdaRole 需要在 IAM 控制台提前创建,不然无法 deploy 成功;
  • 该函数使用 NodeJS 12.x 运行环境;
  • handler 的代码是从之前创建的 lambda 目录中加载的。该路径是相对于你启动 CDK 的位置,该路径是项目的根目录;
  • handler 函数的名称是 hello.handler(hello 是文件名,handler 是导出的函数名);
  • 预先构建的 Lambda 角色与 role 属性关联;

任务7. 在终端中 bootstrap 环境

堆栈包含很多的资源或大量的 Lambda 函数,它们可能需要在 S3 存储桶中预置这些内容,然后提供给堆栈。 cdk bootstrap 命令可以为你创建必要的 S3 资源。

在这个实验中,堆栈包含一个 Amazon S3 存储桶,AWS CDK 在存储桶的部署过程中使用该存储桶存储模板和资产,因此您将运行 cdk bootstrap 命令;

查看创建的存储桶:

  1. $ aws s3 ls
  2. $ cdk bootstrap
  3. $ aws s3 ls
  4. 2020-11-30 03:03:35 cdktoolkit-stagingbucket-1gdmngjaxoigu

image.png

此外,项目结构中也会创建一个 cdk.out 目录。该目录的创建时由于 cdk deploy 命令,并且是默认情况下写入合成模板的目录。

任务8. 部署堆栈

在步骤中,将使用 CloudFormation 部署堆栈。

如果想查看将在堆栈中创建了哪些资源,可以运行 cdk synthesize 命令。它将打印出 CloudFormation 模板,该模板将用于在终端中创建资源。

  1. $ cdk synth

输出结果:

  1. Resources:
  2. HelloHandler2E4FBA4D:
  3. Type: AWS::Lambda::Function
  4. Properties:
  5. Code:
  6. S3Bucket:
  7. Ref: AssetParameters5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09S3Bucket923DBB46
  8. S3Key:
  9. Fn::Join:
  10. - ""
  11. - - Fn::Select:
  12. - 0
  13. - Fn::Split:
  14. - "||"
  15. - Ref: AssetParameters5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09S3VersionKey7138363F
  16. - Fn::Select:
  17. - 1
  18. - Fn::Split:
  19. - "||"
  20. - Ref: AssetParameters5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09S3VersionKey7138363F
  21. Handler: hello.handler
  22. Role:
  23. Fn::Join:
  24. - ""
  25. - - "arn:aws:iam::"
  26. - Ref: AWS::AccountId
  27. - :role/CDKPrimerHelloHandlerLambdaRole
  28. Runtime: nodejs12.x
  29. Metadata:
  30. aws:cdk:path: CdkPrimerStack/HelloHandler/Resource
  31. aws:asset:path: asset.5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09
  32. aws:asset:property: Code
  33. CDKMetadata:
  34. Type: AWS::CDK::Metadata
  35. Properties:
  36. Modules: aws-cdk=1.50.0,@aws-cdk/assets=1.50.0,@aws-cdk/aws-cloudwatch=1.50.0,@aws-cdk/aws-ec2=1.50.0,@aws-cdk/aws-events=1.50.0,@aws-cdk/aws-iam=1.50.0,@aws-cdk/aws-kms=1.50.0,@aws-cdk/aws-lambda=1.50.0,@aws-cdk/aws-logs=1.50.0,@aws-cdk/aws-s3=1.50.0,@aws-cdk/aws-s3-assets=1.50.0,@aws-cdk/aws-sqs=1.50.0,@aws-cdk/aws-ssm=1.50.0,@aws-cdk/cloud-assembly-schema=1.50.0,@aws-cdk/core=1.50.0,@aws-cdk/cx-api=1.50.0,@aws-cdk/region-info=1.50.0,jsii-runtime=node.js/v10.23.0
  37. Condition: CDKMetadataAvailable
  38. Parameters:
  39. AssetParameters5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09S3Bucket923DBB46:
  40. Type: String
  41. Description: S3 bucket for asset "5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09"
  42. AssetParameters5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09S3VersionKey7138363F:
  43. Type: String
  44. Description: S3 key for asset version "5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09"
  45. AssetParameters5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09ArtifactHash7BADB98B:
  46. Type: String
  47. Description: Artifact hash for asset "5dcceeae13d19ccc24fbf80b527b5cd04a655da5c0e8368ad045334917bfaa09"
  48. Conditions:
  49. CDKMetadataAvailable:
  50. Fn::Or:
  51. - Fn::Or:
  52. - Fn::Equals:
  53. - Ref: AWS::Region
  54. - ap-east-1
  55. - Fn::Equals:
  56. - Ref: AWS::Region
  57. - ap-northeast-1
  58. - Fn::Equals:
  59. - Ref: AWS::Region
  60. - ap-northeast-2
  61. - Fn::Equals:
  62. - Ref: AWS::Region
  63. - ap-south-1
  64. - Fn::Equals:
  65. - Ref: AWS::Region
  66. - ap-southeast-1
  67. - Fn::Equals:
  68. - Ref: AWS::Region
  69. - ap-southeast-2
  70. - Fn::Equals:
  71. - Ref: AWS::Region
  72. - ca-central-1
  73. - Fn::Equals:
  74. - Ref: AWS::Region
  75. - cn-north-1
  76. - Fn::Equals:
  77. - Ref: AWS::Region
  78. - cn-northwest-1
  79. - Fn::Equals:
  80. - Ref: AWS::Region
  81. - eu-central-1
  82. - Fn::Or:
  83. - Fn::Equals:
  84. - Ref: AWS::Region
  85. - eu-north-1
  86. - Fn::Equals:
  87. - Ref: AWS::Region
  88. - eu-west-1
  89. - Fn::Equals:
  90. - Ref: AWS::Region
  91. - eu-west-2
  92. - Fn::Equals:
  93. - Ref: AWS::Region
  94. - eu-west-3
  95. - Fn::Equals:
  96. - Ref: AWS::Region
  97. - me-south-1
  98. - Fn::Equals:
  99. - Ref: AWS::Region
  100. - sa-east-1
  101. - Fn::Equals:
  102. - Ref: AWS::Region
  103. - us-east-1
  104. - Fn::Equals:
  105. - Ref: AWS::Region
  106. - us-east-2
  107. - Fn::Equals:
  108. - Ref: AWS::Region
  109. - us-west-1
  110. - Fn::Equals:
  111. - Ref: AWS::Region
  112. - us-west-2

使用 cdk deploy 部署堆栈:

  1. $ cdk deploy

image.png
cdk deploy 命令将 CloudFormation 模板压缩为一个 zip 文件,然后将该 zip 文件上传到由 cdk deploy 操作,然后创建一个新的 CloudFormation 堆栈,该堆栈创建模板中的指定资源。

任务9. 在 CloudFormation 控制台查看堆栈

image.png
在 CloudFormation 控制台会看到 3 个堆栈:

  • aws-cloud9-Lab 堆栈是启动 Cloud9 IDE 创建的堆栈;
  • CDKToolkit 堆栈是由 cdk bootstrap 命令创建的,创建 S3 存储桶来保存堆栈需要的资源;
  • CdkPrimerStack 是由 cdk deploy 创建的,CdkPrimerStack 依据 construct 创建了 Lambda 函数;

任务10. 测试Lambda 函数

打开 Lambda 控制台,选择名称以 CdkPrimerStack-HelloHandler 开头的 Lambda 函数。

函数代码 部分,这里将看到与 Cloud9 控制台中创建的 Lambda 函数代码相同的 construct,现在是 Lambda 函数。
image.png

创建 测试事件 ,事件模板选择 apigateway-aws-proxy,活动名称 cdkHelloTest:
image.png
image.png
现在已经成功创建并测试了使用 CDK 创建的 Lambda 函数。

之后我们会在函数之前添加 API 网关。API 网关将公开一个公共 HTTP 访问节点,Internet 上的任何人都可以使用诸如 curl 或浏览器来访问它。

任务11. 安装 API Gateway Construct Library

这里我们将安装 API 网关模块。

将 API Gateway 安装到 CDK 堆栈,需要安装 API Gateway Construct Library:

  1. $ npm i @aws-cdk/aws-apigateway@1.50.0

任务12. 将 LambdaRestApi Construct 添加到堆栈

这里,将修改 lib/cdk-primer-stack.ts 文件以导入 API 网关模块,并将 LambdaRestApi Construct 添加到堆栈。

  1. import * as cdk from '@aws-cdk/core';
  2. //BEGIN TODO:1 - Import the Lambda module
  3. import * as lambda from '@aws-cdk/aws-lambda';
  4. import * as iam from '@aws-cdk/aws-iam';
  5. //END TODO:1
  6. //BEGIN TODO:3 - Add API Gateway module
  7. import * as apigw from '@aws-cdk/aws-apigateway';
  8. //TODO:3 End
  9. export class CdkPrimerStack extends cdk.Stack {
  10. constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
  11. super(scope, id, props);
  12. //BEGIN TODO:2 - Define a Lambda resource
  13. const PrimerRole = iam.Role.fromRoleArn(this, 'Role', 'arn:aws:iam::${Token[AWS::AccountId.0]}:role/CDKPrimerHelloHandlerLambdaRole', {
  14. mutable: false,
  15. });
  16. const hello = new lambda.Function(this, 'HelloHandler', {
  17. runtime: lambda.Runtime.NODEJS_12_X, // execution environment
  18. code: lambda.Code.fromAsset('lambda'), // code loaded from "Lambda" directory
  19. handler: 'hello.handler', // file is "hello", function is "handler"
  20. role: PrimerRole
  21. });
  22. //END TODO:2
  23. //BEGIN TODO:4 - Defines an API Gateway REST API resource backed by your "hello" function.
  24. new apigw.LambdaRestApi(this, 'Endpoint', {
  25. handler: hello,
  26. cloudWatchRole: false
  27. });
  28. //END TODO:4
  29. }
  30. }
  • 新的代码中定义了一个 API 网关,该网关将代理所有请求到 Lambda 函数;
  • 添加属性 cloudWatchRole: false 可阻止 CDK 在 CloudFormation 模板中生成角色;

任务13. 查看即将进行的更改

这里,学习如何在实际部署更改之前查看要对堆栈实施的更改,这里通过 cdk diff 实现的。

  1. $ cdk diff

它将向堆栈中添加 10个新资源:
image.png

任务14. 部署堆栈

将上面的更改使用 cdk deploy 部署到堆栈中。

  1. $ cdk deploy

这里将看到一条警告信息:

  1. This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
  2. (NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
  3. Do you wish to deploy these changes (y/n)?

这是一条警告信息,部署堆栈会带来一些风险,它将更改 IAM 语句。为了使应用正常运行,需要运行这些更改。

部署成功后,搜到一条输出信息:

  1. CdkPrimerStack
  2. Outputs:
  3. CdkPrimerStack.Endpoint = URL-OF-API-GATEWAY-ENDPOINT
  4. Stack ARN:
  5. arn:aws:cloudformation:us-west-2:ACCOUNT-ID:stack/CdkPrimerStack/6dfb7ea0-e61d-11ea-afd3-0a83d4208d3a

这是由 API 网关 construct 自动添加的堆栈输出,其中包括 API 网关端点的 URL。

任务15. 测试应用程序

这里,使用 curl 和浏览器测试与应用程序的连接性。

  1. $ curl https://pzipen13x0.execute-api.us-west-1.amazonaws.com/prod/

image.png

浏览器中打开上面的 URL,网页显示: Hello, CDK! You've hit /

下面的实验中,我们将编写自己的名为 HitCounter 的 construct。该 construct 可以附加到用作 API 网关后端的任何 Lambda 函数,并且它将计算向每个 URL path 发出了多少个请求。它会将此数字存储在 DynamoDB 表中。

如果要重复使用 HitCounter construct 或将其公开使用,可以将其发布到 NPM。你还可以在发布前通过使用 package.json 文件中的 cdk 关键字,将它添加到 CDK Construct Catalog,它是一个多语言 CDK 库。

任务16. 创建 HitCounter Construct

这里,将在 lib 目录下创建 hitcounter.ts 文件,并在其中为你的应用程序创建新的 Construct。
AWS Cloud Development Kit 入门示例 - 图12

  1. import * as cdk from '@aws-cdk/core';
  2. import * as lambda from '@aws-cdk/aws-lambda';
  3. export interface HitCounterProps {
  4. /** the function for which you want to count url hits **/
  5. downstream: lambda.IFunction;
  6. }
  7. export class HitCounter extends cdk.Construct {
  8. constructor(scope: cdk.Construct, id: string, props: HitCounterProps) {
  9. super(scope, id);
  10. }
  11. }
  • 声明了一个名为 HitCounter 的新 Construct Class;
  • 构造函数参数是 scope、id 和 props,可以将它们传导向 cdk.Construct 父类;
  • props 参数是 HitCounterProps 类型,它只有一个 lambda.IFunction 类型的 downstream 属性。你可以在前面的 Lambda 函数中调用它来计数。

任务17. 创建 Hit Counter Lambda Handler

这里,将在 lambda 目录创建一个名为 hitcounter.js 文件。

  1. const { DynamoDB, Lambda } = require('aws-sdk');
  2. exports.handler = async function(event) {
  3. console.log("request:", JSON.stringify(event, undefined, 2));
  4. // create AWS SDK clients
  5. const dynamo = new DynamoDB();
  6. const lambda = new Lambda();
  7. // update dynamo entry for "path" with hits++
  8. await dynamo.updateItem({
  9. TableName: process.env.HITS_TABLE_NAME,
  10. Key: { path: { S: event.path } },
  11. UpdateExpression: 'ADD hits :incr',
  12. ExpressionAttributeValues: { ':incr': { N: '1' } }
  13. }).promise();
  14. // call downstream function and capture response
  15. const resp = await lambda.invoke({
  16. FunctionName: process.env.DOWNSTREAM_FUNCTION_NAME,
  17. Payload: JSON.stringify(event)
  18. }).promise();
  19. console.log('downstream response:', JSON.stringify(resp, undefined, 2));
  20. // return response back to upstream caller
  21. return JSON.parse(resp.Payload);
  22. };

代码中依赖了两个环境变量:

  • HITS_TABLE_NAME 是用于存储的 DynameoDB 表的名称;
  • DOWNSTREAM_FUNCTION_NAME 是 downstream Lmabda 函数名称。

由于表名称和 downstream 函数名称是在你部署应用时才生成的,因此,你需要在 construct 代码指定这些值。

任务18. 将资源添加到 hit counter construct

这里,需要安装 DynamoDB 模块,然后,对 lib/hitcounter.ts 文件做三个更新:

  • 导入 DynamoDB 模块;
  • 定义 Lambda 函数;
  • 将 DynamoDB 表添加到 HitCounter construct 中;

终端中运行命令来安装 DynamoDB Construct Lib:

  1. npm i @aws-cdk/aws-dynamodb@1.50.0

编辑 lib/hitcounter.ts 文件:

  1. import * as cdk from '@aws-cdk/core';
  2. import * as lambda from '@aws-cdk/aws-lambda';
  3. //BEGIN TODO:5
  4. import * as iam from '@aws-cdk/aws-iam';
  5. import * as dynamodb from '@aws-cdk/aws-dynamodb';
  6. //END TODO:5
  7. export interface HitCounterProps {
  8. /** the function for which you want to count url hits **/
  9. downstream: lambda.IFunction;
  10. }
  11. export class HitCounter extends cdk.Construct {
  12. //BEGIN TODO:6
  13. /** allows accessing the counter function */
  14. public readonly handler: lambda.Function;
  15. //END TODO:6
  16. constructor(scope: cdk.Construct, id: string, props: HitCounterProps) {
  17. super(scope, id);
  18. //BEGIN TODO:7
  19. const PrimerHitCounterRole = iam.Role.fromRoleArn(this, 'Role', 'arn:aws:iam::${Token[AWS::AccountId.0]}:role/HelloHitCounterServiceRole', {
  20. mutable: false,
  21. });
  22. const table = new dynamodb.Table(this, 'Hits', {
  23. partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING }
  24. });
  25. this.handler = new lambda.Function(this, 'HitCounterHandler', {
  26. runtime: lambda.Runtime.NODEJS_12_X,
  27. handler: 'hitcounter.handler',
  28. role: PrimerHitCounterRole,
  29. code: lambda.Code.fromAsset('lambda'),
  30. environment: {
  31. DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
  32. HITS_TABLE_NAME: table.tableName
  33. }
  34. });
  35. //END TODO:7
  36. }
  37. }
  • 为 IAM 和 DynamoDB 模块添加了导入语句;
  • 添加了以个 construct,该 construct 创建了一个名为 PrimerHitCounterRole 角色
    • HitCounterHandler 函数使用此角色;
    • 该角色具有附加的 AWSLambdaBasicExecutionRole 策略以及名为 HelloHitCounterDefaultPolicy 的预构建策略。此策略授予必要的 DynamoDB 权限,以读/写表中的值;
    • 代码中的 HelloHitCounterServiceRole 角色需要在 IAM 控制台创建,不然无法 deploy 成功;
  • 定义了一个 DynamoDB 表,其中 path 作为分区键。
  • 定义了一个绑定到 lambda/hitcounter.handler 的 Lambda 函数;
  • 将环境变量链接到资源的 functionName 和 tableName;

该 functionName 和 tableName,当你部署堆栈的时候才会赋值。

任务19. 将 Hit Counter 添加到堆栈

这里,需要更新 lib/cdk-primer-stack.ts 文件。

  • 将 HitCounter 导入到文件中(TODO:8);
  • 将关联着 HelloHitCounter 函数的 helloWithCounter construct 添加到 lib/cdk-primer-stack.ts 文件(TODO:9);
  • 将 API Gateway handler 从 hello 更新到 helloWithCounter.handler(TODO:10);
    • 这意味着只要你的 HTTP 端点被命中,API Gateway 就会将请求路由到 HitCounter handler,该 handler 将记录 hit 并将请求传递给 hello 函数,然后,响应将以相反的顺序一直传递回用户; ```typescript import as cdk from ‘@aws-cdk/core’; //BEGIN TODO:1 - Import the Lambda module import as lambda from ‘@aws-cdk/aws-lambda’; import as iam from ‘@aws-cdk/aws-iam’; //END TODO:1 //BEGIN TODO:3 - Add API Gateway module import as apigw from ‘@aws-cdk/aws-apigateway’; //BEGIN TODO:8 import { HitCounter } from ‘./hitcounter’; //END TODO:8

//TODO:3 End export class CdkPrimerStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); //BEGIN TODO:2 - Define a Lambda resource const PrimerRole = iam.Role.fromRoleArn(this, ‘Role’, ‘arn:aws:iam::${Token[AWS::AccountId.0]}:role/CDKPrimerHelloHandlerLambdaRole’, { mutable: false, }); const hello = new lambda.Function(this, ‘HelloHandler’, { runtime: lambda.Runtime.NODEJS_12_X, // execution environment code: lambda.Code.fromAsset(‘lambda’), // code loaded from “Lambda” directory handler: ‘hello.handler’, // file is “hello”, function is “handler” role: PrimerRole }); //END TODO:2

  1. //BEGIN TODO:9
  2. const helloWithCounter = new HitCounter(this, 'HelloHitCounter', {
  3. downstream: hello
  4. });
  5. //END TODO:9
  6. //BEGIN TODO:4 - Defines an API Gateway REST API resource backed by your "hello" function.
  7. new apigw.LambdaRestApi(this, 'Endpoint', {
  8. //BEGIN TODO:10
  9. handler: helloWithCounter.handler,
  10. //END TODO:10
  11. cloudWatchRole: false
  12. });

//END TODO:4 } }

  1. <a name="T9Vyl"></a>
  2. ## 任务20. 授予权限以允许 Lambda 读写 DynamoDB
  3. 这里,将给 `lib/hitcounter.ts` 添加权限。
  4. 将两个权限添加到文件(TODO: 11):
  5. - 第一个权限向 Lambda 角色(HitCounter)授予对 DynamoDB 表的读写权限;
  6. - 第二个权限授予 Lambda 角色(HitCounter)权限,以调用下游 Lambda 函数;
  7. ```typescript
  8. import * as cdk from '@aws-cdk/core';
  9. import * as lambda from '@aws-cdk/aws-lambda';
  10. //BEGIN TODO:5
  11. import * as iam from '@aws-cdk/aws-iam';
  12. import * as dynamodb from '@aws-cdk/aws-dynamodb';
  13. //END TODO:5
  14. export interface HitCounterProps {
  15. /** the function for which you want to count url hits **/
  16. downstream: lambda.IFunction;
  17. }
  18. export class HitCounter extends cdk.Construct {
  19. //BEGIN TODO:6
  20. /** allows accessing the counter function */
  21. public readonly handler: lambda.Function;
  22. //END TODO:6
  23. constructor(scope: cdk.Construct, id: string, props: HitCounterProps) {
  24. super(scope, id);
  25. //BEGIN TODO:7
  26. const PrimerHitCounterRole = iam.Role.fromRoleArn(this, 'Role', 'arn:aws:iam::${Token[AWS::AccountId.0]}:role/HelloHitCounterServiceRole', {
  27. mutable: false,
  28. });
  29. const table = new dynamodb.Table(this, 'Hits', {
  30. partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING }
  31. });
  32. this.handler = new lambda.Function(this, 'HitCounterHandler', {
  33. runtime: lambda.Runtime.NODEJS_12_X,
  34. handler: 'hitcounter.handler',
  35. role: PrimerHitCounterRole,
  36. code: lambda.Code.fromAsset('lambda'),
  37. environment: {
  38. DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
  39. HITS_TABLE_NAME: table.tableName
  40. }
  41. });
  42. //END TODO:7
  43. //BEGIN TODO:11
  44. // grant the Lambda role read/write permissions to your table
  45. table.grantReadWriteData(this.handler);
  46. // grant the Lambda role invoke permissions to the downstream function
  47. props.downstream.grantInvoke(this.handler);
  48. //END TODO:11
  49. }
  50. }

任务21. 将更新部署到堆栈并测试

这里,会将最新的更新部署到堆栈中,这将导致以前的 Lambda 权限被删除并添加新权限。

运行 cdk deploy 部署更新到堆栈;

demoUser01:~/environment/cdk-primer (master) $ cdk deploy
Do you wish to deploy these changes (y/n)? y
CdkPrimerStack: deploying...
[0%] start: Publishing 1967a68f930e8a45e00391958bc85e16ad9ccbb07e2bb75a8dd5000a272b315b:current
[100%] success: Published 1967a68f930e8a45e00391958bc85e16ad9ccbb07e2bb75a8dd5000a272b315b:current
CdkPrimerStack: creating CloudFormation changeset...


 ✅  CdkPrimerStack

Outputs:
CdkPrimerStack.Endpoint8024A810 = https://pzipen13x0.execute-api.us-west-1.amazonaws.com/prod/

Stack ARN:
arn:aws:cloudformation:us-west-1:093103642919:stack/CdkPrimerStack/4e04c260-32bb-11eb-9e5d-06c1d6f23d3b

image.png :::info 注意 IAM Statements Changes:

  • 首先删除先前的 Lambda 权限以减号 [-] 列出,并以红色文本列出;
    • 删除了4条现有语句。
  • 然后,将添加新的 Lambda 权限以加号 [+] 和绿色文本列出:

    • 添加了4个新语句。 :::
  • 从 Cloud9 命令行或在浏览器中访问: ```shell $ curl https://pzipen13x0.execute-api.us-west-1.amazonaws.com/prod/

HTTP/2 200 content-type: text/plain content-length: 25 x-amzn-requestid: adc12025-44eb-432c-bc64-de6137e7d594 x-amz-apigw-id: RyBXrHZevHcFjwQ= x-amzn-trace-id: Root=1-5f43e297-8cf1a4779d35fa94b166a2e7;Sampled=0 x-cache: Miss from cloudfront via: 1.1 1ec2938341958d70d56193d709c89def.cloudfront.net (CloudFront) x-amz-cf-pop: SEA19-C1 x-amz-cf-id: P10lh5XX9VrJxGl8AcYIpqFSxIBTS0eoSUSU0lbJ5BRxH6hs9uFAJw==

Hello, CDK! You’ve hit /


- 在浏览器中进行几次测试,最后更改 URL,以将条目添加到 hit counter:
   - OUTPUT-URL/test1
   - OUTPUT-URL/test2
   - OUTPUT-URL/test3
- 输出结果:
   - Hello, CDK! You've hit /test1
   - Hello, CDK! You've hit /test2
   - Hello, CDK! You've hit /test3

- 查看 DynamoDB 控制台,打开 `CdkPrimerStack-HelloHitCounter` 表:

        ![](https://cdn.nlark.com/yuque/0/2020/png/1471554/1606745332282-74ed3b15-c7be-486e-8042-40c6e61c22e7.png#align=left&display=inline&height=189&margin=%5Bobject%20Object%5D&originHeight=189&originWidth=285&size=0&status=done&style=none&width=285)

<a name="Qm2em"></a>
## 任务22. Testing Constructs
这部分实验,将使用 `Fine-Grained Assertions(精细断言)` 和 `Validation(验证)` 类型测试。

- `Fine-Grained Assertions` 测试生成的 CloudFormation 模板的特定方向,例如“此资源指定属性具有某个特定的值”。这些测试在开发新功能时会有帮助,因为添加的任何代码都会导致测试失败,即使现有功能仍然有效。这种情况时,精细断言测试将确保现有功能不受影响;
- `Validation` 测试可确保你的 CDK construct 在传递无效数据时引发错误,从而帮助你“快速失败”。

**AWS CDK Assert Library:**<br />下面的任务中,将使用 AWS CDK assert(@aws-cdk/assert)library。该库包含了几个用于编写单元测试和集成测试的辅助函数。

这个实验主要使用 `haveResource` 函数,当你关心特定类型的资源存在并且某些属性设置为特定值时,可使用此辅助函数:
```typescript
expect(stack).to(haveResource('AWS::CertificateManager::Certificate', {
    DomainName: 'test.example.com',
    // Note: some properties omitted here

    ShouldNotExist: ABSENT
}));

ABSENT 是一个值,用于断言对象中的特定键未设置(或设置为 undefined )。

任务23. Fine-grained assertion tests

这里假定你已创建 hit counter construct. HitCounter construct 创建一个简单的 DynamoDB 表。现在创建一个测试来验证是否正在创建表。

打开 cdk-primer/test/cdk-primer.test.ts 文件,使用以下内容替换原有内容:

import { expect as expectCDK, haveResource } from '@aws-cdk/assert';
import cdk = require('@aws-cdk/core');
import { HitCounter }  from '../lib/hitcounter';
import * as lambda from '@aws-cdk/aws-lambda';

test('DynamoDB Table Created', () => {
  const stack = new cdk.Stack();
  // WHEN
  new HitCounter(stack, 'MyTestConstruct', {
    downstream:  new lambda.Function(stack, 'TestFunction', {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: 'lambda.handler',
      code: lambda.Code.inline('test')
    })
  });
  // THEN
  expectCDK(stack).to(haveResource("AWS::DynamoDB::Table"));
});

此测试正在做测试,以确保 synthesized stack 包含一个 DynamoDB 表。

使用以下命令运行测试:

$ npx jest

 PASS  test/cdk-primer.test.ts (5.811s)
  ✓ DynamoDB Table Created (279ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        6.941s
Ran all test suites.

任务24. Validation tests

除了测试创建的 Lambda 函数,你可能还需要测试创建的 环境变量 DOWNSTREAM_FUNCTION_NAMEHITS_TABLE_NAME

之前在创建 Lambda 函数时,环境变量值其实是对其他构造的引用:

this.handler = new lambda.Function(this, 'HitCounterHandler', {
  runtime: lambda.Runtime.NODEJS_12_X,
  handler: 'hitcounter.handler',
  code: lambda.Code.asset('lambda'),
  environment: {
    DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
    HITS_TABLE_NAME: table.tableName
  }
});

此时,实际上还不知道 functionName 或 tableName 的值是什么,因为 AWS CDK 会计算一个 hash 值以附加到 construct 名称的末尾。因此,现在仅使用一个虚拟值。运行测试后,测试将 失败 并显示期望值。

打开 test/cdk-primer.test.ts 文件,将新测试添加到文件中:

import { expect as expectCDK, haveResource } from '@aws-cdk/assert';
import cdk = require('@aws-cdk/core');
import { HitCounter }  from '../lib/hitcounter';
import * as lambda from '@aws-cdk/aws-lambda';

test('DynamoDB Table Created', () => {
  const stack = new cdk.Stack();
  // WHEN
  new HitCounter(stack, 'MyTestConstruct', {
    downstream:  new lambda.Function(stack, 'TestFunction', {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: 'lambda.handler',
      code: lambda.Code.inline('test')
    })
  });
  // THEN
  expectCDK(stack).to(haveResource("AWS::DynamoDB::Table"));
});

//BEGIN TODO:12
test('Lambda Has Environment Variables', () => {
  const stack = new cdk.Stack();
  // WHEN
  new HitCounter(stack, 'MyTestConstruct', {
    downstream:  new lambda.Function(stack, 'TestFunction', {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: 'lambda.handler',
      code: lambda.Code.inline('test')
    })
  });
  // THEN
  expectCDK(stack).to(haveResource("AWS::Lambda::Function", {
    Environment: {
      Variables: {
        DOWNSTREAM_FUNCTION_NAME: "TestFunction",
        HITS_TABLE_NAME: "MyTestConstructHits"
      }
    }
  }));
})
//END TODO:12

执行以下命令运行测试:

npx jest

测试应该会失败,并且应该能够从预期的输出中获取变量的正确值:

"Runtime": "nodejs12.x",
            "Environment": {
              "Variables": {
                "DOWNSTREAM_FUNCTION_NAME": {
                  "Ref": "TestFunction22AD90FC"
                },
                "HITS_TABLE_NAME": {
                  "Ref": "MyTestConstructHits24A357F0"
                }
              }

以输出部分获得的值,更新代码中 DOWNSTREAM_FUNCTION_NAMEHITS_TABLE_NAME 变量:

import { expect as expectCDK, haveResource } from '@aws-cdk/assert';
import cdk = require('@aws-cdk/core');
import { HitCounter }  from '../lib/hitcounter';
import * as lambda from '@aws-cdk/aws-lambda';

test('DynamoDB Table Created', () => {
  const stack = new cdk.Stack();
  // WHEN
  new HitCounter(stack, 'MyTestConstruct', {
    downstream:  new lambda.Function(stack, 'TestFunction', {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: 'lambda.handler',
      code: lambda.Code.inline('test')
    })
  });
  // THEN
  expectCDK(stack).to(haveResource("AWS::DynamoDB::Table"));
});

test('Lambda Has Environment Variables', () => {
  const stack = new cdk.Stack();
  // WHEN
  new HitCounter(stack, 'MyTestConstruct', {
    downstream:  new lambda.Function(stack, 'TestFunction', {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: 'lambda.handler',
      code: lambda.Code.inline('test')
    })
  });
  // THEN
  expectCDK(stack).to(haveResource("AWS::Lambda::Function", {
    Environment: {
      Variables: {
        //BEGIN TODO:13
        DOWNSTREAM_FUNCTION_NAME: {"Ref": "VALUE_GOES_HERE"},
        HITS_TABLE_NAME: {"Ref": "VALUE_GOES_HERE"}
        //END TODO:13
      }
    }
  }));
});

再次运行测试,这次会测试通过:

npx jest

 PASS  test/cdk-primer.test.ts (5.45s)
  ✓ DynamoDB Table Created (282ms)
  ✓ Lambda Has Environment Variables (107ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        6.229s
Ran all test suites.

任务25. 清理资源

cdk destory