ADG Оставить заявку
Блог DevOps 4 мин чтения

Terraform 0.12 beta: мигрировали 40 модулей на HCL2 - for_each заменил count-хак, upgrade-инструмент справился с большей частью синтаксиса

Перешли с Terraform 0.11 на 0.12 beta: HCL2, for_each по map вместо count-костылей, dynamic blocks. terraform 0.12upgrade закрыл большую часть синтаксических проблем автоматически.

Контекст момента

Terraform 0.12 beta - HCL2, for_each по map и set, dynamic blocks, строгая типизация переменных

HashiCorp выпустили Terraform 0.12 beta. В январе мы тестировали alpha на одном модуле и пришли к выводу, что миграция - это работа, которую надо планировать отдельно, а не обновление бинарника. С тех пор мы эту работу сделали: перенесли несколько десятков модулей с 0.11 на 0.12. Фиксируем что вышло.

Масштаб работы

Модулей набралось порядка сорока. Разного размера: от маленьких специализированных до больших общих, которые переиспользуются на нескольких managed-проектах. Большинство написаны ещё под 0.11, часть - ещё старше, с синтаксическими паттернами из 0.10.

Первым делом прогнали terraform 0.12upgrade по каждому модулю. Инструмент делает то, что обещали: переписывает синтаксис автоматически. Убирает лишние кавычки вокруг чисел и булевых значений, правит интерполяции, перебивает типы переменных с устаревших строковых констант на нормальные типы. На простых модулях - работает хорошо, после прогона terraform validate проходит сразу. На сложных - закрывает примерно две трети проблем, остальное требует ручного разбора.

Особенно часто upgrade-инструмент не справлялся там, где логика строилась вокруг count с вычисляемыми значениями. Там и семантика меняется, и не только синтаксис - автоматический рефакторинг тут разумно останавливается.

for_each вместо count-хака

Главное, ради чего вообще всё это затевалось. В 0.11 стандартный способ создать несколько ресурсов из map выглядел примерно так: конвертируешь map в список ключей через keys(), передаёшь в count, потом обращаешься к нужному значению через element(values(var.my_map), count.index). Работает, пока не нужно удалить что-то из середины - при удалении элемента сдвигаются индексы, и Terraform пересоздаёт все последующие ресурсы.

В 0.12 этого нет. for_each итерируется напрямую по map или set:

resource "aws_route53_record" "records" {
  for_each = var.dns_records

  zone_id = var.zone_id
  name    = each.key
  type    = each.value.type
  ttl     = each.value.ttl
  records = each.value.values
}

При удалении одного DNS-адреса из var.dns_records - удаляется ровно он. Остальные Terraform не трогает. Для DNS-записей, IAM-политик, security group rules - это разница между «делаем аккуратно» и «молимся чтобы пронесло».

Переписали несколько модулей, которые создавали ресурсы через count+map-трюк. В некоторых местах это заодно убрало двадцать строк вспомогательных locals - они были нужны только чтобы сгладить несоответствие между map и индексами count.

Dynamic blocks

Второй по полезности момент. В 0.11 не было способа динамически генерировать вложенные блоки ресурса - приходилось либо дублировать, либо выносить конфигурацию наружу. Теперь это работает:

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 блоков - выглядит как мелочь, пока не вспоминаешь модули где этих копипастных ingress-блоков было штук двенадцать и каждый менялся вручную.

Строгая типизация

Типы переменных - ещё одно место, где 0.12 решает реальную проблему, а не просто меняет синтаксис. В 0.11 переменная типа "map" могла нести что угодно, структура документировалась только комментарием. В 0.12 это описывается явно:

variable "dns_records" {
  type = map(object({
    type   = string
    ttl    = number
    values = list(string)
  }))
}

Terraform теперь валидирует структуру при вызове модуля и даёт внятную ошибку, если что-то не так, - а не молча падает где-то в середине plan. Несколько раз уже поймали реальные ошибки конфигурации именно на этапе валидации типов, до apply.

Что осталось за бортом

Не всё прошло гладко. Несколько модулей с нетривиальными count-конструкциями - где count был условием, а не итератором - пришлось переписывать довольно существенно. В паре мест поменялась семантика: то, что раньше создавало ноль ресурсов, теперь требует явного count = 0 или переделки на for_each с пустым map. Мелочь, но если не заметить - plan выглядит неожиданно.

Ещё момент: state. После миграции синтаксиса адреса ресурсов, которые раньше были resource.name[0], стали resource.name["key"]. Это значит, что при первом apply после миграции Terraform видит: «удалить старые ресурсы, создать новые». Для stateless ресурсов - нормально. Для баз данных и всего, что хранит данные, - нужен terraform state mv до apply, иначе получаем пересоздание. Сделали это аккуратно, но несколько нервных минут было.

Итог

Миграция заняла больше, чем хотелось, и меньше, чем боялись. terraform 0.12upgrade честно сделал большую часть механической работы. Ручная часть - это преимущественно count-to-for_each рефакторинг и state mv там где адреса поменялись.

Результат стоит усилий: for_each по map - это то, чего давно не хватало, и теперь несколько модулей стали заметно проще. Строгие типы переменных - меньше сюрпризов при вызове модулей из разных окружений.

0.12 beta на production - можно. Но перед apply после миграции - terraform plan внимательно, и особенно смотреть на адреса ресурсов в state.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.