Logo
Section 1 / 11

设置最终应用程序的环境

学习如何使用 AWS 的 REST API。

导入

设置最终应用程序的环境

学习如何使用 AWS 的 REST API。

我们准备好进行最后的动手实践了!我们将再次编写一个相对完整的应用程序,这次更强调测试驱动开发、与外部世界通信以及数据存储。

REST API

在这个应用中,我们将处理酒店预订。相比上一个项目,我们的范围有所扩展:现在将引入 REST API,并将数据保存到数据库中。所有内容仍将部署在 AWS 云环境中。

...
REST API

不过,我们也会省略一些功能。我们不会处理用户注册和身份验证。此外,由于时间关系,我们也不会编写前端(比如网站)。这只会分散我们的注意力,而我们的目标是编写功能性的 TypeScript 代码。尽管如此,React 框架本身具有很强的功能性特点,鼓励使用纯函数并由小组件(如按钮和列表)构建应用。因此,在精神上,它与功能型后端非常契合。

我们还将随着开发逐步扩展项目。首先从一个接收预订请求的端点开始。由于我们要修改数据,添加预订的正确 HTTP 方法是 POST。我们期望收到基本的信息,包括酒店、开始和结束日期,以及预订日期。结束日期是退房要求的时间,房间将在当天某个时间点重新释放。我们还会收到用户的 ID,假设这是由我们的认证服务随请求一同发送的。系统将验证接收到的信息,查找酒店,并完成预订。

环境设置

让我们开始设置环境。显然,我们需要一个新的空项目。在项目根目录下添加一个 package.json:

1{ 2 "name": "leanpub-reservations", 3 "version": "1.0.0", 4 "description": "Example reservations project with fp-ts and aws", 5 "scripts": { 6 "build-all-local": "lerna clean -y && lerna exec -- npm ci", 7 "build-all": "lerna exec -- npm run build", 8 "test-all": "lerna exec -- npm run test" 9 }, 10 "dependencies": {}, 11 "devDependencies": { 12 "jest": "^26.5.2", 13 "ts-jest": "^26.4.1", 14 "typescript": "^4.0.3", 15 "ts-loader": "^5.3.3", 16 "webpack": "^5.3.0", 17 "webpack-cli": "^4.1.0", 18 "lerna": "^3.22.1" 19 }, 20 "author": "Sam Van Overmeire", 21 "license": "ISC" 22}

其中一些内容可能看起来很熟悉。我们添加了一些稍后会用到的开发依赖项。新增的是 lerna。它对我们很有帮助,因为每个 Lambda 将拥有独立的 package.json 和配置。借助一些 lerna 命令,我们可以从项目根目录运行所有必要的构建和测试命令。例如,scripts 部分中的 build-all 命令告诉 lerna 在每个包含 package.json 的文件夹中执行 npm run build。我们可以在名为 lerna.json(位于项目根目录)的文件中指定 lerna 应该查找这些文件夹的目录。

1{ 2 "packages": [ 3 "lambdas/*" 4 ], 5 "version": "1.0.0" 6}

在我们的场景中,lerna 需要检查一个名为 lambdas 的目录,我们稍后会创建它。

注意:使用 webpack 时,我们还可以通过 entry-points 的方式来构建多个项目,以生成输出的 JS 文件。我们仍然需要自定义代码来打包成 ZIP 文件,但当有大量 Lambda 时,这是一种不错的解决方案。

注意:由于无服务器(serverless)是一个相对较新的领域,项目设置的标准仍不够统一,尤其与 Java 开发者熟悉的 Maven 标准化结构相比。总体而言,这种做法与语言无关,我们希望将各个 Lambda 分离,这样更容易保持每个 Lambda 的专注性,使代码更清晰,压缩包更小,从而加快启动速度。对于共享代码,我们可以创建工具文件夹。

我们需要一个 SAM 模板来描述基础设施。添加如下内容:

1AWSTemplateFormatVersion: 2010-09-09 2Transform: AWS::Serverless-2016-10-31 3 4Parameters: 5 Environment: 6 Type: String 7 Default: dev 8 9Globals: 10 Function: 11 Runtime: nodejs12.x 12 Handler: main.handler 13 Environment: 14 Variables: 15 ENVIRONMENT: !Ref Environment 16 17Resources: 18 CreateLambda: 19 Type: AWS::Serverless::Function 20 Properties: 21 MemorySize: 512 22 Timeout: 15 23 CodeUri: lambdas/create/dist/create.zip 24 Policies: 25 - AWSLambdaExecute 26 Events: 27 Api: 28 Type: Api 29 Properties: 30 Path: /create 31 Method: GET 32 RestApiId: !Ref RestApi 33 34 RestApi: 35 Type: AWS::Serverless::Api 36 DeletionPolicy: "Retain" 37 Properties: 38 StageName: Prod 39 40Outputs: 41 ApiGatewayUrl: 42 Description: "API Prod stage endpoint" 43 Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
  • 第 4–7 行:我们在基础设施中添加了一个参数。Parameters 中的值是可自定义的,必须在调用 CloudFormation 时传入(虽然可以设置默认值,此处为 dev)。Environment 是常见参数,因为通常需要区分不同环境(开发、预发布、生产等)。这里仅用于演示概念,因为我们只部署到一个环境。
  • 第 9–15 行:Globals 是 SAM 模板中的一个特殊部分,有助于避免重复。在这里,我们可以设置应用于模板中所有函数的通用值。因此,每个 Lambda 都将以 Node 运行,主文件中有一个 handler 函数。它们还会有一个名为 ENVIRONMENT 的环境变量。由于使用了 !Ref(即伪参数),该变量的值等于我们传入的 Environment 参数值,默认为 dev。
  • 第 24–31 行:大部分内容与之前类似。目前,函数只有基本权限,例如向 CloudWatch 写入日志,这得益于 AWSLambdaExecute 策略。Events 部分是新的。我们之前看到它可以用来按计划触发 Lambda。但这里,我们告诉 AWS:当对 /create 端点发起 HTTP GET 请求时,应触发此 Lambda(注意:这只是演示用途,后续方法将改为 HTTP POST)。
  • 第 32–38 行:该 Lambda 被置于 API Gateway 后面,这是一个由 AWS 管理的 REST API 服务。它为我们生成一个 URL,并根据请求的方法和路径决定流量去向。在本例中,唯一可用的是使用 GET 方法访问 /create,该请求会被路由到第 18 行指定的 Lambda。同样注意 !Ref:我们希望告诉 CreateLambda 它所处的 API 的 ID,使用 RestApiId。我们也可以在另一个模板或堆栈中创建 API,并传入或硬编码这个 ID。但使用 !Ref,我们告诉 AWS 查找名为 RestApi 的资源,并填入其 ID。
  • 第 40–43 行:Outputs 是堆栈创建后产生的值。这里我们输出 API Gateway 的基础 URL,以便测试应用。如果我们想调用创建接口,只需获取这个输出(可在 CloudFormation 控制台查看),然后附加 /create。

注意:如果我们频繁在编写 CloudFormation 模板时出错并感到沮丧,可以选择使用更高层次的工具,如 AWS CDK,或者添加像 cfn-lint 这样的工具来检查模板错误(如不存在的资源类型、无效值、伪参数误用等)。此外,IDE 如 IntelliJ 也有 CloudFormation 插件,可提供即时反馈。

项目根目录的最后一个步骤是添加一个 Bash 脚本,用于整合所有流程。

1#!/bin/bash 2 3set -eo pipefail 4 5STACKNAME="reservations-example" 6 7if [ $# -lt 1 ]; then 8 echo "Needs a bucket!" 9 echo "Usage ./setup.sh <bucket>" 10 exit 1 11fi 12 13BUCKET=$1 14 15echo "Running build" 16# npm run test-all => activate this when we actually have tests 17npm run build-all 18 19echo "Deploying stack" 20aws cloudformation package \ 21--template-file infrastructure.yaml \ 22--s3-bucket "${BUCKET}" \ 23--output-template-file outputSamTemplate.yaml 24 25aws cloudformation deploy \ 26--template-file outputSamTemplate.yaml \ 27--stack-name ${STACKNAME} \ 28--capabilities CAPABILITY_IAM 29 30echo "Removing temporary SAM file" 31rm outputSamTemplate.yaml

除了 stackname 和 npm run build-all 外,其余内容与之前的脚本完全相同。npm run test-all 命令被注释掉,因为我们目前还没有测试。在专业环境中,我们可能会考虑使用 CodePipeline、Jenkins 或 CircleCI 来部署代码。AWS SAM CLI 也提供了一些命令,帮助我们构建和部署无服务器项目。

现在创建一个 lambdas 目录,并在其内部创建一个名为 create 的子目录。

1{ 2 "name": "leanpub-reservations-create-reservation", 3 "version": "1.0.0", 4 "description": "Lambda for creating a new reservation", 5 "scripts": { 6 "clean": "rm -rf node_modules && rm -rf dist", 7 "webpack": "webpack", 8 "zip": "cd dist && zip -r create.zip *", 9 "build": "npm i && webpack && npm run zip", 10 "test": "jest --coverage" 11 }, 12 "dependencies": { 13 "fp-ts": "^2.8.3" 14 }, 15 "devDependencies": { 16 "@types/jest": "^26.0.14" 17 }, 18 "author": "Sam Van Overmeire", 19 "license": "ISC" 20}

脚本与前一章类似。但这一次,lerna 将调用它们,而不是我们直接运行。同样,我们需要 jest、webpack 和 ts config 配置。

1module.exports = { 2 roots: [ 3 '<rootDir>', 4 ], 5 testMatch: [ 6 '**/__tests__/**/*.+(ts|tsx|js)', 7 ], 8 transform: { 9 '^.+\\.(ts|tsx)$': 'ts-jest', 10 }, 11};
1const path = require("path"); 2 3module.exports = { 4 resolve: { 5 extensions: [".js", ".jsx", ".json", ".ts", ".tsx"], 6 }, 7 output: { 8 libraryTarget: "commonjs", 9 path: path.join(__dirname, "dist"), 10 filename: "[name].js", 11 }, 12 target: "node", 13 externals: ["aws-sdk"], 14 module: { 15 rules: [{ test: /\.tsx?$/, loader: "ts-loader" }], 16 }, 17};
1{ 2 "compilerOptions": { 3 "lib": [ 4 "es2017", 5 "esnext" 6 ], 7 "esModuleInterop": true, 8 "moduleResolution": "node", 9 "noUnusedLocals": true, 10 "noUnusedParameters": true, 11 "sourceMap": true, 12 "target": "es2017", 13 "outDir": "dist", 14 "noImplicitThis": true, 15 "noImplicitAny": true, 16 "strictBindCallApply": true 17 }, 18 "exclude": [ 19 "node_modules", 20 "__tests__", 21 "dist" 22 ] 23}

特别重要的是排除测试文件夹。否则,webpack 会扫描这些文件并报错,提示找不到我们正在讨论的 Jest 内容(因为 Jest 是开发依赖项)。现在创建一个 src 文件夹,并在其中放置一个入口文件:

1export const handler = async () => { 2 return { 3 statusCode: 200, 4 body: JSON.stringify({ id: 'some-id' }), 5 } 6};

这也很熟悉。这是一个非常基础的 Lambda 处理函数,由 AWS 在调用 CreateLambda 时触发。仅仅返回一个“hello world”字符串是不够的,因为缺少状态码,任何调用该端点的人都会认为发生了错误,并抛出内部服务器错误。因此,我们返回一个 HTTP statusCode,并在 body 中放入一个示例预订 ID 作为字符串。

实现

No configuration found for ID: f5a5c3e84fafd5f47f7e331d47284922348fa1469c08b25c9a330760d303d97f

现在,我们可以运行设置脚本,并传入一个 S3 存储桶名称,它将生成 ZIP 包并部署我们的应用。

  1. 在终端中运行以下命令,将应用部署到 AWS:
1./setup.sh THE_NAME_OF_YOUR_BUCKET
  1. 如果部署成功,我们可以在 CloudFormation 控制台中找到新后端的基础 URL。我们也可以使用以下命令获取它:
1aws cloudformation describe-stacks \ 2--stack-name reservations-example \ 3--query "Stacks[0].Outputs[0].OutputValue"

该命令从堆栈中获取信息,并选择第一个(也是唯一一个)堆栈的第一个(也是唯一一个)输出,其中应包含 URL。如果我们在浏览器中使用该 URL 并附加 /create 路径,应该会返回一个包含 id 属性的对象,其值为 some-id。

小贴士:一些有用的 JavaScript 和 TypeScript

编写我们的第一个创建 Lambda 函数代码