Infrastruktur-Code kann chaotisch sein. YAML-Dateien, die sich über Kilometer erstrecken, JSON, das einem die Augen bluten lässt, und dann sind da noch die Bash-Skripte, die nur mit Klebeband und Gebeten zusammengehalten werden. Aber was wäre, wenn wir die Sicherheit und Ausdruckskraft einer stark typisierten Sprache auf unseren Infrastruktur-Code übertragen könnten?

Hier kommen Kotlin und Arrow-kt ins Spiel. Mit den DSL-Building-Fähigkeiten von Kotlin und den funktionalen Programmierwerkzeugen von Arrow-kt können wir eine IaC-Lösung schaffen, die:

  • Typensicher ist: Fehler werden zur Kompilierzeit erkannt, nicht erst, wenn der Produktionsserver brennt
  • Komponierbar ist: Komplexe Infrastruktur aus einfachen, wiederverwendbaren Komponenten aufbauen
  • Ausdrucksstark ist: Ihre Infrastruktur auf eine Weise beschreiben, die für Menschen tatsächlich Sinn ergibt

Die Bühne bereiten

Bevor wir loslegen, stellen wir sicher, dass wir unsere Werkzeuge bereit haben. Sie benötigen:

  • Kotlin (vorzugsweise 1.5.0 oder später)
  • Arrow-kt (wir verwenden Version 1.0.1)
  • Ihren bevorzugten IDE (IntelliJ IDEA wird für die Kotlin-Entwicklung sehr empfohlen)

Fügen Sie die folgenden Abhängigkeiten zu Ihrer build.gradle.kts-Datei hinzu:


dependencies {
    implementation("io.arrow-kt:arrow-core:1.0.1")
    implementation("io.arrow-kt:arrow-fx-coroutines:1.0.1")
}
Kotlin

Unsere DSL Stück für Stück aufbauen

Beginnen wir mit der Definition einiger grundlegender Bausteine für unsere Infrastruktur. Wir erstellen ein einfaches Modell für Server und Netzwerke.

1. Unseren Bereich definieren


sealed class Resource
data class Server(val name: String, val size: String) : Resource()
data class Network(val name: String, val cidr: String) : Resource()
Kotlin

Dies gibt uns eine grundlegende Struktur, mit der wir arbeiten können. Nun erstellen wir eine DSL, um diese Ressourcen zu definieren.

2. Die DSL erstellen


class Infrastructure {
    private val resources = mutableListOf()

    fun server(name: String, init: ServerBuilder.() -> Unit) {
        val builder = ServerBuilder(name)
        builder.init()
        resources.add(builder.build())
    }

    fun network(name: String, init: NetworkBuilder.() -> Unit) {
        val builder = NetworkBuilder(name)
        builder.init()
        resources.add(builder.build())
    }
}

class ServerBuilder(private val name: String) {
    var size: String = "t2.micro"

    fun build() = Server(name, size)
}

class NetworkBuilder(private val name: String) {
    var cidr: String = "10.0.0.0/16"

    fun build() = Network(name, cidr)
}

fun infrastructure(init: Infrastructure.() -> Unit): Infrastructure {
    val infrastructure = Infrastructure()
    infrastructure.init()
    return infrastructure
}
Kotlin

Jetzt können wir unsere Infrastruktur so definieren:


val myInfra = infrastructure {
    server("web-server") {
        size = "t2.small"
    }
    network("main-vpc") {
        cidr = "172.16.0.0/16"
    }
}
Kotlin

Typensicherheit mit Arrow-kt hinzufügen

Unsere DSL sieht gut aus, aber lassen Sie uns mit etwas funktionaler Programmierkunst von Arrow-kt noch einen Schritt weiter gehen.

1. Validierte Ressourcen

Verwenden wir zunächst Arrow's Validated, um sicherzustellen, dass unsere Ressourcen korrekt definiert sind:


import arrow.core.*

