Когда мы говорим о бессерверной архитектуре, мы обычно выходим далеко за рамки модели «функция как услуга» (FaaS), одной из реализаций которой являются функции AWS Lambda.

Причин высокой привлекательности AWS Lambda две: первая — автоматическое масштабирование (причем в обе стороны), вторая — возможность оплаты по факту использования. Тем не менее, чтобы в полной мере использовать эти возможности и все преимущества бессерверной архитектуры, другие компоненты инфраструктуры должны обладать такой же гибкостью.

Как же будет выглядеть такого рода архитектура в интернет-проекте?

Мы в Theodo очень любим бессерверные технологии и внедряем их во всё новые и новые проекты. Какие-то сервисы и модели мы применяем наиболее широко. Поэтому мы решили поделиться опытом в разработке архитектуры веб-приложений. Если бессерверные технологии вам в новинку и хотелось бы получить общие рекомендации и ответы на вопросы, то вы обратились по адресу!

Откровенный и вопиющий спойлер (не бойтесь, мы успеем погрузиться в каждый аспект темы):

На схеме вверху каждый блок — это стандартная четко выделенная область применения или техническая возможность, характерная для большинства бессерверных архитектур. Это необязательно будут микросервисы или стеки, если говорить на языке CloudFormation (это мы обсудим чуть позже).

Мы рекомендуем полномасштабный переход на AWS Lambda c использованием событийных микросервисов, написанных на TypeScript

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

Amazon Web Services

Конкурентный рынок облачных технологий весьма насыщен: AWS, GCP, Azure, IBM Cloud, Alibaba Cloud — каждая из этих компаний предлагает свое замечательное решение и развивается беспрецедентно высокими темпами.

Бен Эллерби (Ben Ellerby) сравнил трех крупнейших облачных провайдеров в этой статье: мы отдали предпочтение решениям AWS. С точки зрения бессерверных технологий они оказались самыми продвинутыми. Именно решения AWS позволяют нам максимально приблизиться к полностью бессерверной архитектуре. Чтобы это проиллюстрировать, в следующей части статьи мы подробно рассмотрим каждый из AWS-сервисов, входящих в состав наших архитектурных блоков.

Node.js с использованием TypeScript

JavaScript — это один из самых популярных языков программирования в мире, с огромным сообществом разработчиков (посмотрите статистику на GitHub). Мир бессерверных вычислений не сильно выпадает из этой статистики. Например, по данным Datadog, 39% всех развернутых сегодня систем AWS Lambda работает на JavaScript, хоть Python и лидирует с 47%. TypeScript делает еще один шаг вперед и добавляет к JavaScript еще один замечательный уровень защиты. И наконец, JavaScript отлично работает в AWS Lambda с подавляющим большинством сценариев использования!

Serverless Framework

Именно в этом фреймворке по большей части реализуется практика IaC («инфраструктура как код») — поверх CloudFormation. Задав функцию AWS Lambda, которая будет реагировать на HTTP-событие, Serverless Framework автоматически развернет связанный ресурс API-шлюза, а также соответствующий маршрут вместе с новой функцией AWS Lambda. Затем, когда будет достигнута предельная емкость фреймворка и нам понадобится более сложная конфигурация сервисов, мы сможем легко добавить CloudFormation.

C гордостью сообщаем: команда Theodo настолько уверовала в Serverless Framework, что решила стать официальным партнером фреймворка!

Узкоспециализированные функции AWS Lambda…

Lambda — это именно функция. У нее есть одна задача, и она выполняет ее хорошо. Нужно подтянуть в интерфейс список элементов? Создайте для этого функцию AWS Lambda. Нужно отправить пользователю подтверждение регистрации по электронной почте? Создайте еще одну функцию AWS Lambda. Разумеется, узкоспецифичный код (например, для описания сущностей данных) может быть факторизован и предоставлен в общее пользование в виде отдельной папки утилит. Однако работать с таким кодом нужно очень внимательно, потому что любое его изменение повлияет на все связанные с ним функции AWS Lambda, — а поскольку эти функции тестируются и развертываются независимо друг от друга, легко можно что-то упустить (TypeScript нам тут в помощь).

…разделение на микросервисы…

