Разрабатываем и отлаживаем serverless-приложения на AWS Lambda локально

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привет, на связи Иван Резник (Backend Team Lead) и Антон Гурленко (Backend Developer) из Amazing Apps. Наша компания делает мобильные приложения в категории Health & Fitness, которые скачали уже более 100 млн человек. Сегодня расскажем о том, как мы разрабатываем и отлаживаем наши serverless-бэкенды локально, без консоли AWS, с какими сложностями сталкивались по пути, и зачем написали свой фреймворк для отладки.

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

Эта статья как раз об одной из таких сложностей — неудобстве локальной разработки и отладки serverless-приложений. Статья будет полезна тем, кто планирует или начинает вести разработку Serverless-приложений. Наше решение не обязательно подойдет всем, но подход может натолкнуть вас на интересные мысли.

Вкратце о serverless вообще и об Amazon Lambda в частности

Serverless — хороший способ получить быстро масштабируемый бэкенд для вашего приложения, не тратя лишних денег и времени на инфраструктуру.

Amazon — де-факто главный клауд-провайдер — предоставляет для таких случаев категорию услуг FaaS (Function As A Service). Это возможность бессерверного запуска кусков кода (функций), благодаря чему разработчики могут писать и обновлять код в процессе. Написанные функции вызываются при наступлении какого-либо события.

Основные особенности FaaS:

  • FaaS позволяет намного проще масштабировать код и вводить микросервисы — в функциях реализуется только бизнес-логика.
  • Приложения, написанные в рамках FaaS, являются stateless (не сохраняющие состояние).
  • В рамках FaaS серверный процесс не выполняется, а срабатывает триггерное событие, которое инициирует вызов функции, например, HTTP-вызов.

У нас в Amazing Apps AWS Lambda используются для бэкендов наших приложений уже 2,5-3 года. Однако несколько вещей нас долгое время не устраивали:

  • неудобно было отлаживать изменения локально; сложно запустить сразу все эндпоинты, чтобы отладить систему;
  • некоторые элементы невозможно протестировать локально (например, json-схемы для валидации).

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

В чем сложности подхода?

Изначально для локальной разработки мы использовали AWS SAM (Serverless Application Model), но он имел много недостатков:

  • Для его работы каждый эндпоинт должен был быть описан в конфигурационном файле, который не использовался при деплое самого сервера. По факту, мы были вынуждены делать +1 конфиг большого размера, в котором большая часть — это просто копипаст;
  • Для установки зависимостей было необходимо вручную билдить «layer» — архив, который содержит библиотеки, конфиги, дополнительные данные, и только потом запускать локально код;
  • Не было нужной нам валидации атрибутов HTTP-запроса (Query parameters, Path parameters, Headers, Body).

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

От SAMserver требовалось, чтобы он мог собрать и запустить локальный сервер из минимальных конфигов (которые потом переиспользуются на продакшене). В отличие от официального SAM, SAMserver умеет работать в обычном сервер-режиме (flask), а не запускать каждый реквест в докер контейнере. Конечно, это не полная эмуляция lambda-энвайронмента, но, как показала практика, этого достаточно в 99.9% случаев (по крайней мере, для наших задач).

Кроме того, такой подход:

  • делает разработку более responsive (latency реквеста меньше, поскольку не используется докер);
  • позволяет использовать дебаггер;
  • дает возможность не билдить новый layer при добавлении dependency;
  • позволяет в случае необходимости запустить приложение для отладки как в старом режиме, так и в новом.

Поскольку SAMserver — это wrapper над официальным SAM, это дает нам возможность более оперативно фиксить баги и расширять его функционал (например — эмуляция cognito-авторизации, валидация реквеста (header, body, query и т.д.), advanced logging, тестирование и профайлинг и т.д.).

Техническая реализация: эмулируем лямбды

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

Кодовая база у нас находится в одном проекте (монолит), и состоит как из python-кода, так и из конфигов всех ресурсов (Lambda, SQS, jobs). Существует много фреймворков для разработки serverless-приложений — serverless, cdk, SAM, и т.д. Исторически так сложилось, что для наших проектов мы используем SAM.

Для его работы нужно описать конфигурацию всех ресурсов в конфигурационном файле template.yaml. Дальше этот конфиг используется и для локальной разработки, и для деплоя в разные среды. Описывать template.yaml полностью нам не подходило — поскольку он использовался для локальной разработки и для деплоя в разные environments — нужно было поддерживать в актуальном состоянии три таких файла, которые отчасти были бы копипастом друг друга.

Решение — генерировать этот конфиг динамически.

Для каждого ресурса (lambda, api gateway, SQS, cron jobs, etc) описывается минимальный конфиг, а наш СІ билдит финальный темплейт в зависимости от того, куда деплоится код — production или stage. Такой подход достаточно гибкий, так как дает возможность конфигурировать все ресурсы из проекта, но при деплое СІ сделает еще много дополнительных конфигураций, общих для всех проектов, включая политики, доступы и так далее.

Каждая лямбда — это точка входа в приложение, которая вызывает свой контроллер. По факту, это отдельный python-модуль и соответственно эндпоинт. При деплое в каждую лямбду ложится весь код проекта. Дополнительно указывается, какой контроллер должна запустить конкретная лямбда при инвоуке. Такой подход дает возможность по максимуму переиспользовать код и поддерживать структуру проекта в более-менее привычном формате (не serverless).

Как это работает в SAM:

SAM дает возможность поднять локальный сервер из всех лямбд или запускать каждую лямбду отдельно. Для максимальной эмуляции среды лямбды, SAM запускает каждый реквест в отдельном докер-контейнере. У этого подхода есть как свои плюсы, так и минусы.

Из плюсов — среда запуска кода, максимально приближенная к «боевой» лямбде.

Из минусов:

  • Сложно добавлять новые зависимости — нужно пересобирать layer (а когда-то его еще нужно было пушить в AWS).
  • Большой latency — каждый реквест запускается в докер-контейнере.
  • SAM при старте импортирует не все лямбды, а только ту конкретную, которая должна обработать реквест (это вполне нормально, так работает реальная лямбда). Получается, что исполняются не все модули, а только те, которые берут участие в иерархии импорта, начиная с корневого модуля, который импортирует сама лямбда. Это тоже хорошо — импортируется только то, что нужно для корректной отработки запроса.

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

  • Сложно дебажить — либо старыми добрыми «принтами», либо с помощью плагина для PyCharm от AWS, которым крайне неудобно пользоваться.
  • Локально эмулируется далеко не все — ряд фич невозможно потестить локально. Например, в нашем случае наиболее важной была валидация реквестов с помощью json schema.

Решение

SAMserver — это надстройка над SAM. Соответственно, он поддерживает все то, что поддерживает SAM, а еще имеет ряд полезных фич и багфиксов.

Также, как и СІ, SAMserver автоматически собирает минимальный template.yaml, необходимый для запуска приложения, и еще поднимает flask-сервер на его основе.

Как показала практика, эмуляция лямбды при локальной разработке не очень важна, запустить код на машине-хосте в обход докера вполне достаточно. Поэтому в SAMserver есть дополнительный режим, который позволяет запустить код «напрямую», без докера. Для конечного пользователя (девелопера), это выглядит как разработка привычного flask-приложения.

Такой подход имеет ряд плюсов:

  • чтобы добавить новую зависимость, достаточно установить ее в virtual environment с помощью pip;
  • нет прослойки с докером — теперь все запросы отрабатывают максимально быстро;
  • при старте сервера импортируются все лямбды и выполняется весь код проекта — так же, как в обычном flask-приложении, и сразу видно все ошибки рефакторинга;
  • код легко дебажится нативными средствами PyCharm;
  • можно самому допилить все недостающие фичи или исправить баги, не дожидаясь апдейтов от Amazon.

Валидируем json-схемы

