Как стать автором
Обновить
1096.96
OTUS
Цифровые навыки от ведущих экспертов

Паттерны модулей Terraform

Время на прочтение8 мин
Количество просмотров5.4K
Автор оригинала: Aaron Kalair

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

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

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

Конфигурирование ресурсов AWS с настройками по умолчанию для конкретной компании

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

В связи с этим мы создаем модули для конкретной компании, которые оборачивают ресурсы AWS и устанавливают значения по умолчанию, которые нам подходят.

Например, мы всегда хотим, чтобы RDS storage_encrypted_enabled и S3 Public Access были отключены.

Поэтому мы создаем модули типа company_name_s3_bucket, которые конфигурируют S3-бакет с обязательными аргументами компании, а затем позволяют настраивать другие аргументы с помощью входных переменных.

Тогда любой, кому нужен S3-бакет, будет использовать  модуль company_name_s3_bucket вместо ресурса Terraform s3_bucket от провайдера AWS, и нам никогда не придется беспокоиться о людях, которые забывают установить разумные значения по умолчанию.

Автоматическое создание ресурсов AWS более низкого уровня

Функционирование одних ресурсов AWS зависит от других.

Например, вы не можете создать экземпляр RDS без aws_db_subnets, поэтому наш модуль company_name_mysql создает aws_db_subnets, которые затем передаются в aws_database_instance.

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

Группировка ресурсов AWS, необходимых для создания функциональных сервисов

У нас есть модули Terraform, которые объединяют несколько ресурсов AWS и другие модули Terraform для создания функциональных сервисов, необходимых нашим приложениям, одним из примеров которых является кластер MongoDB.

Наш модуль company_name_mongodb_cluster организован следующим образом:

company_name_mongodb_cluster
  -> mongodb_node_a -> company_name_mongo_node_module
  -> mongodb_node_b -> company_name_mongo_node_module
  -> mongodb_node_c -> company_name_mongo_node_module
  -> cluster_security_group -> aws_security_group_resource
  -> cluster_backup_schedule -> aws_dlm_lifecycle_policy_resouce
  -> cluster_iam_policy -> aws_iam_policy_resource
  -> cluster_iam_role -> aws_iam_role

С созданием модуля company_name_mongo_node_module:

company_name_mongo_node_module
  -> cloudinit_data -> template_cloudinit_config_resource
  -> ebs_volume -> aws_ebs_volume_resource
  -> database_instance -> aws_instance_resource
  -> dns_entry -> aws_route53_record_resource

Здесь вложены два модуля, которые мы написали сами.

Модуль company_name_mongo_node_module позволяет нам последовательно создавать EC2 инстансы для запуска mongod без необходимости копирования и вставки.

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

Пользователь company_name_mongodb_cluster просто передает несколько необходимых параметров и получает кластер MongoDB, настроенный в соответствии с нашими требованиями по доступности (3 узла), безопасности (соответствующая IAM роль и Security Group, изолирующая кластер) и бэкапу с помощью DataLifeCycleManager.

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

Как глубоко вложить модули

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

Насколько мелкими и специфичными должны быть ваши модули и стоит ли создавать длинные цепочки модулей с многократно используемыми компонентами?

Например, представьте:

networking_module ->
  vpc_module ->
    subnet_module ->
      route_table_module

Поначалу это кажется достаточно симпатичным.

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

Если вы хотите обновить модуль route_table_module, нужно сделать бамп-версию каждого родительского модуля (их 3), чтобы опубликовать это изменение.

Если вы хотите передать специфические параметры route_table в модуль route_table_module, придется сделать это через множество других модулей, несмотря на то, что им они не нужны.

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

Кроме того, это предполагает совершенный мир, где каждая среда является идентичной и нуждается во всех ресурсах.

На практике это не так, поэтому модуль принимает переменные типа enable_subnet_b, которые передаются в оператор count, например, для конкретного ресурса, который мы хотим создать только в определенных средах:

count = var.enable_subnet_b ? 1 : 0

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

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

Где хранить модули

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

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

environments/
  dev/
    vpc.tf
  prod/
  modules/
    vpc/
    mongo/

И ссылайтесь на модули с помощью:

source = "../modules/vpc"

Это хорошо работает, когда вы только начинаете.

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

Поскольку все модули неверсионированы и находятся в одном Git-репо, невозможно, чтобы модули в prod/ использовали другую версию модуля, чем в dev/.

Конечно, можно использовать -ignore или сказать людям ничего не запускать в prod/, пока вы тестируете изменения, но это — случайное событие, которое только и ждет, чтобы произойти.

Поэтому вы захотите перенести свои модули в собственные Git-репозитории.

После того как они будут разделены, у вас будет два способа их выпустить .

Git Refs

Вы можете использовать Git refs в поле source для ссылки на модуль, например.

source = "github.com/hashicorp/example?ref=<BRANCH OR TAG>"

Если вы протегируете каждый мерджинг в master с номером версии, то сможете легко контролировать, какая версия модуля будет развернута в каждой среде.

Это работает даже если ваши модули находятся в приватном git-репозитории, системе, запускающей Terraform, просто нужен доступ к этому репозиторию с помощью SSH-ключа или имени пользователя и пароля.

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

Реестр модулей Terraform

Реестр модулей Terraform может импортировать модули из Github и позволяет вам ссылаться на них, как:

source = "app.terraform.io/<YOUR COMPANY>/<MODULE>/<NAMESPACE>"
version = "~> 1.1.0"

Он является бесплатным для 5 пользователей и предусматривает платные тарифные планы для дополнительных пользователей.

Самым большим преимуществом реестра модулей Terraform является нечеткая блокировка версий, например, ~> 1.1.0 означает последнюю минорную версию из дерева 1.1.

Таким образом, без изменения кода вы можете запустить terraform init -upgrade и извлечь все выпуски с исправлениями из реестра модулей.

Изначально мы перешли от ссылок Git к реестру модулей для этой нечёткой блокировки версий, но в итоге оказалось, что она используется не так часто, поэтому было решено вручную контролировать все бамп-версии.

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

Важно помнить, что при такой свободной блокировке версий каждому необходимо выполнить terraform init -upgrade в одно и то же время, чтобы убедиться, что все используют одинаковые версии модулей, и кто-то другой, выполнив команду terraform apply, не отменит изменения.

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

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

Наконец, стоит отметить, что если вы планируете использовать Terragrunt, то он не поддерживает реестр модулей Terraform.

Bash-скрипты как модули

Terraform поддерживает ресурсы данных, которые могут быть самыми разными, такими как AMI или EC2 Instance UserData.

С помощью этого можно делать интересные вещи, например, создавать модули для UserData, которые необходимо запускать как часть процесса Cloudinit при появлении инстанса EC2.

Например, если вам нужно запустить chef-клиент при появлении вашего инстанса EC2, можно создать модуль, определяющий образцовый bash-скрипт, такой как

… <assorted setup work>
chef-client -E ${environment} -N $node_name …

И вывести его из модуля в виде

output "file" {
  value = templatefile( 
    "${path.module}/chef.sh.tmpl", {
      environment = var.environment
    }
  )
}

Затем каждый инстанс EC2, которому необходимо его запустить, может использовать его для создания своего template_cloudinit_config.

module "chef_userdata" {
  source      = "<MODULE_SOURCE>"
  environment = "var.environment"
}
data "template_cloudinit_config" "service" {
  part {
    filename     = "chef.cfg"
    content_type = "text/cloud-config"
    content      = module.chef_userdata.file
  }
}

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

Различные специальные ресурсы для приложений

Помимо больших ресурсов, таких как базы данных и кэши, вашим приложениям может понадобиться множество "мелких" вещей, таких как IAM Roles, SSL-сертификаты или случайные строки для секретов.

Вы можете просто определить их как отдельные ресурсы в файлах app.tf внутри среды Terraform, и применить их terraform apply, например.

resource "aws_acm_certificate" "app" {
  ...
}
resource "random_password" "password" {
  length = 64
}
resource "aws_secretsmanager_secret" "secret" {
  name = "password"
}
resource "aws_secretsmanager_secret_version" "secret" {
  secret_id     = aws_secretsmanager_secret.secret.id
  secret_string = random_password.password.result
}
resource "aws_iam_policy" ""{
  ...
}

Но теперь, когда вы хотите создать те же ресурсы для своего приложения, скажем, в prod, вам придется копипастить их все.

Если вы измените любой из них в dev, следует помнить, что нужно точно воспроизвести изменения в prod.

Наконец, если вы используете Terragrunt, вы не можете просто определять случайные ресурсы на верхнем уровне подобным образом.

Поэтому мы решили, что лучше использовать паттерн модулей app-resource.

При необходимости каждое приложение получает модуль под названием terraform-<BUSINESS_NAME>-app-resources-<APP_NAME>. Здесь вы размещаете все эти различные ресурсы, а затем инстанцируете модуль в своей среде terraform вместо определения всех отдельных ресурсов.

Связывание созданных в Terraform ресурсов с вашими приложениями

После того, как вы создали различные ресурсы AWS в Terraform, вероятно, вашим приложениям потребуется что-то о них знать, например имена S3-бакетов, DNS-адреса баз данных и т. д.

Вы можете вручную скопировать и вставить их в файл настроек вашего приложения или воспользоваться возможностью Terraform создавать записи в AWS Parameter Store и Secrets Manager, чтобы сократить ручной процесс.

Например, наш модуль Database создает записи Parameter Store для DNS-адреса базы данных:

resource “aws_ssm_parameter” “dns_address” {
  name  = /dev/app-a/database_address
  type  = “String”
  value = database_module.outputs.dns_address
}

Затем вы можете либо использовать что-то вроде ExternalSecrets для получения этих данных и ввода их в под K8s, либо поручить вашему приложению самому обращаться к API AWS для поиска значений.


Материал подготовлен в рамках курса «Инфраструктурная платформа на основе Kubernetes».

Всех желающих приглашаем на бесплатное demo-занятие «Контейнерная оркестрация с плавным переходом к k8s». На занятии мы поговорим о том, как было до оркестрации, какие проблемы пытались решить, что сейчас: обзор оркестраторов, плавный переход к k8s. На уроке расскажем основные компоненты k8s и их связь, варианты локального развертывания k8s, а также варианты развертывания на виртуальных машинах или baremetall.
>> РЕГИСТРАЦИЯ

Теги:
Хабы:
Всего голосов 6: ↑5 и ↓1+4
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS