Pull to refresh
68.43
Слёрм
Учебный центр для тех, кто работает в IT

Миграция с Terraform на Terragrunt

Reading time9 min
Views17K
Original author: F. Piva, co-authored with Marius Milea
https://miro.medium.com/max/700/1*FnD69VqaPYSOaKzTUb_njw.jpeg
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/
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
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.

Отдельная радость — управление версиями. Когда нужно что-то изменить в инфраструктуре, мы следуем стандартному процессу наката. Сначала мы отправляем версию в среду разработки, а затем продвигаем ее дальше вплоть до продакшена.

Tags:
Hubs:
Total votes 7: ↑7 and ↓0+7
Comments6

Articles

Information

Website
slurm.io
Registered
Founded
Employees
51–100 employees
Location
Россия
Representative
Антон Скобин