Infrastructure code can be a mess. YAML files stretching for miles, JSON that makes your eyes bleed, and don't even get me started on those bash scripts held together by duct tape and prayers. But what if we could bring the safety and expressiveness of a strongly-typed language to our infrastructure code?

Enter Kotlin and Arrow-kt. With Kotlin's DSL-building capabilities and Arrow-kt's functional programming tools, we can create an IaC solution that's:

  • Type-safe: Catch errors at compile-time, not when your production server is on fire
  • Composable: Build complex infrastructure from simple, reusable components
  • Expressive: Describe your infrastructure in a way that actually makes sense to humans

Setting the Stage

Before we dive in, let's make sure we have our tools ready. You'll need:

  • Kotlin (preferably 1.5.0 or later)
  • Arrow-kt (we'll be using version 1.0.1)
  • Your favorite IDE (IntelliJ IDEA is highly recommended for Kotlin development)

Add the following dependencies to your build.gradle.kts file:


dependencies {
    implementation("io.arrow-kt:arrow-core:1.0.1")
    implementation("io.arrow-kt:arrow-fx-coroutines:1.0.1")
}

Building Our DSL: One Piece at a Time

Let's start by defining some basic building blocks for our infrastructure. We'll create a simple model for servers and networks.

1. Defining Our Domain


sealed class Resource
data class Server(val name: String, val size: String) : Resource()
data class Network(val name: String, val cidr: String) : Resource()

This gives us a basic structure to work with. Now, let's create a DSL to define these resources.

2. Creating the 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
}

Now we can define our infrastructure like this:


val myInfra = infrastructure {
    server("web-server") {
        size = "t2.small"
    }
    network("main-vpc") {
        cidr = "172.16.0.0/16"
    }
}

Adding Type Safety with Arrow-kt

Our DSL is looking good, but let's kick it up a notch with some functional programming goodness from Arrow-kt.

1. Validated Resources

First, let's use Arrow's Validated to ensure our resources are correctly defined:


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. Composing Validations

Now let's update our Infrastructure class to use these validations:


class Infrastructure {
    private val resources = mutableListOf()

    fun validateAll(): ValidatedNel> =
        resources.traverse { resource ->
            when (resource) {
                is Server -> resource.validate()
                is Network -> resource.validate()
            }
        }

    // ... rest of the class remains the same
}

Taking It Further: Resource Dependencies

Real infrastructure often has dependencies between resources. Let's model this using 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)
    }

Now we can express dependencies in our DSL:The Power of CompositionOne of the beautiful things about this approach is how easily we can compose complex infrastructure from simpler parts. Let's create a higher-level abstraction for a web application:Wrapping UpWe've just scratched the surface of what's possible with a type-safe Infrastructure DSL. By leveraging Kotlin's language features and Arrow-kt's functional programming toolkit, we've created a powerful, expressive, and safe way to define infrastructure.Some key takeaways:Type safety catches errors early, saving you from costly runtime errorsComposability allows you to build complex infrastructure from simple, reusable partsFunctional programming concepts like Validated and Kleisli provide powerful tools for modeling complex relationships and constraintsFood for ThoughtAs you continue to develop your Infrastructure DSL, consider these questions:How would you extend this DSL to support different cloud providers?Could you use this approach to generate CloudFormation templates or Terraform configurations?How might you incorporate cost estimation into your DSL?Remember, the goal isn't just to replicate existing IaC tools in Kotlin, but to create a more expressive, type-safe way of defining infrastructure that catches errors early and makes your intentions clear. Happy coding, and may your servers always be up and your latency low!