Чтобы не заставлять команды разработчиков толкаться и мешать друг другу, не раздувать файлы package.json и serverless.yml до немыслимых размеров (кстати, в CloudFormation довольно щедрое ограничение на число ресурсов — 500), не тратить много времени на развертывание CloudFormation, а также лучше ориентироваться в базе кода и четко разграничивать ответственность между функциями AWS Lambda, мы устанавливаем границы и разделяем проект на отдельные микросервисы. Бен Эллерби написал здесь о полезной методологии мозгового штурма под названием EventBridge Storming, которая как раз и помогает определить эти границы.

В нашем монорепозитории: 1 микросервис = 1 стек CloudFormation = 1 файл serverless.yml + 1 файл package.json. Кроме того, микросервисы управляют своими собственными сущностями данных и не предоставляют их в общее пользование другим микросервисам.

Раньше мы рекомендовали переходить на полноценный JavaScript, однако есть множество причин, по которым вам, возможно, захочется использовать другой язык. А может быть, вы предпочтете поэтапно перейти на бессерверные вычисления на чистом JavaScript. Неоспоримое преимущество микросервисов при бессерверных вычислениях заключается в том, что можно легко комбинировать технологии в рамках архитектуры, сохраняя ее простоту и согласованность, а также не зависящие от используемых технологий интерфейсы между микросервисами.

…взаимодействие на языке событий

Эти микросервисы должны быть полностью независимыми друг от друга - если один из них перестанет работать или претерпит кардинальные изменения, остальные компоненты системы не должны из-за этого сильно страдать. Чтобы это стало возможно, функции Lambda общаются друг с другом исключительно через бессерверную шину событий — EventBridge. В этой статье Бен Эллерби (это снова он) подробно рассказывает о том, что делает сервис EventBridge таким полезным.

Для каждого типа функций требуется особая бессерверная архитектура с конкретным набором AWS-сервисов

Установив контекст, давайте пройдемся по архитектурным блокам приведенной выше устрашающей диаграммы и посмотрим, какие «самые бессерверные» сервисы можно найти в AWS.

Фронтенд (разработка)

Наш сияющий бессерверный бэкенд должен каким-то образом скармливать данные фронтенду. Чтобы облегчить фронтенд-разработку при использовании AWS, мы используем Amplify.

Amplify — это сразу несколько полезных вещей: инструмент CLI, инструмент IaC, SDK, а также набор UI-компонентов. Мы используем при разработке фронтенда JS SDK, чтобы ускорить интеграцию с ресурсами (например, Cognito для аутентификации), развернутыми при помощи других инструментов IaC, таких как Serverless Framework.

Хостинг сайтов

Большинство современных веб-сайтов — это одностраничные приложения (Single Page Applications, SPA). Это полнофункциональные динамические приложения, упакованные в единый набор статических файлов, которые загружает пользовательский браузер, когда он впервые обращается к URL-адресу. В среде AWS мы размещаем эти файлы на файловом хранилище S3 и предоставляем к нему доступ через CDN-сеть CloudFront.

Тем не менее тенденция явно склоняется в пользу SSR-сайтов, использующих отрисовку на стороне сервера, как в случае технологии Next.js. Для настройки SSR-сайта в бессерверной инфраструктуре мы используем Lambda@Edge внутри CloudFront. Это позволяет реализовать отрисовку на стороне сервера, располагая функции AWS Lambda как можно ближе к конечным пользователям. Чтобы погрузиться в тему, прочитайте эту статью.

Домен и сертификат

Разумеется, для нашего сайта мы не можем довольствоваться необработанным автоматически сгенерированным S3-адресом, поэтому мы формируем сертификаты и привязываем их к CloudFront с помощью Certificate Manager, а затем для управления доменами используем Route 53.

Бизнес-API

Наконец, мы должны организовать взаимодействие сайта с бэкендом, чтобы начать получать и отправлять данные. Для этого мы используем API-шлюз, который позволяет обрабатывать HTTP-соединения и маршруты, синхронно вызывая Lambda-функцию для каждого маршрута. Наши Lambda-функции содержат бизнес-логику взаимодействия с DynamoDB, позволяющую хранить и потреблять данные.

Как мы уже говорили, мы используем событийную логику, то есть быстро отвечаем пользователям, а параллельно, так сказать за кадром, продолжаем обрабатывать пользовательский запрос в асинхронном режиме. DynamoDB, например, предоставляет доступ к потокам, которые могут асинхронно вызвать Lambda-функции в качестве реакции на любое изменение данных. Большинство бессерверных сервисов поддерживают аналогичную возможность.

DynamoDB — это огромная отдельная тема, и в этой статье Роб Кронин (Rob Cronin) дает обнадеживающий комментарий о классических проблемах, связанных с широко известной базой данных NoSQL.