sealed class ValidationError
object InvalidServerName : ValidationError()
object InvalidNetworkCIDR : ValidationError()

fun Server.validate(): ValidatedNel =
    if (name.isNotBlank()) this.validNel()
    else InvalidServerName.invalidNel()

fun Network.validate(): ValidatedNel =
    if (cidr.matches(Regex("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/\\d{1,2}$"))) this.validNel()
    else InvalidNetworkCIDR.invalidNel()
Kotlin

2. Validierungen zusammensetzen

Aktualisieren wir nun unsere Infrastructure-Klasse, um diese Validierungen zu verwenden:


class Infrastructure {
    private val resources = mutableListOf()

    fun validateAll(): ValidatedNel> =
        resources.traverse { resource ->
            when (resource) {
                is Server -> resource.validate()
                is Network -> resource.validate()
            }
        }

    // ... der Rest der Klasse bleibt gleich
}
Kotlin

Weiter gehen: Ressourcenabhängigkeiten

Echte Infrastruktur hat oft Abhängigkeiten zwischen Ressourcen. Lassen Sie uns dies mit Arrow's Kleisli modellieren:


import arrow.core.*
import arrow.fx.coroutines.*

typealias ResourceDep = Kleisli

fun server(name: String): ResourceDep = Kleisli { infra ->
    infra.resources.filterIsInstance().find { it.name == name }.some()
}

fun network(name: String): ResourceDep = Kleisli { infra ->
    infra.resources.filterIsInstance().find { it.name == name }.some()
}

fun attachToNetwork(server: ResourceDep, network: ResourceDep): ResourceDep =
    Kleisli { infra ->
        val s = server.run(infra).getOrElse { return@Kleisli None }
        val n = network.run(infra).getOrElse { return@Kleisli None }
        println("Attaching ${s.name} to ${n.name}")
        Some(Unit)
    }
Kotlin

Jetzt können wir Abhängigkeiten in unserer DSL ausdrücken:Die Kraft der KompositionEines der schönen Dinge an diesem Ansatz ist, wie einfach wir komplexe Infrastruktur aus einfacheren Teilen zusammensetzen können. Lassen Sie uns eine höherstufige Abstraktion für eine Webanwendung erstellen:AbschließendWir haben gerade erst die Oberfläche dessen angekratzt, was mit einer typensicheren Infrastruktur-DSL möglich ist. Durch die Nutzung der Sprachfunktionen von Kotlin und des funktionalen Programmierwerkzeugs von Arrow-kt haben wir eine leistungsstarke, ausdrucksstarke und sichere Möglichkeit geschaffen, Infrastruktur zu definieren.Einige wichtige Erkenntnisse:Typensicherheit erkennt Fehler frühzeitig und spart Ihnen teure LaufzeitfehlerKomponierbarkeit ermöglicht es Ihnen, komplexe Infrastruktur aus einfachen, wiederverwendbaren Teilen zu erstellenFunktionale Programmierkonzepte wie Validated und Kleisli bieten leistungsstarke Werkzeuge zur Modellierung komplexer Beziehungen und EinschränkungenDenkanstößeWährend Sie Ihre Infrastruktur-DSL weiterentwickeln, sollten Sie diese Fragen in Betracht ziehen:Wie würden Sie diese DSL erweitern, um verschiedene Cloud-Anbieter zu unterstützen?Könnten Sie diesen Ansatz verwenden, um CloudFormation-Vorlagen oder Terraform-Konfigurationen zu generieren?Wie könnten Sie Kostenschätzungen in Ihre DSL integrieren?Denken Sie daran, dass es nicht nur darum geht, bestehende IaC-Tools in Kotlin zu replizieren, sondern eine ausdrucksstärkere, typensichere Möglichkeit zu schaffen, Infrastruktur zu definieren, die Fehler frühzeitig erkennt und Ihre Absichten klar macht. Viel Spaß beim Programmieren, und mögen Ihre Server immer laufen und Ihre Latenz niedrig sein!