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 и дал команде единую точку правды. Теперь по крайней мере понятно кто применяет изменения и куда они идут.