Обзор
Вступление
Недавно я закончил работу на проекте, на котором провел почти 5 лет в качестве Platform Engineer работая c AWS Serverless стеком. Моя роль была в команде по взаимодействию с партнерами. У компании много партнеров и под каждую задачу мы разрабатывали отдельные сервисы. Иногда большие, иногда маленькие. Я занимался полным циклом, от проработки архитектуры будущего сервиса до поддержки в Production.
Что такое Serverless?
В Европе популярны облака и serverless, его здесь любят за возможность быстро запускать проекты и экономить деньги.
Serverless убирает необходимость настраивать сервер и платить за него, когда он не нужен. Технически, вы по-прежнему используете сервер, просто он очень маленький и легкий, заточен под определенную функцию и работает ровно столько, сколько необходимо, чтобы обслужить пользователя.
Если, например, вашим сайтом пользуются только днем, то ночью вы ничего не платите. Плюс, если половина пользователей только смотрит сайт, но не делает заказ, вы не платите за ресурсы необходимые для заказа для этой половины пользователей. Отсюда и экономия. При этом, из-за того, что функции “легкие” с точки зрения ресурсов их можно запускать очень много одновременно и за короткое время.
Это описание для вычислительных ресурсов, но подход serverless также применяется к хранению, базам данных, обмену сообщениями/событиями, аналитике и даже AI. Главной отличительной чертой serverless являются:
- автоматическая масштабируемость (scaling), и
- модель оплаты за использование (pay-as-you-go)
Стек
Я работал в облаке от Amazon - Amazon Web Services (AWS). У них есть порядка 20 сервисов, которые в той или иной степени считаются serverless. Я кратко расскажу про те, которые входили в наш go-to стек и с которыми я лично работал.
AWS Lambda - Lambda это тот самый сервер-без-сервера. Вместо того, чтобы программировать одно большое приложение, вы пишите небольшой кусочек кода - функцию, которая выполняет конкретную задачу. Сохраняете её в сервисе Lambda, настраиваете как и когда её запускать и оно начинает работать
Amazon SQS (Simple Queue Service) - это сервис очередей. Он похож на RabbitMQ, у вас есть producer - который пишет в очередь и consumer который из нее читает
Amazon DynamoDB - это база данных от Amazon на собственной архитектуре. Она одновременно key/value и document
Amazon S3 - это сервис для работы с файлами. Один из первых сервисов AWS который стал доступен пользователям и он же первый serverless сервис на их платформе. Он позволяет сохранять и получать файлы через API без предварительной настройки дисков, RAID массивов и т.д. обеспечивая высокую надежность и доступность
AWS Step Functions - это сервис оркестратор для управления и координации других сервисов AWS (Lambda, DynamoDB, SQS и др.). Он основан на стейт-машине и позволяет создавать воркфлоу в визуальном редакторе или json файлах
Amazon API Gateway - это веб-сервер для API, он позволяет как направлять веб-запросы в ваш собственный код так и в другие сервисы AWS
Amazon EventBridge - ну и последний сервис в списке, это шина событий для построения event-based архитектуры
В зависимости от задачи мы использовали некоторые или даже все сервисы из этого списка, плюс другие сервисы по необходимости. Здесь же стоит отметить, что для деплоя всего AWS-native мы старались использовать AWS CloudFormation - их инструмент для Infrastructure as Code, а для всего остального - Terraform. Хорошая выстроенная платформа CI/CD позволяля быстро разворачивать пайплайны, плюс процесс разворачивания новых сервисов ускорялся за счет cookiecutter шаблонов.
Типичные задачи
Итак, какие же задачи, мы решали с помощью serverless? Их можно объединить в 3 группы:
- HTTP API
- работа с данными
- гибрид первого и второго
HTTP API
Cоздание HTTP API было стандартизировано. Есть домен заведенный на Route 53 (сервис DNS). Для нового API мы создаем инстанс API Gateway и подключаем к этому домену. Запросы отправляются в нужный API Gateway на основании префикса в пути. Т.е. api.mycorp.com/users/v1 отправляется в Users Gateway, а api.mycorp.com/orders/v1 в Orders Gateway.
Каждый gateway (шлюз) описан с помощью OpenAPI спецификации, так при необходимости мы легко можем посмотреть или поделиться документацией к любому конкретному API. API Gateway внутри может иметь несколько endpoints, т.е. /users/v1/info, /users/v1/settings по префиксу оба уйдут в Users Gateway, но внутри за них могут отвечать отдельные функции.
Gateway может проверять аутентификацию и авторизацию, манипулировать заголовками или даже телом запроса/ответа и отправлять запрос дальше в наш собственный код, например, в Lambda или в какой-нибудь другой сервис AWS, например EventBridge.
По умолчанию сервис API Gateway имеет лимит по времени - 29 секунд для обычных HTTP запросов. Его можно увеличить если сделать запрос в тех. поддержку. Но на практике, если сервис не успевает отработать синхронный запрос за этот лимит, это сигнал, что что-то не так и скорее всего его нужно переписать на асинхронный паттерн. Другой выход это WebSockets, но в силу специфики сервисов мы к нему не прибегали.
Работа с данными
В отличие от API, при работе с данными зачастую больше вариативность в использовании сервисов. Чаще всего выстраивается цепочка
Приход данных может быть, например:
- файл который загружают в наш S3 бакет
- файл который мы скачиваем с SFTP сервера
- эвент в платформенной шине данных
- опрос базы данных по расписанию или эвенту
Процессинг чаще всего сводится к трансформации и/или насыщению данных. Уход так же может быть файлом или эвентом.
Я уже упоминал про лимиты в сервисе API Gateway, такие лимиты есть в каждом сервисе AWS и в случае работы с данными обязательно знать эти лимиты заранее, чтобы еще на этапе проектирования понимать что возможно реализовать, а что нет. Недавно Amazon, в попытке унификации, увеличил лимиты для многих serverless сервисов до 1 MB (раньше было 256 KB). Только для DynamoDB ограничение по-прежнему осталось 400 KB, чтобы сохранить время ответа в пределах < 10 ms.
Если мы знаем, что данные укладываются в эти ограничения и нет требования по долгосрочному хранению, мы можем спокойно прогонять их по сервисам in-flight. Например использовать SQS очередь на входе, чтобы быстро принимать данные, но обрабатывать в комфортном темпе в фоне.
Как только есть вероятность что данные не уместятся в лимиты в каком-то из сервисов в цепочке, вместо того чтобы отлавливать такие ошибки и пытаться с ними что-то делать, можно перейти на S3. Например для архитектуры выше, на входе мы будем сохранять данные в файл на s3 и отправлять в очередь путь к файлу, так Lambda прочитав сообщение из очереди сможет пойти в s3 и скачать файл для обработки.
Если процессинг сложный, зависит от нескольких сервисов или нужно запускать несколько потоков параллельно мы добавляем AWS Step Functions. Они могут ждать пока сервис обрабатывает данные, повторять выполнение в случае ошибок и многое другое. Цепочка тогда будет выглядеть так.
Гибрид HTTP и данных
3 группа это сервисы, которые включают как HTTP API так и работу с данными. Они сложнее потому что задействуют разные интерфейсы и разные паттерны.
Приведу пример абстрактного сервиса по обмену данными. Он построен на event-based архитектуре.
Есть внешняя система, в которой периодически появляются новые документы. Когда это происходит, внешняя система посылает специальное уведомление на HTTP API нашего сервиса. Уведомление содержит список новых документов (1 или более) в виде массива идентификаторов.
После проверки аутентичности отправителя и уведомления, HTTP API посылает эвент в EventBridge шину из которой по заданным параметрам запускается Step Function и входящий эвент подается на вход.
Для каждого элемента в массиве, степ функция запускается отдельный параллельный поток, в котором:
- скачивается оригинал документа
- документ трансформируется
- трансформированный документ сохраняется в корпоративной системе
Подходы
За время работы я лично решил с десяток различных задач и еще пачку в коллаборации с коллегами. В реальности сервисы сложнее, а диаграммы больше. Можно долго приводить примеры, но я хочу рассказать про общие подходы, которые, на мой взгляд, типичны для serverless.
Микросервисы
Использование serverless подталкивает тебя писать небольшие специализированные функции. В таком подходе тяжело писать “жирные” сервисы, которые будут делать все и сразу, поэтому serverless часто сочетается с микросервисной архитектурой.
Если же предметная область требует более тесной связанности компонентов, может получиться распределённый монолит - отдельные части системы изолированы, но взаимодействуют друг с другом через общие контракты. В таких проектах особенно важно заранее продумывать схемы данных и стратегию версионирования.
Впрочем, есть нюанс. К serverless решениям AWS также относит AWS Fargate — сервис для запуска контейнеров без необходимости управлять серверами. Поэтому ничто не мешает взять привычное приложение «всё в одном», упаковать его в контейнер и запускать через Fargate. Формально такой подход тоже будет относиться к serverless модели.
Эвенты
Эвенты (события) — важная часть serverless архитектуры. Даже если приложение не построено вокруг событийной модели, вам, скорее всего, всё равно придётся использовать эвенты для взаимодействия между компонентами. Зачастую они выступают клеем, который связывает отдельные части системы.
Допустим сервис получает на вход и работает с файлами, но чтобы сигнализировать что появился новый файл, легче всего использовать именно эвенты. Например в S3 бакете можно просто включить настройку, чтобы он начал отправлять эвенты о новых файлах в EventBridge.
Хорошей практикой здесь будет разделять эвенты на внутренние и внешние (или сервисные и доменные). К ним предъявляются разные требования по стабильности. Если эвент потребляют сервисы другой команды, он фактически становится частью публичного контракта системы. Любые изменения в его структуре могут привести к сбоям у потребителей, поэтому внешние эвенты требуют более аккуратного версионирования и контроля изменений. С внутренними обычно работать проще.
Независимо от того, используете вы serverless или нет, статья Martin Fowler про Event-Driven остаётся актуальной. Полезно заранее определить типы эвентов, которые используются в системе, и продумать стратегию их эволюции. И ещё один практический совет: по возможности используйте Event Registry — единый каталог эвентов и их схем. Ну или по крайней мере уделите особое внимание документации и версионированию эвентов.
Итеративность
На мой взгляд, serverless хорошо сочетается с итеративной разработкой. Когда значительную часть инфраструктурных задач и задач по безопасности (см. модель Shared Responsibility) берёт на себя платформа, стоимость доставки небольших изменений заметно снижается. Выпускать изменения небольшими порциями становится проще.
Конечно, сама по себе serverless-платформа не делает процесс разработки итеративным. Но она убирает часть организационных и технических препятствий, которые часто мешают частым релизам. А чем меньше путь от идеи до продакшена, тем проще проверять гипотезы и корректировать направление развития продукта.
Деньги
Serverless дает много возможностей и гибкости в плане запуска приложений. Кажется, что модель оплаты за использование должна экономить деньги, но как только разработчики получают доступ к практически неограниченному масштабированию она может выстрелить в ногу.
Думаю все могут легко представить ситуацию, когда настроили процессинг данных “на недельку” и забыли про него. Или частый пример из мира serverless это S3 бакеты, которые хранят терабайты уже никому не нужных данных. Ну и, пожалуй, самый классический пример, это когда приложение внезапно стало популярным и мы проснулись с кучей пользователей. Все ситуации такого рода ведут к неожиданным счетам. Что в свою очередь может вызвать большое сопротивление у бизнеса, потому что со старыми добрыми серверами такого не было.
Поэтому мониторинг расходов и бюджетные лимиты стоит настраивать с самого начала. В AWS для этого есть встроенные инструменты, но важно не только отслеживать общие затраты, а понимать их структуру. Мой совет - лучше сразу считать не просто потраченные деньги по ресурсам, но и отслеживать какая конкретно это команда. Ключевой практикой здесь становится тегирование. Теги можно применять практически ко всем ресурсам в AWS, и чем раньше внедрён строгий стандарт тегирования по командам и сервисам, тем проще контролировать и анализировать расходы в будущем.
Организация кода
Когда работаешь с кодом на таком уровне детализации как функции, возникает вопрос, а как лучше его организовать? Насколько мне известно, здесь нет золотого стандарта. Все делают по разному. Я лично сталкивался с такими вариантами:
- монорепозиторий - все Lambda функции хранятся и деплоятся из одного репозитория
- один репозиторий на функцию - каждая функция живёт отдельно, а связь между ними задаётся через группы или именование
- сервисные репозитории - один репозиторий объединяет все функции конкретного сервиса или доменной области
Монорепозиторий - удобно, особенно на старте. Хорошо видно, что уже есть - легче переиспользовать наработки. Но по мере эволюции команды или компании различные подходы начинают наслаиваться, а доменные области пересекаться. Поддерживать такой репозиторий в чистоте становится сложнее.
Я не говорю здесь про Google или Meta — это компании с совершенно другим уровнем организационной и инженерной зрелости
В целом я бы сказал, что монорепозиторий это хороший старт, особенно когда вы не до конца понимаете, как будет выглядеть работа с serverless в будущем. Но главное не прозевать момент когда он превратится в помойку и во-время декомпозировать или наводить порядок.
Один репозиторий на функцию создает очень изолированную структуру и усложняет обмен знаниями и координацию. Я пробовал внедрить его лично и быстро отказался. Слишком много операционной нагрузки без явного выигрыша.
Сервисные репозитории это как раз подход который мы использовали на проекте. Он также несет некоторую операционную нагрузку и важно это понимать. Обновление сервиса шаблона вызывает каскадное обновление всех сервисов, которые были из него созданы. Но открывая проект, с которым вы еще не работали, ваша когнитивная нагрузка гораздо меньше, потому что многие вещи организованы привычным образом. При этом большинство ресурсов необходимых для конкретной задачи сгруппированы и изолированы в одном месте, вам не нужно пробираться через лишнее.
Чтобы было чуть более понятно, я приведу пример структуры абстрактного сервиса на Python:
infrastructure/
├── template.yaml
src/
├── my_super_service
│ ├── api/
│ │ └── main.py
│ ├── persistence/
│ │ ├── model.py
│ │ └── repository.py
│ ├── processing/
│ │ └── main.py
│ ├── config.py
│ └── __init__.py
tests/
├── unit/
│ ├── api/
│ │ └── test_main.py
│ ├── processing/
│ │ └── test_main.py
│ ├── conftest.py
│ └── test_config.py
.coveragerc
.gitignore
Jenkinsfile
Makefile
openapi.yaml
Pipfile
Pipfile.lock
README.md
template.yamlИдея здесь в том, что у сервиса есть API и фоновый процессинг и они существуют внутри одного кода, используя общие слои вроде доступа к базе данных.
Стандартизированный README.md задает контекст и правила работы с сервисом. Makefile унифицирует команды - например, тесты запускаются одинаково - командой make test, независимо от внутренней реализации. Jenkinsfile описывает CI/CD пайплайн (на его месте может быть GitLab CI или другой инструмент) используя единый подход к сборке и деплою.
Файл template.yaml описывает ресурсы AWS и используется пайплайном для деплоя. Все зависимости упаковывается в Lambda слой (Layer), чтобы переиспользовать их между функциями. При этом API и процессинг деплоятся как отдельные Lambda-функции, но используют общий слой зависимостей и общий код (вся папка src).
Вот так будет выглядеть часть template.yaml которая определяет Lambda слой:
...
LambdaDependencies:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: !Sub "${AWS::StackName}-dependencies"
ContentUri: dependencies/
CompatibleRuntimes:
- python3.14
RetentionPolicy: Delete
Metadata:
BuildMethod: python3.14
...Вот так, определение API функции:
...
ApiFunction:
Type: AWS::Serverless::Function
Properties:
Handler: my_super_service.api.main.lambda_handler
MemorySize: 512
Layers:
- !Ref LambdaDependencies
...А вот так, определение функции процессинга:
...
ProcessingFunction:
Type: AWS::Serverless::Function
Properties:
Handler: my_super_service.processing.main.lambda_handler
MemorySize: 1769
Layers:
- !Ref LambdaDependencies
...Помимо кода, в проекте есть и другие ресурсы, как например SQS очередь, они описываются в отдельном файле infrastructure/template.yaml, который деплоится как подстек и связан с родительским стеком.
Я ни в коем случае не утверждаю, что это единственно верный способ организации serverless приложения. Важно выбрать подход, который не усложняет систему больше, чем приносит пользы, и который команда сможет поддерживать по мере роста.
Observability
Я хотел написать, что это преимущество serverless, но на самом деле это скорее преимущество облачных платформ в целом. Мониторинг доступен из коробки: большинство базовых метрик собираются автоматически, а включение расширенных метрик обычно не вызывает сложностей. Кастомизация тоже относительно простая. В результате фокус смещается с того, как и что мониторить, на что именно стоит смотреть и реагировать.
Приведу пример. Допустим, есть приложение с Lambda-функцией и SQS-очередью. Опустим детали того, как сообщения попадают в очередь — для примера это не критично. Важно, что Lambda читает сообщения и выполняет некоторый процессинг. AWS CloudWatch по умолчанию сохраняет десятки метрик:
- количество вызовов функции
- использование памяти
- время выполнения
- количество одновременно работающих инстансов
- количество ошибок выполнения
- количество сообщений в очереди
- размер сообщений
- возраст самого старого сообщения
- количество доступных сообщений
- количество сообщений, ожидающих обработки
и так далее.
Какую из этих метрик выбрать, чтобы разбудить дежурного инженера среди ночи при превышении порога?
Ответ - никакую. Все эти метрики без сомнения важны и помогают понимать состояние системы. Но не имеет большого значения, сколько раз была вызвана функция, если все сообщения успешно обрабатываются. И даже количество ошибок не всегда критично, если есть механизмы повторов и обработка в итоге завершается успешно. В таком случае навешивание алертов на каждую метрику может привести к мониторинговому шуму, за которым легко пропустить действительно важные сигналы.
Но что тогда использовать для алертов в этом примере? Связка Lambda + SQS часто дополняется специальной очередью Dead-Letter Queue (DLQ). В таком подходе, если сообщение не удаётся обработать после заданного числа попыток, оно попадает в DLQ. Это предотвращает бесконечные повторы и расход ресурсов на заведомо проблемное сообщение. При этом само сообщение не теряется, его можно разобрать вручную и понять причину ошибки. И вот за количеством сообщений в этой очереди имеет смысл пристально следить. В этом примере, это то, что действительно требует внимания со стороны инженера.
Я знаю, что я только что сделал. В изначальных условиях не было речи ни про какую DLQ, я добавил её позже ¯\_(ツ)_/¯. Но в этом, если честно, и смысл - observability редко живёт в отрыве от контекста. Подход к мониторингу должен быть рациональным — важно понимать, какие сигналы действительно имеют значение и в какой момент.
Заключение
Serverless не дает “магическое удешевление” или “простоту”, а меняет то, как ты работаешь с системой в целом.
- Меняется скорость разработки и доставки - когда тебе не нужно думать про инфраструктуру в каждом новом сервисе, можно быстрее переходить от идеи к рабочему коду и проверке гипотез
- Появляется масштабирование “по умолчанию” - в большинстве случаев тебе не нужно заранее закладывать мощность или думать о пиковых нагрузках. Система сама подстраивается под реальную нагрузку
- Уменьшается операционная рутина - меньше ручного управления серверами, меньше задач по поддержке окружений, меньше “вечных” задач по апгрейдам и патчам
При этом serverless не убирает сложность полностью, он её просто переносит в другие области: архитектуру, observability, контракты между сервисами и контроль стоимости. Если попытаться свести всё к одной мысли — serverless не упрощает системы, он делает их более дробными и распределёнными. А дальше уже от команды зависит, станет ли это преимуществом или источником сложности.
Лично для меня serverless и работа на этом проекте в целом стали драйвером развития скиллов архитектуры и проектирования.