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