Terraform 0.6: state в git и plan перед apply - первые шаги к IaC без Puppet
Описываем инфраструктуру тестового окружения в публичном облаке через Terraform 0.6.x: state-файл в git, обязательный plan, никакого Puppet для облака.
Terraform 0.6.x активно используется командами для управления облачной инфраструктурой как кодом в начале 2016 года
Несколько недель назад мы наконец-то попробовали Terraform на практике. Не на боевой инфраструктуре, конечно, - взяли тестовое окружение одного из проектов на сопровождении в публичном облаке и написали конфигурацию с нуля. Рассказываем, что получилось и где споткнулись.
Контекст: зачем вообще
До сих пор облачные ресурсы у нас создавались руками через веб-консоль или CLI провайдера. Три виртуалки, балансировщик, несколько групп безопасности - казалось бы, немного. Но воспроизвести окружение точь-в-точь через полгода уже нетривиально: кто-то добавил правило в security group и забыл задокументировать, кто-то поднял четвёртую виртуалку для отладки и не удалил. Классика.
Puppet мы используем для конфигурирования серверов внутри периметра, но для управления самими облачными ресурсами - типами инстансов, сетями, дисками - он не очень подходит. Это разные уровни абстракции.
Terraform нацелен именно на provisioning инфраструктуры: создать, изменить, удалить облачные объекты через API провайдера. Причём для нескольких провайдеров сразу, если нужно. Нас интересовал один провайдер, но концепция показалась правильной.
Как это выглядит
Конфигурация Terraform - это .tf-файлы в HCL (HashiCorp Configuration Language). Синтаксис похож на упрощённый JSON с возможностью комментариев и интерполяции переменных. Объявляешь провайдера, потом описываешь ресурсы:
provider "openstack" {
auth_url = "${var.os_auth_url}"
tenant_name = "${var.os_tenant}"
user_name = "${var.os_user}"
password = "${var.os_password}"
}
resource "openstack_compute_instance_v2" "app" {
name = "app-01"
image_name = "Ubuntu 14.04"
flavor_name = "m1.small"
key_pair = "deploy"
security_groups = ["default", "app-servers"]
}
resource "openstack_networking_floatingip_v2" "app_fip" {
pool = "external"
}
resource "openstack_compute_floatingip_associate_v2" "app_fip" {
floating_ip = "${openstack_networking_floatingip_v2.app_fip.address}"
instance_id = "${openstack_compute_instance_v2.app.id}"
}
output "app_ip" {
value = "${openstack_networking_floatingip_v2.app_fip.address}"
}
Terraform сам разбирается с порядком создания: понимает что floating IP надо привязать после того, как создан инстанс, потому что видит зависимость через интерполяцию.
Три вещи, которые понравились сразу
Первое - terraform plan. Перед применением изменений Terraform показывает что именно будет создано, изменено или удалено. Зелёный плюс, жёлтая тильда, красный минус. Очень трудно ошибиться незаметно. Мы сделали правилом: никакого apply без предварительного plan и его ревью. Кажется очевидным, но без этого инструмента такой дисциплины не было.
Второе - state-файл как единственная истина. Terraform хранит текущее состояние инфраструктуры в terraform.tfstate - JSON-файл с описанием всех созданных ресурсов и их ID в облаке. Без него Terraform не знает что уже существует. Мы положили state в git вместе с конфигурацией. Простое решение с очевидными ограничениями (только один человек работает с инфраструктурой одновременно), но для нашего масштаба достаточно. По крайней мере, история изменений есть.
Третье - идемпотентность. Запустить plan или apply дважды подряд безопасно. Если состояние соответствует конфигурации - Terraform ничего не делает и честно сообщает об этом. Это снимает страх «а вдруг что-то лишнее пересоздастся».
Что не так с state в git
Честно говоря, решение класть terraform.tfstate в git работает, но с оговорками.
State содержит чувствительные данные - например, пароли от баз данных, если вы создаёте RDS-инстансы через Terraform. В нашем случае секретов в state не было, но это надо проверять заранее. .gitignore для state - тоже не выход, потому что тогда теряешь версионирование.
Параллельная работа двух инженеров с одним state - источник конфликтов и потенциального рассинхрона. Решение - remote state через S3 или consul, но это дополнительная инфраструктура. Пока обходимся договорённостью «один рабочий в одно время».
Как мы организовали структуру
Для одного окружения файлы разложили так:
infra/
main.tf # основные ресурсы
variables.tf # объявление переменных
outputs.tf # выходные значения
terraform.tfvars # значения переменных (в .gitignore - там облачные credentials)
terraform.tfstate
terraform.tfstate.backup
Credentials облачного провайдера - в terraform.tfvars, который не коммитится. Структура ресурсов - в main.tf. Получается документация инфраструктуры в виде кода: открываешь репозиторий и видишь что запущено.
Ansible рядом с Terraform
Terraform создаёт виртуальные машины, Ansible их конфигурирует. Это разные инструменты для разных задач, и они хорошо дополняют друг друга.
После terraform apply у нас есть IP-адреса новых серверов через terraform output. Дальше передаём их в Ansible-инвентарь и запускаем playbook-и. Автоматизировать склейку между двумя инструментами можно, но пока это ручной шаг - скопировать output и вставить в inventory. Некрасиво, но работает.
Что ещё не попробовали
Terraform умеет в модули - переиспользуемые блоки конфигурации. Мы пока всё держим в одном main.tf, потому что окружение небольшое. При росте числа ресурсов модули будут нужны.
Remote state с блокировкой - тоже не трогали. Consul или S3 для хранения state решают проблему параллельной работы, но добавляют зависимость. Отложили до момента когда проблема станет реальной, а не гипотетической.
Где сейчас
На тестовом окружении всё работает. Вся инфраструктура описана кодом, история изменений в git, воспроизведение окружения занимает минуты вместо часа в консоли провайдера. Puppet для конфигурирования серверов остался, Terraform занял место рядом - для provisioning облачных ресурсов.
Переносить на боевые окружения пока не торопимся - хотим набрать уверенности на тестовом. Но уже понятно что концепция работает и на текущие задачи инструмента хватает.