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/


Интересные статьи

Интересные статьи

Прошу приветствовать одну из первых DI библиотек для Kotlin multiplatform — DI.kt. Вы можете спросить: «А зачем нам ещё DI либы?». Долгое время полноценного DI для Kotlin Multiplatform не было. С...
Все разработчики знают, что есть два способа сделать дело: первый — вручную, медленно, нервно, сложно, либо второй – автоматизировано, быстро и еще сложнее. Например, я мог бы продолжить писать эту ст...
Все делают это. Ну ладно, не все, но большинство. Пишут скрипты, чтобы симулировать свои проекты на Verilog, SystemVerilog и VHDL. Однако, написание и поддержка таких скр...
TL;DR; настройка режима работы системы охлаждения сервера Supermicro Optimal не обеспечивает стабильность работы LSI-контроллера MegaRAID 9361-8i в холодном ЦОД-е. Мы стараемся не ис...
Добрый день, дорогие обитатели Хабра! Не так давно мне представилась возможность реализовать небольшой проект без особых требований по технической части. То есть, я был волен выбирать стек тех...