开始使用 AWS、无服务器和 TypeScript
大约五个月前,我加入了fleet.space团队,负责构建云基础设施以支持他们的纳米卫星星座和工业物联网网络,从此踏入了无服务器的世界。
我之前一直很难找到一份关于如何使用 TypeScript 构建新服务的全面指南,所以现在我来写一份我梦寐以求的指南。我们不会提供任何示例代码,我们只会专注于构建一个强大的基础模板,供你所有服务复用。
我们要做的第一件事是安装无服务器框架。我发现它比官方的 AWS SAM 模板有更好的支持。
首先,让我们通过 npm i -g serverless 将 Serverless 安装为系统的全局依赖项。接下来,我们将创建一个项目 mkdir typescript-serverless。在该目录中,我们使用以下命令搭建一个新的 Serverless 模板:
sls create --template aws-nodejs-typescript
这将使用 TypeScript 生成一个基本的模板,但它缺少很多我反复使用的强大配置。所以让我们来设置一下。如果您不使用 VS Code,请删除此模板初始化的烦人的 VS Code 目录,然后运行 npm i 来安装无服务器框架的基本依赖项。
无服务器插件
与 SAM 相比,无服务器框架有一个优势,那就是有很多社区插件围绕它构建,可以帮助你完成各种工作(如果需要,你也可以自行构建)。它的扩展性非常强。我几乎在每项服务中都会用到以下插件:
此插件允许您在函数级别(而非默认的项目级别)定义 IAM 权限。如果只有一个函数需要访问 DynamoDB,则无需授予所有函数访问权限。
在无服务器世界中,多区域部署本质上是免费的(至少与容器/EC2 相比)。唯一的复杂性在于保持 DynamoDB 同步。这可以通过全局表来实现。
这个我最近不怎么用,但它只是一个开发依赖,用起来很方便。它能让你在本地调用你的 lambda API。
对于无服务器来说,这是一个潜在的风险,尤其是在频繁部署的情况下。Lambda 会对您部署的每个函数进行版本控制,并且存储量有严格的限制。此插件将删除不需要的旧版本,并防止这些细微的错误影响您的生产环境。
npm i -D serverless-iam-roles-per-function serverless-create-global-dynamodb-table serverless-offline serverless-prune-plugin
我们还将添加aws-sdk和aws-lambda
npm i aws-sdk aws-lambda.
Lambda电动工具
初入无服务器领域,我真正困扰的一件事就是可观察性和可追溯性。跨服务边界,甚至跨边界基础设施(Lambda > SQS > Lambda > Kinesis > Lambda > DynamoDB 等)的调试都十分痛苦。幸好,我遇到了一套强大的Lambda工具,它们在任何服务中都必不可少。
-
@dazn/lambda-powertools-cloudwatchevents-客户端
-
@dazn/lambda-powertools-correlation-ids
-
@dazn/lambda-powertools-logger
-
@dazn/lambda-powertools-pattern-basic
-
@dazn/lambda-powertools-lambda-客户端
-
@dazn/lambda-powertools-sns-客户端
-
@dazn/lambda-powertools-sqs-客户端
-
@dazn/lambda-powertools-dynamodb-客户端
-
@dazn/lambda-powertools-kinesis-客户端
我只需导入所有这些,然后让 webpack tree shake 处理掉我没有使用的内容。
npm i @dazn/lambda-powertools-cloudwatchevents-client @dazn/lambda-powertools-correlation-ids @dazn/lambda-powertools-logger @dazn/lambda-powertools-pattern-basic @dazn/lambda-powertools-lambda-client @dazn/lambda-powertools-sns-client @dazn/lambda-powertools-sqs-client @dazn/lambda-powertools-dynamodb-client @dazn/lambda-powertools-kinesis-client
代码检查
代码库中另一个必不可少的重要工具是 linting。ESLint 是我每天使用的最强大的工具之一。让我们配置它,使其能够与 TypeScript 和无服务器框架协同工作。我们需要以下开发依赖项。
npm i -D eslint eslint-config-airbnb-base typescript-eslint eslint-plugin-import eslint-import-resolver-alias eslint-plugin-module-resolver @typescript-eslint/eslint-plugin @typescript-eslint/parser
现在,我们需要创建一个 .eslintrc.json 配置文件来定义规则。我喜欢以下规则。此 gist 还包含一些我们将在最后设置的模块别名的映射,以及一些我们稍后将设置的 Jest 配置。
{ | |
"extends": [ | |
"airbnb-base", | |
"plugin:jest/all", | |
"plugin:import/errors", | |
"plugin:import/warnings", | |
"plugin:import/typescript", | |
"plugin:@typescript-eslint/recommended" | |
], | |
"plugins": [ | |
"jest", | |
"@typescript-eslint" | |
], | |
"root": true, | |
"globals": {}, | |
"rules": { | |
"import/no-unresolved": [2, {"commonjs": true, "amd": true}], | |
"import/prefer-default-export": "off", | |
"max-len": ["error", { | |
"code": 150, | |
"ignoreComments": true, | |
"ignoreTrailingComments": true, | |
"ignoreUrls": true, | |
"ignoreStrings": true, | |
"ignoreTemplateLiterals": true | |
}] | |
}, | |
"parser": "@typescript-eslint/parser", | |
"env": {}, | |
"overrides": [], | |
"settings": { | |
"import/resolver": { | |
"alias": { | |
"map": [ | |
["@src", "./src"], | |
["@tests", "./tests"], | |
["@queries", "./queries"] | |
], | |
"extensions": [ | |
".ts", | |
".js" | |
] | |
} | |
} | |
} | |
} |
{ | |
"extends": [ | |
"airbnb-base", | |
"plugin:jest/all", | |
"plugin:import/errors", | |
"plugin:import/warnings", | |
"plugin:import/typescript", | |
"plugin:@typescript-eslint/recommended" | |
], | |
"plugins": [ | |
"jest", | |
"@typescript-eslint" | |
], | |
"root": true, | |
"globals": {}, | |
"rules": { | |
"import/no-unresolved": [2, {"commonjs": true, "amd": true}], | |
"import/prefer-default-export": "off", | |
"max-len": ["error", { | |
"code": 150, | |
"ignoreComments": true, | |
"ignoreTrailingComments": true, | |
"ignoreUrls": true, | |
"ignoreStrings": true, | |
"ignoreTemplateLiterals": true | |
}] | |
}, | |
"parser": "@typescript-eslint/parser", | |
"env": {}, | |
"overrides": [], | |
"settings": { | |
"import/resolver": { | |
"alias": { | |
"map": [ | |
["@src", "./src"], | |
["@tests", "./tests"], | |
["@queries", "./queries"] | |
], | |
"extensions": [ | |
".ts", | |
".js" | |
] | |
} | |
} | |
} | |
} |
我还会调整 tsconfig 文件,添加 inlineSource、esModuleInterop、sourceRoot 和 baseUrl。以下要点还预填充了一些我们稍后会设置的模块别名信息。如果需要,您可以暂时注释掉路径中的任何内容。
{ | |
"compilerOptions": { | |
"lib": ["es2017"], | |
"removeComments": true, | |
"moduleResolution": "node", | |
"noUnusedLocals": true, | |
"noUnusedParameters": true, | |
"sourceMap": true, | |
"target": "es2017", | |
"outDir": "lib", | |
"inlineSources": true, | |
"esModuleInterop": true, | |
"sourceRoot": "/", | |
"baseUrl": "./", | |
"paths": { | |
"@src/*": ["src/*"], | |
"@queries/*": ["queries/*"], | |
"@tests/*": ["tests/*"] | |
} | |
}, | |
"include": ["./**/*.ts"], | |
"exclude": [ | |
"node_modules/**/*", | |
".serverless/**/*", | |
".webpack/**/*", | |
"_warmup/**/*", | |
".vscode/**/*" | |
] | |
} |
{ | |
"compilerOptions": { | |
"lib": ["es2017"], | |
"removeComments": true, | |
"moduleResolution": "node", | |
"noUnusedLocals": true, | |
"noUnusedParameters": true, | |
"sourceMap": true, | |
"target": "es2017", | |
"outDir": "lib", | |
"inlineSources": true, | |
"esModuleInterop": true, | |
"sourceRoot": "/", | |
"baseUrl": "./", | |
"paths": { | |
"@src/*": ["src/*"], | |
"@queries/*": ["queries/*"], | |
"@tests/*": ["tests/*"] | |
} | |
}, | |
"include": ["./**/*.ts"], | |
"exclude": [ | |
"node_modules/**/*", | |
".serverless/**/*", | |
".webpack/**/*", | |
"_warmup/**/*", | |
".vscode/**/*" | |
] | |
} |
测试
我将为每个服务编写测试,因此在基础模板中配置一个测试运行器是合理的。我个人比较喜欢Jest,所以我们就用它吧。
再次,我们需要用一些 npm dev 依赖项来填充 node_modules 的黑洞。
npm i -D jest babel-jest @babel/core @babel/preset-env @babel/preset-typescript
确保 Jest 在您的 .eslintrc.json 中配置为插件,并且在 env 下设置了 jest/globals(如果您复制了上面的要点,那么您已经在那里拥有它了)。
我们需要创建一个 Babel .config 以使 Jest 能够工作。
module.exports = { | |
presets: [ | |
[‘@babel/preset-env', { targets: { node: ‘current’ } }], | |
‘@babel/preset-typescript’, | |
], | |
}; |
module.exports = { | |
presets: [ | |
[‘@babel/preset-env', { targets: { node: ‘current’ } }], | |
‘@babel/preset-typescript’, | |
], | |
}; |
此时,我们应该检查 Jest 是否正常工作以及配置是否正确。让我们创建一个测试目录并添加一个示例测试。创建一个测试文件,并添加一个虚拟测试,tests/example.test.ts。
describe('who tests the tests?', () => {
it('can run a test', () => {
expect.hasAssertions();
expect(1).toBe(1);
});
});
如果您使用的是 WebStorm,可以按 Ctrl+Shift+R 直接从 IDE 运行此测试。否则,让我们更新 package.json 以添加测试脚本(以及 lint 和 TS 编译检查)。
在您的 package.json 文件中,更新脚本部分以包含以下内容:
"scripts": {
"test": "NODE_ENV=test ./node_modules/.bin/jest --ci --verbose",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"buildtest": "tsc --noEmit"
},
我通常会在 CI/CD 流水线中运行所有这些,以防止不良代码进入生产环境。现在,您可以从控制台运行 npm run test 来运行测试套件。希望您的测试套件能够运行并通过。理想情况下,您的 IDE 也不会在 example.test.ts 文件中抛出 linting 错误。
到这里,运行 npm run lint,看看默认模板是否存在任何 linting 错误。你可能会遇到 webpack.config 和自动搭建的 handler.ts 文件的一些错误。让我们来清理一下这些问题。
在 webpack.config 文件的顶部,添加*/* eslint-disable @typescript-eslint/no-var-requires */*
并取消注释模板自带的 Fork TS Checker Webpack 插件的默认设置。这样应该就能解决这个问题了。
对于 handler.ts 文件,只需从 hello 函数中删除未使用的 context-function 签名。
代码放在 /src 中
我喜欢的一个惯例是将所有领域逻辑放在 /src 目录中,并将配置(和 /tests)留在根目录中。创建一个 /src 目录,并将 handler.ts 文件移动到 /src 目录中。
如果您决定采用此约定,则需要转到 serverless.yml 并将处理程序的路径更新为 src/handler.hello。
让我们在 serverless.yml 文件中配置我们的无服务器插件。
service:
name: typescript-serverless
...
plugins:
- serverless-offline
- serverless-webpack
- serverless-iam-roles-per-function
- serverless-create-global-dynamodb-table
- serverless-prune-plugin
...
此时,您应该能够在终端中离线运行 sls,并进行干净的编译和构建,启动无服务器离线端点。
➜ typescript-serverless git:(master) ✗ sls offline
Serverless: Bundling with Webpack...
Time: 398ms
Built at: 27/02/2020 11:24:42 pm
Asset Size Chunks Chunk Names
src/handler.js 6.33 KiB src/handler [emitted] src/handler
Entrypoint src/handler = src/handler.js
[./src/handler.ts] 316 bytes {src/handler} [built]
[source-map-support/register] external "source-map-support/register" 42 bytes {src/handler} [built]
Serverless: Watching for changes...
Serverless: Starting Offline: dev/us-east-1.
Serverless: Routes for hello:
Serverless: GET /hello
Serverless: POST /{apiVersion}/functions/typescript-serverless-dev-hello/invocations
Serverless: Offline [HTTP] listening on http://localhost:3000
Serverless: Enter "rp" to replay the last request
希望您能看到这个。您应该能够访问 localhost:3000 并查看可用的 API 端点列表。如果您访问 /hello,应该会看到我们在 src/handler.ts 中返回的 APIGatewayProxyEvent 的转储。
import { APIGatewayProxyHandler } from 'aws-lambda';
import 'source-map-support/register';
export const hello: APIGatewayProxyHandler = async (event) => ({
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!',
input: event,
}, null, 2),
});
无服务器配置
现在我们有了一个可用的 API 网关端点,让我们在无服务器框架中配置更多选项。
-
为函数设置 X-Ray 跟踪
-
设置一些默认的 .env 变量
-
锁定 Serverless 版本
-
使用默认值设置阶段和区域配置
-
设置全局 DynamoDB 插件
-
设置 lambda 版本的自动修剪(我们只保留最后 3 个版本)
service:
name: typescript-serverless
custom:
webpack:
webpackConfig: ./webpack.config.js
includeModules: true
serverless-iam-roles-per-function:
defaultInherit: true *# Each function will inherit the service level roles too.
globalTables:
regions: # list of regions in which you want to set up global tables
- us-east-2 # Ohio (default region to date for stack)
- ap-southeast-2 # Sydney (lower latency for Australia)
createStack: false
prune: # automatically prune old lambda versions
automatic: true
number: 3
plugins:
- serverless-offline
- serverless-webpack
- serverless-iam-roles-per-function
- serverless-create-global-dynamodb-table
- serverless-prune-plugin
provider:
name: aws
runtime: nodejs12.x
frameworkVersion: ‘1.64.1’
stage: ${opt:stage, 'local'}
region: ${opt:region, 'us-east-2'}
apiGateway:
minimumCompressionSize: 1024 *# Enable gzip compression for responses > 1 KB
environment:
DEBUG: '*'
NODE_ENV: ${self:provider.stage}
AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1
tracing:
lambda: true
iamRoleStatements:
- Effect: Allow
Action:
- xray:PutTraceSegments
- xray:PutTelemetryRecords
Resource: "*"
functions:
hello:
handler: src/handler.hello
events:
- http:
method: get
path: hello
在提供程序下,我们为服务设置了默认阶段、区域以及一些全局 .env 变量。我们还设置了 X-Ray 跟踪,以便在服务部署后轻松调试。
我们还锁定了 Serverless 框架的版本。我以前从未这样做过,当 Serverless 升级版本并破坏我们的某个插件时,部署管道就会中断。截至撰写本文时,版本号为 1.64.1。
最后,我们设置阶段和区域配置,并使用 local 和 us-east-2 的一些默认值。这些配置在部署期间作为 CLI 参数(可选)设置。
最后,我们为该服务配置了一些全局 IAM 角色语句。(这些是我们将要设置的全局角色。其他所有角色都将在每个函数级别进行设置)。
注意:您需要在初始多区域部署时注释掉全局表。此插件可能无法按预期工作。部署失败的原因概述如下。
模块别名
最后要配置的是模块别名。真让我吃惊,都2020年了,居然还有人构建 Node 应用而不使用模块别名。相对导入路径对我来说太脆弱了,所以让我们设置一些别名吧。
我们将设置三个默认值(src、test 和 query),以便将来有人可以了解如何设置它们。我们还将在示例处理程序中使用 import from query 来确保 TypeScript 能够正确编译和解析。
现在有点乱,不过我们先设置一下吧。值得一试。
首先,让 webpack 了解模块别名并更新解析对象。
resolve: {
extensions: ['.mjs', '.json', '.ts'],
symlinks: false,
cacheWithContext: false,
alias: {
'@src': path.resolve(__dirname, './src'),
'@queries': path.resolve(__dirname, './queries'),
'@tests': path.resolve(__dirname, './tests'),
},
},
接下来,我们将通过更新compilerOptions路径让我们的.tsconfig知道模块别名(我们也可以参考之前的tsconfig要点)。
"paths": {
"@src/*": ["src/*"],
"@queries/*": ["queries/*"],
"@tests/*": ["tests/*"]
}
最后,我们会通知 ESLint,这样在使用别名时就不会出现恼人的 linting 错误。(同样,如果你只是复制了之前的 gist,你会发现它已经完成了。)
"settings": {
"import/resolver": {
"alias": {
"map": [
["@src", "./src"],
["@tests", "./tests"],
["@queries", "./queries"]
],
"extensions": [
".ts",
".js"
]
}
}
}
好的,现在来确保配置正确。
让我们创建一个 /queries 目录,并添加 querys/exampleQuery.ts 来验证我们的别名。我们会尽量简化这个模块,以测试编译是否仍然有效。
export const echo = (sound: string): string => sound;
我们只需接受一个参数并直接返回即可。如果这不起作用,就会出现编译时错误。
现在在 src/handler.ts 中,我们导入这个模块,并使用我们设置的别名,并尝试在响应中使用它。让我们更新响应中的消息。
import { APIGatewayProxyHandler } from ‘aws-lambda';
import { echo } from ‘@queries/exampleQuery';
import 'source-map-support/register’;
export const hello: APIGatewayProxyHandler = async (event) => ({
statusCode: 200,
body: JSON.stringify({
message: echo(‘Module aliasing is really the best’),
input: event,
}, null, 2),
使用别名可以让重构变得简单得多。你的 IDE 也应该足够智能,能够使用别名自动导入(WebStorm 就是这样的)。
这就是全部内容了。现在,你应该可以开始编写代码,并利用 TypeScript、ESLint 和 Jest 的所有优势构建你的服务了。我将在以后的文章中介绍如何使用这些强大的工具,以及如何设置 SQS、SNS、Kinesis 和 DynamoDB。
您可以在我的 GitHub上找到整个入门模板。
有关如何通过配置 IAM 角色将此项目部署到您的 AWS 账户的信息,请阅读此处。
鏂囩珷鏉ユ簮锛�https://dev.to/michael_timbs/get-started-with-aws-serverless-and-typescript-5hgf