Наша команда из шести backend-разработчиков пишет на Kotlin. Когда пришло время вынести Kubernetes-манифесты из GitOps-репозитория в полноценный IaC, встал вопрос инструмента. Terraform — индустриальный стандарт, но HCL для нас чужой язык. Pulumi обещает infra-as-real-code на familiar-стеке. Я потратил две недели на прототипирование обоих подходов и поделюсь выводами.
Terraform: проверенный, но чужой
Terraform мы использовали полтора года для provisioning серверов в Yandex Cloud. HCL выучить несложно — это declarative-язык с минимальным синтаксисом. Но когда дело дошло до Kubernetes, начались проблемы. HCL не поддерживает циклы с условиями в полной мере, нет нормальной типизации, а отладка — это чтение логов на пол-экрана.
Вот наш типичный деплоймент на Terraform:
resource "kubernetes_namespace" "app" {
metadata {
name = "myapp"
}
}
resource "kubernetes_deployment" "app" {
metadata {
name = "myapp"
namespace = kubernetes_namespace.app.metadata.0.name
}
spec {
replicas = 3
selector {
match_labels = {
app = "myapp"
}
}
template {
metadata {
labels = {
app = "myapp"
}
}
spec {
container {
name = "myapp"
image = "cr.yandex/myproj/myapp:latest"
port {
container_port = 8080
}
resources {
limits = {
cpu = "500m"
memory = "512Mi"
}
}
}
}
}
}
}
Выглядит знакомо, но обратите внимание: нет никакой валидации на этапе компиляции. Опечатка в имени поля container_port обнаружится только при terraform apply. Для нас, привыкших к строгой типизации Kotlin, это было болью.
Pulumi: инфраструктура на Kotlin
Pulumi позволил нам описывать ту же инфраструктуру на Kotlin. Автодополнение в IDE, типобезопасность, возможность выносить общую логику в функции и классы — всё, к чему мы привыкли в прикладном коде.
import pulumi.kotlin.kubernetes.apps.v1.Deployment
import pulumi.kotlin.kubernetes.core.v1.Container
import pulumi.kotlin.kubernetes.core.v1.Namespace
val appNamespace = Namespace("app") {
metadata { name = "myapp" }
}
Deployment("app") {
metadata {
name = "myapp"
namespace = appNamespace.metadata.name
}
spec {
replicas = 3
selector {
matchLabels = mapOf("app" to "myapp")
}
template {
metadata {
labels = mapOf("app" to "myapp")
}
spec {
containers = listOf(Container {
name = "myapp"
image = "cr.yandex/myproj/myapp:latest"
ports = listOf(Port { containerPort = 8080 })
resources = ResourceRequirements {
limits = mapOf(
"cpu" to Quantity("500m"),
"memory" to Quantity("512Mi")
)
}
})
}
}
}
}
Разница ощущается сразу. IDE подсвечивает ошибки до запуска. Я могу создать data class AppConfig и переиспользовать его для дева, стейджа и прода. Могу написать unit-тесты, которые проверяют, что деплоймент создан с правильным количеством реплик.
Управление состоянием
Terraform хранит state в .tfstate — JSON-файле, который нужно лочить через S3/DynamoDB при командной работе. Pulumi хранит state в своём SaaS-бэкенде (бесплатно для небольших команд) или в self-hosted сервисе. У обоих подходов есть backends с блокировками, но у Pulumi ещё и шифрование из коробки и удобный веб-интерфейс для просмотра стека.
Мы выбрали self-hosted Pulumi Backend в контейнере — он лёгкий и полностью под нашим контролем.
Тестирование инфраструктуры
С Pulumi на Kotlin мы смогли написать тесты с JUnit. Например, проверяем, что каждый деплоймент имеет ресурсные лимиты:
import org.junit.jupiter.api.Test
import pulumi.kotlin.testing.PulumiTest
class InfraTest : PulumiTest() {
@Test
fun `deployment has resource limits`() {
val stack = testAsync { AppsStack(it) }
val deployment = stack.resources
.filterIsInstance()
.first()
val container = deployment.spec?.template?.spec?.containers?.first()
assertNotNull(container?.resources?.limits)
assertTrue(container!!.resources.limits!!.containsKey("memory"))
}
}
С Terraform такие тесты возможны через terraform-validate или tflint, но это YAML-конфиги с regex-проверками — далёко от удобства JUnit.
Экосистема и кривая обучения
У Terraform огромная экосистема: тысячи провайдеров, модули на Terraform Registry, куча документации и stackoverflow-ответов. У Pulumi провайдеры — те же (он транслирует вызовы в Terraform-провайдеры), но модулей меньше, и документация по Kotlin-SDK местами скудная.
Кривая обучения: HCL — два дня для базовых вещей. Pulumi на Kotlin — три-четыре дня, но потом скорость роста гораздо выше, потому что ты работаешь в знакомой парадигме.
Наш выбор
Мы выбрали Pulumi. Главные причины: типобезопасность, возможность реального тестирования, единый язык для инфраструктуры и прикладного кода, и тот факт, что новым разработчикам в команде не нужно учить ещё один язык. Terraform остался у нас для cloud-level provisioning (сети, VPC, IAM) — там его declarative-стиль работает отлично. А Kubernetes-манифесты теперь живут в Kotlin-репозитории.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье