Terraform 0.7: разбиваем монолит на модули и убираем state из git
Terraform 0.7 принёс нормальные data sources и доработанные модули. Показываем как разбить конфиг на network/compute/security и вынести state в S3 для работы командой.
Terraform 0.7 вышел в августе 2016 с поддержкой data sources и улучшенными модулями для структурирования инфраструктурного кода
В феврале мы писали про первый опыт с Terraform 0.6 на тестовом окружении. Тогда всё держали в одном main.tf, state лежал в git, параллельная работа решалась договорённостью «только один человек в одно время». Для одного человека с одним проектом это работало. Когда проектов стало несколько, а инженеров в команде четыре - стало очевидно, что схема не масштабируется.
Terraform 0.7 выходит в августе, и мы воспользовались поводом чтобы переосмыслить структуру. На одном из проектов сопровождения провели полный рефакторинг: монолитный конфиг разбили на модули, state перенесли в S3.
Проблема с монолитом
В одном main.tf на 400 строк неплохо видно как запутываются зависимости. Security group ссылается на VPC, инстанс - на security group и subnet, subnet - на VPC. Всё в одном файле, и когда надо поменять правило в security group - нервно смотришь на весь state, потому что план показывает изменения рядом с вещами, которые трогать не планировал.
Второй симптом: у нас несколько окружений (prod, staging, dev), и код из prod мы хотим переиспользовать в staging с другими параметрами. Копипастой это не решается - расходятся мгновенно.
Три модуля: network, compute, security
Разбивка получилась такая:
infra/
modules/
network/
main.tf
variables.tf
outputs.tf
compute/
main.tf
variables.tf
outputs.tf
security/
main.tf
variables.tf
outputs.tf
envs/
prod/
main.tf
terraform.tfvars
staging/
main.tf
terraform.tfvars
network - VPC, подсети, routing tables, internet gateway. Всё что относится к сетевой топологии. Модуль отдаёт наружу ID сети и подсетей через outputs.
security - security groups с правилами. Входящие правила для app-серверов, для БД, для bastion. Принимает на вход ID VPC, отдаёт ID групп.
compute - инстансы, load balancer, autoscaling если есть. Принимает ID подсетей и security groups, знает про ami и типы инстансов.
В envs/prod/main.tf это собирается:
module "network" {
source = "../../modules/network"
vpc_cidr = "${var.vpc_cidr}"
az_list = "${var.az_list}"
}
module "security" {
source = "../../modules/security"
vpc_id = "${module.network.vpc_id}"
}
module "compute" {
source = "../../modules/compute"
subnet_ids = "${module.network.private_subnet_ids}"
sg_app_id = "${module.security.sg_app_id}"
instance_type = "${var.instance_type}"
ami_id = "${var.ami_id}"
}
envs/staging/main.tf - тот же код, другой terraform.tfvars: меньший instance_type, одна AZ вместо трёх, другой CIDR. Модули не дублируются.
Data sources вместо хардкода
В 0.7 data sources стали заметно удобнее. Раньше AMI-ID приходилось хардкодить в переменных и обновлять руками при смене образа. Теперь:
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*"]
}
}
resource "aws_instance" "app" {
ami = "${data.aws_ami.ubuntu.id}"
instance_type = "${var.instance_type}"
}
AMI подбирается автоматически по фильтрам - свежий Ubuntu 16.04 в нужном регионе. Аналогично для поиска существующих ресурсов, созданных вне Terraform: data source позволяет получить VPC по тегу, не указывая его ID явно. Это критично при постепенной миграции, когда часть инфраструктуры ещё создана руками.
Remote state в S3
Это изменение дало больше всего. State в git - это:
- Мерж-конфликты при параллельной работе
- Чувствительные данные в репозитории (пароли RDS, ключи)
- Нет блокировки: два человека запускают
applyодновременно и получают рассинхрон state
S3-бэкенд решает первые два пункта, блокировку - через DynamoDB (или просто договорённостью, если DynamoDB лишняя зависимость):
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "prod/terraform.tfstate"
region = "eu-west-1"
}
}
State живёт в S3 с включённым versioning - история сохраняется, можно откатиться. Бакет закрыт от публичного доступа, доступ через IAM-роли. Sensitive данные в state остаются чувствительными, но хотя бы не в git.
Теперь у каждого окружения свой ключ в S3: prod/terraform.tfstate, staging/terraform.tfstate. Инженеры работают параллельно с разными окружениями без конфликтов.
Как это меняет workflow
С модулями и remote state команда из четырёх человек работает примерно так:
- Каждый берёт задачу в своём окружении или в своём модуле
terraform planпоказывает только изменения в рамках текущего apply - не весь state- PR в git содержит изменения
.tf-файлов, но не state - State не надо коммитить и не надо резолвить конфликты
Осталась одна проблема: блокировка. Если два человека делают apply в prod одновременно - state могут испортить. Пока решаем организационно: prod трогает один человек, с явным анонсом в чат. Это не идеально, но честнее чем делать вид что проблемы нет.
Что стало ощутимо лучше
Модульная структура - это в первую очередь про читаемость и зоны ответственности, а не про переиспользование само по себе. Когда нужно поправить правило файрвола, открываешь modules/security/ и работаешь там. Compute тебя не отвлекает. Plan показывает только security group changes.
Переход с монолита не мгновенный: пришлось разбить state, сделать terraform import для ресурсов которые уже существовали, проверить что зависимости между модулями передаются корректно. Заняло полдня. Но это разовая работа.
Data sources убрали несколько классов ошибок связанных с устаревшими захардкоженными ID. Это та вещь, которую начинаешь ценить когда один раз деплоишь в регион где старый AMI не существует.