Модули и мульти-аккаунт: композиция, окружения, радиус поражения
IaC в масштабе — композиция и изоляция: версионированные модули дают контрактный блок, но избыточная абстракция рождает god-модуль, где одна правка расходится повсюду. Продвигай тот же код с per-env-входами; аккаунт-на-окружение под Organizations — настоящая граница поражения.
Ты мерджишь улучшение в одну строку в общий модуль vpc — ужесточаешь правило по умолчанию для security-group — и ставишь тег. Ничего не деплоится; модули не деплоят себя сами. Модуль подключён как source = ".../vpc" без версии, поэтому он отслеживает ветку по умолчанию. Через три дня коллега запускает рутинный terraform apply в проде, чтобы добавить один-единственный алерт CloudWatch. Его init подтягивает свежий модуль vpc, план теперь хочет изменить продовую security-group, которую никто не трогал, и поскольку дифф — «всего лишь ужесточение правила» — он его подтверждает. Половина продового флота теряет путь ingress; мост инцидента поднимается раньше, чем кто-либо свяжет правку алерта с VPC-модулем, который они даже не открывали. Тот же модуль стоит за 40 стеками в dev, stage и prod, так что следующий apply в любом из них сделал бы то же самое. Композиция дала тебе переиспользование; незапиненная версия дала тебе поражение по всему флоту на рутинной правке.
Модули и композиция: контракт и два режима отказа
Модуль — это переиспользуемая, параметризованная единица инфраструктуры: модуль Terraform, конструкт CDK или вложенный стек / StackSet CloudFormation. Его входы и выходы образуют контракт: вызывающие передают переменные, потребляют выходы и относятся к внутренностям как к чёрному ящику. Механизм, который делает это безопасным в масштабе, — версионирование: ты публикуешь модуль в реестр (Terraform Registry, Git-тег, артефакт в S3/Artifactory), и потребители ссылаются на точную версию, так что отрецензированный, протестированный блок остаётся замороженным, пока кто-то намеренно его не обновит.
# Вызов версионированного модуля — версионная привязка и есть весь смысл.
module "vpc" {
source = "app.terraform.io/acme/vpc/aws"
version = "4.2.1" # зафиксировано. НЕ "~> 4.0" на горячем пути, НИКОГДА bare branch ref.
cidr_block = var.cidr_block
availability_zones = var.azs
enable_nat_gateway = var.env == "prod" # вход, а не форкнутая копия
}
# Потребители подключаются к ВЫХОДНОМУ контракту модуля, не к его внутренностям.
module "app" {
source = "app.terraform.io/acme/ecs-service/aws"
version = "2.7.0"
subnet_ids = module.vpc.private_subnet_ids # контракт
}Компромисс — это настоящая золотая середина с обрывом по обе стороны. Недостаток абстракции — каждая команда копипастит свой собственный VPC-HCL — означает, что одну и ту же правку приходится делать в десятке мест, и они расходятся: VPC у dev медленно отклоняется от продового, пока «работает в стейдже» не перестаёт предсказывать прод. Избыток абстракции — обратная ловушка: god-модуль с 60 входными переменными и десятком фича-флагов, которые никто до конца не понимает, где правка в одну строку расходится по всем потребителям, а абстракция скрывает больше, чем прячет. Режим отказа god-модуля в том, что о нём становится труднее рассуждать, чем о дублировании, которое он заменил, — и, как показал хук, единичная правка теперь дотягивается до каждого стека, который его вызывает.
Сеньорское правило: создавай модуль для действительно повторяющегося паттерна со стабильным интерфейсом, а не для всего подряд. Если ты написал один и тот же VPC трижды — выноси в модуль. Если ты абстрагируешь то, что используется однажды, ты добавил косвенность без отдачи. Хороший модуль маленький, с узкой поверхностью входов и меняется редко; god-модуль на 60 переменных — это запах, а не победа.
Продвижение по окружениям: один код, разные входы
Дисциплина, благодаря которой прод реально похож на стейдж, такова: одна и та же версия модуля работает в каждом окружении, а окружения различаются только входами — файлами tfvars, файлами параметров CloudFormation или контекстом CDK — но никогда не форкнутым кодом. В тот момент, когда dev и prod становятся разным кодом, а не одним кодом с разными переменными, «прошло в стейдже» перестаёт быть свидетельством о проде.
# envs/prod/terraform.tfvars — тот же модуль, prod-входы
env = "prod"
cidr_block = "10.0.0.0/16"
azs = ["eu-central-1a", "eu-central-1b", "eu-central-1c"]
instance_count = 6# envs/dev/terraform.tfvars — тот же модуль, dev-входы
env = "dev"
cidr_block = "10.9.0.0/16"
azs = ["eu-central-1a"]
instance_count = 1То, как ты разделяешь окружения, напрямую влияет на радиус поражения, — и именно здесь окупается модель состояния из урока 03. Workspaces держат одну конфигурацию и переключают срез состояния (terraform workspace select prod); дёшево, но один неверный select — и ты применяешь план прода против намерения dev, и все окружения делят один бэкенд. Директория-на-окружение даёт каждому окружению свой корень и свой файл состояния, так что повреждённое или залоченное состояние dev никогда не сможет тронуть состояние прода. Отдельное состояние на окружение — сеньорский выбор по умолчанию именно ради этой изоляции. Продвижение тогда — намеренное действие: ты применяешь версию модуля 4.2.1 к dev, даёшь ей вызреть и пройти проверки, затем применяешь ту же самую версию к stage, затем к prod — никогда не позволяя проду молча уплыть к версии, которой стейдж не видел.
Режим отказа при ошибке здесь тонкий: команда использует workspaces «чтобы попроще», кто-то запускает apply в workspace прода, считая, что он в dev, и нет второй границы, которая это поймала бы. Окружения, разделённые директориями и аккаунтами, превращают этот fat-finger в ошибку не-той-директории, которую ты замечаешь, а не в продовый сбой, который ты вызываешь.
Мульти-аккаунт: настоящая граница радиуса поражения
Изоляция состояния защищает тебя от механики IaC; она не останавливает разогнавшийся ресурс или плохую IAM-политику в одном окружении от того, чтобы дотянуться до другого внутри того же AWS-аккаунта. Самая прочная граница, которую даёт AWS, — это сам аккаунт. Под AWS Organizations ты запускаешь аккаунт-на-окружение (часто также на команду или на ворклоад): поражение в dev не может тронуть prod, потому что IAM-принципалы, сервисные квоты и биллинг — всё ограничено рамками аккаунта. Криво написанная политика iam:* в dev не даёт ничего в prod; разогнавшаяся рекурсия Lambda, накапавшая $40k, остаётся в счёте dev, а не prod.
Service Control Policies (SCP — политики управления сервисами, задающие максимальные права на уровне аккаунта) — это ограждение на уровне всей организации: они задают максимальные права, которые любой принципал в аккаунте вообще может иметь, — даже root аккаунта и админы. SCP, запрещающий класс действий, применяется ко всему, что ниже, независимо от IAM-грантов:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPublicS3OrgWide",
"Effect": "Deny",
"Action": "s3:PutBucketPublicAccessBlock",
"Resource": "*",
"Condition": {
"Bool": { "s3:PublicAccessBlockEnabled": "false" }
}
}
]
}Control Tower (landing zone) автоматизирует этот базовый уровень — структуру организации, аккаунт безопасности/архива логов и SCP-ограждения — так что новые аккаунты поднимаются уже огороженными. Чтобы деплоить в множество аккаунтов, ты используешь CloudFormation StackSets (один шаблон, развёрнутый на целевой набор аккаунтов и регионов) или, в Terraform, алиасы провайдера с assume-role — одну конфигурацию, которая принимает роль на аккаунт и применяет там:
provider "aws" {
alias = "prod"
region = "eu-central-1"
assume_role { role_arn = "arn:aws:iam::111111111111:role/terraform-deploy" }
}
provider "aws" {
alias = "dev"
region = "eu-central-1"
assume_role { role_arn = "arn:aws:iam::222222222222:role/terraform-deploy" }
}▸Почему это работает
Почему граница — это аккаунт, а не просто отдельная IAM-роль или тег? Потому что аккаунт — единственная граница, которую AWS обеспечивает сразу по всем трём осям IAM, квот и биллинга, и единственная, через которую единичная ошибка не может дотянуться. Общий аккаунт с ролями на команду всё равно делит сервисные квоты аккаунта (одна команда исчерпывает лимит EIP или конкурентности Lambda для всех), делит один счёт (нельзя чисто атрибутировать или ограничить разгон) и находится в одном чрезмерно широком Allow от кросс-окруженческого доступа. SCP добавляют потолок, который никакая IAM-политика внутри аккаунта не может превысить, — но они существуют только потому, что аккаунт — это единица, которой управляет Organizations. Аккаунт-на-окружение — это больше настройки, чем тег; это также разница между «ошибка dev разоряет песочницу» и «ошибка dev кладёт прод».
Кросс-стек-связывание и поражение от общего модуля
Реальные хозяйства — это не один стек, а множество, и они ссылаются друг на друга. Сетевой аккаунт экспортирует VPC ID; стек приложения его импортирует. Механизмы: data-source terraform_remote_state в Terraform читает выходы другого состояния; SSM Parameter Store хранит общие значения, которые один стек пишет, а другие читают; экспорты CloudFormation (Fn::ImportValue) связывают стеки по имени.
data "terraform_remote_state" "network" {
backend = "s3"
config = { bucket = "acme-tf-state", key = "prod/network/terraform.tfstate", region = "eu-central-1" }
}
resource "aws_lb" "app" {
subnets = data.terraform_remote_state.network.outputs.public_subnet_ids
}Это связывание покупает композицию, но создаёт упорядоченность и связанность: производитель должен примениться раньше потребителя, и ты не можешь удалить экспортированное значение, пока другой стек всё ещё его импортирует — CloudFormation отклоняет удаление, а план потребителя в Terraform ломается. «Безобидное» переименование выхода превращается в кросс-стек-инцидент.
Самый острый край — поражение от общего модуля из хука. Поднятие версии широко используемого модуля — или, хуже, ссылка на движущуюся мишень вроде ветки или latest — молча меняет каждого потребителя при его следующем apply. Когда за модулем стоит 40 стеков, незапиненный source означает, что следующий рутинный apply в любом из них подтянет новый код и предложит изменение, которого никто не запрашивал. Исправление механическое и не обсуждается: пиньте версии модулей, поднимайте их в намеренном PR и прокатывайте новую версию по dev → stage → prod, как любое другое изменение. Досягаемость единичной правки равна ровно числу потребителей; пиннинг делает эту досягаемость по-явному-согласию, а не автоматической. Та же логика масштабирует SCP: одно утверждение в корне организации может заблокировать целый класс действий сразу по N аккаунтам — колоссальный рычаг для ограждения и колоссальный сбой, если оно неверно, поэтому SCP тоже выкатывают как код.
Платформенная команда ведёт dev, stage и prod для критичного для выручки продукта. Они хотят максимально прочную гарантию, что ошибка в dev — плохая IAM-политика, разогнавшийся ресурс, неверный apply — не может дотянуться до prod, и готовы вложиться в настройку. Выбери стратегию изоляции окружений.
Общий модуль "vpc" подключён как source = ".../vpc" без пина версии и стоит за 40 стеками в dev/stage/prod. Кто-то мерджит правку в одну строку в ветку по умолчанию модуля. Что произойдёт?
- 01Что такое контракт модуля и какие два режима отказа задают границы того, где стоит создавать модуль?
- 02Почему аккаунт-на-окружение — настоящая граница радиуса поражения и как сюда вписываются SCP и кросс-аккаунтные деплои?
Структурирование инфраструктуры как кода в масштабе — это две дисциплины: композиция и изоляция. Модуль — модуль Terraform, конструкт CDK или вложенный стек/StackSet CloudFormation — это переиспользуемая единица, чьи входы и выходы — её контракт; ты версионируешь его в реестре, чтобы отрецензированный блок оставался замороженным до намеренного апгрейда. Создавай его для действительно повторяющегося паттерна со стабильным интерфейсом, потому что оба обрыва реальны: недостаток абстракции — это копипаст, который расходится между командами, а избыток абстракции — это god-модуль на 60 переменных, где одна строка расходится по всем потребителям. Продвижение — это одна и та же версия модуля, запущенная с разными per-env-входами — tfvars, файлами параметров или контекстом CDK, — так что прод реально похож на стейдж; ты разделяешь окружения по состоянию (директория- или аккаунт-на-окружение, сеньорский выбор по умолчанию), а не форкаешь код, и намеренно двигаешь вызревшую версию dev → stage → prod. Самая прочная граница — аккаунт: под AWS Organizations аккаунт-на-окружение изолирует IAM, сервисные квоты и биллинг одновременно, так что ошибка dev не может дотянуться до prod или разорить его, а SCP задают потолок прав на уровне организации, и Control Tower запекает базу; ты деплоишь по аккаунтам через StackSets или алиасы провайдера Terraform, которые принимают роль на аккаунт. Стеки ссылаются друг на друга через remote state, SSM Parameter Store или экспорты CloudFormation, что покупает композицию ценой упорядоченности и связанности — нельзя удалить экспортированное значение, которое ещё импортируют. Сквозная опасность всего этого — досягаемость: радиус поражения единичной правки равен числу потребителей, поэтому пиньте версии модулей (незапиненный бамп молча меняет каждого потребителя при его следующем apply) и относись к SCP на уровне организации с той же осторожностью, что и к деплою. Композиция даёт тебе переиспользование; пиннинг и границы аккаунтов решают, будет это переиспользование рычагом или поражением по всему флоту. Теперь, когда увидишь модуль без пина версии или команду, делящую один AWS-аккаунт на несколько окружений, — ты сразу поймёшь, какой радиус поражения это означает и как его закрыть.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.