Terraform 0.12 GA: мигрировали треть модулей - конфиги стали читаться как код
Terraform 0.12 вышел в мае с HCL2, for-выражениями и нормальной типизацией. Рассказываем как прошла миграция модулей и что из этого вышло на практике.
Terraform 0.12 GA (май 2019): новый синтаксис HCL2, первоклассные выражения, улучшенная типизация переменных
В мае HashiCorp выпустили Terraform 0.12 GA. Без «alpha», без «beta», без оговорок - финальный релиз с новым HCL2, первоклассными выражениями и переработанной системой типов. В январе мы гоняли alpha на тестовом модуле и пришли к выводу, что миграция - это работа, которую надо планировать отдельно. С тех пор прошло полгода, и мы эту работу сделали. Не всю, но значительную часть.
Масштаб оказался больше, чем казалось
В январе у нас было «несколько десятков модулей разного размера». К маю инвентаризация дала точную картину: тридцать с лишним модулей, часть которых живёт активно, часть - в состоянии «работает, не трогать». Плюс конфигурации конкретных инфраструктур под каждый клиентский проект, которые эти модули используют.
terraform 0.12upgrade - инструмент от HashiCorp для автоматической конвертации - закрывает примерно то что и обещал: убирает лишние кавычки вокруг чисел, переписывает простые интерполяции, чинит type = "list" на type = list(string). Но там где логика строилась на count-трюках или null_resource с условиями - инструмент либо конвертирует формально неверно, либо оставляет как есть с предупреждением. Такие места пришлось проходить руками.
Итого: около трети модулей потребовали ручной работы сверх того что сделал upgrade-инструмент. Не катастрофа, но и не «запустил скрипт и готово».
Что реально изменилось к лучшему
Это не преувеличение: конфиги после миграции стали другими. Не просто синтаксически - по смыслу.
For-выражения вместо count-костылей. Раньше чтобы получить список имён из переменной, преобразованный в верхний регистр, либо делалось снаружи Terraform (в пайплайне), либо городился null_resource с count и element(). Теперь это [for name in var.names : upper(name)] прямо в locals. Читается сразу, без расшифровки.
for_each вместо count для ресурсов. Разница принципиальная - мы её видели ещё в alpha и она не разочаровала. При count удаление элемента из середины списка пересоздаёт все последующие ресурсы из-за сдвига индексов. С for_each по map - удаляется только нужный. Для IAM-ролей, DNS-записей, правил security group это меняет операционную картину.
Нормальная типизация переменных. type = object({name = string, port = number}) вместо type = "map" с комментарием «тут строка, тут число, тут список». Переменные стали самодокументированными. terraform validate теперь ловит несоответствие типов до plan, а не в момент apply когда уже потрачено время.
Динамические блоки. Несколько модулей с security group rules избавились от copy-paste блоков ingress {} через dynamic "ingress" { for_each = var.rules ... }. Было семь одинаковых блоков с разными портами - стал один динамический с переменной. Очевидно, но раньше язык просто не позволял.
Где было тяжелее всего
Самые трудоёмкие места - это условные ресурсы через count = 0/1 с нетривиальной логикой. В HCL1 был популярный паттерн: count = "${var.enable_thing ? 1 : 0}", и дальше к этому ресурсу обращались через element(module.thing.output_list, 0) потому что при count = 0 вывод - пустой список, а не null. В HCL2 появился null как значение и тернарник без кавычек, но семантика поменялась тонко: count = 0 теперь означает что ресурса нет, а его output просто не существует. Места где на это опирались через element(..., 0) нужно переписывать осмысленно, а не механически.
Ещё один момент: template_file data source формально работает, но несколько мест мы переписали на встроенный templatefile() - он чище и не требует data-блока только ради генерации строки.
Стратегия миграции
Мигрировали по принципу «сначала листовые модули, потом корневые конфигурации». Листовой модуль не зависит от других наших модулей - можно конвертировать и проверять изолированно. Корневая конфигурация использует несколько модулей и мигрировала последней, когда все зависимости уже на 0.12.
Параллельно держали два remote state backend: часть инфраструктур работала на 0.11, часть - на 0.12. State-файлы между версиями несовместимы напрямую, но если мигрировать конфигурацию без изменения ресурсов, Terraform 0.12 подхватывает существующий state нормально. Ни одного вынужденного terraform import не понадобилось.
На сопровождаемых проектах клиентский код трогали только с явным согласованием: объясняли что за миграция, что план до и после должен показать zero diff. Все прошло без инцидентов, хотя на паре инфраструктур план показал diff в тегах - оказалось, что 0.12 иначе вычисляет некоторые строковые интерполяции в значениях тегов. Поправили.
Что не сделали
Новые возможности HCL2 - object() типы переменных, полноценные условные выражения - мы добавляем только в новые модули или при переписывании старых. Механически заменять всё ради «стиля» смысла нет: работающий код лучше тронутого без причины.
Registry-версионирование модулей настроили ещё в прошлом году, и это сильно помогло: ограничение version = "~> 2.0" в source позволило держать 0.11-совместимые версии модулей для ещё не мигрированных инфраструктур, пока параллельно публиковали 0.12-совместимые под новой мажорной версией.
Итог
Миграция прошла без боли - медленнее чем хотелось бы, но предсказуемо. Главное что изменилось: читать конфиги теперь приятно. Звучит субъективно, но на практике это означает что новый человек в команде разбирается в чужом модуле за минуты, а не за час с матом. Условия - это условия, циклы - это циклы, типы - это типы. Terraform перестал быть языком угадывания.