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

Terraform 0.9: единый remote backend для всей команды

Terraform 0.9 переосмыслил remote state: теперь backend - отдельный блок с инициализацией. Переезжаем на S3 + DynamoDB lock и описываем структуру workspace.

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

Terraform 0.9 (март 2017) - remote state backends вынесены в отдельный слой с `terraform init`, data sources для чтения внешних outputs, разделение plan/apply

В феврале мы описывали переезд на Terraform 0.8 и первый опыт с remote state через S3. Тогда главной болью было то, что backend-конфигурация жила отдельно от кода: terraform remote config с кучей флагов, которые каждый инженер вводил вручную, и никакой гарантии что флаги у всех одинаковые. В марте вышел Terraform 0.9, и вот с ним история наконец сложилась в нечто вменяемое.

Что поменялось в 0.9

Главное изменение - terraform init. Раньше это была команда для скачивания провайдеров, которую запускали по необходимости. В 0.9 init стала точкой входа: она читает блок backend {} в конфигурации, подключается к remote state, и только после этого можно запускать plan. Если backend не инициализирован - plan отказывается работать. Звучит строго, зато теперь нельзя случайно применить изменения к локальному state пока коллеги правят тот же ресурс в удалённом.

Второе - блокировки через DynamoDB стали частью официальной документации, а не «вот есть такая опция». Схема простая:

terraform {
  backend "s3" {
    bucket         = "company-tfstate"
    key            = "prod/network/terraform.tfstate"
    region         = "eu-west-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

DynamoDB-таблица нужна одна на весь аккаунт, в ней хранятся lease-записи. Когда кто-то запускает terraform apply, запись появляется в таблице; если в этот момент другой инженер пытается запустить свой apply для того же ключа state - получает ошибку с именем того кто держит лок и временем начала. Это не панацея от всего, но ситуацию «двое apply-ят одновременно» закрывает надёжно.

Третье - data sources стали первоклассным синтаксисом, не хаком. terraform_remote_state теперь официально задокументирован и стабилен - именно то что нам было нужно для связи между state-файлами разных компонентов.

Как мы переехали

Переезд занял несколько дней, большую часть которых потратили не на код, а на договорённости. Структура state-ов у нас уже была, описанная в феврале: environments/{env}/{component}/terraform.tfstate. Нужно было привести backend-блоки в соответствие и прогнать terraform init для каждого компонента.

Практически это выглядело так: берёшь директорию компонента, добавляешь блок terraform { backend "s3" {} }, запускаешь init - и Terraform спрашивает «хочешь перенести существующий локальный state в backend?». Отвечаешь yes, проверяешь что state появился в S3. Повторяешь для каждого компонента. Скучно, но безопасно - Terraform не даёт потерять состояние при миграции.

Хуже с workspace-ами. В 0.9 появились terraform workspace для изоляции окружений внутри одного набора конфигурации. Мы потратили полдня обсуждая стоит ли переключиться на workspaces вместо отдельных директорий для prod/staging. Решили не переключаться: у workspaces ключ в S3 выглядит как env:/staging/path/to/state, и при взгляде на бакет непонятно что откуда. Отдельные директории с явными путями читаются лучше.

Итоговая структура бакета:

company-tfstate/
  prod/
    network/terraform.tfstate
    compute/terraform.tfstate
    database/terraform.tfstate
  staging/
    network/terraform.tfstate
    compute/terraform.tfstate

Каждый .tfstate - это S3 object с включённым versioning. Если что-то пошло не так после apply, можно откатить object к предыдущей версии и пересобрать из него картину мира. Пока не пользовались, но приятно что есть.

Разделение plan и apply

Ещё одно изменение в 0.9 - terraform plan -out=plan.bin и terraform apply plan.bin. Сохранённый план гарантирует что apply применит именно то что было показано в plan, без сюрпризов от промежуточных изменений в инфраструктуре.

Для сопровождения это критично. В нашем GitLab-пайплайне plan запускается в CI при каждом MR, сохраняется как артефакт. Ревьюер смотрит что будет изменено ещё до мержа. После мержа - ручной запуск apply с тем же plan-файлом. Пайплайн описан отдельно, но суть в том что между «показать diff» и «применить» теперь есть барьер который нельзя случайно перешагнуть.

Что не понравилось

Backend-конфигурацию нельзя параметризировать через переменные. То есть нельзя написать:

backend "s3" {
  bucket = var.state_bucket  # так не работает
}

Значение должно быть строковым литералом. Поэтому у нас один backend-блок на компонент с захардкоженным путём. Обходим через -backend-config флаг в init, передавая часть конфигурации снаружи - но это добавляет обертку в виде скрипта вокруг terraform-команд, что немного раздражает.

Второй момент - data sources для terraform_remote_state читают state напрямую, то есть layout state-файла становится публичным API. Если переименуешь output в одном компоненте - сломаются все кто его читает через remote state. Пока масштаб небольшой и договорились вести список outputs в README каждого компонента.

В целом 0.9 сделал то что должен был сделать: убрал хаос вокруг remote state и дал команде единую точку правды. Теперь по крайней мере понятно кто применяет изменения и куда они идут.

Контакт

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

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