开始使用 AWS、无服务器和 TypeScript

2025-06-11

开始使用 AWS、无服务器和 TypeScript

大约五个月前,我加入了fleet.space团队,负责构建云基础设施以支持他们的纳米卫星星座和工业物联网网络,从此踏入了无服务器的世界。

我之前一直很难找到一份关于如何使用 TypeScript 构建新服务的全面指南,所以现在我来写一份我梦寐以求的指南。我们不会提供任何示例代码,我们只会专注于构建一个强大的基础模板,供你所有服务复用。

我们要做的第一件事是安装无服务器框架。我发现它比官方的 AWS SAM 模板有更好的支持。

首先,让我们通过 npm i -g serverless 将 Serverless 安装为系统的全局依赖项。接下来,我们将创建一个项目 mkdir typescript-serverless。在该目录中,我们使用以下命令搭建一个新的 Serverless 模板:

sls create --template aws-nodejs-typescript
Enter fullscreen mode Exit fullscreen mode

这将使用 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
Enter fullscreen mode Exit fullscreen mode

我们还将添加aws-sdkaws-lambda

npm i aws-sdk aws-lambda.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

代码检查

代码库中另一个必不可少的重要工具是 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
Enter fullscreen mode Exit fullscreen mode

现在,我们需要创建一个 .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"
]
}
}
}
}
view raw .eslintrc.json hosted with ❤ by GitHub
{
"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"
]
}
}
}
}
view raw .eslintrc.json hosted with ❤ by GitHub

我还会调整 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/**/*"
]
}
view raw tsconfig.json hosted with ❤ by GitHub
{
"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/**/*"
]
}
view raw tsconfig.json hosted with ❤ by GitHub

测试

我将为每个服务编写测试,因此在基础模板中配置一个测试运行器是合理的。我个人比较喜欢Jest,所以我们就用它吧。

再次,我们需要用一些 npm dev 依赖项来填充 node_modules 的黑洞。

npm i -D jest babel-jest @babel/core @babel/preset-env @babel/preset-typescript
Enter fullscreen mode Exit fullscreen mode

确保 Jest 在您的 .eslintrc.json 中配置为插件,并且在 env 下设置了 jest/globals(如果您复制了上面的要点,那么您已经在那里拥有它了)。

我们需要创建一个 Babel .config 以使 Jest 能够工作。

module.exports = {
presets: [
[@babel/preset-env', { targets: { node: ‘current’ } }],
@babel/preset-typescript’,
],
};
view raw babel.config.js hosted with ❤ by GitHub
module.exports = {
presets: [
[@babel/preset-env', { targets: { node: ‘current’ } }],
@babel/preset-typescript’,
],
};
view raw babel.config.js hosted with ❤ by GitHub

此时,我们应该检查 Jest 是否正常工作以及配置是否正确。让我们创建一个测试目录并添加一个示例测试。创建一个测试文件,并添加一个虚拟测试,tests/example.test.ts。

describe('who tests the tests?', () => {
  it('can run a test', () => {
    expect.hasAssertions();
    expect(1).toBe(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

如果您使用的是 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"
 },
Enter fullscreen mode Exit fullscreen mode

我通常会在 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

    ...

Enter fullscreen mode Exit fullscreen mode

此时,您应该能够在终端中离线运行 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
Enter fullscreen mode Exit fullscreen mode

希望您能看到这个。您应该能够访问 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),
    });
Enter fullscreen mode Exit fullscreen mode

无服务器配置

现在我们有了一个可用的 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

Enter fullscreen mode Exit fullscreen mode

在提供程序下,我们为服务设置了默认阶段、区域以及一些全局 .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'),
      },
    },
Enter fullscreen mode Exit fullscreen mode

接下来,我们将通过更新compilerOptions路径让我们的.tsconfig知道模块别名(我们也可以参考之前的tsconfig要点)。

    "paths": {
      "@src/*": ["src/*"],
      "@queries/*": ["queries/*"],
      "@tests/*": ["tests/*"]
    }
Enter fullscreen mode Exit fullscreen mode

最后,我们会通知 ESLint,这样在使用别名时就不会出现恼人的 linting 错误。(同样,如果你只是复制了之前的 gist,你会发现它已经完成了。)

    "settings": {
      "import/resolver": {
        "alias": {
          "map": [
            ["@src", "./src"],
            ["@tests", "./tests"],
            ["@queries", "./queries"]
          ],
          "extensions": [
            ".ts",
            ".js"
          ]
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

好的,现在来确保配置正确。

让我们创建一个 /queries 目录,并添加 querys/exampleQuery.ts 来验证我们的别名。我们会尽量简化这个模块,以测试编译是否仍然有效。

export const echo = (sound: string): string => sound;
Enter fullscreen mode Exit fullscreen mode

我们只需接受一个参数并直接返回即可。如果这不起作用,就会出现编译时错误。

现在在 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),
Enter fullscreen mode Exit fullscreen mode

使用别名可以让重构变得简单得多。你的 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
PREV
如何创建简单的 CI/CD 管道
NEXT
了解如何通过 CGO 在 Go 中使用 C 库,那么让我们使用 C 库吧!