Настройка Serverless Framework для веб-приложения
Serverless Framework — это инструмент развёртывания и управления serverless-функциями поверх провайдеров (AWS, GCP, Azure, Cloudflare). Он не абстрагирует облако полностью — вы всё ещё работаете с провайдер-специфичными ресурсами, но конфигурация, деплой и управление окружениями объединены в один serverless.yml.
Установка и базовая структура
npm install -g serverless
serverless --version # 3.x или 4.x
# Новый проект
serverless create --template aws-nodejs-typescript --path my-service
cd my-service
npm install
Структура проекта:
my-service/
├── serverless.yml # основная конфигурация
├── serverless.env.yml # переменные окружения (не в git)
├── src/
│ ├── functions/
│ │ ├── api/
│ │ │ ├── handler.ts
│ │ │ └── index.ts # конфиг функции
│ │ └── worker/
│ │ ├── handler.ts
│ │ └── index.ts
│ └── libs/
│ ├── api-gateway.ts # хелперы для ответов
│ └── lambda.ts # middleware-обёртка
├── tsconfig.json
└── package.json
serverless.yml — правильная конфигурация
service: my-web-service
frameworkVersion: '3'
plugins:
- serverless-esbuild # быстрая сборка TypeScript
- serverless-offline # локальная разработка
- serverless-dotenv-plugin # .env файлы
provider:
name: aws
runtime: nodejs20.x
region: eu-west-1
stage: ${opt:stage, 'dev'}
memorySize: 512
timeout: 10
logRetentionInDays: 14
environment:
NODE_ENV: ${self:provider.stage}
DATABASE_URL: ${env:DATABASE_URL}
JWT_SECRET: ${env:JWT_SECRET}
# IAM-права — минимально необходимые
iam:
role:
statements:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: 'arn:aws:s3:::${self:custom.bucketName}/*'
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:PutItem
- dynamodb:UpdateItem
Resource: !GetAtt UsersTable.Arn
# API Gateway v2 (HTTP API) — дешевле и быстрее REST API
httpApi:
cors:
allowedOrigins:
- https://my-site.com
- http://localhost:3000
allowedHeaders:
- Content-Type
- Authorization
allowedMethods:
- GET
- POST
- PUT
- DELETE
custom:
bucketName: my-service-${self:provider.stage}-assets
esbuild:
bundle: true
minify: ${strToBool(${ssm:/my-service/minify, 'false'})}
sourcemap: true
target: node20
platform: node
concurrency: 10
serverless-offline:
httpPort: 3001
lambdaPort: 3002
functions:
- ${file(src/functions/api/index.ts)}
- ${file(src/functions/worker/index.ts)}
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${self:provider.stage}-users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
- AttributeName: sk
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
- AttributeName: sk
KeyType: RANGE
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
Конфигурация функции
// src/functions/api/index.ts
import type { AWS } from '@serverless/typescript';
const apiFunction: AWS['functions'] = {
api: {
handler: 'src/functions/api/handler.main',
events: [
{
httpApi: {
method: 'ANY',
path: '/api/{proxy+}',
authorizer: {
name: 'jwtAuthorizer',
type: 'jwt',
identitySource: '$request.header.Authorization',
issuerUrl: 'https://cognito-idp.eu-west-1.amazonaws.com/${env:COGNITO_POOL_ID}',
audience: ['${env:COGNITO_CLIENT_ID}'],
},
},
},
],
environment: {
// Переопределяем provider-уровень если нужно
},
},
};
export default apiFunction;
Handler с middleware
// src/libs/lambda.ts
import middy from '@middy/core';
import middyJsonBodyParser from '@middy/http-json-body-parser';
import httpErrorHandler from '@middy/http-error-handler';
import cors from '@middy/http-cors';
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
type Handler = (event: APIGatewayProxyEventV2) => Promise<APIGatewayProxyStructuredResultV2>;
export const middyfy = (handler: Handler) =>
middy(handler)
.use(middyJsonBodyParser())
.use(httpErrorHandler())
.use(cors({ origin: process.env.ALLOWED_ORIGIN ?? '*' }));
// src/libs/api-gateway.ts
export const formatJSONResponse = (response: Record<string, unknown>, statusCode = 200) => ({
statusCode,
body: JSON.stringify(response),
headers: { 'Content-Type': 'application/json' },
});
// src/functions/api/handler.ts
import { middyfy } from '@libs/lambda';
import { formatJSONResponse } from '@libs/api-gateway';
const handler = async (event) => {
const { body, pathParameters, queryStringParameters } = event;
return formatJSONResponse({ message: 'ok', data: body });
};
export const main = middyfy(handler);
Управление окружениями
# Разные стейджи
serverless deploy --stage dev
serverless deploy --stage staging
serverless deploy --stage prod
# Параметры через SSM Parameter Store (безопаснее env vars)
serverless deploy --stage prod --param="dbPassword=$(aws ssm get-parameter --name /prod/db-password --with-decryption --query Parameter.Value --output text)"
SSM в serverless.yml:
provider:
environment:
DB_PASSWORD: ${ssm:/my-service/${self:provider.stage}/db-password}
API_KEY: ${ssm:/my-service/api-key~true} # ~true = decrypt SecureString
Локальная разработка
npm run dev # serverless offline start
# Вызов функции локально
serverless invoke local --function api --data '{"pathParameters": {"proxy": "users"}}'
# Логи в реальном времени (prod)
serverless logs --function api --tail --stage prod
Оптимизация размера бандла
esbuild + tree shaking значительно уменьшают cold start. Но не все зависимости bundleable:
custom:
esbuild:
bundle: true
exclude:
- '@aws-sdk/*' # уже есть в Lambda runtime для Node 18+
- 'pg-native' # native addon — исключаем, используем чистый pg
external:
- 'sharp' # native binary — отдельный слой Lambda
Для sharp (обработка изображений) создаём Lambda Layer:
mkdir -p layer/nodejs
cd layer/nodejs
npm install sharp
cd ../..
# В serverless.yml:
# layers:
# sharp:
# path: layer
# compatibleRuntimes: [nodejs20.x]
CI/CD интеграция
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- name: Deploy to prod
run: npx serverless deploy --stage prod
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Сроки
Базовая настройка Serverless Framework с одной функцией и деплоем в dev/prod — 1 день. Полноценная структура с несколькими функциями, DynamoDB, SSM-секретами и CI/CD — 3–4 дня. Миграция существующего Express-приложения на serverless-архитектуру — 1–2 недели в зависимости от количества зависимостей.







