Меня зовут Владислав Танков, в 2018–2020 годах я учился в корпоративной магистратуре JetBrains в ИТМО, а с 2017 года я работаю в компании JetBrains.
Летом 2018 года на хакатоне JetBrains я и нескольких моих коллег попытались сделать инструмент для языка Kotlin, упрощающий создание Serverless-приложений путем анализа кода приложения.
После хакатона, уже в рамках научной работы в корпоративной магистратуре JetBrains, я решил продолжить развитие этого проекта. Инструмент за два года существенно расширился и оброс функциональностью, но сохранил свое название — Kotless, или Kotlin Serverless Framework.
Что такое Serverless
Для начала вспомним, из чего состоит простейшая платформа Serverless-вычислений. Такая платформа включает в себя три основных компонента:
- систему исполнения Serverless-функций — небольших приложений, обрабатывающих те или иные события;
- набор различных интерфейсов из внешнего мира (или облачной платформы, такой как AWS) к событийной системе платформы, например HTTP-интерфейс;
- саму событийную систему, обеспечивающую передачу событий от интерфейсов к функциям и результатов обработки от функций к интерфейсам.
Трёх этих компонентов хватает для того, чтобы построить достаточно сложное приложение. К примеру, Web-приложение — это просто внешний HTTP-интерфейс (в случае AWS это будет APIGateway) и для каждого обрабатываемого ресурса (вида /route/my) своя Serverless функция-обработчик. Можно построить и более сложное приложение, использующее базы данных и само вызывающее другие Serverless-функции, как на картинке.
Ну хорошо, построить такие приложения можно, но зачем?
У Serverless-приложений есть несколько неоспоримых преимуществ, оправдывающих приседания с архитектурой.
- Serverless-функции не работают тогда, когда они не нужны. Действительно, функция лишь обрабатывает события — зачем ей занимать вычислительные ресурсы, если событий нет?
- Serverless-функции могут параллельно обрабатывать однотипные события. То есть, если /route/my стал очень популярен и его запросила тысяча пользователей одномоментно, то Serverless-платформа может просто запустить 1000 обработчиков, по одному на событие.
Вместе эти пункты складываются в, пожалуй, одну из важнейших Serverless-мантр: Serverless-приложение масштабируется от нуля до бесконечности. Такое приложение не тратит деньги, когда оно не востребовано, и способно обрабатывать тысячи запросов в секунду, когда это необходимо.
Проблема
Давайте рассмотрим очень простой пример на языке Kotlin:
@Get("/my/route")
fun handler() = "Hello World"
Довольно очевидно, что такое приложение может быть реализовано с помощью Serverless-подхода. На первый взгляд, достаточно создать HTTP-интерфейс с некоторым DNS-адресом и сопоставить /my/route с обработчиком fun handler().
В действительности, чтобы создать такое приложение, потребуется куда больше действий, чем добавить одну аннотацию. К примеру, в случае AWS:
- Потребуется реализовать интерфейс-обработчик конкретного события, в данном случае RequestStreamHandler.
- Потребуется описать инфраструктуру Serverless-приложения: описать HTTP API приложения, описать все функции-обработчики и связать их функции с интерфейсом, аккуратно выбрав разрешения.
- Наконец, придется собрать все функции-обработчики, загрузить в Serverless-платформу и развернуть соответствующую инфраструктуру.
Не так уж и мало действий для такого простого приложения, не правда ли?
Для посвященных в таинство Infrastructure as Code замечу, что, безусловно, часть процесса может быть автоматизирована, но сама эта автоматизация требует изучения совершенного нового подхода (собственно, описания инфраструктуры как кода) и нового языка. Это кажется излишне сложной задачей для разработчика, желающего развернуть элементарное приложение.
Возможно ли сделать как-то проще? В некоторых случаях (и конкретно в этом) — да!
Infrastructure in Code
Давайте зайдем с другой стороны: вместо того, чтобы заставлять пользователя описывать инфраструктуру, попытаемся вывести её из уже написанного кода пользователя.
Снова рассмотрим тот же самый пример:
@Get("/my/route")
fun handler() = "Hello World"
Мы знаем, что пользователь хочет, чтобы запросы на /my/route обрабатывались данной функцией — так давайте синтезируем инфраструктуру, которая создаст HTTP API с /my/route, создаст нужную Serverless-функцию и выполнит всю необходимую магию, чтобы их соединить!
В своей статье на Automated Software Engineering 2019 я назвал данный подход Infrastructure in Code. Фактически мы извлекаем описание инфраструктуры из кода приложения, определяющего её неявно, то есть она действительно содержится «внутри» кода.
Стоить отметить, что здесь и далее рассматривается исключительно синтез HTTP API приложений. Аналогичный подход может быть применён и для обработки очередей, и для обработки событий облачной платформы, но это вопрос дальнейшего развития Kotless.
Реализация
Надеюсь, к этому моменту идея понятна, и остаётся три главных вопроса:
- Как извлечь информацию из кода?
- Как создать инфраструктуру по этой информации?
- Как запустить приложение в облаке?
Анализ
С этим нам поможет Kotlin Compiler Embeddable.
Несмотря на то, что в примере речь идет об аннотациях, в действительности HTTP API приложения, в зависимости от используемой библиотеки, может определяться совершенно по-разному, например так:
//ktor-like style
get("my-route") {
"Hello World"
}
Для анализа произвольного кода Kotlin Compiler Embeddable оказался и привычнее, и удобнее (в силу наличия большого числа примеров).
На данный момент Kotless может анализировать три основных фреймворка:
- Kotless DSL — собственный фреймворк Kotless, использующий аннотации;
- Spring Boot — популярный Web-фреймворк, анализируются аннотации;
- Ktor — популярный Kotlin Web-фреймворк, анализируются extension-функции.
В процессе анализа кода собирается Kotless Schema — это некоторое платформенно-независимое представление Serverless-приложения. Оно используется для синтеза инфраструктуры и позволяет добиться независимости процесса анализа от конкретной облачной платформы.
Синтез
Синтезировать мы будем Terraform-код. Terraform был выбран как один из наиболее популярных инструментов Infrastructure as Code с большим спектром поддерживаемых облачных платформ, что гарантирует Kotless возможность поддержки новых облачных платформ и стабильность при развертывании приложений.
Синтез производится из Kotless Schema, содержащей описание HTTP API приложения и его функций, а также некоторые дополнительные данные (например, желаемое DNS-имя).
Для самого синтеза используется специально созданная библиотека Terraform DSL. Код синтеза выглядит примерно так:
val resource = api_gateway_rest_api("tf_name") {
name = "aws_name"
binary_media_types = arrayOf(MimeType.PNG)
}
DSL гарантирует форматирование и ссылочную целостность между различными Terraform-ресурсами, что существенно упрощает расширение набора синтезируемых ресурсов.
Синтезированный код развертывается в облачной платформе простым применением Terraform.
Запуск
Остается запустить приложение на Serverless-платформе. Как уже упоминалось, все Serverless-функции — это по сути обработчики некоторых событий, в нашем случае HTTP-запросов.
Необходимо соединить фреймворк, с помощью которого создается приложение (к примеру, Spring Boot) и Serverless-платформу. Для этого в момент сборки приложения Kotless добавляет в код приложения специальный «диспетчер» — платформо-зависимый обработчик событий, служащий адаптером между используемым в приложении фреймворком и облачной платформой.
Инструмент
Сам инструмент, включающий в себя весь описанный конвейер создания инфраструктуры, был реализован как плагин к build-системе Gradle. При этом все основные модули являются отдельными библиотеками, что существенно упрощает поддержку других build-систем.
Использование плагина элементарно. После настройки у пользователя есть всего одна Gradle задача — deploy, которая производит все необходимые действия для развертывания текущего приложения в облаке.
Настройка со стороны пользователя тоже довольно проста. Сначала применяется сам плагин:
plugins {
io("io.kotless") version "0.1.5" apply true
}
После этого пользователь добавляет необходимый ему фреймворк:
dependencies {
//Kotless DSL
implementation("io.kotless", "lang", "0.1.5")
}
И наконец, настраивает доступ к AWS, чтобы Kotless мог произвести развертывание:
kotless {
config {
bucket = "kotless.s3.example.com"
terraform {
profile = "example"
region = "us-east-1"
}
}
}
Локальный запуск
Легко заметить, что последний пункт требует знакомства пользователя с AWS и наличия у него как минимум AWS аккаунта. Такие требования отпугивали пользователей, желающих сначала локально попробовать, подойдет ли им инструмент.
Именно поэтому Kotless поддерживает режим локального запуска. Используя стандартные возможности выбранного фреймворка (и Ktor, и Spring Boot, и Kotless DSL, естественно, умеют запускать приложения локально), Kotless развертывает приложение на машине пользователя.
Более того, Kotless может запустить эмуляцию AWS (используется LocalStack), чтобы пользователь смог локально проверить, что приложение ведет себя ожидаемо.
Дальнейшее развитие
За время написания Kotless (и вместе с ним магистерской диссертации) я успел представить его на ASE 2019, KotlinConf 2019 и в подкасте Talking Kotlin. В общем, инструмент был принят благосклонно, хотя к концу 2019 года уже и не казался такой новинкой (к тому времени стали популярны и Zappa, и Claudia.js, и AWS Chalice).
Тем не менее, на данный момент Kotless, пожалуй, наиболее известный инструмент своего класса в мире Kotlin, и я безусловно планирую его развивать.
В ближайшее время я планирую стабилизировать текущие API и функциональность, подготовить туториалы и демо-проекты для того, чтобы упростить изучение инструмента для новых пользователей.
К примеру, в планах подготовить набор туториалов по созданию чат-ботов с помощью Kotless. Кажется, Serverless-технологии отлично подходят для такого варианта использования (да и пользователи Kotless уже пишут Telegram-ботов), но отсутствие подходящего инструментария существенно мешает повсеместному использованию.
Наконец, одним из важнейших моментов всей архитектуры инструмента является его платформо-независимость. В не столь отдаленном будущем я надеюсь реализовать поддержку Google Cloud Platform и Microsoft Azure, что позволит приложениям переезжать из облака в облако буквально с помощью одной кнопки.
Хочется надеяться, что Kotless и аналогичные инструменты действительно помогут внедрению Serverless-технологий в широкие массы и всё больше приложений будут потреблять ресурсы только тогда, когда они работают, чуть уменьшая энтропию вселенной :)
APXEOLOG
Спасибо за статью. Есть парочка вопросов:
Не конкретно по теме вашей статьи, но рядом. Как сейчас решается проблема с cold start'ом java-based лямбд?
Как в случае использования вашего метода происходит работа с дополнительными параметрами — маппинги, авторизация, переменные среды, настройки тамймаутов и пр.