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

Nu kunnen we afhankelijkheden in onze DSL uitdrukken:De Kracht van CompositieEen van de mooie dingen van deze aanpak is hoe gemakkelijk we complexe infrastructuur kunnen samenstellen uit eenvoudigere delen. Laten we een hoger niveau abstractie voor een webapplicatie maken:AfrondenWe hebben slechts het oppervlak bekrast van wat mogelijk is met een type-veilige Infrastructure DSL. Door gebruik te maken van de taalfeatures van Kotlin en de functionele programmeertoolkit van Arrow-kt, hebben we een krachtige, expressieve en veilige manier gecreëerd om infrastructuur te definiëren.Enkele belangrijke punten:Typeveiligheid vangt fouten vroeg op, waardoor je dure runtime-fouten bespaartCompositie stelt je in staat om complexe infrastructuur te bouwen uit eenvoudige, herbruikbare delenFunctionele programmeerconcepten zoals Validated en Kleisli bieden krachtige tools voor het modelleren van complexe relaties en beperkingenStof tot NadenkenTerwijl je je Infrastructure DSL verder ontwikkelt, overweeg deze vragen:Hoe zou je deze DSL uitbreiden om verschillende cloudproviders te ondersteunen?Zou je deze aanpak kunnen gebruiken om CloudFormation-sjablonen of Terraform-configuraties te genereren?Hoe zou je kostenraming in je DSL kunnen opnemen?Onthoud, het doel is niet alleen om bestaande IaC-tools in Kotlin te repliceren, maar om een meer expressieve, type-veilige manier te creëren om infrastructuur te definiëren die fouten vroeg opvangt en je bedoelingen duidelijk maakt. Veel programmeerplezier, en moge je servers altijd draaien en je latentie laag zijn!