Terraform 0.12 alpha: протестировали HCL2 на реальном модуле - старые конфиги ломаются, новый синтаксис стоит усилий
Прогнали Terraform 0.12 alpha на тестовом модуле: HCL2 ломает старые конфиги без предупреждения, но for-выражения и нормальные типы оправдывают инвентаризацию legacy-кода.
Terraform 0.12 alpha/preview - переход на HCL2 с новым синтаксисом, типовой системой и выражениями for/for_each
HashiCorp в конце декабря выложили альфу Terraform 0.12. Не beta, не RC - именно alpha, с честным предупреждением «не используйте в продакшне». Мы не использовали, но один тестовый модуль через неё прогнали - хотелось понять реальный масштаб работы, которая нас ждёт. В ноябре 2017-го мы уже писали про preview HCL2, тогда это было только документацией и обещаниями. Теперь есть что пощупать руками.
Вывод короткий: сломается всё. Но кое-что из нового синтаксиса реально хорошее.
Как тестировали
Взяли один из наших внутренних модулей - примерно 300 строк HCL, описывает типовую конфигурацию с несколькими ресурсами, переменными и locals. Модуль использовался на нескольких managed-проектах, написан ещё под 0.11, живёт в GitLab. Подняли отдельный контейнер с terraform 0.12 alpha binary, изолированно от рабочих окружений. Запустили terraform init и terraform validate - посмотрели что посыплется.
Посыпалось почти сразу.
Что конкретно ломается
Интерполяции ${...} внутри строк. В HCL2 они не запрещены полностью, но их поведение поменялось. Конструкция "${var.project}-${var.env}" теперь должна быть просто "${var.project}-${var.env}" - синтаксически похоже, но вот count = "${length(var.list)}" в числовом контексте уже не работает. Число должно быть числом: count = length(var.list). Кавычки убираются, и это ломает каждую вторую строку в типичном модуле.
Типы переменных. type = "list" и type = "map" - в мусор. Теперь это type = list(string), type = map(string), type = object({...}). Типы стали настоящими типами с параметрами, а не строковыми константами. Это правильно, но найти и исправить надо в каждом variable-блоке.
template_file data source. Наш модуль местами использовал data "template_file" для генерации строк из шаблонов. В 0.12 это не отвалилось, но рядом с ним в выводе validate появилось несколько предупреждений о том, что шаблоны теперь лучше писать через встроенные функции templatefile() и format(). Не fatal, но зафиксировали.
null_resource с count-трюками. У нас был один условный ресурс через count = "${var.enable_feature ? 1 : 0}". В 0.12 тернарный оператор работает, но уже без кавычек и без интерполяционного синтаксиса: count = var.enable_feature ? 1 : 0. Мелочь, но в модуле таких мест несколько.
Итого за один terraform validate - примерно два десятка ошибок на 300 строк. Не катастрофа, но это один небольшой модуль. У нас в работе несколько десятков модулей разного размера.
Что реально хорошее
Ради чего вообще это всё: новые возможности языка, которых в HCL1 не было и которые обходились через костыли с count и null_resource.
For-выражения. В 0.12 можно написать:
locals {
upper_names = [for name in var.names : upper(name)]
name_map = {for s in var.names : s => upper(s)}
}
До этого примерно то же самое делалось через null_resource с count и element(), или не делалось вообще - данные преобразовывались снаружи Terraform. Теперь это часть языка.
for_each для ресурсов. Вместо count с индексами - нормальный итератор по map или set:
resource "aws_iam_user" "users" {
for_each = toset(var.user_names)
name = each.value
}
Разница с count принципиальная: при удалении элемента из середины списка через count Terraform пересоздаёт все последующие ресурсы (потому что индексы сдвигаются). С for_each по map - удаляется только нужный. Для баз, IAM-ролей, DNS-записей это меняет дело.
Нормальная типовая система. object({...}) с явными полями - переменная теперь самодокументирована. Раньше переменная типа map могла нести что угодно, и только комментарий объяснял что именно. Теперь это описывается в типе.
Динамические блоки. Вложенные блоки ресурса можно генерировать динамически:
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
Security group rules через переменную без copy-paste блоков - это то, чего нам не хватало.
Что с обратной совместимостью
Никакой. 0.12 alpha чётко говорит: старые конфиги не работают, нужна миграция. HashiCorp обещают написать команду terraform 0.12upgrade по аналогии с тем, что было для 0.11 - она должна автоматически переписать большую часть синтаксиса. Для нашего тестового модуля мы прогнали её вручную: примерно 60-70% ошибок она закрывает сама, остальное - ручная работа, особенно там где типы переменных были сложными или где логика строилась на count-трюках.
Авто-конвертер реально помогает, но слепо доверять ему нельзя - после прогона нужен внимательный review, потому что семантика некоторых конструкций меняется, а не только синтаксис.
Что делаем сейчас
После этого теста стало понятно: миграция на 0.12 - это не обновление бинарника. Это работа, которую надо планировать отдельно. Начали инвентаризацию: сколько у нас модулей, какого размера, где самые запутанные count-конструкции, где template_file и другие вещи которые придётся переписывать.
После перехода на Module Registry у нас появилась централизация - это помогает: обновить модуль в одном месте и прогнать миграцию там, а не ловить копии по десяти репозиториям. Но количество модулей от этого не уменьшилось.
Пока план такой: держать 0.11 на продакшне, не торопиться. Следить за тем как alpha перейдёт в beta и RC. Параллельно - инвентаризация и аудит того, что у нас есть. Когда выйдет GA и финальный upgrade-инструмент, не хочется выяснять масштаб работы в последний момент.
0.12 alpha - не для production, и мы именно так к ней и отнеслись. Но посмотреть стоило: теперь понятно за что терпеть миграцию.