Как это работает в SAM: валидации не предусмотрено в принципе. В итоге у нас возникают проблемы в духе «выглядит валидно, локально работает, на продакшен-лямбде не запускается или даже не деплоится».

Решение

В SAMserver мы берем конфиги, которые использует AWS API Gateway, и проводим валидацию параметров, используя ту версию json-схемы, которую использует AWS API Gateway. Плюс, к этому мы дописали валидацию параметров http request (headers, query-parameters, path-parameters).

Это помогло нам валидировать конфиги AWS API Gateway локально и отлавливать часть ошибок, связанных с параметрами запроса, еще на этапе разработки и отладки.

Добавляем правильный дебаг-режим

Как это работает в SAM: до появления SAMserver для дебаг-режима можно было использовать специальный плагин в PyCharm. Но для его запуска для каждой лямбды нужно было писать лишние конфиги (что неудобно), и в итоге дебаг чаще всего делался с помощью обычного print().

Решение

Так как мы можем запускать наши лямбды в режиме Flask, у нас есть возможность использовать обычный дебаггер, встроенный в PyCharm. Никаких дополнительных конфигов, дебага принтами и возможных ошибок.

Дополнительно: тестируем и профилируем

Так как у нас есть Flask-режим запуска лямбд, то уже не составляет проблем написать тестовый client с помощью Wergzeug.test Client.

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

Для отслеживания производительности кода добавили возможность запуска лямбд с профайлером. В качестве профайлера использовали pyinstrument Profiler.

Вывод

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

Так как мы — распределенная и большая команда, у нас есть необходимость дать каждому из разработчиков все необходимые инструменты для решения задач локально, независимо от других членов команды. С появлением SAMserver любой человек из команды может разрабатывать для лямбд без необходимости входа в AWS консоль, полностью локально.

Кроме того, это позволило значительно упростить онбординг для новых членов команды — на первых порах разработчик пишет так, как если бы он писал обычный код под Flask-приложение, и это здорово помогает не запутаться на старте.

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

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

👍НравитсяПонравилось16
В избранноеВ избранном8
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Мені здається, що тут більше проблема в тому, що код потрібно відрефакторити та зменшити залежності. Якщо ви дублікуюєте один код в різні лямбда, тим паче він там не використовується — це не оптимально, і породжує проблеми які у Вас були при використанні SAM, особливо коли щось фіксите і одночасно ламаєте поряд
Скільки не потрібних бібліотек у вас в layer, які не використовуються в конкретній лямбді? Який розмір деплоймент пакету лямбди?

В таком подходе нет большой проблемы и вот почему:
лейер всего 1, все лямбды его используют, а так как при равномерной нагрузке или при прогревании всегда есть «разогретые» лямбды, то cold starts в 99% случаев не происходят и размер лейера ни на что не влияет. Подобный подход я видел и в других фреймворках, например Zappa.

Более того, считаю что не удобно и не оптимально иметь отдельные списки зависимостей под каждую лямбду. Хотя иногда в этом есть смысл, при жирных зависимостях (например 1 лямбда отвечает за работу с PDF, тогда соотв зависимость можно добавить только на эту конкретную лямбду). Обычно же для стандартной апишки не более 5-25 либ, общим объемом до 30мб.

Количество библиотек в леере не доставляет нам проблем, а вот держать список requirements для каждой лямбды отдельно, учитывая что их 100+ для большинства наших проектов это как раз лишняя головная боль и место потенциальных багов

какой-то lambda-layer-monolith подход. Такое...свои +/-

Хорошая библиотека, но она не покрывает нам весь необходимый функционал по этому все же решили писать свое решение

отлаживаем наши serverless-бэкенды локально, без консоли AWS

почему возникла надобность в поднятии AWS инфраструктуры локально?
Что это вам дает? Ведь все равно интеграционное тестирование нужно провести в AWS, а для юнит-тестов и докера хватает.

Не имея локально, разработка превращается в то что 70% рабочего времени ждёшь пока зальётся очередной билд что бы что-то проверить

Подписаться на комментарии