インフラストラクチャのコードは混乱しがちです。果てしなく続くYAMLファイル、目が痛くなるJSON、そしてガムテープと祈りでつながれたbashスクリプト。ですが、もしインフラストラクチャのコードに強く型付けされた言語の安全性と表現力を持ち込むことができたらどうでしょうか?
ここで登場するのがKotlinとArrow-ktです。KotlinのDSL構築能力とArrow-ktの関数型プログラミングツールを使えば、次のようなIaCソリューションを作成できます:
- 型安全: 本番サーバーが炎上する前にコンパイル時にエラーをキャッチ
- コンポーザブル: シンプルで再利用可能なコンポーネントから複雑なインフラを構築
- 表現力豊か: 人間にとって理解しやすい方法でインフラを記述
準備を整える
始める前に、ツールを準備しましょう。必要なものは:
- Kotlin(できれば1.5.0以降)
- Arrow-kt(バージョン1.0.1を使用します)
- お気に入りのIDE(Kotlin開発にはIntelliJ IDEAが強く推奨されます)
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()
}
}
// ... クラスの残りは同じ
}
さらに進める: リソースの依存関係
実際のインフラはしばしばリソース間に依存関係があります。これを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 }.toOption()
}
fun network(name: String): ResourceDep = Kleisli { infra ->
infra.resources.filterIsInstance().find { it.name == name }.toOption()
}
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)
}