Асинхронные задачи

Управление нашей архитектурой устроено при помощи событий, поэтому многие Lambda-функции асинхронны и запускаются событиями EventBridge и S3, потоками DynamoDB и так далее. Например, у нас может быть асинхронная Lambda-функция, которая будет отвечать за отправку пользователю приветственного письма после успешной регистрации.

Обработка отказов играет решающую роль в распределенной асинхронной системе. Поэтому в случае асинхронных Lambda-функций мы используем DLQ — очередь недоставленных сообщений — и передаем окончательное сообщение об отказе сначала в службу уведомлений SNS (Simple Notification Service), а затем в службу очередей SQS (Simple Queue Service). Сейчас нам приходится поступать так, потому что на данный момент невозможно привязать SQS напрямую к DLQ-очереди Lambda-функций.

Передача пуш-сообщений от бэкенда к фронтенду

В асинхронном режиме фронтенд уже не может ограничиваться показом иконки загрузчика в ожидании XHR-ответа. Нужны состояния ожидания и передача пуш-данных от бэкенда. Для этого мы используем API WebSocket от нашего API-шлюза, который поддерживает постоянное соединение WebSocket и запускает Lambda-функции, только когда приходят сообщения. Я написал статью для глубокого погружения в тему. Там я рассказал, почему мы выбрали WebSocket, а не другие решения и как лучше его внедрять.

Загрузка файлов

Поскольку обработка потока загружаемых файлов из Lambda-функции может оказаться весьма дорогостоящим делом, в S3 Lambda-функции могут сгенерировать подписанный (защищенный) URL-адрес загрузки, который будет использоваться нашим фронтендом для прямой загрузки файлов в S3. Приятный момент заключается в том, что в данном случае (как и в большинстве сервисов AWS) можно сделать другую асинхронную Lambda-функцию, которая будет слушать изменения файла S3 и обрабатывать любые последующие операции.

Пользователи и аутентификация

В Cognito есть все, что нам нужно: аутентификация, управление пользователями, контроль доступа, а также интеграция с внешним поставщиком удостоверений. Хоть этот сервис и славится тем, что с ним сложновато работать, это компенсируется большим набором возможностей. Ну и как обычно, у него есть выделенный SDK для взаимодействия с Lambda-функциями и он умеет отправлять события, запускающие Lambda-функции.

В нашем примере мы проиллюстрируем возможность привязать собственные авторизаторы Cognito к нашим маршрутам API-шлюза. Мы также назначили одну Lambda-функцию для обновления аутентификационных токенов, а другую — для получения списка пользователей.

Здесь я бы хотел предостеречь: Cognito еще не готов к тому, чтобы ему можно было полностью доверить управление сущностью пользователя. У него есть ряд ограничений, например по числу поддерживаемых атрибутов. Если для вас важна гибкость, пожалуй, лучше хранить пользовательские атрибуты в DynamoDB.

Машина состояний

В некоторых ситуациях наша логика и поток данных могут стать довольно сложными. Чтобы нам не пришлось вручную управлять этим потоком изнутри Lambda-функций (а следить за происходящим и структурировать весь процесс — дело непростое), у AWS есть то, что нам нужно: сервис Step Functions.

Мы объявляем машину состояний через CloudFormation — включая любые последующие шаги и состояния, все ожидаемые либо неожиданные результаты — и назначаем этим шагам некие стандартные действия (например, wait или choice) либо Lambda-функцию (выбираем из поддерживаемых интеграций). Затем мы можем в реальном времени, используя интерфейс AWS, следить за работой машины (на основе логов). На каждом из этих шагов мы можем задать обработку повторных и неудачных попыток. Бен Эллерби более подробно описал сервис в этой статье.

Чтобы привести более конкретный пример, предположим, что мы хотим реализовать email-рассылку помощью SaaS и убедиться, что все сообщения были высланы адресатам.

  • Шаг 1 — Lambda: дает SaaS указание выполнить email-рассылку и получает в ответ идентификатор рассылки.

  • Шаг 2 — Task Token Lambda: получает callback-токен от Step Function, связывает его с идентификатором рассылки, а затем ждет обратного вызова от SaaS.

  • Шаг 3 (вне потока) — Lambda: вызывается перехватчиком из SaaS при изменении статуса рассылки (ожидание, архивация, неудача, успех) и возобновляет поток по статусу кампании, используя соответствующий callback-токен.

  • Шаг 4 — выбор: в зависимости от статуса, если рассылка еще не была успешно выполнена, вернуться к шагу 2.

  • Шаг 5 (заключительный) — Lambda: обновляет данные о пользователях после рассылки.

