Pull to refresh
695.45
OTUS
Цифровые навыки от ведущих экспертов

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

Level of difficultyEasy
Reading time6 min
Views4.7K

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

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 13: ↑10 and ↓3+8
Comments15

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS