Читатель прислал референс-диаграмму serverless-приложения на AWS — Route 53, CloudFront, S3, API Gateway, Lambda, DynamoDB, SQS, Cognito, CloudWatch — с просьбой разобрать её «на минималках». Хорошая новость: такая схема читается не по названиям сервисов, а по одной логике — у запроса пользователя есть две дорожки, статическая и динамическая, а всё остальное — обвязка вокруг них. Разберём схему слой за слоем и соберём её уменьшенную копию одним CloudFormation-шаблоном; во второй части цикла ту же архитектуру соберём self-hosted на обычной VM.
Содержание
Открыть содержание
- Как читать любую облачную схему
- Дорожка статики: Route 53 → CloudFront → S3
- Дорожка динамики: API Gateway → Lambda → DynamoDB
- Фоновые задачи: Lambda → SQS → Worker
- Обвязка: Cognito и CloudWatch
- Serverless-стек против «всё на одной VM»
- Что нужно, чтобы собрать схему за вечер
- Как проверить, что всё работает
- Сколько это стоит в месяц
- Итог
Как читать любую облачную схему
Классическое self-hosted приложение — это сервер: nginx, код, PostgreSQL и cron на одной VM. Так работает половина интернета, и в этом нет ничего постыдного, но у схемы есть четыре боли, которые не лечатся вниманием:
- счёт идёт 24/7 — VM платит за себя и в час пик, и в четыре утра, когда трафика нет;
- масштабирование — руками — выросла нагрузка: покупаем VM побольше, переносим всё, проверяем;
- сбой — ваш — переполнился диск, утекла память, протух сертификат: чинит тот, на кого показывает мониторинг;
- патчи — ваши — ОС, библиотеки, TLS-сертификаты: всё это чей-то вторник, и обычно ваш.
Облачная схема того же приложения — не «сложность ради моды», а разбор того же сервера на управляемые сервисы: каждый прямоугольник на диаграмме закрывает одну из этих болей, и за каждый из них кто-то ещё содержит дежурного инженера. Референс, на который стоит ровняться, — AWS Well-Architected.
Правило чтения любой облачной диаграммы: найдите пользователя и проведите от него два пути — откуда приезжает статика и кто отвечает на API-запросы. Всё, что не попало в эти пути, — обвязка: очередь, авторизация, логи. Дальше идём ровно по этим двум дорожкам.
Дорожка статики: Route 53 → CloudFront → S3
Первая дорожка — то, что браузер грузит до любого вашего кода: HTML, JS, CSS, картинки.
- Route 53 — DNS: превращает
app.example.comв адрес, с которого браузеру ехать. Managed-зона за $0.50/мес резолвится без вашего участия и не ложится вместе с вашим сервером. Боль, которую снимает: собственный DNS-сервер и вопрос «а почему у нас не открывается». - CloudFront — CDN: держит копию статики в точке рядом с пользователем (сотни edge-локаций), отдаёт её за миллисекунды, терминирует TLS и прикрывает фронт встроенным Shield. Боль: файлы едут из одного дата-центра через полмира, и каждый заход в приложение платит за это латентностью.
- S3 — объектное хранилище: статика лежит в ведре файлами, без единого вашего процесса. Хостинг в S3 стоит центы в месяц, а надёжность — 11 девяток доступности.
Итог дорожки: index.html отдаётся с edge-ноды рядом с пользователем, и в этот момент ни один ваш процесс не занят.
Дорожка динамики: API Gateway → Lambda → DynamoDB
Вторая дорожка включается, когда пользователь нажимает кнопку и нужен ваш код.
- API Gateway — парадная дверь API: HTTPS-endpoint, роуты, лимиты и — ключевое — проверка токена до того, как запрос дойдёт до кода. Один публичный адрес, за которым стоит конвейер.
- Lambda — ваш код в облаке без серверов: загрузили функцию, её вызвали по HTTP-событию, она отработала — заплатили за миллисекунды работы. Масштабируется сама от нуля до тысяч параллельных вызовов: один пользователь или волна с Hacker News — разница только в счёте. Плата за это — холодные старты: первый вызов после паузы тратит сотни миллисекунд на инициализацию, дальше функция тёплая.
- DynamoDB — единственная база в схеме: serverless NoSQL без серверов, коннектов и пулов соединений — только HTTPS-вызовы по ключу. Почему её одной хватает: приложение stateless, ответы укладываются в единицы миллисекунд, оплата on-demand — за фактические запросы. Чем платим: модель доступа жёсткая — partition key и sort key вместо SQL, JOIN и агрегаций нет; схему запросов проектируют до кода, а не после.
Фоновые задачи: Lambda → SQS → Worker
Не всё, что начинается кликом, должно закончиться в том же запросе. Отправка письма, ресайз картинки, генерация отчёта — секунды работы, которые пользователю ждать незачем: его интересует ответ, а не письмо.
- SQS — распределённая очередь: обработчик кладёт в неё сообщение и сразу отвечает 200 OK; сообщение переживает рестарт воркера, а при ошибке возвращается в очередь. Боль, которую снимает: потерянные письма и cron-костыли вида «каждые пять минут проверяй таблицу».
- Worker — та же Lambda, только триггер у неё не HTTP, а сообщения очереди: пришли — обработали, пусто — не существует и не стоит ничего.
Ответ пользователю отрывается от тяжёлой работы: интерфейс живёт в миллисекундах, письмо приходит, когда приходит.
Обвязка: Cognito и CloudWatch
- Cognito — регистрация, логин, токены: готовый user pool с экраном входа, восстановлением пароля и JWT, который API Gateway проверяет на входе. Кода авторизации в приложении нет вообще: нет токена — нет запроса.
- CloudWatch — логи и метрики всего стека одной строкой: каждая Lambda пишет в свою log-группу, у API Gateway, DynamoDB и SQS из коробки есть метрики вызовов, ошибок и латентности. Искать, «куда писать логи», не нужно — они уже написаны, вопрос только в том, чтобы их смотреть.
Serverless-стек против «всё на одной VM»
| Всё на одной VM | Serverless-стек AWS | |
|---|---|---|
| Что обслуживаете | ОС, патчи, nginx, СУБД, TLS | ничего — только код и шаблон |
| Масштабирование | вручную, в основном вертикально | само, от 0 до тысяч параллельных вызовов |
| Счёт при простое | полный, 24/7 | около нуля: вызовов нет — платить не за что |
| Сбой в 03:00 | ваш: диск, память, процесс | их: чинит AWS, вы смотрите метрики |
| База | PostgreSQL/MySQL с коннектами | DynamoDB по HTTPS, без коннектов |
| Деплой | SSH и скрипты «надеюсь, не забыл» | один template.yaml, обновление стека |
| Порог входа | низкий: Linux знают все | нужно выучить сервисы и их лимиты |
Что нужно, чтобы собрать схему за вечер
Сначала мораль всей диаграммы: на ней нет ни одного сервера, контейнера или CI-пайплайна. Весь стек — один CloudFormation-шаблон: деплой = обновление стека, откат = предыдущая версия стека, ответ на «как у нас устроен прод» = файл в репозитории.
Нужны AWS-аккаунт, SAM CLI и AWS CLI. Минимальный проект — два файла:
minimal-app/
├── template.yaml
└── src/
└── index.mjs
src/index.mjs — вся бизнес-логика демо: пишем строку в DynamoDB и возвращаем счётчик:
import { DynamoDBClient, PutItemCommand, ScanCommand } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({});
const table = process.env.TABLE_NAME;
export const handler = async () => {
const at = new Date().toISOString();
await ddb.send(new PutItemCommand({
TableName: table,
Item: { id: { S: crypto.randomUUID() }, at: { S: at } },
}));
const { Count } = await ddb.send(new ScanCommand({ TableName: table, Select: "COUNT" }));
return { statusCode: 200, body: JSON.stringify({ ok: true, visits: Count, at }) };
};
template.yaml — HTTP API с проверкой Cognito-токена, Lambda, таблица, user pool:
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
Table:
Type: AWS::Serverless::SimpleTable # DynamoDB, partition key = id
Properties:
PrimaryKey:
Name: id
Type: String
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UsernameAttributes: ["email"]
AutoVerifiedAttributes: ["email"]
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
ExplicitAuthFlows:
- ALLOW_ADMIN_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
Auth: # без валидного токена — мимо
DefaultAuthorizer: CognitoJwt
Authorizers:
CognitoJwt:
IdentitySource: "$request.header.Authorization"
JwtConfiguration:
Issuer: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}"
Audience:
- !Ref UserPoolClient
Function:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs20.x
Handler: index.handler
CodeUri: src/
Environment:
Variables:
TABLE_NAME: !Ref Table
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref Table
Events:
Visit:
Type: HttpApi
Properties:
ApiId: !Ref HttpApi
Outputs:
ApiUrl:
Value: !Sub "https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com/"
UserPoolId:
Value: !Ref UserPool
UserPoolClientId:
Value: !Ref UserPoolClient
TableName:
Value: !Ref Table
Деплой — две команды, дальше одна:
sam build
sam deploy --guided # первый раз: регион, имя стека, "save to config"
sam deploy # все последующие
Кто не хочет SAM: почти тот же файл понимает связка aws cloudformation package + aws cloudformation deploy — мета-типы SAM разворачиваются в «настоящие» ресурсы на этапе packaging. Статику из первой дорожки (S3 + CloudFront) в шаблон добавляют так же — ещё два ресурса в Resources.
Как проверить, что всё работает
Первая проверка — авторизация, одной строкой и без токена:
API=$(aws cloudformation describe-stacks --stack-name minimal-app \
--query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text)
curl -i "$API" # 401 — гейтвей не пустил: проверка токена работает
Вторая — полный круг: пользователь, токен, запрос, данные. Переменные берём из Outputs стека:
POOL_ID=$(aws cloudformation describe-stacks --stack-name minimal-app \
--query "Stacks[0].Outputs[?OutputKey=='UserPoolId'].OutputValue" --output text)
CLIENT_ID=$(aws cloudformation describe-stacks --stack-name minimal-app \
--query "Stacks[0].Outputs[?OutputKey=='UserPoolClientId'].OutputValue" --output text)
aws cognito-idp admin-create-user --user-pool-id $POOL_ID \
--username [email protected] \
--user-attributes Name=email,[email protected] Name=email_verified,Value=true
aws cognito-idp admin-set-user-password --user-pool-id $POOL_ID \
--username [email protected] --password "Demo-pass-1!" --permanent
TOKEN=$(aws cognito-idp admin-initiate-auth --user-pool-id $POOL_ID \
--client-id $CLIENT_ID --auth-flow ADMIN_NO_SRP_AUTH \
--auth-parameters [email protected],PASSWORD="Demo-pass-1!" \
--query "AuthenticationResult.IdToken" --output text)
curl -H "Authorization: $TOKEN" "$API" # {"ok":true,"visits":1,...}
curl -H "Authorization: $TOKEN" "$API" # visits:2 — каждый вызов пишет строку
Третья — данные и логи на месте, без настройки:
TABLE=$(aws cloudformation describe-stacks --stack-name minimal-app \
--query "Stacks[0].Outputs[?OutputKey=='TableName'].OutputValue" --output text)
aws dynamodb scan --table-name $TABLE --select COUNT
sam logs --stack-name minimal-app --tail
(need to add TableName output to template)
Четыре проверки, ноль настроек: гейтвей отбивает анонима, токен открывает дверь, Lambda пишет строку, логи лежат в CloudWatch — схема из диаграммы живая.
Сколько это стоит в месяц
- Навсегда бесплатно — это не 12-месячный триал, а always free: Lambda — 1 млн запросов и 400 000 ГБ·с каждый месяц, DynamoDB — 25 ГБ хранения и 25 млн запросов. Демо и внутренний инструмент в эти лимиты влезают целиком.
- Копейки: hosted-зона Route 53 — $0.50/мес, S3 и CloudFront — центы при скромном трафике, HTTP API Gateway — $1 за миллион запросов (сто тысяч запросов в месяц — это $0.10).
- Итог: маленький рабочий проект живёт в пределах $1–2/мес, и важнее не сумма, а форма счёта — он растёт с вызовами, а не с календарём: трафика нет, платить почти не за что.
Итог
На этой схеме нет ничего, что нужно обслуживать: каждый вызов кем-то оплачен и кем-то восстановлен после сбоя — вы платите не за серверы, а за миллисекунды чужой работы, и вся архитектура помещается в один файл. Это не «серверы — зло»: serverless меняет характер работы — вместо операционной системы вы изучаете чужие лимиты, холодные старты и жёсткую модель DynamoDB, и часть гибкости оставляете на столе. Но для продукта, который должен работать уже завтра, пока мониторинга и дежурств ещё нет, это самый дешёвый в эксплуатации вариант из существующих — и соседний по серии «на минималках» WAF на минималках защищает ровно такие приложения. Во второй части соберём ту же архитектуру self-hosted — на своей VM за $0/мес — и увидим, чем реально приходится платить за отказ от чужой работы.