Terraform 0.12 в production: мигрируем модули с HCL2 и заодно разгребаем технический долг
Начали миграцию инфраструктурного кода с Terraform 0.11 на 0.12: переписываем модули под HCL2, убираем count-костыли и разбираемся с remote state.
Terraform 0.12+ в широком использовании: HCL2, выразительные типы, for-expressions - команды переходят с 0.11
В январе 2019-го мы прогнали Terraform 0.12 alpha на тестовом модуле, зафиксировали масштаб работы и честно отложили. Тогда же написали про это: alpha - не для production, подождём GA. GA вышел в мае 2019-го. Прошло ещё несколько месяцев. И вот в январе 2020-го мы наконец сели и начали делать это по-настоящему.
Причина, которая сдвинула дело с места: один из новых клиентских проектов на managed-инфраструктуре завели сразу на 0.12, а старые модули туда не встают. Можно было держать два параллельных мира, но это дорого в обслуживании - решили начать систематическую миграцию.
Инвентаризация: что вообще у нас есть
Перед тем как что-то трогать, пересчитали модули. Оказалось больше, чем помнили: часть написана под конкретный проект и с тех пор лежит, часть активно используется на нескольких клиентах, часть - промежуточная, то есть «как будто нужна, но кто последний раз запускал - непонятно».
Разложили по трём корзинам:
- Активные - используются прямо сейчас, обновлять в первую очередь, осторожно.
- Архивные - лежат в репозитории, но последний
terraform applyбыл больше полугода назад. Мигрировать, но без спешки - сначала разобраться, живы ли они вообще. - Зомби - модули, у которых непонятен владелец и статус. Самый неприятный класс.
Зомби-модули - отдельная тема. Не все из них нужны, но просто удалить страшно. Приняли промежуточное решение: замигрировать синтаксис через terraform 0.12upgrade, не трогая логику, и на этом остановиться. Если никто не спросит про них через квартал - можно думать об архивировании.
Что делает terraform 0.12upgrade
Официальный инструмент миграции синтаксиса. Запускается внутри директории с модулем, переписывает .tf-файлы. Работает в большинстве случаев: убирает лишние кавычки вокруг чисел и булевых значений, переписывает интерполяции там, где они стали избыточны, обновляет явные type = "list" на type = list(string).
Работает не везде. Там где сложная логика через count с арифметикой и тернарными операторами - инструмент либо не трогает (оставляет deprecated синтаксис с предупреждением), либо трогает неправильно. Таких мест примерно 20-30% от объёма изменений - это ручная работа.
Типичный pattern, который инструмент не вытягивает сам:
# 0.11 - создание или не создание ресурса через count
resource "aws_route53_record" "alias" {
count = "${var.create_alias == "true" ? 1 : 0}"
...
}
В 0.12 это переписывается чисто:
resource "aws_route53_record" "alias" {
count = var.create_alias ? 1 : 0
...
}
Но там, где вокруг этого навешана логика с element() и concat() - уже нужно думать.
Remote state и зависимости между модулями
Отдельная боль миграции - terraform_remote_state. В проектах с несколькими слоями инфраструктуры (сеть отдельно, вычисления отдельно, приложения отдельно) remote state - это способ передавать outputs между слоями. В 0.11 outputs из remote state читались как динамический map: data.terraform_remote_state.vpc.outputs["subnet_ids"].
В 0.12 это работает иначе: data.terraform_remote_state.vpc.outputs.subnet_ids - прямое обращение к атрибуту. Синтаксически лучше, но это означает, что оба модуля - тот который пишет output и тот который его читает - нужно обновлять согласованно. Нельзя замигрировать только один слой.
Это создаёт ограничение на порядок миграции: идти нужно снизу вверх, от базовых слоёв к верхним. Сначала сетевые модули, потом всё, что на них завязано. Звучит логично, но в реальности у нас есть проекты, где граф зависимостей не такой чистый, как хотелось бы.
Что попутно чистим
Раз уж трогаем код, пользуемся возможностью убрать то, что давно мозолило глаза:
count-костыли для списков уходят в пользу for_each. Разница принципиальная: при изменении элемента в середине списка через count Terraform пересоздаёт всё с этого индекса. for_each по map работает точечно. Для DNS-записей, IAM-ролей, security groups - это меняет поведение apply-а на production.
template_file data source заменяем на templatefile(). Не обязательно прямо сейчас, но раз уж трогаем файл - проще сделать сразу.
Переменные без типов - самое распространённое у нас. variable "tags" {} без явного типа работало, но теперь заменяем на type = map(string) или type = object({...}) там, где структура известна. Модуль становится самодокументированным - видно, что именно принимает переменная.
Дублирующиеся locals - в некоторых старых модулях одно и то же значение вычислялось в двух местах немного по-разному. Видимо, когда писалось, добавить в locals было лень. Чистим.
Где сейчас
Из активных модулей замигрировано около половины. Самые простые шли быстро - terraform 0.12upgrade плюс ручная правка за час. Сложные, с нетривиальной логикой через count и несколькими слоями remote state - занимают день-два каждый, потому что нужно понять что это делает, убедиться что после миграции делает то же самое, и проверить на dev-окружении перед тем как катить на production.
Параллельное существование 0.11 и 0.12 на разных проектах управляемо, если использовать tfenv - переключение версий по .terraform-version файлу в директории. Но это временное решение, не архитектурное.
Ориентир - закрыть активные модули до конца февраля. Архивные и зомби - отдельный разговор на потом.