В этой статье хорошо объяснено, как работают токены Task Token.

Безопасность

Управление идентификацией и доступом (Identity & Access Management, IAM) — эта система нужна для того, чтобы филигранно управлять всеми AWS-доступами — со стороны разработчиков, конвейеров CI/CD, а также сервисов AWS, которые обращаются друг к другу. Поначалу это может выглядеть пугающе, однако преимущество тут в широких возможностях тонкой настройки. Они позволяют нам хорошо продумать и детализировать любое действие, которое мы хотим разрешить конкретному «потребителю». Это означает, что каждый уровень нашей инфраструктуры защищен по умолчанию. Бен Эллерби выступал на ServerlessDays Nashville на эту тему.

Что касается данных повышенной секретности, таких как API-ключ SaaS, то мы безопасно храним их в Systems Manager (в Parameter Store). Запрашиваем мы эти данные из наших файлов в Serverless Framework или в CloudFormation — или даже напрямую из исходного кода (при помощи соответствующего SDK). Полезно упомянуть, что система Secrets Manager выполняет аналогичные действия. Также в данном случае полезен сервис Key Management Service (KMS) — он помогает нам управлять ключами шифрования.

Если вас заинтересовала эта тема, Sat G написал отличную обзорную статью о безопасности в бессерверной среде — там можно найти более полную информацию.

Мониторинг

Мы де-факто используем CloudWatch в качестве службы мониторинга. Все сервисы AWS автоматически поддерживают простейшие метрики и логи, которые отсылаются в CloudWatch и дают нам базовую информацию. Однако мы можем пойти гораздо дальше: отправлять пользовательские метрики и логи, создавать информационные панели, устанавливать срабатывание сигналов тревоги при достижении пороговых значений, выполнять сложные запросы вглубь данных и отображать их на графиках произвольного формата.

Мы не прекращаем поиск альтернативных вариантов. X-Ray, например, комплексно отслеживает весь путь запросов в рамках нашей распределенной системы, а затем красочно отображает его в динамике. Иногда, правда, он «сбивается со следа», поскольку пока еще не поддерживает некоторые сервисы, такие как EventBridge (а это как раз центральный элемент нашей архитектуры). Еще один сервис, ServiceLens, развертывается поверх X-Ray и CloudWatch и выглядит потрясающе. Есть также ряд перспективных внешних (с точки зрения AWS) решений: Thundra, Epsagon, Lumigo. Однако мы еще не успели в полной мере их опробовать.

Команда Theodo гордится тем, что добавила собственный компонент в экосистему: если вы хотите повысить у себя качество разработки и уровень наблюдаемости, вам обязательно стоит попробовать инструмент Serverless-Dev-Tools.

Бессерверные вычисления — это потенциал, способный перерасти в успех, и мы хотим вам помочь сделать решительный шаг в его сторону

Мир бессерверных вычислений развивается не просто быстро, а очень быстро. Вступая в этот мир, вы словно открываете для себя совершенно новую вселенную, полную ошеломляющих возможностей, где столько всего еще предстоит сделать, — и это поистине захватывающе!

Команда Theodo каждую неделю открывает для себя новые сервисы, инструменты и модели. Вот почему я намерен постоянно обновлять и актуализировать эту статью, чтобы идти в ногу со временем и делиться нашими лучшими передовыми наработками. Подпишитесь на меня в Твиттере — обещаю, будет много всего интересного!

Рассказав вам об основных понятиях бессерверной архитектуры, мы хотим продемонстрировать, насколько она дешевле классической! Для этого мы создали калькулятор расходов: заполните всего несколько полей, и вы узнаете, во сколько обойдутся вам бессерверные вычисления.

Я, безусловно, продолжу работать над всеми этими темами, поэтому, если вам интересно, напишите мне в твиттер.

Надеюсь, что в этой статье вы нашли ответы на интересующие вас вопросы. Я буду чрезвычайно рад ответить на любые вопросы, получить ваши отзывы — и улучшить рекомендации, предложенные в этой статье.

П.С. От переводчика

Присоединяйтесь к сообществу Serverless в Telegram: Yandex Serverless Ecosystem. Мы регулярно встречаемся в виртуальном пространстве и похоже созревает потребность в очной встрече.