基础设施代码可能会变得一团糟。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)
    }

现在我们可以在我们的 DSL 中表达依赖关系:组合的力量这种方法的一个美妙之处在于我们可以轻松地从简单的部分组合复杂的基础设施。让我们为一个 web 应用程序创建一个更高层次的抽象:总结我们只是触及了类型安全基础设施 DSL 的可能性。通过利用 Kotlin 的语言特性和 Arrow-kt 的函数式编程工具包,我们创建了一种强大、富有表现力且安全的方式来定义基础设施。关键要点:类型安全可以及早捕获错误,避免代价高昂的运行时错误可组合性允许你从简单、可重用的部分构建复杂的基础设施函数式编程概念如 ValidatedKleisli 提供了强大的工具来建模复杂的关系和约束思考在你继续开发你的基础设施 DSL 时,考虑这些问题:你如何扩展这个 DSL 以支持不同的云提供商?你能否使用这种方法生成 CloudFormation 模板或 Terraform 配置?你如何将成本估算纳入你的 DSL?记住,目标不仅仅是用 Kotlin 复制现有的 IaC 工具,而是创建一种更具表现力、类型安全的方式来定义基础设施,及早捕获错误并明确你的意图。祝编码愉快,愿你的服务器始终在线,延迟始终较低!