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

Terraform 0.12: вынесли инфру трёх стендов в один модуль с for_each - новое окружение теперь одна строка в tfvars

Рефакторинг под Terraform 0.12 GA: три стенда из отдельных директорий переехали в единый модуль с for_each по map. Новое окружение - одна запись в tfvars.

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

Terraform 0.12 GA - сообщество рефакторит модули на HCL2; мы вынесли инфру трёх стендов в единый модуль с for_each

После того как мы перешли на Terraform 0.12 beta и перемигрировали несколько десятков модулей, встал следующий вопрос: а что делать с инфраструктурой, которая у нас размножалась копированием директорий? Подход «скопировал dev, поменял пять переменных, назвал staging» - классический, понятный, и одновременно классически проблемный. Пришло время от него избавиться.

Как было

На одном из managed-проектов инфраструктура трёх стендов - dev, staging, production - жила в трёх отдельных директориях. Структура одинаковая, ресурсы те же, конфигурация 90% совпадает. Отличия: размеры инстансов, количество реплик, пара feature-флагов.

Обновить что-то общее означало: зайти в три директории, применить одно и то же изменение три раза, не забыть ни одну. Разумеется, иногда забывали. Разумеется, потом долго выясняли почему staging ведёт себя не так как dev.

Это всё было терпимо в 0.11, потому что нормальной альтернативы не было. count на уровне целого окружения - не то, что имеет смысл. Workspace-ы есть, но они не решают проблему разных конфигураций разных стендов: один workspace - один набор переменных, никакой многострочной map-конфигурации per-environment. Поэтому жили с директориями.

В 0.12 появился for_each по map на уровне модуля - и с этим уже можно было что-то сделать.

Что сделали

Написали единый модуль, который принимает описание окружения как объект:

variable "environments" {
  type = map(object({
    instance_type    = string
    replica_count    = number
    enable_debug_api = bool
    subnet_ids       = list(string)
  }))
}

Корневой конфиг вызывает модуль через for_each:

module "env" {
  for_each = var.environments
  source   = "./modules/environment"

  name             = each.key
  instance_type    = each.value.instance_type
  replica_count    = each.value.replica_count
  enable_debug_api = each.value.enable_debug_api
  subnet_ids       = each.value.subnet_ids
}

А в terraform.tfvars теперь лежит простой map:

environments = {
  dev = {
    instance_type    = "t3.medium"
    replica_count    = 1
    enable_debug_api = true
    subnet_ids       = ["subnet-aaa", "subnet-bbb"]
  }
  staging = {
    instance_type    = "t3.large"
    replica_count    = 2
    enable_debug_api = false
    subnet_ids       = ["subnet-ccc", "subnet-ddd"]
  }
  production = {
    instance_type    = "c5.xlarge"
    replica_count    = 3
    enable_debug_api = false
    subnet_ids       = ["subnet-eee", "subnet-fff"]
  }
}

Добавить новый стенд - одна новая запись в этом map. Terraform создаёт всё, что нужно, и именует ресурсы через each.key. Никакого копирования директорий, никакого ручного применения изменений в трёх местах.

Что неожиданно хорошего

Строгая типизация переменных. object({...}) с явными полями работает как документация: открыл variables.tf модуля - сразу понятно что именно передаётся для каждого окружения. До этого в аналогичных местах стояло type = map, и что именно ожидалось внутри - знали только те, кто написал.

Именование ресурсов через each.key. Ресурсы получают имена вида module.env["dev"].aws_instance.app - адрес однозначный, человекочитаемый. По сравнению с индексами в старом count-паттерне (resource[0], resource[1]) - разница заметная при чтении plan.

Ошибки конфигурации ловятся на validate. Если добавить новое окружение с опечаткой в ключе или не тем типом значения - terraform validate даёт внятную ошибку до любого plan. Раньше при копировании директорий ошибка проявлялась иногда только на apply, и в середине apply.

Что потребовало внимания

Миграция state-а была аккуратной работой. Ресурсы, которые раньше жили в отдельных директориях с отдельными state-файлами, нужно было либо переселить в единый state через terraform state mv, либо собрать state-ы в один. Мы пошли по пути импорта: прогнали terraform import для каждого ресурса по новым адресам. Немного муторно, зато state чистый и без дублирования.

Ещё момент: не все ресурсы одинаково хорошо живут в цикле через for_each. Там где логика окружения нетривиальная - например, production требует дополнительных ресурсов которых нет в dev - приходится либо выносить их за пределы общего модуля, либо использовать условный count = each.value.some_flag ? 1 : 0 внутри. Оба варианта рабочие, но читаемость модуля немного страдает когда таких условий много.

Итог

Результат стоил затраченного времени. Три директории с дублирующимися конфигами превратились в один модуль и один tfvars с понятной структурой. Изменение, которое раньше требовало синхронного редактирования трёх мест, теперь делается в одном.

Подход не универсальный - если стенды реально сильно отличаются архитектурно, пытаться запихать их в один for_each-цикл скорее навредит. Но когда окружения отличаются только параметрами, а не структурой, - это именно то, для чего for_each по map и предназначен. 0.12 наконец дал язык, в котором это можно выразить нормально.

Контакт

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

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