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