Перейти к содержимому
HoginHogin
Назад

Cloud-архитектура приложения на AWS на минималках: serverless-стек — что делает каждый сервис и как читается схема (часть 1/2)

9 мин чтения

Читатель прислал референс-диаграмму serverless-приложения на AWS — Route 53, CloudFront, S3, API Gateway, Lambda, DynamoDB, SQS, Cognito, CloudWatch — с просьбой разобрать её «на минималках». Хорошая новость: такая схема читается не по названиям сервисов, а по одной логике — у запроса пользователя есть две дорожки, статическая и динамическая, а всё остальное — обвязка вокруг них. Разберём схему слой за слоем и соберём её уменьшенную копию одним CloudFormation-шаблоном; во второй части цикла ту же архитектуру соберём self-hosted на обычной VM.

Содержание

Открыть содержание

Как читать любую облачную схему

Классическое self-hosted приложение — это сервер: nginx, код, PostgreSQL и cron на одной VM. Так работает половина интернета, и в этом нет ничего постыдного, но у схемы есть четыре боли, которые не лечатся вниманием:

Облачная схема того же приложения — не «сложность ради моды», а разбор того же сервера на управляемые сервисы: каждый прямоугольник на диаграмме закрывает одну из этих болей, и за каждый из них кто-то ещё содержит дежурного инженера. Референс, на который стоит ровняться, — AWS Well-Architected.

Правило чтения любой облачной диаграммы: найдите пользователя и проведите от него два пути — откуда приезжает статика и кто отвечает на API-запросы. Всё, что не попало в эти пути, — обвязка: очередь, авторизация, логи. Дальше идём ровно по этим двум дорожкам.

Дорожка статики: Route 53 → CloudFront → S3

Первая дорожка — то, что браузер грузит до любого вашего кода: HTML, JS, CSS, картинки.

Итог дорожки: index.html отдаётся с edge-ноды рядом с пользователем, и в этот момент ни один ваш процесс не занят.

Дорожка динамики: API Gateway → Lambda → DynamoDB

Вторая дорожка включается, когда пользователь нажимает кнопку и нужен ваш код.

Две дорожки запроса

Фоновые задачи: Lambda → SQS → Worker

Не всё, что начинается кликом, должно закончиться в том же запросе. Отправка письма, ресайз картинки, генерация отчёта — секунды работы, которые пользователю ждать незачем: его интересует ответ, а не письмо.

Тяжёлое — в очередь

Ответ пользователю отрывается от тяжёлой работы: интерфейс живёт в миллисекундах, письмо приходит, когда приходит.

Обвязка: Cognito и CloudWatch

Serverless-стек против «всё на одной VM»

Всё на одной VMServerless-стек 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 — схема из диаграммы живая.

Сколько это стоит в месяц

Итог

На этой схеме нет ничего, что нужно обслуживать: каждый вызов кем-то оплачен и кем-то восстановлен после сбоя — вы платите не за серверы, а за миллисекунды чужой работы, и вся архитектура помещается в один файл. Это не «серверы — зло»: serverless меняет характер работы — вместо операционной системы вы изучаете чужие лимиты, холодные старты и жёсткую модель DynamoDB, и часть гибкости оставляете на столе. Но для продукта, который должен работать уже завтра, пока мониторинга и дежурств ещё нет, это самый дешёвый в эксплуатации вариант из существующих — и соседний по серии «на минималках» WAF на минималках защищает ровно такие приложения. Во второй части соберём ту же архитектуру self-hosted — на своей VM за $0/мес — и увидим, чем реально приходится платить за отказ от чужой работы.


Поделиться:

Следующая статья
SafeLine WAF от Chaitin: self-hosted WAF без конфигурирования правил — и Pro-лицензия бесплатно за статью