Il codice dell'infrastruttura può essere un disastro. File YAML che si estendono per chilometri, JSON che fanno sanguinare gli occhi, e non parliamo nemmeno di quegli script bash tenuti insieme con nastro adesivo e preghiere. Ma cosa succederebbe se potessimo portare la sicurezza e l'espressività di un linguaggio fortemente tipizzato nel nostro codice infrastrutturale?
Entra in gioco Kotlin e Arrow-kt. Con le capacità di costruzione DSL di Kotlin e gli strumenti di programmazione funzionale di Arrow-kt, possiamo creare una soluzione IaC che sia:
- Tipizzata: intercetta gli errori al momento della compilazione, non quando il tuo server di produzione è in fiamme
- Componibile: costruisci infrastrutture complesse da componenti semplici e riutilizzabili
- Espressiva: descrivi la tua infrastruttura in un modo che abbia senso per gli esseri umani
Preparazione
Prima di iniziare, assicuriamoci di avere gli strumenti pronti. Avrai bisogno di:
- Kotlin (preferibilmente 1.5.0 o successivo)
- Arrow-kt (useremo la versione 1.0.1)
- Il tuo IDE preferito (IntelliJ IDEA è altamente raccomandato per lo sviluppo in Kotlin)
Aggiungi le seguenti dipendenze al tuo file build.gradle.kts
:
dependencies {
implementation("io.arrow-kt:arrow-core:1.0.1")
implementation("io.arrow-kt:arrow-fx-coroutines:1.0.1")
}
Costruire il nostro DSL: un pezzo alla volta
Iniziamo definendo alcuni blocchi di base per la nostra infrastruttura. Creeremo un semplice modello per server e reti.
1. Definire il nostro dominio
sealed class Resource
data class Server(val name: String, val size: String) : Resource()
data class Network(val name: String, val cidr: String) : Resource()
Questo ci dà una struttura di base con cui lavorare. Ora, creiamo un DSL per definire queste risorse.
2. Creare il DSL
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
}
Ora possiamo definire la nostra infrastruttura in questo modo:
val myInfra = infrastructure {
server("web-server") {
size = "t2.small"
}
network("main-vpc") {
cidr = "172.16.0.0/16"
}
}
Aggiungere la sicurezza dei tipi con Arrow-kt
Il nostro DSL sembra buono, ma portiamolo a un livello superiore con un po' di programmazione funzionale di Arrow-kt.
1. Risorse validate
Per prima cosa, usiamo il Validated
di Arrow per assicurarci che le nostre risorse siano definite correttamente:
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. Comporre le validazioni
Ora aggiorniamo la nostra classe Infrastructure
per utilizzare queste validazioni:
class Infrastructure {
private val resources = mutableListOf()
fun validateAll(): ValidatedNel> =
resources.traverse { resource ->
when (resource) {
is Server -> resource.validate()
is Network -> resource.validate()
}
}
// ... il resto della classe rimane lo stesso
}
Andare oltre: dipendenze delle risorse
L'infrastruttura reale spesso ha dipendenze tra le risorse. Modelliamo questo usando il Kleisli
di Arrow:
import arrow.core.*
import arrow.fx.coroutines.*
typealias ResourceDep = Kleisli
fun server(name: String): ResourceDep = Kleisli { infra ->
infra.resources.filterIsInstance().find { it.name == name }.toOption()
}
fun network(name: String): ResourceDep = Kleisli { infra ->
infra.resources.filterIsInstance().find { it.name == name }.toOption()
}
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)
}