Зачем усложнять разработку с AWS Lambda?

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

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

В данной статье проанализируем одну наметившуюся тенденцию, которая мне не особо нравится. Появился ряд публикаций, в которых авторы рекомендуют структурировать сложные проекты с Lambda. Они пишут о разделении ответственности, различных слоях кода и его переиспользовании. Сами по себе эти подходы не представляют сложности, но в совокупности они начинают усложнять разработку Lambda и задают тенденцию, направленную против модульности и простоты сервиса. 

Характеристики идеальной среды 

Идеальная среда для Lambda должна обладать следующими характеристиками: 

  • простота; 
  • модульность; 
  • изолированность; 
  • актуальность. 

Простота 

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

Когда я только начинал заниматься бессерверной разработкой, многие интересовались, как мне удается управлять всеми этими функциями Lambda. Во-первых, приходилось объяснять, что бессерверные приложения создаются в коде и по принципу “Инфраструктура как код” (IaC), а не щелчками в консоли AWS. Кроме того, приложения обычно состоят из множества микросервисов, обладающих функциями для поддержания данного сервиса. Это значит, что все функции не хранятся в одном месте, и процесс управления ими упрощается. 

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

Еще одна характеристика “простой” функции Lambda  —  удобочитаемость. Легко ли читается код? Приходится ли углубляться на 5 слоев, чтобы понять действие кода? Если при беглом взгляде на файл можно определить, где выполняется процесс, то он считается простым. 

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

Следует явно прописывать код функции, чтобы при взгляде на него становилось понятно, что он делает. Никто не горит желанием, пробираться через вложенные вызовы методов и множество файлов, чтобы обнаружить, что функция сохраняет сущность (англ. entity) в DynamoDB. Такая ненужная сложность мешает разработчикам определять, как именно решается бизнес-задача. 

Модульность 

Функция должна решать одну задачу. В редких случаях она самостоятельно выполняет весь бизнес-процесс. Она реализует один дискретный фрагмент логики и возвращает результат (или запускает событие для асинхронного продолжения процесса). 

Рассмотрим базовый CRUD API. Каждая конечная точка поддерживается отдельной функцией Lambda. Конечная точка create (создать) вызывает createEntityFunction. Конечная точка delete (удалить) вызывает deleteEntityFunction и т.д. DynamoDB Streams или события EventBridge представляют собой отличный способ отреагировать на новую или обновленную сущность и организовать последующие действия.

Обратимся в качестве примера к моему приложению Gopher Holes Unlimited. Как видно, в нем одна функция обновляет существующего суслика, а другая связывает сусликов с их норами

Когда пользователь обновляет суслика, он может передать идентификатор его норы. В случае предоставления id норы в DynamoDB создается ссылка. Это позволяет снова использовать нору суслика в запросе, если пользователь захочет посмотреть все норы, связанные с определенным сусликом. Но в приложении для обеспечения модульности логика разделена.

Чтобы связать нору с сусликом необязательно ограничиваться одним способом, есть и другие варианты. Разделение логики создания ссылки на собственную функцию позволяет переиспользовать ее в различных бизнес-процессах. Можно связать нору с событием, добавить ее как часть рабочего процесса Step Function или даже сделать ее собственной конечной точкой. Поскольку она модульная, ее можно переиспользовать как угодно. 

Изолированность 

В самом начале освоения бессерверных технологий я сделал для себя открытие. В ту пору я занимался толстыми клиентскими приложениями, написанными на  .NET. Как правило, в работе применялся спагетти-код, который вызывался отовсюду. Бывало исправляешь ошибку в файле A и создаешь две новые в файлах B и C, поскольку они задействуют общий код и ожидают разного поведения. 

В случае с Lambda подобного не происходило. Функция  —  это дискретная единица кода. Она полностью автономна. Изменения в функции A никак не влияют на функцию B.

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

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

С другой стороны, использование общих слоев логики и данных подразумевает обновление только одного места в коде при внесении изменений. Недостаток данного метода состоит в том, что изменения общего кода могут случайно нарушить другую несвязанную функцию Lambda. Модульные тесты помогают выявлять такие непредвиденные случаи, но и они не идеальны! 

Актуальность

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

Однако в январе 2023 года AWS выпустила элементы управления средой выполнения (англ. runtime management controls). Они предоставляют пользователям возможность выбирать момент применения патчей (англ. patch) к среде выполнения, в которой работают функции. Пользователи могут выбрать автоматические обновления (как и было), обновления среды выполнения при изменении кода функции или обновление вручную. 

Если у вас нет особо веских причин не принимать обновления по мере их появления, следует игнорировать эту функциональность. 

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

Испытав на себе всю тяжесть эксплуатационной нагрузки по управлению всеми этими версиями, мы быстро поняли, что просчитались. 

Конечно, управление средой выполнения Lambda лежит на AWS, а не на потребителях. Но без обновлений до последний версии код будет все больше отставать от важных патчей безопасности, улучшенных возможностей среды выполнения и оптимальных решений. В конечном итоге вы застынете во времени, поскольку с каждым разом обновляться до последней и лучшей версии станет все труднее. 

Дальнейшие действия 

Многие читают статью и думают, что производственное ПО корпоративного уровня так не создается. А почему нет? 

Я только что поделился личным опытом создания двух полностью бессерверных приложений, которыми ежедневно пользуются десятки тысяч людей. Команда поддерживает ПО без особых проблем. Нагрузка по эксплуатации и разработке удерживается на низком уровне за счет изолирования функций. 

При этом команда разработчиков накапливает опыт и осваивает код независимо от того, как он структурирован. Они становятся одинаково компетентными в вопросах изолированных функций или функций, применяющих общие слои бизнес-логики и доступа к данным. 

При желании минимизировать возможный ущерб в случае неудачи позаботьтесь о том, чтобы функции были небольшими и отдельными. Пусть код будет простым, а назначение функции понятным с первого взгляда. Зачем из-за ненужной абстракции и наследования тратить время, гадая, что делает код?

Абстракция ради абстракции вредна. Чем больше ясности, тем лучше сопровождение. 

Ум  —  штука непростая. Не каждый поймет умное решение. Помните, что вы не единственный, кто будет сопровождать написанный вами код. Возможно, сейчас над ним работаете только вы, а вот через два года улучшать его будет уже кто-то другой. Вместо того чтобы искать умное решение в 10 строк кода, выберите лучше простое в 20. 

Создавайте функции по принципу единственной ответственности. Пусть они выполняют одну задачу и делают это хорошо. Объединяйте функции в Step Functions или управляйте ими с помощью событий. Выгодно пользуйтесь преимуществами модульности. 

Следуя принципу простоты, я добился успеха. Кому-то такой подход к разработке с помощью Lambda может показаться наивным, но он работает! Данный процесс не должен быть сложнее, чем разработка и соединение структурных блоков.

Моя позиция в этом вопросе может вызывать споры. Но пока практикуемый подход доказывает свою эффективность, я буду его придерживаться! 

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Allen Helton: Are We Making Lambda Too Hard?

Предыдущая статьяПрактическое применение KSP
Следующая статья6 современных возможностей JavaScript, о которых не знает большинство разработчиков