Serverless функции на TypeScript: продвинутая работа с AWS Lambda

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Serverless функции на TypeScript: продвинутый уровень в AWS Lambda

Serverless-архитектура стала стандартом де-факто для построения масштабируемых и экономичных приложений. AWS Lambda — один из самых популярных сервисов для реализации бессерверных функций. Если базовый синтаксис и триггеры (API Gateway, S3, DynamoDB) уже освоены, наступает время для продвинутых техник. В этой статье мы рассмотрим ключевые аспекты профессиональной разработки Lambda-функций на TypeScript: от оптимизации холодного старта до интеграции с VPC и использования сложных паттернов.

1. Оптимизация холодного старта и управление памятью

Холодный старт (cold start) — главный враг производительности serverless-приложений. На TypeScript эта проблема усугубляется этапом компиляции. Рассмотрим три основных метода борьбы.

1.1. Снижение размера пакета деплоя

Используйте esbuild для сборки вместо стандартного tsc. Он выполняет tree-shaking и минификацию, что уменьшает размер бандла в 5-10 раз. Пример конфигурации для serverless.yml:

plugins:
  - serverless-esbuild
custom:
  esbuild:
    bundle: true
    minify: true
    sourcemap: false
    target: 'node18'
    exclude: ['aws-sdk'] # SDK уже есть в среде Lambda

1.2. Использование Provisioned Concurrency

Для критичных к задержкам функций (например, обрабатывающих API-запросы) настройте Provisioned Concurrency. Это держит заданное количество "горячих" контейнеров постоянно запущенными:

functions:
  apiHandler:
    handler: src/handler.apiHandler
    provisionedConcurrency: 5 # Всегда 5 готовых инстансов

1.3. Оптимизация импортов и кода

Избегайте "тяжелых" библиотек внутри функции. Например, вместо aws-sdk v3 целиком импортируйте только нужные клиенты:

// Плохо: import * as AWS from 'aws-sdk';
// Хорошо:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

2. Работа с Lambda Layers и зависимостями

Lambda Layers позволяют вынести общий код и зависимости (например, ORM, валидаторы) за пределы функции. Это ускоряет деплой и уменьшает размер каждого отдельного пакета.

2.1. Создание слоя с общими утилитами

Структура проекта может выглядеть так:

project/
├── layers/
│   └── common/
│       └── nodejs/
│           ├── package.json
│           └── node_modules/
├── functions/
│   ├── createUser.ts
│   └── getUser.ts
└── serverless.yml

В serverless.yml подключаем слой:

layers:
  commonLibs:
    path: layers/common
    compatibleRuntimes:
      - nodejs18.x
functions:
  createUser:
    handler: functions/createUser.handler
    layers:
      - { Ref: CommonLibsLambdaLayer }

2.2. Типизация для слоев

Чтобы TypeScript "видел" типы из слоя, создайте tsconfig.json с путями:

{
  "compilerOptions": {
    "paths": {
      "@common/*": ["layers/common/nodejs/*"]
    }
  }
}

Теперь в функции можно писать:

import { validateEmail } from '@common/validators';

3. Работа с VPC и безопасность

Lambda-функции, работающие внутри VPC, теряют доступ к публичному интернету (если не настроен NAT Gateway). Это критично для подключения к RDS, ElastiCache или приватным API.

3.1. Настройка Lambda в VPC

Конфигурация через serverless.yml:

provider:
  vpc:
    securityGroupIds:
      - sg-12345
    subnetIds:
      - subnet-abc
      - subnet-def

functions:
  dbHandler:
    handler: src/db.handler
    vpc: true # наследует настройки провайдера

3.2. Паттерн "Thin Handler" для соединений

Чтобы не создавать соединение к БД при каждом вызове, используйте глобальную переменную вне обработчика. Lambda "замораживает" контекст между вызовами:

import { Pool } from 'pg';

let pool: Pool;

export const handler = async (event: any) => {
  if (!pool) {
    pool = new Pool({
      host: process.env.DB_HOST,
      database: 'mydb',
      max: 2, // не более 2 соединений на контейнер
    });
  }
  const client = await pool.connect();
  try {
    const result = await client.query('SELECT NOW()');
    return { statusCode: 200, body: JSON.stringify(result.rows) };
  } finally {
    client.release();
  }
};

4. Продвинутые паттерны: Saga, Idempotency и Observability

4.1. Паттерн Saga для распределенных транзакций

Когда одна бизнес-операция затрагивает несколько сервисов (например, заказ -> оплата -> склад), используйте цепочку Lambda с компенсирующими действиями. Реализуем простую Saga с помощью Step Functions:

// Заглушка для шага "Резервирование товара"
export const handler = async (event: { orderId: string; itemId: string }) => {
  // Логика бронирования
  return { status: 'reserved', ...event };
};

// Компенсирующее действие (отмена брони)
export const compensate = async (event: { orderId: string; itemId: string }) => {
  // Логика отмены
  return { status: 'cancelled', ...event };
};

4.2. Идемпотентность через DynamoDB

Чтобы избежать дублирования при повторных вызовах (например, при повторах от SQS), используйте таблицу идемпотентности:

const idempotencyKey = event.headers['x-idempotency-key'];

const existing = await ddbDocClient.get({
  TableName: 'IdempotencyTable',
  Key: { pk: idempotencyKey }
});

if (existing.Item) {
  return existing.Item.response; // возвращаем сохраненный ответ
}

// Выполняем логику...
const result = await processOrder(event);

// Сохраняем ключ и результат
await ddbDocClient.put({
  TableName: 'IdempotencyTable',
  Item                

Блоги

Рекомендаций по книгам