Infrastructuurcode kan een rommeltje zijn. YAML-bestanden die kilometers lang zijn, JSON die je ogen doet bloeden, en laten we het niet eens hebben over die bash-scripts die met ducttape en gebeden bij elkaar worden gehouden. Maar wat als we de veiligheid en expressiviteit van een sterk getypeerde taal naar onze infrastructuurcode konden brengen?
Maak kennis met Kotlin en Arrow-kt. Met de DSL-bouwmogelijkheden van Kotlin en de functionele programmeertools van Arrow-kt kunnen we een IaC-oplossing creëren die:
- Type-veilig is: Fouten worden op compile-tijd opgevangen, niet wanneer je productieomgeving in brand staat
- Compositie: Bouw complexe infrastructuur uit eenvoudige, herbruikbare componenten
- Expressief: Beschrijf je infrastructuur op een manier die daadwerkelijk begrijpelijk is voor mensen
De Basis Leggen
Voordat we beginnen, laten we ervoor zorgen dat we onze tools klaar hebben. Je hebt nodig:
- Kotlin (bij voorkeur 1.5.0 of later)
- Arrow-kt (we gebruiken versie 1.0.1)
- Je favoriete IDE (IntelliJ IDEA wordt sterk aanbevolen voor Kotlin-ontwikkeling)
Voeg de volgende afhankelijkheden toe aan je build.gradle.kts
bestand:
dependencies {
implementation("io.arrow-kt:arrow-core:1.0.1")
implementation("io.arrow-kt:arrow-fx-coroutines:1.0.1")
}
Onze DSL Bouwen: Stuk voor Stuk
Laten we beginnen met het definiëren van enkele basisbouwstenen voor onze infrastructuur. We maken een eenvoudig model voor servers en netwerken.
1. Onze Domein Definiëren
sealed class Resource
data class Server(val name: String, val size: String) : Resource()
data class Network(val name: String, val cidr: String) : Resource()
Dit geeft ons een basisstructuur om mee te werken. Laten we nu een DSL maken om deze resources te definiëren.
2. De DSL Maken
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
}
Nu kunnen we onze infrastructuur als volgt definiëren:
val myInfra = infrastructure {
server("web-server") {
size = "t2.small"
}
network("main-vpc") {
cidr = "172.16.0.0/16"
}
}
Typeveiligheid Toevoegen met Arrow-kt
Onze DSL ziet er goed uit, maar laten we het een stap verder brengen met wat functionele programmeergoedheid van Arrow-kt.
1. Gevalideerde Resources
Laten we eerst Arrow's Validated
gebruiken om ervoor te zorgen dat onze resources correct zijn gedefinieerd:
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. Validaties Componeren
Laten we nu onze Infrastructure
klasse bijwerken om deze validaties te gebruiken:
class Infrastructure {
private val resources = mutableListOf()
fun validateAll(): ValidatedNel> =
resources.traverse { resource ->
when (resource) {
is Server -> resource.validate()
is Network -> resource.validate()
}
}
// ... rest van de klasse blijft hetzelfde
}
Verder Gaan: Resource Afhankelijkheden
Echte infrastructuur heeft vaak afhankelijkheden tussen resources. Laten we dit modelleren met behulp van Arrow's Kleisli
:
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)
}