Для разработчиков не секрет, что создание нового сервиса влечет за собой немало рутиной настройки: билд скрипты, зависимости, тесты, docker, k8s дескрипторы. Раз мы выполняем эту работу, значит текущих шаблонов IDE недостаточно. Под катом мои попытки автоматизировать все до одной кроссплатформенной кнопки "сделать хорошо" сопровождаемые кодом, примерами и финальным результатом.
Если перспективы создания сервисов в один клик с последующим автоматическим деплоем в Digital Ocean звучат заманчиво, значит эта статья для вас.
Начнем создавать наш шаблон и прежде всего рассмотрим организацию сборки. Несмотря на любовь многих к maven за его простоту и декларативность, использовать будем gradle, ибо он современее и позволяет писать скрипт сборки на одном языке с проектом. Помимо самого Kotlin плагина, нам потребуется еще один:
plugins {
kotlin("jvm") version "1.4.30"
// Для сборки с портативной jvm
id("org.beryx.runtime") version "1.12.1"
}
Из зависимостей, в качестве серверного фреймворка был выбран "родной" для Kotlin Ktor. Для тестирования используется связка JUnit + Hamkrest с его выразительным DSL, позволяющим писать тесты таким образом:
assertThat("xyzzy", startsWith("x") and endsWith("y") and !containsSubstring("a"))
Собираем все вместе, ориентируясь на Java 15+
dependencies {
implementation("com.github.ajalt.clikt:clikt:3.1.0")
implementation("io.ktor:ktor-server-netty:1.5.1")
testImplementation("org.junit.jupiter:junit-jupiter:5.8.0-M1")
testImplementation("com.natpryce:hamkrest:1.8.0.1")
testImplementation("io.mockk:mockk:1.10.6")
}
application {
mainClass.set("AppKt")
}
tasks {
test {
useJUnitPlatform()
}
compileKotlin {
kotlinOptions.jvmTarget = "15"
}
}
В исходный код генерируемый шаблоном по умолчанию добавлен entry-point обработки аргументов командой строки, заготовка для роутинга, и простой тест (заодно служащий примером использования Hamkrest).
Из того что следует отметить, позволил себе небольшую вольность с официальным Kotlin codestyle чуть-чуть поправив его в .editorsconfig:
[*.{kt, kts, java, xml, html, js}]
max_line_length = 120
indent_size = 2
continuation_indent_size = 2
В первую очередь, это вопрос вкуса, но ещё так в одну строчку помещается больше кода, что помогает избежать переноса в выражениях внутри класса и функции, лямбды, например.
Чтобы собрать приложение используйте команду
gradle clean test runtime
Портативное приложение (в директории build/image) уже можно использовать, запуская традиционным образом через исполняемый файл в build/image/bin
Осталось написать Dockerfile, его привожу целиком. Сборка и запуск разделены и производятся в два этапа:
# syntax = docker/dockerfile:experimental
FROM gradle:jdk15 as builder
WORKDIR /app
COPY src ./src
COPY build.gradle.kts ./build.gradle.kts
RUN --mount=type=cache,target=./.gradle gradle clean test install
FROM openjdk:15 as backend
WORKDIR /root
COPY --from=builder /app/build/install/app ./
Приложение работает в контейнере с jdk (а не с jvm) для удобной диагностики c помощью jstack/jmap и других инструментов поставляемых с jdk.
Сконфигурируем запуск приложения при помощи Docker Compose:
version: "3.9"
services:
backend:
build: .
command: bin/app
ports:
- "80:80"
Теперь мы можем запускать наш сервис на целевой машине, без дополнительных зависимостей в виде jdk/gradle, при помощи простой команды
docker-compose up
Как деплоить сервис в облако? Выбрал Digital Ocean из-за дешевой стоимости и простоты управления. Благодаря тому что мы только что сконфигурировали сборку и запуск в контейнере, можно выбрать наш репозиторий с проектом в разделе Apps Platform и... все! Файлы конфигурации Docker будут подцеплены автоматически, мы увидим логи сборки, а после этого получим доступ к веб адресу, логам приложения, консоли управления, простым метрикам потребления памяти и процессорного времени. Выглядит это удовольствие примерно так и стоит 5$ в месяц:
При последующих изменениях в master ветке репозитория, передеплой запустится автоматически, удобно.
Наконец, все описанное в статье, подробно задокументировано в README.md файле шаблона, чтобы после создания проекта, последующая сборка и деплой не вызывали сложностей.
Использовать шаблон чтобы получить готовый репозиторий, можно просто нажав кнопочку "Use this template"
на GitHub:
github.com/demidko/Projekt-portable
Или, если вам нужен вариант с self-executable jar без портативной jvm:
github.com/demidko/Projekt-jar
После этого остается только написать логику :) Как и любой подход, этот шаблон не лишен недостатков, поэтому интересно услышать предложения, комментарии и критику.
UbuRus
Используйте application плагин, он куда удобнее, не нужно добавлять зависимость на еще один плагин и он не является хаком как вся концепция shadow jar
А еще можно сделать руками кеширование базового слоя со всеми зависимостями используя только application plugin
TestNG при Junit 5 тоже выглядит странным, я бы не рекомендовал его сегодня как решение "по умолчанию"
Ну и нативный mockk куда лучше mockito-kotlin, конечно только если у вас уже не готовое приложение и вы мигрируете на Kotlin новые/старые тесты
Reformat Автор
Расскажите пожалуйста чем плоха идея self-executable jar'ок? Мне это казалось удобным способом дистрибьюции.
UbuRus
Например, несколько библиотек в приложении используют HOCON для конфигурации и предоставляют reference.conf с настройками по умолчанию в classpath. Такая конфигурация будет работать как есть в application плагине, а вот с shadowJar придется поплясать.
Не зря в shadow плагине есть куча мерджеров которые умеют склеивать (это к примеру) сервисы поставляемые через Service Loader API, да и вообще куча конфигурации которая нужна просто потому что shadow jar это хак. Хуже всего если у вас бинарные данные (набор картинок по одному урлу или конфигурация в протобаф, тут придется писать свой кастомный мерджер)
Можно зайти в issues и увидеть кучу проблем связанных с этим https://github.com/johnrengelman/shadow/issues/640
Reformat Автор
Спасибо. Переделал это. А можно взглянуть на таску с кеширующим слоем в докере?
UbuRus
Таска просто генерит папочки, остальная логика в Jenkins:
Смысл в таске такой (реализация подсмотрена в application/distibution плагине):