С полезным стартовым шаблоном

В 2019 году я впервые окунулся в мир бессерверных технологий, когда присоединился к команде fleet.space, чтобы построить облачную инфраструктуру для поддержки их группировки наноспутников и промышленной сети IoT.

Я действительно изо всех сил пытался найти исчерпывающее руководство по созданию новых сервисов с помощью TypeScript, поэтому здесь я пишу то, что мне хотелось бы иметь. Мы не будем рассматривать какой-либо пример кода - мы просто сосредоточимся на создании надежного базового шаблона, который вы можете повторно использовать для всех своих сервисов.

Первым делом мы установим Serverless Framework. Я считаю, что у него лучшая поддержка, чем у официальных шаблонов AWS SAM.

Во-первых, давайте установим Serverless как глобальную зависимость в нашей системе через npm i -g serverless. Затем мы создадим проект mkdir typescript-serverless. Внутри этого каталога создадим новый бессерверный шаблон с помощью следующей команды:

sls create --template aws-nodejs-typescript

Это сгенерирует простой шаблон с TypeScript, но ему не хватает мощной конфигурации, которую я использую снова и снова. Итак, давайте рассмотрим некоторые из этих вещей. Если вы не используете VS Code, удалите надоедливый каталог VS Code, который инициализирует этот шаблон, а затем запустите npm i, чтобы установить базовые зависимости Serverless Framework.

Бессерверные плагины

У Serverless Framework есть одно преимущество перед SAM в том, что вокруг нее построено множество плагинов сообщества, которые помогут вам в работе (и вы можете создать свои собственные, если необходимо). Он очень расширяемый. Плагины, которые я использую почти во всех сервисах:

Этот плагин позволяет вам определять разрешения IAM на уровне функции, а не на уровне проекта по умолчанию. Если только одна функция должна касаться DynamoDB, нам не нужно предоставлять им доступ ко всем.

В бессерверном мире развертывание в нескольких регионах практически бесплатно (в любом случае, по сравнению с контейнерами / ec2). Единственная сложность - это синхронизация DynamoDB. Это можно сделать с помощью глобальной таблицы.

Этим я не всегда пользуюсь в наши дни, но это только зависимость от разработчиков, и она очень удобна. Это позволит вам локально вызывать лямбда-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 ›SQS› Lambda ›Kinesis› Lambda ›DynamoDB и т. Д.) Была сложной задачей. К счастью, я наткнулся на отличный набор powertools для Lambda, который необходим в любом сервисе.

  • @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

Я просто импортирую все это и позволяю дрожащему дереву веб-пакетов беспокоиться о том, чтобы избавиться от того, что я не использую.

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

Линтинг

Следующая важная вещь, без которой я не могу жить в кодовой базе, - это линтинг. ESLint - один из самых больших костылей, которыми я пользуюсь каждый день. Давайте настроим его для работы с TypeScript и Serverless Framework. Нам потребуются следующие зависимости разработчиков.

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 файл конфигурации, чтобы определить наши правила. Мне нравятся следующие правила. Эта сущность также включает отображение псевдонимов для некоторых псевдонимов модулей, которые мы настроим в конце, и некоторую конфигурацию Jest, которую мы настроим через секунду.

Я также настрою свой tsconfig файл, чтобы добавить inlineSource, esModuleInterop, sourceRoot и baseUrl. Следующая суть также предварительно заполняет некоторую информацию о псевдонимах модулей, которую мы настроим позже. Вы можете пока закомментировать что угодно в путях, если хотите.

Тестирование

Я напишу тесты для каждой службы, поэтому имеет смысл настроить средство запуска тестов в нашем базовом шаблоне. Мне лично нравится Jest, так что мы это сделаем.

И снова нам нужно заполнить черную дыру нашего node_modules некоторыми зависимостями npm dev.

  • jest
  • babel-jest
  • @babel/core
  • @babel/preset-env
  • @babel/preset-typescript
npm i -D jest babel-jest @babel/core @babel/preset-env @babel/preset-typescript

Убедитесь, что Jest настроен как плагин в вашем .eslintrc.json и что вы установили jest/globals в env (если вы скопировали суть выше, у вас уже будет это там).

Нам нужно создать Babel .config, чтобы Jest работал.

На этом этапе мы должны проверить, работает ли Jest и правильно ли он настроен. Давайте создадим каталог tests и добавим пример теста. Создайте тестовый файл, а давайте добавим фиктивный тест tests/example.test.ts.

Если вы используете WebStorm, вы можете нажать Ctrl + Shift + R, чтобы запустить этот тест прямо из вашей IDE. В противном случае давайте обновим наш package.json, чтобы добавить тестовый скрипт (и проверку компиляции lint и TS, пока мы там работаем).

В вашем package.json файле обновите раздел скриптов, включив в него следующее:

Я обычно запускаю все это в конвейерах CI / CD, чтобы предотвратить попадание плохого кода в производственную среду. Теперь вы можете запустить npm run test с консоли, чтобы запустить набор тестов. Надеюсь, ваш набор тестов работает и проходит успешно. В идеале ваша среда IDE также не будет выдавать вам ошибки линтинга в вашем example.test.ts файле.

