Kotlin вместо bash. Прокачиваем автоматизацию на сервере

от автора

Для решения задач автоматизации рутинных процессов для системных администраторов и 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, шаблоны разработки.

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


ссылка на оригинал статьи https://habr.com/ru/companies/otus/articles/742050/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *