Настройка AWS SAM для Serverless-бэкенда
AWS SAM (Serverless Application Model) — это официальный AWS-инструмент для определения serverless-ресурсов. Он расширяет CloudFormation упрощённым синтаксисом: вместо 50 строк CloudFormation для Lambda + API Gateway + IAM Role пишется 10 строк SAM. На выходе — полноценный CloudFormation стек, который версионируется и откатывается.
В отличие от Serverless Framework, SAM — это AWS-нативный инструмент без абстракции над другими провайдерами. Это его преимущество: полная совместимость с CloudFormation, нативная интеграция с AWS CDK, прямая работа с SAM CLI для локального тестирования с реальными service mocks.
Установка и первый проект
# macOS
brew tap aws/tap
brew install aws-sam-cli
# Linux
pip install aws-sam-cli
sam --version # SAM CLI, version 1.x
# Инициализация проекта
sam init --runtime nodejs20.x --dependency-manager npm --app-template hello-world --name my-backend
Структура проекта
my-backend/
├── template.yaml # SAM-шаблон (расширенный CloudFormation)
├── samconfig.toml # конфиг деплоя (регион, стек, S3 bucket)
├── src/
│ ├── handlers/
│ │ ├── api.ts
│ │ ├── auth.ts
│ │ └── worker.ts
│ └── shared/
│ ├── db.ts
│ └── response.ts
├── events/ # тестовые события для sam local invoke
│ ├── api-get.json
│ └── api-post.json
└── __tests__/
└── unit/
template.yaml — полная конфигурация
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: My Web Backend
Globals:
Function:
Runtime: nodejs20.x
Architectures: [arm64] # Graviton2 — до 20% дешевле x86
MemorySize: 512
Timeout: 10
Environment:
Variables:
NODE_ENV: !Ref Stage
TABLE_NAME: !Ref MainTable
ALLOWED_ORIGIN: !Ref AllowedOrigin
Layers:
- !Ref DepsLayer
Api:
Cors:
AllowOrigin: !Sub "'${AllowedOrigin}'"
AllowHeaders: "'Content-Type,Authorization,X-Request-Id'"
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
Parameters:
Stage:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
AllowedOrigin:
Type: String
Default: 'http://localhost:3000'
JwtSecret:
Type: AWS::SSM::Parameter::Value<String>
Default: /my-backend/jwt-secret
Resources:
# HTTP API (v2) — дешевле REST API ~71%
ApiGateway:
Type: AWS::Serverless::HttpApi
Properties:
StageName: !Ref Stage
Auth:
DefaultAuthorizer: LambdaAuthorizer
Authorizers:
LambdaAuthorizer:
AuthorizerPayloadFormatVersion: '2.0'
FunctionArn: !GetAtt AuthFunction.Arn
Identity:
Headers: [Authorization]
EnableSimpleResponses: true
# Основная Lambda
ApiFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/api.handler
Events:
AnyRoute:
Type: HttpApi
Properties:
ApiId: !Ref ApiGateway
Method: ANY
Path: /api/{proxy+}
Auth:
Authorizer: LambdaAuthorizer
HealthCheck:
Type: HttpApi
Properties:
ApiId: !Ref ApiGateway
Method: GET
Path: /health
Auth:
Authorizer: NONE
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MainTable
- S3ReadPolicy:
BucketName: !Ref AssetsBucket
Environment:
Variables:
JWT_SECRET: !Ref JwtSecret
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2022
Sourcemap: true
EntryPoints: [src/handlers/api.ts]
External: ['@aws-sdk/*']
# Authorizer Lambda
AuthFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/auth.handler
Policies:
- SSMParameterReadPolicy:
ParameterName: my-backend/jwt-secret
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2022
EntryPoints: [src/handlers/auth.ts]
# Фоновый воркер — SQS триггер
WorkerFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/worker.handler
MemorySize: 256
Timeout: 300
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt WorkerQueue.Arn
BatchSize: 10
FunctionResponseTypes: [ReportBatchItemFailures]
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MainTable
- SQSSendMessagePolicy:
QueueName: !GetAtt WorkerQueue.QueueName
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
EntryPoints: [src/handlers/worker.ts]
# Lambda Layer для общих зависимостей
DepsLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: !Sub '${AWS::StackName}-deps'
ContentUri: layer/
CompatibleRuntimes: [nodejs20.x]
CompatibleArchitectures: [arm64]
Metadata:
BuildMethod: nodejs20.x
# DynamoDB
MainTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Retain # не удалять при sam delete
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- { AttributeName: pk, AttributeType: S }
- { AttributeName: sk, AttributeType: S }
- { AttributeName: gsi1pk, AttributeType: S }
KeySchema:
- { AttributeName: pk, KeyType: HASH }
- { AttributeName: sk, KeyType: RANGE }
GlobalSecondaryIndexes:
- IndexName: gsi1
KeySchema:
- { AttributeName: gsi1pk, KeyType: HASH }
- { AttributeName: sk, KeyType: RANGE }
Projection: { ProjectionType: ALL }
TimeToLiveSpecification:
{ AttributeName: ttl, Enabled: true }
# SQS Queue
WorkerQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 360 # > Lambda timeout
RedrivePolicy:
deadLetterTargetArn: !GetAtt DLQ.Arn
maxReceiveCount: 3
DLQ:
Type: AWS::SQS::Queue
Properties:
MessageRetentionPeriod: 1209600 # 14 дней
# S3
AssetsBucket:
Type: AWS::S3::Bucket
Properties:
CorsConfiguration:
CorsRules:
- AllowedOrigins: [!Ref AllowedOrigin]
AllowedMethods: [GET, PUT]
AllowedHeaders: ['*']
MaxAge: 3600
Outputs:
ApiUrl:
Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/${Stage}'
TableName:
Value: !Ref MainTable
BucketName:
Value: !Ref AssetsBucket
Handler и authorizer
// src/handlers/auth.ts — Lambda Authorizer (simple response format)
import type { APIGatewayRequestSimpleAuthorizerHandlerV2 } from 'aws-lambda';
import { verify } from 'jsonwebtoken';
export const handler: APIGatewayRequestSimpleAuthorizerHandlerV2 = async (event) => {
const token = event.headers?.authorization?.replace('Bearer ', '');
if (!token) return { isAuthorized: false };
try {
const payload = verify(token, process.env.JWT_SECRET!) as Record<string, unknown>;
return {
isAuthorized: true,
context: {
userId: String(payload.sub),
role: String(payload.role ?? 'user'),
},
};
} catch {
return { isAuthorized: false };
}
};
// src/shared/response.ts
export const ok = (data: unknown, statusCode = 200) => ({
statusCode,
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
});
export const err = (message: string, statusCode = 400) => ({
statusCode,
body: JSON.stringify({ error: message }),
headers: { 'Content-Type': 'application/json' },
});
samconfig.toml — конфиг деплоя
version = 0.1
[default.deploy.parameters]
stack_name = "my-backend-dev"
s3_bucket = "my-backend-sam-artifacts-eu-west-1"
s3_prefix = "my-backend"
region = "eu-west-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Stage=dev AllowedOrigin=http://localhost:3000"
[prod.deploy.parameters]
stack_name = "my-backend-prod"
s3_bucket = "my-backend-sam-artifacts-eu-west-1"
region = "eu-west-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Stage=prod AllowedOrigin=https://my-site.com"
Локальная разработка
# Сборка
sam build
# Локальный API с моком DynamoDB (через Docker)
sam local start-api --env-vars env.json --port 3001
# env.json для локального запуска
# {
# "ApiFunction": {
# "TABLE_NAME": "local-table",
# "JWT_SECRET": "dev-secret"
# }
# }
# Вызов конкретной функции
sam local invoke ApiFunction --event events/api-get.json
# Логи в реальном времени
sam logs --stack-name my-backend-dev --tail
Деплой
# Первый деплой с guided setup
sam deploy --guided
# Последующие — по конфигу
sam build && sam deploy
# Prod
sam build && sam deploy --config-env prod
# Откат
aws cloudformation rollback-stack --stack-name my-backend-prod
Сроки
Базовый SAM-бэкенд с одним Lambda + HTTP API + DynamoDB — 1–2 дня. Полноценная структура с authorizer, SQS worker, S3, SSM-секретами и samconfig для нескольких окружений — 4–5 дней. Добавление к существующему проекту (если уже есть логика в Express или Fastify) — 3–5 дней на адаптацию под Lambda handler.







