基础设施代码可能会变得一团糟。YAML 文件长得像一条无尽的路,JSON 让你眼花缭乱,更不用说那些用胶带和祈祷维持的 bash 脚本了。但是,如果我们能将强类型语言的安全性和表达能力引入到我们的基础设施代码中呢?
这时,Kotlin 和 Arrow-kt 就派上用场了。利用 Kotlin 的 DSL 构建能力和 Arrow-kt 的函数式编程工具,我们可以创建一个 IaC 解决方案,它具有以下特点:
- 类型安全:在编译时捕获错误,而不是在生产服务器出问题时
- 可组合:从简单、可重用的组件构建复杂的基础设施
- 表达性强:以一种对人类来说真正有意义的方式描述你的基础设施
准备工作
在我们深入之前,先确保工具准备就绪。你需要:
- Kotlin(最好是 1.5.0 或更高版本)
- Arrow-kt(我们将使用 1.0.1 版本)
- 你最喜欢的 IDE(强烈推荐 IntelliJ IDEA 用于 Kotlin 开发)
在你的 build.gradle.kts
文件中添加以下依赖项:
dependencies {
implementation("io.arrow-kt:arrow-core:1.0.1")
implementation("io.arrow-kt:arrow-fx-coroutines:1.0.1")
}
逐步构建我们的 DSL
让我们从定义一些基础的基础设施构建块开始。我们将为服务器和网络创建一个简单的模型。
1. 定义我们的领域
sealed class Resource
data class Server(val name: String, val size: String) : Resource()
data class Network(val name: String, val cidr: String) : Resource()
这为我们提供了一个基本的结构。现在,让我们创建一个 DSL 来定义这些资源。
2. 创建 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
}
现在我们可以这样定义我们的基础设施:
val myInfra = infrastructure {
server("web-server") {
size = "t2.small"
}
network("main-vpc") {
cidr = "172.16.0.0/16"
}
}
使用 Arrow-kt 增加类型安全性
我们的 DSL 看起来不错,但让我们用 Arrow-kt 的一些函数式编程增强它。
1. 验证资源
首先,让我们使用 Arrow 的 Validated
来确保我们的资源定义正确:
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. 组合验证
现在让我们更新 Infrastructure
类以使用这些验证:
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
}
更进一步:资源依赖
真实的基础设施通常在资源之间有依赖关系。让我们使用 Arrow 的 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)
}