Для решения задач автоматизации рутинных процессов для системных администраторов и DevOps (которые, кроме всего прочего, нередко занимаются созданием сборочных скриптов, которые могут не только подготовить базовую среду выполнения, но и могут взаимодействовать с другими системами для обеспечения полного цикла CI/CD) чаще всего используются или bash-сценарии (zsh, ash или язык любой другой оболочки) или python. Первое решение косвенно используется и в описании Dockerfile, поскольку сценарий исполняемых команд принципиально ничем не отличается от запуска скрипта в какой-либо shell, второй подход чаще ассоциируется с автоматизацией, связанных с взаимодействием с хранилищами данных — например, для создания учетных записей в LDAP или базе данных, отправки уведомлений и тд.
Но несправедливо было бы обойти стороной возможность создания исполняемых сценариев на языке Kotlin, которые могут стать полноценной заменой bash-сценариям и могут использовать не только в сочетании с Gradle, но и как самостоятельные решения автоматизации. В этой статье мы рассмотрим несколько примеров использования Kotlin Scripting (KTS) для автоматизации в распределенной системе, будем использовать долгоживущие скрипты с ожиданием заданий через RabbitMQ, а также поработаем с файловой системой, внешними сервисами, а также попробуем использовать KTS для сборки Docker-контейнеров.
Прежде всего нужно отметить, что Kotlin Scripting (далее KTS) — не новая технология и она достаточно давно используется для описания сценария сборки приложений с использованием gradle (они могут быть созданы как для мобильной платформы, так и для бэкэнда, при этом исходный текст может быть написан на любой технологии, под которую есть поддержка в gradle, в том числе Java, Groovy, Scala и даже Python с проектов pygradle). При этом она может использоваться и без gradle и запускаться через консольный вариант компилятора Kotlin. Начнем с установки компилятора (например, через brew):
brew install kotlin
Сам файл сценария является обычным исходным текстом на Kotlin, но с несколькими важными дополнениями:
поскольку KTS является самодостаточным и не использует систему сборки для подготовки к выполнению, то все зависимости указывается через аннотации
@file:DependsOn("…")
, в этом случае для запуска должен использоватьсяkscript
.код сценария выполняется сразу (и напоминает режим REPL), при этом мы можем использовать определения функций, классов, задействовать объекты классов любых подключенных зависимостей
как и в обычном Kotlin-приложении можно использовать корутины (если подключить зависимость
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1
сценарий может быть быть интегрирован в существующее Kotlin-приложение (через запуск
ScriptingHost
и использование методаeval
для выполнения кода).KTS-сценарий может использовать все возможности библиотек по взаимодействию с базами данных, другими серверами (например, LDAP через JNDI или вспомогательные библиотеки), а также встроенные возможности по манипуляции объектами файловой системы из java.nio.
Создадим простой KTS и попробуем его запустить, как обычно запускается bash-сценарий.
import java.io.*
val username = System.getProperty("user.name")
println("Hello $username")
val home = System.getProperty("user.dir")
println("Home dir is $home")
val profile = File(home+"/.profile")
println("Profile file is exists ${profile.exists()}")
if (profile.exists()) {
println("Total lines is ${profile.readLines().size}")
}
Этот сценарий использует возможности доступа к файлам (проверка наличия, чтение содержимого) и извлечения информации об пользователе, запустившем java-процесс. Для запуска сценария используем консольную утилиту kotlinc
:
kotlinc -script test.kts
Но можно использовать и более привычный для сценариев синтаксис, для этого добавим в первую строку инструкцию для запуска (shebang):
#!/usr/bin/env -S kotlinc -script
Теперь файл может сделать исполняемым и запустить как любое другое приложение:
chmod +x test.kts
./test.kts
Аналогично могут быть выполнены другие операции с файловой системой (создание и удаление каталогов и файлов, копирование и перемещение файлов), при этом если запускать процесс от имени другого пользователя (например, с использованием SUID-флага и заменой владельца), то сценарий может получить доступ на модификацию и к каталогам за пределами домашнего каталога пользователя и /tmp. Рассмотрим более сложный случай, когда внутри сценария может быть выполнен какой-либо внешний процесс, для этого можно использовать класс Runtime.
val runtime = Runtime.getRuntime()
val result = runtime.exec("ls -1 $home")
val r = String(result.inputStream.readAllBytes(), Charsets.UTF_8).split("\n")
println("Execution result: $r")
Из result также можно получить errorStream
для чтения из потока ошибок и outputStream
для отправки данных в стандартный поток ввода (stdin) в запущенном приложении.
Далее попробуем извлечь аргументы командной строки в сценарии, они будут доступны через предопределенную переменную args в тексте сценария (обратите внимание, что первый аргумент доступен по индексу 0, а не 1 как было бы в bash):
val filename = args[0]
println("File $filename contains ${File(filename).readLines().size}")
Важно, что для взаимодействия с системными службами нам необязательно запускать внешние команды и, например, для управления Docker-контейнерами можно использовать Java/Kotlin реализацию API. Также можно взаимодействовать с графическим интерфейсом (например, отправлять уведомления) и управлять системными службами (systemd) через D-Bus (например, можно использовать этот API).
Аналогично могут быть использованы драйверы JDBC и JNDI для выполнения миграций базы данных (например, можно применить Flyway), управления каталогом учетных данных на основе LDAP (например, LDAPtive), взаимодействие с API (например, через okhttp или Ktor Client), а также можно использовать интерактивный режим через вызов readln() или иные формы работы с потоками ввода-вывода (можно даже использовать изменение цвета отображаемого в консоли текста через эту библиотеку).
Для выполнения сценариев внутри Docker можно использовать образ с предустановленным kscript (может также использоваться на основной системе):
docker run -i kscripting/kscript - < script.kts
Наиболее интересным выглядит сценарий фонового выполнения длительного задания, например подписки на задания из очереди сообщений RabbitMQ. Такие задачи разумно запускать в отдельном треде (через ThreadPoolExecutor) или использовать корутины.
В первом случае
val workerPool: ExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
workerPool.submit {
//код задачи
}
Для корутин можно сделать простую реализацию метода launch:
@file:Repository("https://jcenter.bintray.com")
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
import java.io.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.intrinsics.createCoroutineUnintercepted
import kotlinx.coroutines.delay
fun launch(block: suspend () -> Unit) {
val callback = object : Continuation<Unit> {
override val context: CoroutineContext = EmptyCoroutineContext
override fun resumeWith(result: Result<Unit>) {}
}
block.createCoroutineUnintercepted(callback).resumeWith(Result.success(Unit))
}
launch {
delay(1000)
println("From coroutine")
}
Тут можно будет заметить проблему, что корутина не будет ожидать завершения и при завершении выполнения основного кода в kts и здесь можно использовать встроенный способ запуска runBlocking
:
@file:Repository("https://jcenter.bintray.com")
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
runBlocking {
delay(10)
println("From coroutine")
}
Создадим скрипт для выполнения задач на сервере, которые поступают через очередь сообщений (в этом случае RabbitMQ), для этого будем завершать корутину когда consumer отключается:
@file:Repository("https://jcenter.bintray.com")
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
@file:DependsOn("com.rabbitmq:amqp-client:5.9.0")
import java.io.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.suspendCoroutine
runBlocking {
suspendCoroutine<Unit> { coroutine ->
val factory = ConnectionFactory()
val connection = factory.newConnection("amqp://guest:guest@localhost:5672/")
val channel = connection.createChannel()
channel.queueDeclare("tasks")
println("Waiting for messages...")
val deliverCallback = DeliverCallback { consumerTag: String?, delivery: Delivery ->
val message = String(delivery.body, StandardCharsets.UTF_8)
//обработка задания из сообщения
}
val cancelCallback = CancelCallback { consumerTag: String? ->
println("[$consumerTag] was canceled")
coroutine.resumeWith()
}
channel.basicConsume(QUEUE_NAME, true, "worker", deliverCallback, cancelCallback)
}
}
Для запуска миграций или автоматизации внутри Dockerfile также можно использовать KTS:
FROM kscripting/kscript
ADD migrate.kts /tmp
kscript /tmp/migrate.kts
Мы рассмотрели несколько простых сценариев использования KTS для автоматизации задач на сервере, которые также могут быть интегрированы в CI/CD и позволяет выполнять сложные задачи по манипуляции с файлами и объектами операционной системы, применения миграции, управления внешними системами, при этом остаются все возможности языка Kotlin и любых подключаемых библиотек, совместимых с JVM.
В заключение порекомендую бесплатный открытый урок, посвященный архитектуре бэкенд-приложения в рисковом проекте. Что будет обсуждаться на встрече:
архитектурные и частично организационные меры, позволяющие снизить риски при разработке;
инструменты PMBoK и TDD/MDD;
элементы чистой архитектуры: модульная разработка, DI, DDD, шаблоны разработки.
Упор будет сделан на практических аспектах, включая ситуации, когда идеальные условия работы недостижимы и приходится жертвовать некоторыми из принципов. Записаться можно по ссылке.
Комментарии (13)
dyadyaSerezha
16.06.2023 11:11А есть библиотеки по работе с сервисами, процессами и прочим на других машинах в сети?
sshikov
16.06.2023 11:11val connection = factory.newConnection("amqp://guest:guest@localhost:5672/")
а вот это по вашему, не другая машина? В смысле, тут конечно та же самая, но это ведь и есть то что вы просите, по сети. Если другие машины это поддерживают — то уж на платформе JVM с библиотеками редко когда бывают проблемы.
dmitriizolotov Автор
16.06.2023 11:11Можно например использовать https://github.com/jkelly467/kossh для ssh-подключения на другую машину и там уже выполнять команды и разбирать ответ, можно даже сделать обертки для извлечения необходимой информации из текстового ответа. Непосредственно подключаться к POSIX на удаленном системе без ssh не позволяет безопасность, но если кто-то знает способ выполнения POSIX-запросов на другую linux-машину, то напишите пожалуйста.
Leetc0deMonkey
16.06.2023 11:11+2Как говорится, как умею, так и пою. Спасибо что не C++. Поддерживать вот это потом всё кто будет?
sshikov
16.06.2023 11:11+1А какие проблемы поддерживать код на котлине? Нынче разработчиков на нем полно (потому что андроид).
Я вам так скажу — я лично тоже для таких же примерно целей уже много лет выбираю груви. Ну, просто так исторически сложилось — он появился несколько раньше (в 2003, я пользуюсь примерно с 2006). Я много раз переписывал баш на груви, и ни разу за много лет об этом не пожалел. И те кто потом этим пользовался и поддерживал — тоже не жаловались. Не вижу ровно никаких причин, почему на котлине будет не так. Это не значит что вариант универсален, но он вполне широко применим.
Leetc0deMonkey
16.06.2023 11:11А какие проблемы поддерживать код на котлине? Нынче разработчиков на нем полно (потому что андроид).
Разработчиков, или девопсов?
sshikov
16.06.2023 11:11Ну да, возможно это не одни и те же люди. Я не хочу сказать, что найти таких наверняка будет так же легко, как тех, кто пишет на том же питоне. Но и не так сложно, как на хаскеле или эрланге, к примеру.
Leetc0deMonkey
16.06.2023 11:11-1А зачем? Уже есть устоявшийся де-факто стандарт автоматизации bash или ps. Ну вот зачем изобретать велосипеды на ваших любимых языках? Просто потому что можете? Ну, ОК, в качестве академического эксперимента выложенное на гите вполне сгодится. Но, пожалуйста, не надо всё это тащить в промышленный прод.
sshikov
16.06.2023 11:11Ну вот возьмем опять же груви (просто, у меня опыта с котлином мало, а как по мне — это очень близкие инструменты) — тут даже особо и доказывать не надо, на нем основаны такие широко распространенные инструменты, как jenkins и gradle, и тем инструментам уже десятки лет. Так что это далеко не велосипеды, и вовсе не мои.
Но, пожалуйста, не надо всё это тащить в промышленный прод.
Мы вроде в ваш прод ничего и не тащим. А наш прод уже давно на этом основан, о чем я вам и толкую с самого начала.
Уже есть устоявшийся де-факто стандарт автоматизации bash или ps.
Ну вот просто представьте, что вам это кажется с вашей личной (и вероятно даже вполне верной для вас) точки зрения? И все будет более понятно. Я видел кучу компаний, где ни то ни другое не является "стандартом автоматизации". Более того, лично я в своих проектах искореняю попытки использования баша как средства разработки чего либо более 100 строк размером. У нас просто с вами разные проекты, мои не такие как вы делаете, а ваши не такие как мои.
randomsimplenumber
16.06.2023 11:11Однострочники на kotlin получаются ещё более громоздкие чем на python;)
AcckiyGerman
Python