[Перевод] Миграция с Terraform на Terragrunt
https://miro.medium.com/max/700/1*FnD69VqaPYSOaKzTUb_njw.jpeg
Введение
В Bestmile мы используем Terraform для AWS IaC. Но чем больше развивалась наша инфраструктура, тем запутаннее становился код Terraform.
Код Terraform стало сложнее обслуживать. Он терял эффективность.Terraform — отличный инструмент, но нуждается в дополнениях. Здесь-то и пригодится Terragrunt.
Terragrunt — это обертка (wrapper) для Terraform, которая расширяет его функционал и устраняет некоторые ограничения. Terragrunt взаимодействует с Terraform с помощью кода HCL (HashiCorp Configuration Language), поэтому Terragrunt будет выполнять код Terraform в зависимости от того, как вы определите код HCL. Именно он дает дополнительные преимущества, как описано ниже, и превращает Terragrunt в волшебный инструмент.
Чего очень не хватает в одном Terraform
В Terraform нет зависимостей между модулями: два модуля просто невозможно включить в цепочку зависимостей. В Terragrunt есть оператор dependencies, который позволяет указывать порядок для модулей.
Нет повторных попыток для известных ошибок: некоторые ошибки Terraform можно устранить, просто еще раз выполнив команду apply.
Невозможно развернуть одну и ту же версию модуля Terraform во всех окружениях, если, например, мы хотим внести изменение в модуль VPC Terraform. В идеале изменение должно деплоиться как артефакт с указанием версии и распространением по всем окружениям. Terragrunt решает эту задачу с помощью определенной ветки или тега для деплоя.
В Terraform невозможно соблюдать в конфигурациях принцип DRY во всех окружениях (бакет S3, регион, таблица DynamoDB и т. д.) — для каждого подкомпонента инфраструктуры (EKS, S3, IAM, MSK…) приходилось каждый раз переопределять бэкенд-конфигурацию Terraform. Чем больше подкомпонентов, тем больше работы для нашей команды SRE. Terragrunt решает эту проблему с помощью функции path_relative_to_include()
, которая помогает определить текущий каталог.
Когда кода Terraform становится очень много, его приходится упорядочивать по папкам. В Terraform просто нет возможности выполнять глобальную команду plan или apply для всех папок. В Terragrunt для этого есть plan-all и apply-all.
Комментарий к переводу.
Мне не раз приходилось рассказывать людям, что такое Терраформ и как его использовать. И каждый раз речь заходит про два подхода к описанию инфраструктуры, оба из которых неудачные: разделение кода на папки с большим количеством повторений или хранение кода в одной папке с сложностями в деплое. Террагрант идеально решает обе проблемы, закрывая главные недостатки Терраформа и экономит большое количество времени на создание и поддержку инфраструктуры. Павел Замошин, автор курсов Слёрм по Terraform, Site Reliability Engineer в Malwarebytes.
https://xkcd.com/303/
Процесс миграции
Структурные изменения кода для миграции из Terraform в Terragrunt
Для миграции Terragrunt не требуется серьезно переделывать существующий код Terraform, но прежде чем начать, нужно разобраться, как выглядит репозиторий Terraform:
$ tree
.
├── dev
│ ├── efs.tf
│ ├── eks.tf
...omitted for brevity...
│ ├── vpc.tf
├── mgmt
│ ├── eks.tf
│ ├── mgmt.tf
│ ├── outputs.tf
├── modules
│ ├── efs
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variables.tf
│ ├── eks
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variables.tf
...omitted for brevity...
│ ├── vpc
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variables.tf
├── prod
│ ├── efs.tf
│ ├── eks.tf
...omitted for brevity...
│ ├── vpc.tf
├── staging
│ ├── efs.tf
│ ├── eks.tf
...omitted for brevity...
│ ├── vpc.tf
Этот код мы долго использовали для выполнения инфраструктуры AWS.
После миграции на Terragrunt репозиторий выглядит так:
$ tree
.
├── README.md
├── atlantis.yaml
├── live
│ ├── account.hcl
│ └── us-east-1
│ ├── dev
│ │ ├── efs
│ │ │ └── terragrunt.hcl
│ │ ├── eks
│ │ │ └── terragrunt.hcl
│ │ ├── env.hcl
...omitted for brevity...
│ │ ├── vpc
│ │ │ └── terragrunt.hcl
│ ├── mgmt
│ │ ├── env.hcl
│ │ ├── vpc
│ │ │ └── terragrunt.hcl
│ ├── prod
│ │ ├── efs
│ │ │ └── terragrunt.hcl
│ │ ├── eks
│ │ │ └── terragrunt.hcl
│ │ ├── env.hcl
...omitted for brevity...
│ │ ├── vpc
│ │ │ └── terragrunt.hcl
│ ├── region.hcl
│ └── staging
│ │ ├── efs
│ │ │ └── terragrunt.hcl
│ │ ├── eks
│ │ │ └── terragrunt.hcl
│ │ ├── env.hcl
...omitted for brevity...
│ │ ├── vpc
│ │ │ └── terragrunt.hcl
└── terragrunt.hcl
Пример Terragrunt
Мы покажем, как реализовать функционал VPC (или любой другой компонент AWS) с помощью Terragrunt. Он состоит из двух частей: модуль Terraform и код Terragrunt.
Официальный модуль Terraform можно найти здесь. Но это может быть и кастомный модуль.
Код Terragrunt тоже состоит из двух частей:
./terragrunt.hcl
./live/account.hcl
./live/us-east-1/region.hcl
./live/us-east-1/dev/env.hcl
./live/us-east-1/prod/env.hcl
Где ./live/us-east-1/dev/dev.hcl
выглядит так:
# Set common variables for the environment.
# This is automatically pulled in in the root terragrunt.hcl configuration to
# feed forward to the child modules.
locals {
dev_region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))
# VPC VARIABLES
vpc_private_subnets_range = local.dev_region_vars.locals.dev_vpc_private_subnets_range
vpc_public_subnets_range = local.dev_region_vars.locals.dev_vpc_public_subnets_range
vpc_azs = local.dev_region_vars.locals.dev_vpc_azs
vpc_cidr = local.dev_region_vars.locals.dev_vpc_cidr
...ommited for brevity...
}
А ./live/us-east-1/region.hcl
выглядит так:
# Set common variables for the region.
# This is automatically pulled in in the root terragrunt.hcl configuration to
# configure the remote state bucket and pass forward to the child modules as inputs.
locals {
aws_region = "us-east-1"
# DEV VPC
dev_vpc_private_subnets_range = ["10.10.1.0/24", "10.10.2.0/24",
"10.10.3.0/24"]
dev_vpc_public_subnets_range = ["10.10.101.0/24",
"10.10.102.0/24", "10.10.103.0/24"]
dev_vpc_azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
dev_vpc_cidr = "10.10.0.0/16"
...ommited for brevity...
}
Сам код Terragrunt, который находится в ./live/us-east-1/dev/vpc/terragrunt.hcl
:
locals {
# Load environment-wide variables
environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
# Extract needed variables for reuse
env = local.environment_vars.locals.environment
private_subnets_range = local.environment_vars.locals.vpc_private_subnets_range
public_subnets_range = local.environment_vars.locals.vpc_public_subnets_range
azs = local.environment_vars.locals.vpc_azs
cidr = local.environment_vars.locals.vpc_cidr
}
# Terragrunt will copy the Terraform configurations specified by the source parameter, along with any files in the
# working directory, into a temporary folder, and execute your Terraform commands in that folder.
# If the terraform module is in the root directory make sure to set the `//` before the branch or version.
terraform {
source = "git::git@your_git_repo.com:your_team/terragrunt-module-vpc.git//?ref=v1.0"
}
# Include all settings from the root terragrunt.hcl file
include {
path = find_in_parent_folders()
}
# These are the variables we have to pass in to use the module specified in the terragrunt configuration above
inputs = {
env = "${local.env}"
private_subnets_range = "${local.private_subnets_range}"
public_subnets_range = "${local.public_subnets_range}"
azs = "${local.azs}"
cidr = "${local.cidr}"
}
Импорт ресурсов из Terraform в Terragrunt
Импортировать ресурсы из текущего стейта Terraform в новый стейт Terragrunt нужно в два этапа:
Вывести текущий стейт Terraform:
$ terraform state list | grep vpc
...ommited for brevity...
module.dev_vpc.module.vpc.aws_vpc.this[0]
...ommited for brevity...
$ terraform state show module.dev_vpc.module.vpc.aws_vpc.this\[0\]
# module.dev_vpc.module.vpc.aws_vpc.this[0]:
resource "aws_vpc" "this" {
arn = "arn:aws:ec2:us-east-
1:YOUR_ACCOUNT_ID:vpc/vpc-0123456789"
...ommited for brevity...
id = "vpc-0123456789"
...ommited for brevity...
}
Импортировать стейт в Terragrunt:
$ terragrunt import module.vpc.aws_vpc.this\[0\] vpc-0123456789
В этом примере мы используем VPC ID, чтобы импортировать ресурс VPC в Terragrunt. У других компонентов AWS могут быть свои идентификаторы. Например, если мы импортируем правило группы безопасности, идентификатор будет более сложным.
Рабочий процесс Atlantis
Мы используем Atlantis для автоматизации пул-реквестов. При этом с каждым пул-реквестом мы получаем план Terraform в самом запросе, и это серьезно упрощает совместную работу. Это, кстати, далеко не все преимущества Atlantis.
Примечание. Использовать Atlantis необязательно. Работать с Terraform и Terragrunt можно и без него, но он такой полезный, что мы не могли не упомянуть о нем.
Настроить Atlantis для Terragrunt можно в три этапа.
Настройка Terragrunt с Atlantis
Создаем образ Docker с пакетом Terragrunt
FROM runatlantis/atlantis:latest
# Terragrunt version
ARG TERRAGRUNT
ADD https://github.com/gruntwork-io/terragrunt/releases/download/${TERRAGRUNT}/terragrunt_linux_amd64
/usr/local/bin/terragrunt
RUN chmod +x /usr/local/bin/terragrunt
Деплоим Atlantis
Нужно соответствующим образом изменить значения в деплое Atlantis. Мы деплоим сервисы с Helm.
$ helm inspect values stable/atlantis > atlantis-values.yaml
...
edit your file with the proper values
...
$ helm install -f atlantis-values.yaml atlantis
Конфигурируем atlantis.yaml в репозитории Terragrunt и добавляем конкретный рабочий процесс Atlantis
Мы использовали этот чудесный инструмент, чтобы создать конфигурацию Atlantis автоматически:
$ terragrunt-atlantis-config generate --autoplan --parallel=false --
workflow terragrunt --root ./ --output ./atlantis.yaml
$ cat atlantis.yaml
version: 3
automerge: false
parallel_apply: false
parallel_plan: false
projects:
- autoplan:
enabled: true
when_modified:
- '*.hcl'
- '*.tf*'
dir: ./live/us-east-1/dev/vpc
workflow: terragrunt
workflows:
terragrunt:
plan:
steps:
- run: /usr/local/bin/terragrunt plan -no-color -out $PLANFILE
apply:
steps:
- run: /usr/local/bin/terragrunt apply -no-color $PLANFILE
Как видно в выходных данных файла atlantis.yaml
, мы используем конкретный рабочий процесс, чтобы Atlantis выполнял команды Terragrunt. Мы адаптировали рабочий процесс под свои потребности. В документации по Atlantis есть и другие примеры.
Рабочий процесс для совместной работы в Atlantis
Раньше…
Мы выполняли команды Terraform plan и apply локально. Это было неудобно, потому что коллеги не видели, что происходит. Кроме простого пул-реквеста. Выходные данные приходилось отправлять по электронной почте или в Slack, чтобы команда проверила изменения и могла работать дальше.
Это было очень утомительно и ненадежно.
Сейчас…
Открыв для себя магию Atlantis, мы серьезно улучшили рабочий процесс. Никаких больше имейлов с сотнями строк или бесконечных сниппетов в Slack.
Теперь все гораздо проще и приятнее.
С Atlantis каждая команда plan или apply комментируется в пул-реквесте. Вот как теперь выглядит у нас процесс совместной работы:
https://miro.medium.com/max/700/1*f5dHQD0avGNou_aT37WFUg.png
Заминки при миграции
Импорт стейта некоторых ресурсов в Terragrunt не обошелся без проблем, но благодаря активному сообществу Terragrunt нам удалось их решить. Ниже описаны проблемы, с которыми мы столкнулись.
Группы безопасности
Сначала мы использовали кастомный модуль для aws_security_group
, и это привело к проблемам при импорте. Terragrunt/Terraform автоматически создает aws_security_group_rule
, если это правило еще не определено, как описано в этой документации. Чтобы обойти эту проблему, после импорта ресурса группы безопасности мы вручную удалили каждое автоматически созданное правило aws_security_group_rule
.
$ terragrunt state rm module.sg.aws_security_group_rule.workers-1\[0\]
$ terragrunt state rm module.sg.aws_security_group_rule.workers-1\[1\]
$ terragrunt state rm module.sg.aws_security_group_rule.workers-1\[2\]
...
В итоге мы изменили подход к подготовке групп безопасности и стали использовать для этого официальный модуль Terraform, получив больше возможностей.
Повторное создание всех импортированных ресурсов
В зависимости от используемого модуля, Terragrunt иногда норовит создать все ресурсы снова, даже если все прекрасно импортировалось. В итоге нам пришлось изучить оба удаленных стейта (Terragrunt и Terraform). Для начала нужно получить их:
Terragrunt:
$ cd /src/bestmile/terragrunt/live/us-east-1/dev/some_module
$ terragrunt state pull > state.json
Terraform:
$ cd /src/bestmile/terraform/some_module
$ terraform state pull > state.json
Мы увидели, что у некоторых импортированных ресурсов не был задан name_prefix
в стейте Terragrunt.
Все так просто!
Всего лишь надо было задать значение name_prefix
в соответствии со стейтом Terraform.
Мы указали эти переменные в стейте Terragrunt, и нужно было отправить стейт обратно в бэкенд Terragrunt (в нашем случае это были бакеты S3), чтобы изменения были учтены при следующем выполнении команды terragrunt plan.
terragrunt state push state.json
К сожалению, отправка стейта Terragrunt привела к еще одной проблемке.
Не удается отправить стейт Terragrunt
В стейте Terragrunt есть порядковый номер. Увеличьте его на 1, если не можете отправить измененный стейт Terragrunt. (источник)
Сложный импорт не по ID
Чтобы упростить себе жизнь, мы разработали скрипт на Python, который должен искать команду импорта в документации Terraform.
Вот что мы получили:
$ python tg_import.py -e dev -r vpc
terragrunt import module.vpc.aws_eip.nat[0] eip-nat-id-1234567890
terragrunt import module.vpc.aws_internet_gateway.this[0] igw-id-1234567890
terragrunt import module.vpc.aws_nat_gateway.this[0] nat-gw-1234567890
terragrunt import module.vpc.aws_route.private_nat_gateway[0] rt-pv-1234567890
terragrunt import module.vpc.aws_route.public_internet_gateway[0] rt-pub-1234567890
terragrunt import module.vpc.aws_route_table.private[0] rt-tb-pv-1234567890
terragrunt import module.vpc.aws_route_table.public[0] rt-tb-pub-1234567890
terragrunt import module.vpc.aws_route_table_association.private[0] tassoc-pv-1234567890
terragrunt import module.vpc.aws_route_table_association.public[0] tassoc-pub-1234567890
terragrunt import module.vpc.aws_subnet.private[0] subnet-1234567890
terragrunt import module.vpc.aws_subnet.public[0] subnet-9876543210
terragrunt import module.vpc.aws_vpc.this[0] vpc-1234567890
Заключение
Для миграции в Terragrunt нам понадобилось немало сил и времени. Игра стоила свеч?
ДА!
Наши усилия по миграции из Terraform в Terragrunt были направлены на перенос существующего стейта Terraform в стейт Terragrunt.
Одно из главных преимуществ — мы улучшили модули, с помощью которых развертывали инфраструктуру. Код инфраструктуры сократился, потому что с Terragrunt мы причесали базу кода по принципам DRY.
Больше всего наш репозиторий git нравится нам за структуру папок. С таким многоуровневым подходом мы навели в инфраструктуре настоящий порядок. Любой разработчик, даже если он незнаком с Terragrunt, легко разберется в уровнях инфраструктуры Bestmile.
Отдельная радость — управление версиями. Когда нужно что-то изменить в инфраструктуре, мы следуем стандартному процессу наката. Сначала мы отправляем версию в среду разработки, а затем продвигаем ее дальше вплоть до продакшена.