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")
}
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()
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
}
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"
}
}
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()
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
}
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)
}