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

Ora possiamo esprimere le dipendenze nel nostro DSL:La potenza della composizioneUna delle cose belle di questo approccio è quanto facilmente possiamo comporre infrastrutture complesse da parti più semplici. Creiamo un'astrazione di livello superiore per un'applicazione web:ConclusioneAbbiamo appena scalfito la superficie di ciò che è possibile con un DSL per l'infrastruttura tipizzato. Sfruttando le funzionalità del linguaggio Kotlin e il toolkit di programmazione funzionale di Arrow-kt, abbiamo creato un modo potente, espressivo e sicuro per definire l'infrastruttura.Alcuni punti chiave:La sicurezza dei tipi intercetta gli errori in anticipo, risparmiandoti errori costosi a runtimeLa componibilità ti permette di costruire infrastrutture complesse da parti semplici e riutilizzabiliI concetti di programmazione funzionale come Validated e Kleisli forniscono strumenti potenti per modellare relazioni e vincoli complessiCibo per la menteMentre continui a sviluppare il tuo DSL per l'infrastruttura, considera queste domande:Come estenderesti questo DSL per supportare diversi fornitori di cloud?Potresti usare questo approccio per generare modelli CloudFormation o configurazioni Terraform?Come potresti incorporare la stima dei costi nel tuo DSL?Ricorda, l'obiettivo non è solo replicare gli strumenti IaC esistenti in Kotlin, ma creare un modo più espressivo e tipizzato per definire l'infrastruttura che intercetta gli errori in anticipo e rende chiare le tue intenzioni. Buona programmazione, e che i tuoi server siano sempre attivi e la tua latenza bassa!