Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Модули Terraform: Основы

При описании инфраструктуры в Terraform для различных проектов можно заметить, что определенные комбинации ресурсов часто повторяются. Например, когда мы создаем кластер баз данных, мы обычно создаем вместе с ним и базу пользователей. А когда мы создаем несколько серверов, мы часто хотим объединить их в одну виртуальную сеть.

Terraform позволяет описывать наборы взаимосвязанных ресурсов как модули. Модули Terraform близки по своей идее к импортируемым ролям в Ansible. Мы можем подключать их в свой проект, передавать им параметры и на выходе получать комплексное инфраструктурное решение.

В этом уроке мы разберемся, зачем нужны модули Terraform и как их применять.

Какую проблему решают модули

Модули помогают отвлечься от особенностей описания ресурсов и зависимостей конкретного облака. Мы импортируем модуль, передаем в него параметры, а дальше уже сам модуль создает все необходимые ресурсы.

Так модуль с хорошей документацией экономит время, которое мы тратим на изучение ресурсов и зависимостей конкретного провайдера. Модуль помогает быстрее добиться нужного результата.

Возьмем для примера модуль terraform-yc-postgresql. Он описывает создание в облаке Yandex кластера БД PostgreSQL, а также помещение в него баз данных и пользователей.

Ранее мы описывали кластер, его базу и пользователей с помощью обычных ресурсов. Попробуем сделать то же, но с помощью модуля.

Подключаем в инфраструктуру модуль

Возьмем пример описания модуля для простого single-node кластера, уберем лишние настройки и подправим некоторые поля:

module "yandex-postgresql" {
  source      = "github.com/terraform-yc-modules/terraform-yc-postgresql?ref=1.0.2"
  network_id  = yandex_vpc_network.net.id
  name        = "tfhexlet"
  description = "Single-node PostgreSQL cluster for test purposes"

  hosts_definition = [
    {
      zone             = "ru-central1-a"
      assign_public_ip = false
      subnet_id        = yandex_vpc_subnet.subnet.id
    }
  ]

  postgresql_config = {
    max_connections = 100
  }

  databases = [
    {
      name       = "hexlet"
      owner      = var.db_user
      lc_collate = "ru_RU.UTF-8"
      lc_type    = "ru_RU.UTF-8"
      extensions = ["uuid-ossp", "xml2"]
    },
    {
      name       = "hexlet-test"
      owner      = var.db_user
      lc_collate = "ru_RU.UTF-8"
      lc_type    = "ru_RU.UTF-8"
      extensions = ["uuid-ossp", "xml2"]
    }
  ]

  owners = [
    {
      name       = var.db_user
      conn_limit = 15
    }
  ]

  users = [
    {
      name        = "guest"
      conn_limit  = 30
      permissions = ["hexlet"]
      settings = {
        pool_mode                   = "transaction"
        prepared_statements_pooling = true
      }
    }
  ]
}

Модуль обозначается ключевым словом module. Далее мы даем модулю имя как обычным ресурсам Terraform. Мы назовем модуль yandex-postgresql. Так будет понятно, что он делает.

В обязательном для модуля поле source мы указали путь к коду модуля на Github с указанием используемой версии в ref. Так можно ссылаться на любые модули, которые хранятся на Github, в том числе на свои.

В network_id и subnet_id мы добавили ссылки на ресурсы сети и подсети, которые создаем в том же проекте. Также добавили в кластер вторую базу hexlet-test, чтобы проверить, как модуль работает с несколькими ресурсами. Еще добавили дополнительного пользователя guest.

Вместо того чтобы описывать пять отдельных ресурсов Terraform, мы компактно описали все взаимозависимые ресурсы в одном модуле.

Мы технически можем добавить еще один модуль yandex-postgresql-2 с тем же source, описать в нем другой набор параметров и создать два полноценных кластера БД со всем содержимым в одном проекте. Так с помощью модулей мы можем управлять инфраструктурой на более высоком уровне абстракции.

Выполним terraform init, чтобы загрузить модуль:

Initializing modules...

Initializing the backend...

Initializing provider plugins...
- Finding yandex-cloud/yandex versions matching ">= 0.89.0"...
- Finding hashicorp/local versions matching "> 2.2.0"...
- Finding hashicorp/random versions matching "> 3.3.0"...
- Installing yandex-cloud/yandex v0.97.0...
- Installed yandex-cloud/yandex v0.97.0 (unauthenticated)
- Installing hashicorp/local v2.4.0...
- Installed hashicorp/local v2.4.0 (unauthenticated)
- Installing hashicorp/random v3.5.1...
- Installed hashicorp/random v3.5.1 (unauthenticated)

...

Terraform has been successfully initialized!

Помимо провайдера yandex-cloud дополнительно установились провайдеры local и random. Это произошло, так как наш модуль terraform-yc-postgresql использует их, чтобы генерировать случайные пароли и сохранять их в файлы.

Если инициация прошла успешно, мы сможем увидеть код используемого модуля в рабочей папке .terraform/modules внутри проекта. Заглянем внутрь и посмотрим, из чего состоит модуль.

Знакомимся со структурой модуля

Модуль по сути является вложенным проектом Terraform, который содержит все стандартные компоненты: источники данных, ресурсы, переменные, outputs. Модуль принимает из проекта значения переменных и использует их, чтобы параметризировать ресурсы.

Главное отличие модуля от проекта — отсутствие блока provider, поскольку модуль не предназначен для самостоятельного использования. При этом в модуле есть раздел required_providers, который определяет, какие версии провайдеров понадобится доустановить при подключении модуля в проект.

Модуль Terraform может храниться в обычном git-репозитории и версионироваться с помощью тегов git либо использовать для хранения специальный инфраструктурный registry, реализация которого есть, к примеру, в Gitlab.

Сейчас внутри модуля нас интересуют outputs. Модуль создаст кластер, базы, пользователей и сгенерит для них пароли. Нам нужно извлечь эти данные, чтобы передать их ресурсу виртуальной машины.

Обращаемся к полям модуля

Заглянем в outputs.tf подключенного модуля:

output "cluster_fqdns_list" {
  description = "PostgreSQL cluster nodes FQDN list"
  value       = [yandex_mdb_postgresql_cluster.this.host[*].fqdn]
}

output "owners_data" {
  description = "List of owners with passwords."
  sensitive   = true
  value = [
    for u in yandex_mdb_postgresql_user.owner : {
      user     = u.name
      password = u.password
    }
  ]
}

...

output "databases" {
  description = "List of databases names."
  value       = [for db in var.databases : db.name]
}

В cluster_fqdns_list мы сможем прочитать хост, который раньше получали из поля ресурса кластера yandex_mdb_postgresql_cluster.dbcluster.host.0.fqdn. В owners во вложенной структуре хранятся пользователи и пароли БД, в databases — список баз данных.

К полям модулей можно обращаться так же, как к полям ресурсов. Outputs являются полями самого модуля, поэтому мы в общем виде можем получить их значения, если пропишем module.<modulename>.<outputname>.

Пропишем в ресурсе виртуальной машины значения, которые возвращает модуль:

resource "yandex_compute_instance" "vm" {
  ...

  metadata = {
    user-data = <<-EOF
    #!/bin/bash
    #echo 'export DB_HOST="${module.yandex-postgresql.cluster_fqdns_list[0].0}"' >> /etc/environment
    EOF
    ssh-keys  = "ubuntu:${file("~/.ssh/id_rsa.pub")}"
  }

  ...

  provisioner "remote-exec" {
    inline = [
      <<EOT
sudo docker run -d -p 0.0.0.0:80:3000 \
  -e DB_TYPE=postgres \
  -e DB_NAME=${module.yandex-postgresql.databases[0]} \
  -e DB_HOST=${module.yandex-postgresql.cluster_fqdns_list[0].0} \
  -e DB_PORT=6432 \
  -e DB_USER=${module.yandex-postgresql.owners_data[0].user} \
  -e DB_PASS=${module.yandex-postgresql.owners_data[0].password} \
  ghcr.io/requarks/wiki:2.5
EOT
    ]
  }
  ...

Поскольку outputs в этом модуле представляют собой списки, в ресурсе vm мы обращаемся к отдельным элементам списка, чтобы извлечь строковые значения:

  • Берем module.yandex-postgresql.cluster_fqdns_list[0].0, чтобы извлечь строку с хостом кластера
  • Берем module.yandex-postgresql.databases[0], чтобы извлечь имя первой базы данных, созданной в модуле — это будет hexlet
  • Берем module.yandex-postgresql.owners_data[0].user и module.yandex-postgresql.owners_data[0].password, чтобы получить у первого и единственного owner-пользователя имя и пароль для подключения к БД

Также установим зависимость, чтобы виртуальная машина не создавалась до тех пор, пока модуль не развернет кластер БД:

  ...
  depends_on = [module.yandex-postgresql]
}

На этом наша инфраструктура готова к запуску.

Запускаем инфраструктуру

Попробуем запустить terraform apply и посмотрим, как Terraform обработает запрос на создание инфраструктуры с подключенным модулем:

# module.yandex-postgresql.random_password.password["guest"] will be created
+ resource "random_password" "password" {
    ...
  }

# module.yandex-postgresql.random_password.password["me"] will be created
+ resource "random_password" "password" {
    ...
  }

# module.yandex-postgresql.yandex_mdb_postgresql_cluster.this will be created
+ resource "yandex_mdb_postgresql_cluster" "this" {
    ...
  }

На этапе подтверждения видим, что Terraform трансформирует параметры, переданные модулю в отдельные ресурсы, и присваивает им имена вида module.yandex-postgresql.*. Когда мы изменяем параметры, передаваемые модулю, мы будем изменять ресурсы, которые стоят за ним.

Применим изменения и понаблюдаем, как создается инфраструктура:

module.yandex-postgresql.random_password.password["guest"]: Creating...
module.yandex-postgresql.random_password.password["me"]: Creating...
module.yandex-postgresql.random_password.password["me"]: Creation complete after 0s [id=none]
module.yandex-postgresql.random_password.password["guest"]: Creation complete after 0s [id=none]
yandex_vpc_network.net: Creating...
yandex_vpc_network.net: Creation complete after 0s [id=enpn9tfh3hd8s8hara9m]
yandex_vpc_subnet.subnet: Creating...
yandex_vpc_subnet.subnet: Creation complete after 1s [id=e9bk60o99g5i2r01ia47]
module.yandex-postgresql.yandex_mdb_postgresql_cluster.this: Creating...
module.yandex-postgresql.yandex_mdb_postgresql_cluster.this: Still creating... [10s elapsed]
...
module.yandex-postgresql.yandex_mdb_postgresql_cluster.this: Creation complete after 6m3s [id=c9q7s2022m4rife6o7bt]
module.yandex-postgresql.yandex_mdb_postgresql_user.owner["me"]: Creation complete after 37s [id=c9q7s2022m4rife6o7bt:me]
module.yandex-postgresql.yandex_mdb_postgresql_database.database["hexlet"]: Creating...
module.yandex-postgresql.yandex_mdb_postgresql_database.database["hexlet-test"]: Creating...
module.yandex-postgresql.yandex_mdb_postgresql_database.database["hexlet-test"]: Creation complete after 40s [id=c9q7s2022m4rife6o7bt:hexlet-test]
module.yandex-postgresql.yandex_mdb_postgresql_database.database["hexlet"]: Creation complete after 1m16s [id=c9q7s2022m4rife6o7bt:hexlet]
module.yandex-postgresql.yandex_mdb_postgresql_user.user["guest"]: Creating...
module.yandex-postgresql.yandex_mdb_postgresql_user.user["guest"]: Creation complete after 54s [id=c9q7s2022m4rife6o7bt:guest]
yandex_compute_instance.vm: Creating...
yandex_compute_instance.vm: Provisioning with 'remote-exec'...
yandex_compute_instance.vm (remote-exec): (output suppressed due to sensitive value in config)
yandex_compute_instance.vm: Creation complete after 1m27s [id=fhm60f9q8b2ach07atnm]

При использовании логики модуля Terraform сгенерировал случайные пароли для пользователей с помощью провайдера random, создал кластер PostgreSQL, добавил в него базы и пользователей. Лог работы провижнера, создающего контейнер с приложением, оказался скрыт, так как в нем применялись сенситивные переменные.

Выводы

Модуль Terraform — это инструмент для описания комплексных инфраструктурных решений, которые состоят из нескольких взаимосвязанных ресурсов. Модуль можно подключить в свой проект, передать ему набор переменных, и Terraform создаст новые ресурсы на основе логики модуля.

Модули упрощают описание сложных инфраструктурных решений и позволяют переиспользовать инфраструктурный код в разных проектах.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

Задавайте вопросы, если хотите обсудить теорию или упражнения. Команда поддержки Хекслета и опытные участники сообщества помогут найти ответы и решить задачу