Пока мы здесь, запустите npm run lint и посмотрим, нет ли ошибок линтинга в шаблоне по умолчанию. Скорее всего, вы столкнетесь с ошибками при использовании webpack.config и файла handler.ts, созданного автоматически. Давайте проясним это.

Вверху вашего webpack.config файла добавьте */* eslint-disable @typescript-eslint/no-var-requires */* и раскомментируйте значения по умолчанию для плагина Fork TS Checker Webpack, которые поставляются с шаблоном. Это должно решить этот файл.

Для файла handler.ts просто удалите неиспользуемую сигнатуру контекстной функции из функции hello.

Код входит в / 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 offline в своем терминале и получить чистую компиляцию и сборку, запускающую бессерверную автономную конечную точку.

➜  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, все должно увидеть дамп APIGatewayProxyEvent, который мы возвращаем в src/handler.ts.

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, давайте настроим еще несколько параметров в Serverless Framework.

  • Настройка трассировки рентгеновских лучей для функций
  • Установите некоторые переменные по умолчанию .env
  • Заблокируйте версию Serverless
  • Установить настройки сцены и региона по умолчанию
  • Настроить глобальный плагин DynamoDB
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 переменные для службы. Мы также настроили трассировку рентгеновских лучей, чтобы мы могли легко отлаживать нашу службу после ее развертывания.

Мы также заблокировали нашу версию Serverless Framework. Я не делал этого раньше, и конвейеры развертывания прерывались, когда Serverless поднимает версию и ломает один из наших плагинов. На момент написания это версия 1.64.1.

Наконец, мы устанавливаем конфигурации сцены и региона с некоторыми значениями по умолчанию local и us-east-2. Они устанавливаются как аргументы CLI (необязательно) во время развертывания.

Наконец, мы настроили некоторые глобальные операторы роли IAM для службы. (Это единственные глобальные роли, которые мы устанавливаем. Все остальное мы устанавливаем на уровне отдельных функций).

Примечание. Вы хотите закомментировать глобальные таблицы для первоначального мультирегионального развертывания. Этот плагин может работать не так, как вы ожидаете. Причина, по которой развертывание может завершиться неудачей, указана здесь.

Модуль псевдонимов

Последнее, что нужно настроить, - это псевдонимы модулей. Это действительно потрясает меня, когда люди создают приложения Node в 2020 году без использования псевдонимов модулей. Относительные пути импорта для меня слишком хрупкие, поэтому давайте настроим несколько псевдонимов.

Мы собираемся установить три значения по умолчанию (src, test и queries), чтобы кто-нибудь мог войти и узнать, как настроить их в будущем. Мы также будем использовать импорт из запросов в нашем примере обработчика, чтобы убедиться, что 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'),
  },
},

Затем мы сообщим нашему .tsconfig о псевдониме модуля, обновив путь compilerOptions (мы также можем сослаться на tsconfig суть ранее).

"paths": {
  "@src/*": ["src/*"],
  "@queries/*": ["queries/*"],
  "@tests/*": ["tests/*"]
}

Наконец, мы сообщим об этом ESLint, чтобы избежать неприятных ошибок линтинга при использовании псевдонима. (Опять же, это было сделано в предыдущем случае, если вы просто скопировали это.)

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

Хорошо, пора убедиться, что все настроено правильно.

Давайте создадим каталог /queries и добавим queries/exampleQuery.ts для проверки нашего псевдонима. Мы сделаем этот модуль максимально простым, чтобы проверить, что компиляция все еще работает.

export const echo = (sound: string): string => sound;

Мы просто возьмем параметр и вернем его обратно. Если это не сработает, мы получим ошибку времени компиляции.

Теперь, в src/handler.ts, давайте импортируем этот модуль с установленным нами псевдонимом и попробуем использовать его в нашем ответе. Давайте обновим сообщение в нашем ответе.

Использование псевдонимов значительно упрощает рефакторинг. Ваша IDE также должна быть достаточно умной для автоматического импорта с псевдонимами (в любом случае WebStorm есть).

Еще немного конфигурации Jest

Теперь, когда мы используем псевдонимы модулей, мы столкнемся с проблемой при попытке протестировать компоненты. Это потому, что webpack обрабатывает все разрешение пути для нас с нашим импортом, но Jest не работает с транспилированным кодом. Мы можем легко исправить это, сообщив Jest о наших сопоставлениях. Мы можем сделать это в jest.config.js

Jest также позволяет запускать некоторые настройки перед запуском каждого теста. Давайте создадим файл setEnvironment.js, который я определил во фрагменте кода выше в разделе setupFiles.

Этот удобный фрагмент кода перезапишет наши переменные среды перед запуском нашего набора тестов. Здесь мы просто перезаписываем регион и ключи AWS. Вы почти наверняка захотите перезаписать и другие переменные в производственной среде (например, ключи Stripe, учетные данные базы данных и т. Д.).

Заключение

Вот и все. У вас должно получиться начать писать код и создавать свои сервисы со всеми качествами TypeScript, ESLint и Jest. Я расскажу, как использовать некоторые из Powertools и как настроить SQS, SNS, Kinesis и DynamoDB, в одной из будущих статей.

Вы можете найти весь этот стартовый шаблон на моем GitHub.

Для получения информации о том, как развернуть этот проект в своей учетной записи AWS, настроив роли IAM, прочтите здесь.