Файловые пути
В главе 3 вы поместили скрипт пользовательских данных для кластера веб-серверов во внешний файл, user-data.sh, и применили встроенную функцию file, чтобы прочитать его с диска. Неочевидный момент функции file состоит в том, что файловый путь, который она использует, должен быть относительным (поскольку Terraform можно запускать на множестве разных компьютеров) — но относительно чего?
По умолчанию Terraform интерпретирует этот путь относительно текущей рабочей папки. Это не вызывает проблем, если вы используете функцию file в конфигурационном файле Terraform в той же папке, из которой выполняется команда terraformapply (то есть если функция file применяется в корневом модуле). Но если сделать то же самое в модуле, размещенном в отдельной папке, ничего не будет работать.
Решить эту проблему можно с помощью выражения, известного как «ссылка на путь», которое имеет вид path.
• path.module — возвращает путь к модулю, в котором определено выражение.
• path.root — возвращает путь к корневому модулю.
• path.cwd — возвращает путь к текущей рабочей папке. При нормальном использовании Terraform это значение совпадает с path.root, но в некоторых нестандартных случаях Terraform запускается не из папки корневого модуля, что приводит к расхождению этих путей.
Для скрипта пользовательских данных нужен путь, взятый относительно самого модуля, поэтому в источнике данных template_file в файле modules/services/webser-vercluster/main.tf следует применять path.module:
data "template_file" "user_data" {
template = file("${path.module}/user-data.sh")
vars = {
server_port = var.server_port
db_address = data.terraform_remote_state.db.outputs.address
db_port = data.terraform_remote_state.db.outputs.port
}
}
Вложенные блоки
Конфигурацию некоторых ресурсов Terraform можно определять отдельно или в виде вложенных блоков. При создании модулей всегда следует отдавать предпочтение отдельным ресурсам.
Например, ресурс aws_security_group позволяет определять входящие и исходящие правила в виде вложенных блоков. Вы уже это видели в модуле webserver-cluster (modules/services/webserver-cluster/main.tf):
resource "aws_security_group" "alb" {
name = "${var.cluster_name}-alb"
ingress {
from_port = local.http_port
to_port = local.http_port
protocol = local.tcp_protocol
cidr_blocks = local.all_ips
}
egress {
from_port = local.any_port
to_port = local.any_port
protocol = local.any_protocol
cidr_blocks = local.all_ips
}
}
Вы должны модифицировать этот модуль так, чтобы те же входящие и исходящие правила определялись в виде отдельных ресурсов aws_security_group_rule (не забудьте сделать это для обеих групп безопасности в данном модуле):
resource "aws_security_group" "alb" {
name = "${var.cluster_name}-alb"
}
resource "aws_security_group_rule" "allow_http_inbound" {
type = "ingress"
security_group_id = aws_security_group.alb.id
from_port = local.http_port
to_port = local.http_port
protocol = local.tcp_protocol
cidr_blocks = local.all_ips
}
resource "aws_security_group_rule" "allow_all_outbound" {
type = "egress"
security_group_id = aws_security_group.alb.id
from_port = local.any_port
to_port = local.any_port
protocol = local.any_protocol
cidr_blocks = local.all_ips
}
При попытке одновременного использования вложенных блоков и отдельных ресурсов вы получите ошибки, когда правила маршрутизации конфликтуют и переопределяют друг друга. Поэтому вы должны выбрать что-то одно. В связи с этим ограничением при создании модуля всегда следует использовать отдельные ресурсы вместо вложенных блоков. В противном случае ваш модуль получится менее гибким и конфигурируемым.
Например, если все входящие и исходящие правила в модуле webserver-cluster определены в виде отдельных ресурсов aws_security_group_rule, вы сможете сделать этот модуль достаточно гибким для того, чтобы разрешить пользователям добавлять собственные правила за его пределами. Для этого идентификатор aws_security_group нужно экспортировать в виде выходной переменной внутри modules/services/webserver-cluster/outputs.tf:
output "alb_security_group_id" {
value = aws_security_group.alb.id
description = "The ID of the Security Group attached to the load balancer"
}
Теперь представьте, что вам нужно сделать доступным извне дополнительный порт, сугубо для тестирования. Теперь это легко сделать: нужно лишь добавить в файл stage/services/webserver-cluster/main.tf ресурс aws_security_group_rule:
resource "aws_security_group_rule" "allow_testing_inbound" {
type = "ingress"
security_group_id = module.webserver_cluster.alb_security_group_id
from_port = 12345
to_port = 12345
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
Если бы вы определили входящее или исходящее правило в виде вложенного блока, этот код был бы нерабочим. Стоит отметить, что эта же проблема характерна для целого ряда ресурсов Terraform, включая следующие:
•aws_security_group и aws_security_group_rule;
• aws_route_table и aws_route;
•aws_network_acl и aws_network_acl_rule.
Теперь вы готовы развернуть свой кластер веб-серверов сразу в тестовой и промышленной средах. Выполните terraformapply и наслаждайтесь работой с двумя отдельными копиями своей инфраструктуры.
Сетевая изоляция
Показанные в этой главе примеры создают две среды, изолированные как в вашем коде Terraform, так и с точки зрения инфраструктуры: у них есть отдельные балансировщики нагрузки, серверы и базы данных. Но, несмотря на это, они не изолированы на уровне сети. Чтобы не усложнять примеры кода, все ресурсы в этой книге развертываются в одно и то же виртуальное частное облако (VPC). Это означает, что серверы в тестовой и промышленной среде могут взаимодействовать между собой.
В реальных сценариях использования запуск двух окружений в одном облаке VPC чреват сразу двумя рисками. Во-первых, ошибка, допущенная в одной среде, может повлиять на другую. Например, если при внесении изменений в тестовом окружении вы случайно сломаете правила в таблице маршрутизации, это скажется на перенаправлении трафика и в промышленных условиях. Во-вторых, если злоумышленник получит доступ к одной среде, он сможет проникнуть и в другую. Если вы активно меняете код в тестовом окружении и случайно оставите открытым какой-нибудь порт, любой взломщик, проникший внутрь, сможет завладеть не только тестовыми, но и промышленными данными.
В связи с этим, если не считать простые примеры и эксперименты, вы должны размещать каждую среду в отдельном облаке VPC. Для пущей уверенности окружения можно даже разнести по разным учетным записям AWS.
Управление версиями
Если ваши тестовая и промышленная среды ссылаются на папку с одним и тем же модулем, любое изменение в этой папке коснется и той и другой при следующем же развертывании. Такого рода связывание усложняет тестирование изменений в изоляции от промышленного окружения. Вместо этого лучше использовать разные версии модулей: например, v0.0.2 для тестовой среды и v0.0.1 для промышленной, как показано на рис. 4.5.
Рис. 4.5. Применение разных версий модуля в разных окружениях
Во всех примерах с модулями, которые вы видели до сих пор, параметр source содержал локальный файловый путь. Но, помимо файловых путей, модули Terraform поддерживают и другие виды источников, такие как URL-адреса Git/Mercurial и произвольные URL44. Самый простой способ управления версиями модуля — размещение его кода в отдельном Git-репозитории, URL-адрес которого затем прописывается в параметре source. Это означает, что код Terraform будет распределен (как минимум) по двум репозиториям.
•modules — в этом репозитории находятся универсальные модули. Каждый модуль — своего рода «чертеж», который описывает определенную часть вашей инфраструктуры.
•live — репозиторий, который содержит текущую инфраструктуру, развернутую в каждом окружении (stage, prod, mgmt и т. д.). Это такие «здания», которые вы строите по «чертежам», взятым из репозитория modules.
Обновленная структура папок для вашего кода Terraform будет выглядеть примерно так, как на рис. 4.6.
Рис. 4.6. Структура файлов и каталогов с несколькими репозиториями
Чтобы организовать код таким образом, сначала нужно переместить папки stage, prod и global в папку под названием live. Затем вы должны разнести папки live и modules по отдельным Git-репозиториям. Вот пример того, как это делается с папкой modules:
$ cd modules
$ git init
$ git add .
$ git commit -m "Initial commit of modules repo"
$ git remote add origin "(URL OF REMOTE GIT REPOSITORY)"
$ git push origin master
Репозиторию modules можно также назначить тег, который будет использоваться в качестве номера версии. Если вы работаете с сервисом GitHub, это можно сделать в его пользовательском интерфейсе: создайте выпуск (bit.ly/2Yv8kPg), который автоматически создаст тег. Если вы не используете GitHub, можете применить утилиту командной строки Git:
$ git tag -a "v0.0.1" -m "First release of webserver-cluster module"
$ git push --follow-tags
Теперь вы можете работать с разными версиями модуля в тестовой и промышленной средах, указав URL-адрес Git в параметре source. Вот как это будет выглядеть в файле live/stage/services/webserver-cluster/main.tf, если ваш репозиторий modules находится в GitHub по адресу github.com/foo/modules (имейте в виду, что двойная косая черта в URL-адресе Git является обязательной):