Архитектурные паттерны IaC на основе источников 1.1. Декларативный подход и идемпотентность Terraform использует декларативный язык HCL, где инженер описывает целевое конечное состояние («что должно существовать»), в то время как инструмент самостоятельно вычисляет граф зависимостей и шаги для его достижения [22], [4]. Это обеспечивает идемпотентность: многократный запуск одного и того же кода приводит к идентичному результату, исключая риск дублирования ресурсов [21]. В отличие от процедурных инструментов (Ansible, Bash), где повторный запуск может потребовать дополнительных проверок, Terraform сравнивает текущее состояние (state) со свежим кодом и вносит только необходимые изменения [21], [16]. Начиная с версии 0.12 (HCL 2.0), синтаксис был упрощен: переменные больше не требуют обязательной интерполяции ${} везде, где они используются, что сделало код более читаемым для человека [1], [4]. Декларативный подход позволяет уйти от ручного управления («ClickOps») и превратить инфраструктуру в «живую» документацию [1], [5], [29]. 1.2. Минимизация «радиуса поражения» (Blast Radius) Монолитная инфраструктура, где все ресурсы описаны в одном файле или управляются единым файлом состояния, признана критическим антипаттерном [2]. Научное исследование SMELLS-SUS показывает, что «монолитный запах» (Monolithic Infrastructure smell) является наиболее распространенной проблемой и встречается в 9,67% проанализированных сценариев [2]. Ошибка в коде одного компонента (например, опечатка в Security Group) в монолитном стейте может привести к каскадному удалению или повреждению критически важных ресурсов, таких как базы данных всего ландшафта [15]. Эксперты рекомендуют разделять инфраструктуру на мелкие независимые стейты — «переборки» [16]. Опыт компании Selectel подтверждает, что распилка монолита на гранулярные компоненты позволяет ускорить деплой инфраструктуры в 20 раз за счет уменьшения времени опроса API облачного провайдера [5]. 1.3 Слоистая архитектура (Layered Architecture) Рекомендуется разделение ресурсов на независимые логические уровни (слои), каждый из которых имеет свой жизненный цикл и границы ответственности [17], [4609. «Владимир Дроздецкий — Эффективное управление инфрой»], [29]. Типичная иерархия включает следующие слои: Global (IAM, DNS), Network (VPC, подсети), Platform (K8s, базы данных) и Application (микросервисы) [17], [20]. Слой сети является фундаментальным, так как без него невозможна работа большинства других компонентов [29]. Связь между слоями осуществляется через передачу выходных данных (outputs), что в Terragrunt реализуется через блоки dependency, обеспечивающие автоматическую оркестрацию и соблюдение порядка развертывания [16], [18], [29]. Использование команды run-all позволяет развернуть всю иерархию модулей в правильной последовательности одним действием [1], [18], [27]. 2. Организация и структура репо 1. Стратегия разделения: Modules и Live Общепринятым корпоративным стандартом является разделение реализации инфраструктуры от ее непосредственного развертывания по разным репозиториям [20], [30]: Repository «Modules»: Библиотека универсальных, версионируемых «чертежей» (blueprints). Они не содержат специфических данных окружения (IP-адресов, имен) и предназначены для многократного использования [20], [7]. Repository «Live»: Описание реальных «зданий», построенных по чертежам из модулей. Здесь фиксируются конкретные параметры для каждого ландшафта (dev, stage, prod) [7], [20]. 2. Иерархическая структура каталогов (Live-репозиторий) Для обеспечения изоляции и прозрачности рекомендуется древовидная структура, отражающая логику облака [20], [30]: Account (Учетная запись) → Region (Регион) → Environment (Окружение) → Category (Категория) → Component (Компонент) Account: На верхнем уровне разделяются облачные аккаунты (например, prod-account, stage-account), что обеспечивает максимальную изоляцию в рамках безопасности [7], [20]. Region: Внутри аккаунта создаются папки регионов (например, us-east-1, eu-central-1) [20]. Папка _global на этом уровне используется для ресурсов, доступных во всех регионах аккаунта (IAM, DNS) [7]. Environment: Разделение на dev, qa, stage и prod [20], [6]. Каждое окружение обычно соответствует отдельному VPC [7]. Category: Группировка ресурсов по назначению: networking (VPC, подсети), services (приложения, K8s), data-stores (БД, кэши) [20], [29]. Component: Конечная директория с файлом terragrunt.hcl или main.tf, управляющая конкретным ресурсом (например, mysql или vpc) [20]. 3. Организация файлов внутри компонента Для поддержания чистоты кода (принцип DRY) и удобства навигации рекомендуется разделение на функциональные файлы [6], [23]: main.tf: Вызов модулей и описание основных ресурсов [6]. variables.tf: Объявление входных переменных с обязательным описанием (description) и типами [6]. outputs.tf: Описание выходных данных для передачи их между слоями или модулями [6]. providers.tf: Конфигурация провайдеров (AWS, Azure, Yandex) и их версий [23]. backend.tf: Настройка удаленного хранения стейта (S3, GCS) [6]. versions.tf: Фиксация версий Terraform и провайдеров для стабильности сборок [6]. 4. Иерархическое управление переменными в Terragrunt Terragrunt позволяет избежать дублирования за счет автоматического наследования конфигураций по иерархии папок [27], [30]: root.hcl: Находится в корне и содержит общую конфигурацию remote_state и провайдеров для всех ландшафтов [27]. env.hcl / environment.yaml: Хранит переменные, общие для всего окружения (например, env = "prod") [18], [30]. region.hcl / region.yaml: Переменные конкретного региона (например, maintenance_window) [27], [30]. Для автоматического поиска этих файлов используется функция find_in_parent_folders() [30]. 5. Изоляция состояний (State Isolation) Иерархическая структура напрямую влияет на управление состоянием. Gruntwork и эксперты рекомендуют управлять стейтом на уровне каждого отдельного юнита (директории) [20], [30]. Использование Workspaces для Production-сред не рекомендуется, так как они используют один бэкенд и скрывают структуру в CLI [27]. Разделение по папкам гарантирует, что каждый компонент имеет свой изолированный файл состояния, что критически снижает «радиус поражения» (blast radius) в случае ошибки [16], [24]. 6. Нововведения в структуре (Stacks) В 2025 году появилась концепция Terragrunt Stacks, позволяющая упаковывать коллекции связанных юнитов в переиспользуемые стеки [27], [19]. Это переводит переиспользование с уровня отдельных модулей на уровень целых инфраструктурных паттернов (например, «App + DB + Monitoring»), полностью устраняя копипаст конфигураций между ландшафтами [27]. 3. Модульность и стратегии переиспользования Согласно исследованиям и best-practices, модульность является краеугольным камнем масштабируемой и поддерживаемой инфраструктуры. Она позволяет инкапсулировать ее сложность, обеспечивать переиспольщование и минимизировать дублирование кода, что напрямую снижает риск возникновения «монолитного запаха» (Monolithic Infrastructure smell) в проектах [2, 6]. 3.1. Понятие и структура модуля Как описывается в руководствах, в Terraform модулем может считаеться любая директория, содержащая конфигурационные файлы .tf [6]. Модули работают по принципу функций в программировании: они инкапсулируют логику создания определенного набора ресурсов (например, виртуальной машины или кластера БД), принимают входные параметры через переменные и возвращают результаты через выходные значения [21]. Это позволяет абстрагироваться от деталей реализации. Как показано в примерах, для создания переиспользуемого компонента его необходимо выделить в отдельную директорию. Например: modules/object-storage/bucket/main.tf: resource "yandex_storage_bucket" "this" { bucket = var.bucket_name acl = var.acl dynamic "server_side_encryption_configuration" { for_each = var.kms_key_id != null ? [1] : [] content { rule { apply_server_side_encryption_by_default { kms_master_key_id = var.kms_key_id sse_algorithm = "aws:kms" } } } } lifecycle_rule { enabled = true expiration { days = var.object_expiration_days } } } Дабы вызвать данный модуль и корневого каталога, можно сделать следующее: module "static_assets_bucket" { source = "./modules/object-storage/bucket" bucket_name = "myapp-static-assets-prod" acl = "private" kms_key_id = yandex_kms_symmetric_key.bucket_key.id } Источники подчеркивают, что такой подход превращает инфраструктуру в набор переиспользуемых «кирпичиков лего» [6, 21]. 3.2. Использование публичных и готовых модулей Для ускорения разработки и следования лучшим практикам настоятельно рекомендуется использовать готовые модули. Хотя публичный реестр Terraform для Yandex Cloud менее обширен, чем для AWS, существуют как официальные модули от Yandex, так и проверенные сообществом, которые можно найти на GitHub. Использование таких модулей, как отмечается в источниках, позволяет избежать написания сотен строк стандартного кода для настройки, например, виртуальных частных облаков (VPC) или групп безопасности [6, 23]. Тем самым нам не нужно в большинстве случаев изобретать вилосипед. Адаптированный пример того, как можно использовать структурированный модуль для быстрого развертывания сети: module "vpc" { source = "terraform-yc-modules/vpc/yc" version = "~> 1.0" name = "production-network" description = "VPC for production environment" # Определение подсетей в разных зонах доступности subnets = { "ru-central1-a" = "10.0.1.0/24", "ru-central1-b" = "10.0.2.0/24", "ru-central1-c" = "10.0.3.0/24" } # Включение NAT для выхода в интернет из приватных подсетей enable_nat = true } Использование версионированных модулей из проверенных источников является всеобщей стандартной практикой [6, 23]. 3.3. Стратегии проектирования Сложные инфраструктурные решения строятся по принципу композиции («Lego bricks»). Как описывается в литературе, выходные данные одного модуля (например, идентификаторы подсетей) должны передаваться на вход другому (например, модулю создания виртуальной машины), формируя явные и управляемые зависимости [6, 30]. Это обеспечивает слабую, но хоть какую-то связанность компонентов. # 1. Модуль создания кластера Managed PostgreSQL module "prod_postgresql" { source = "../../../modules/data-stores/postgresql" cluster_name = "app-db-prod" network_id = data.yandex_vpc_network.default.id subnet_ids = [yandex_vpc_subnet.private_a.id] host_class = "s2.micro" } # 2. Модуль развертывания приложения, использующий вывод модуля БД module "backend_app" { source = "../../../modules/services/backend-app" name = "backend-api" subnet_id = yandex_vpc_subnet.public_a.id image_id = "blahblahblah" db_host = module.prod_postgresql.cluster_hosts_fqdn[0] db_name = module.prod_postgresql.database_name } Такой подход исключает хардкод значений и делает инфраструктурный код декларативным и прозрачным [30]. 3.4. Динамическое создание ресурсов (итерирование) Для массового создания однотипных ресурсов без копирования кода используются count и for_each. В руководствах отмечается, что for_each предпочтительнее для работы с коллекциями (map, set), так как он создает ресурсы с уникальными идентификаторами в стейте, что делает их безопасными для последующих изменений [6, 10]. # variables.tf variable "vm_instances" { description = "Конфигурация создаваемых виртуальных машин" type = map(object({ cores = number memory = number boot_disk_gb = number image_family = string user_data = string # Cloud-init })) } # main.tf resource "yandex_compute_instance" "vm" { for_each = var.vm_instances name = each.key platform_id = "standard-v3" zone = "ru-central1-a" resources { cores = each.value.cores memory = each.value.memory } boot_disk { initialize_params { size = each.value.boot_disk_gb image_id = data.yandex_compute_image.this[each.key].id } } network_interface { subnet_id = yandex_vpc_subnet.app.id nat = true # Для ВМ, которым нужен внешний IP } metadata = { user-data = each.value.user_data } } # Использование data source для поиска актуального образа по семейству data "yandex_compute_image" "this" { for_each = var.vm_instances family = each.value.image_family } Это позволяет позволяет гибко управлять множеством ресурсов через изменение одной переменной [10]. Как это работает? Допустим, в файле terraform.tfvars или в Terragrunt inputs мы передадим vm_instances = { "web-server-01" = { cores = 2 memory = 4 boot_disk_gb = 30 image_family = "ubuntu-2204-lts" user_data = "#cloud-config\npackage_update: true" }, "database-01" = { cores = 4 memory = 8 boot_disk_gb = 100 image_family = "centos-7" user_data = "#cloud-config\ndisable_root: false" }, "monitoring-01" = { cores = 2 memory = 2 boot_disk_gb = 20 image_family = "debian-11" user_data = "#cloud-config\npackages: ['prometheus']" } } Terraform возьмет каждый элемент этой мапы и создаст отдельный ресурс yandex_compute_instance: Итерация 1: Создается ВМ с именем web-server-01, 2 ядрами, 4 ГБ памяти Итерация 2: Создается ВМ с именем database-01, 4 ядрами, 8 ГБ памяти Итерация 3: Создается ВМ с именем monitoring-01 и т.д 3.5. Версионирование как гарантия стабильности Для надежности production-сред в особненности, источники категорически настаивают на фиксировании версии как внешних провайдеров, так и собственных модулей. Это защищает от непреднамеренных изменений, которые могут быть внесены в апстрим репозитории [6, 21]. Семантическое версионирование (SemVer) позволяет контролировать процесс обновлений. module "network" { source = "git::https://gitlab.sarex.io/terraform/yc-network-module.git?ref=v2.5.1" vpc_name = "production" cidr = "10.100.0.0/16" } 3.6. Оркестрация и соблюдение принципа DRY через Terragrunt Для управления сложными, многоуровневыми деплойментами в облаках и соблюдения принципа DRY рекомендуется использовать инструменты оркестрации, такие как Terragrunt [27, 30]. Он позволяет устранить дублирование в конфигурациях бэкенда и провайдеров, а также явно управлять зависимостями между компонентами. # Автоматическое включение конфигураций из родительских папок include "root" { path = find_in_parent_folders("root.hcl") } # Объявление зависимости от модуля сети dependency "vpc" { config_path = "../network" # Mock-выходы для команд `plan` без доступа к реальному state mock_outputs = { subnet_ids = ["mock-subnet-id-a", "mock-subnet-id-b"] network_id = "mock-network-id" } } # Локальные переменные для удобства locals { common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl")).locals } # Входные переменные для Terraform-модуля inputs = { environment = local.common_vars.environment region = local.common_vars.region subnet_id = dependency.vpc.outputs.subnet_ids[0] # ... другие переменные } Файл root.hcl: hcl # Настройка удаленного бэкенда в Yandex Object Storage с блокировкой через YDB remote_state { backend = "s3" config = { endpoint = "storage.yandexcloud.net" bucket = "company-terraform-state-prod" key = "${path_relative_to_include()}/terraform.tfstate" region = "ru-central1" skip_region_validation = true skip_credentials_validation = true skip_requesting_account_id = true skip_metadata_api_check = true force_path_style = true # Настройка блокировок состояний через Yandex Database (YDB) dynamodb_endpoint = "https://docapi.serverless.yandexcloud.net/ru-central1/b1gxxxxxxxx/etn03j3j45678" dynamodb_table = "terraform_locks" } } # Генерация общего провайдера для Yandex Cloud generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = < В руководства=х предостерегают от частого использования этой команды и рекомендуют сначала убедиться, что ни один другой процесс не использует файл стейта [16, 21]. 4.4. Изоляция окружений (Isolation) Для обеспечения надежного разделения сред разработки (dev), тестирования (stage) и production (prod) в источниках рассматриваются два основных подхода: Workspaces: Позволяют использовать одну и ту же конфигурацию кода для нескольких окружений, но хранят состояния всех окружений в одном бэкенде. Этот подход может подходить для небольших проектов, но не рекомендуется для production из-за риска случайного воздействия на несколько сред и отсутствия полной изоляции [6, 23]. Структура каталогов (Рекомендуемый подход): Обеспечивает полную изоляцию за счет использования разных директорий и, соответственно, разных файлов состояния для каждого окружения. В сочетании с Terragrunt этот подход автоматизируется с помощью функции path_relative_to_include() для динамического формирования ключей (key) в Object Storage [7, 20, 27]. Пример структуры каталогов с Terragrunt: live/ ├── prod/ │ ├── ru-central1/ │ │ ├── vpc/ │ │ │ └── terragrunt.hcl # key = "prod/ru-central1/vpc/terraform.tfstate" │ │ └── k8s/ │ │ └── terragrunt.hcl # key = "prod/ru-central1/k8s/terraform.tfstate" │ └── _global/ │ └── iam/ │ └── terragrunt.hcl # key = "prod/_global/iam/terraform.tfstate" └── dev/ └── ru-central1/ └── vpc/ └── terragrunt.hcl # key = "dev/ru-central1/vpc/terraform.tfstate" Как подчеркивается в источниках Gruntwork, такая структура минимизирует «радиус поражения» (blast radius) и обеспечивает максимальную безопасность [7, 20]. 4.5. Работа с данными из других стейтов Для создания зависимостей между изолированными компонентами инфраструктуры (например, когда модулю приложений нужен ID VPC из модуля сети) в источниках описывается два подхода: Нативный подход Terraform: Использование data source terraform_remote_state для чтения выходных значений из состояния другого модуля. data "terraform_remote_state" "network" { backend = "s3" config = { endpoint = "storage.yandexcloud.net" bucket = "company-name-terraform-state-prod" key = "prod/ru-central1/vpc/terraform.tfstate" region = "ru-central1" skip_region_validation = true skip_credentials_validation = true } } resource "yandex_compute_instance" "app" { name = "app-server" platform_id = "standard-v3" zone = "ru-central1-a" network_interface { subnet_id = data.terraform_remote_state.network.outputs.subnet_ids[0] } } Однако этот подход требует жесткого указания путей к бэкенду и может создать скрытые зависимости [6, 16]. Подход с использованием Terragrunt: Использование блока dependency, который автоматически управляет зависимостями и порядком развертывания. # terragrunt.hcl для модуля приложения dependency "vpc" { config_path = "../vpc" } inputs = { subnet_id = dependency.vpc.outputs.subnet_ids[0] } 4.6. Манипуляции со стейтом через CLI и код Никогда не следует редактировать файл состояния вручную с помощью текстового редактора, так как это почти гарантированно приведет к повреждению данных и неработоспособности инфры [3, 6, 16]. Для безопасного управления состоянием предназначены CLI-команды Terraform: Импорт существующих ресурсов: Привязка ресурсов, созданных вручную или другим способом, под управление Terraform. terraform import yandex_compute_instance.app fhm0b28lgm4qabcde Как описывается в источниках, эта команда добавляет ресурс в состояние, но не создает код, который нужно написать отдельно [6, 21]. Безопасный рефакторинг с moved: Современный способ переименования ресурсов или изменения их адреса без физического пересоздания. # В конфигурации Terraform moved { from = yandex_compute_instance.old_name to = yandex_compute_instance.new_name } Этот блок в книге Robert Hafner'а указывает Terraform на необходимость перенести состояние, а не удалять и создавать заново [21]. Удаление ресурса из управления Terraform: Команда state rm удаляет ресурс из состояния, но не удаляет его физически в облаке. bash terraform state rm yandex_compute_instance.temporary Эта операция должна выполняться с крайней осторожностью!! [16, 21]. Downstream Pipelines и автоматизация в CI/CD При управлении крупной инфраструктурой, состоящей из сотен компонентов, классический подход с единым CI/CD-пайплайном становится неприемлемым. Его выполнение превращается в многочасовую процедуру, а ошибка в одном компоненте приводит к остановке всего процесса и сложностям в локализации проблемы. Как показывают практические кейсы, Downstream (дочерние) Pipelines решают эту проблему, превращая монолитный деплой в набор независимых, параллельных задач, что сокращает время развертывания с часов до минут и кардинально упрощает операционную деятельность [5, 29]. 5.1. Динамическая генерация пайплайна: принцип работы и практическая реализация Суть динамической генерации заключается в том, что структура пайплайна не задается жестко в YAML-файле, а создается скриптом на лету на основе актуальной файловой структуры репозитория live/, как было уже указано выше. Это обеспечивает полное соответствие между инфраструктурным кодом и процессом его доставки. Контекст и исходная структура: Предположим, инфраструктура организована по принципу, описанному в материале Gruntworks - Folder Structure, и структурно выглядит как Окружение/Регион/Компонент: live/ ├── prod/ │ ├── ru-central1-a/ │ │ ├── network/ # Компонент A: VPC, subnets │ │ │ └── terragrunt.hcl │ │ └── k8s-cluster/ # Компонент B: k8s кластер │ │ └── terragrunt.hcl │ └── _global/ │ └── object-storage/ # Компонент C: s3 buckets │ └── terragrunt.hcl └── dev/... # Аналогичная структура для других каталогов Шаг 1: Создание скрипта-генератора Скрипт generate-pipeline.sh выполняет ключевую роль. Он: Сканирует всю иерархию live/ в поисках файлов terragrunt.hcl, игнорируя служебные директории (например, .terragrunt-cache/). Для каждого найденного компонента извлекает из пути его метаданные: окружение (prod, dev), регион (ru-central1-a, _global) и имя компонента (network). Генерирует три стандартные job GitLab CI для каждого компонента: validate, plan и apply. Автоматически назначает переменные окружения (например, TG_ROOT) и, что критически важно, настраивает правило when: manual для стадии apply в production-окружениях. Это обязательная мера безопасности, чтобы production-изменения не катились автоматически. #!/bin/bash echo "stages: [validate, plan, apply]" > .gitlab-ci.generated.yml find live -name "terragrunt.hcl" -not -path "*/.terragrunt-cache/*" | while read CONFIG_FILE; do COMPONENT_DIR=$(dirname "$CONFIG_FILE") ENV=$(echo "$COMPONENT_DIR" | cut -d'/' -f2) REGION=$(echo "$COMPONENT_DIR" | cut -d'/' -f3) COMPONENT_NAME=$(basename "$COMPONENT_DIR") for STAGE in validate plan apply; do JOB_NAME="${STAGE}-${ENV}-${REGION}-${COMPONENT_NAME}" cat >> .gitlab-ci.generated.yml << EOF ${JOB_NAME}: stage: ${STAGE} variables: TG_ROOT: "${COMPONENT_DIR}" ENVIRONMENT: "${ENV}" script: - cd \${TG_ROOT} - terragrunt init -reconfigure -input=false - terragrunt \${CI_JOB_STAGE} -input=false --terragrunt-non-interactive EOF # Ключевое правило безопасности для production if [[ "$STAGE" == "apply" && "$ENV" == "prod" ]]; then echo " when: manual" >> .gitlab-ci.generated.yml fi echo "" >> .gitlab-ci.generated.yml done done Шаг 2: Оркестрация в основном пайплайне Основной файл .gitlab-ci.yml становится минимальным и выполняет лишь две функции — запуск генератора и триггер downstream пайплайна. yaml stages: - generate - trigger generate-config: stage: generate script: ./generate-pipeline.sh artifacts: paths: - .gitlab-ci.generated.yml expire_in: 1 hour trigger-downstream: stage: trigger trigger: include: - artifact: .gitlab-ci.generated.yml job: generate-config strategy: depend Следовательно, при каждом пуше в репозиторий сначала выполняется джоба generate-config, которая создает актуальный файл .gitlab-ci.generated.yml и сохраняет его как артефакт. Затем job trigger-downstream запускает новый пайплайн, используя сгенерированный файл в качестве .gitlab-ci.yml. Шаг 3: Что происходит в сгенерированном downstream пайплайне В результате для структуры из трех компонентов будет создано 9 независимых job, организованных в трех стейджах: -=ВСЕ ДЖОБЫ ВЫПОЛНЯЮТСЯ ПАРАЛЛЕЛЬНО validate: • validate-prod-ru-central1-a-network • validate-prod-ru-central1-a-k8s-cluster • validate-prod-global-object-storage plan (если validate завершился успешно): • plan-prod-ru-central1-a-network • plan-prod-ru-central1-a-k8s-cluster • plan-prod-global-object-storage Стадия 'apply' (manual для prod или manual для всех apply): • apply-prod-ru-central1-a-network (manual) • apply-prod-ru-central1-a-k8s-cluster (manual) • apply-prod-global-object-storage (manual) А вот так будет выглядеть сгенерированный gitlab-ci для компонента network: validate-prod-ru-central1-a-network: stage: validate variables: TG_ROOT: "live/prod/ru-central1-a/network" ENVIRONMENT: "prod" YC_FOLDER_ID: "${YC_PROD_FOLDER_ID}" before_script: - cd ${TG_ROOT} script: - terragrunt init -reconfigure --terragrunt-non-interactive - terragrunt validate -input=false --terragrunt-non-interactive rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' tags: - yc interruptible: true plan-prod-ru-central1-a-network: stage: plan variables: TG_ROOT: "live/prod/ru-central1-a/network" ENVIRONMENT: "prod" YC_FOLDER_ID: "${YC_PROD_FOLDER_ID}" before_script: - cd ${TG_ROOT} script: - terragrunt init -reconfigure --terragrunt-non-interactive - terragrunt plan -input=false --terragrunt-non-interactive -out=tfplan rules: - if: '$CI_COMMIT_BRANCH == "master"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' tags: - yc interruptible: true artifacts: paths: - ${TG_ROOT}/*.plan when: always expire_in: 1 week apply-prod-ru-central1-a-network: stage: apply variables: TG_ROOT: "live/prod/ru-central1-a/network" ENVIRONMENT: "prod" YC_FOLDER_ID: "${YC_PROD_FOLDER_ID}" before_script: - cd ${TG_ROOT} script: - terragrunt init -reconfigure --terragrunt-non-interactive - terragrunt apply -input=false --terragrunt-non-interactive tfplan rules: - if: '$CI_COMMIT_BRANCH == "master"' when: manual tags: - yc interruptible: true Такой подход обеспечивает максимальный параллелизм. Например, пока выполняется plan для network, одновременно может идти validate для object-storage. Стадия apply для всего production-окружения блокируется и требует ручного нажатия кнопки деплоя для каждого компонента в отдельности, что дает полный контроль над разверткой. 5.2. Ключевые преимущества и операционные выгоды подхода Экспоненциальный рост скорости: Вместо последовательного выполнения команд для всех компонентов, они обрабатываются параллельно. Это преобразует время деплоя из линейной зависимости O(n) в константную O(1). Яркий пример — кейс компании Selectel, где внедрение Downstream Pipelines сократило время полного развертывания инфраструктуры с 8 часов до 24 минут [5]. Минимизация «радиуса поражения» (Blast Radius): При использовании единого пайплайна с командой terragrunt run-all ошибка в любом модуле приводит к остановке всего процесса, а откат изменений может быть проблематичным занятием. При гранулярном подходе падение джобы типа apply-prod-ru-central1-a-k8s-cluster никак не повлияет на уже завершенное или еще не начатое развертывание компонентов network или object-storage. Можно точечно исправить ошибку в одном компоненте и перезапустить только его, не затрагивая остальные стабильные части системы [5, 16]. Прозрачность и простой траблшутинг: В интерфейсе GitLab CI/CD каждый компонент представлен отдельной джобой с собственным логом. Это кардинально отличается от гаргантюанского вывода run-all, где логи десятков модулей идут единым потоком. При возникновении проблемы инженер мгновенно видит, в каком именно компоненте и на каком стейдже произошел сбой, и вследствие может посмотреть его лог предметно. [5, 29]. <езопасность и контроль: Правило when: manual для apply в production является не просто рекомендацией, а обязательным guardrail, согласно истоичникам. Оно исключает возможность автоматического применения потенциально разрушительных изменений из-за ошибки в коде или сбоя в CI-системе. Каждое изменение подтвержадется руками, что соответствует принципам GitOps и compliance-требованиям для сред, критичных к аптайму. Управление зависимостями и порядком выполнения: Важным аспектом, который решается уже на уровне конфигурации Terragrunt, а не самого пайплайна, являются зависимости между компонентами. Например, k8s-cluster зависит от выходных данных network (ID подсетей). В файле terragrunt.hcl компонента k8s-cluster это описывается через блок dependency. Во время выполнения terragrunt apply в сгенерированной джобе Terragrunt автоматически прочитает состояние network из remote стейта, получит необходимые выходные переменные и передаст их в модуль. Таким образом, оркестрация зависимостей и параллелизм не конфликтуют: пайплайн может запустить джобы параллельно, а Terragrunt внутри джобы будет ждать, пока состояние зависимого компонента (network) не станет актуальным [18, 27]. 5.3. Статический анализ и валидация Динамическую генерацию можно расширить для создания предварительных стадий проверки. Например, можно создать отдельную стадию lint, которая для каждого компонента запускает: terragrunt validate — проверка синтаксиса Terraform. terragrunt hclfmt --terragrunt-check — проверка соответствия формату. checkov -d . — статический анализ безопасности на наличие уязвимых конфигураций (например, открытых security groups, незашифрованных дисков) [5, 23]. Эти проверки выполняются за секунды и позволяют «отсеять» очевидные проблемы до запуска длительных операций plan и apply, экономя время и ресурсы. 6. Рефакторинг инфры Рефакторинг в Terraform долгое время был доовльно рискованной операцией. Изменение идентификатора ресурса в коде (например, с aws_instance.web на aws_instance.server) воспринималось TF как удаление старого объекта и создание нового, что приводило к простоям и потере данных у ресурсов с состоянием (БД, S3-бакеты) [21, 24]. С выходом версии 1.1 Terraform представил блоки moved — декларативный способ управления состоянием прямо в коде, который пришел на замену ручным и опасным командам в CLI [6, 21]. 6.1. Деструктивный рефакторинг Terraform связывает идентификатор ресурса в файлах .tf с уникальным ID в облаке через стейт [16, 21]. Если мы просто переименуем ресурс в коде, Terraform интерпретирует это как удаление ресурса и создание другого. Это приводит к печальным последствиям — удалению работающего инстанса виртуальной машины, базы данных или бакета [16, 21, 24]. 6.2. Блок moved Блок moved сообщает Terraform, что ресурс сменил свое расположение или имя внутри кода. Это позволяет перепривязать существующую запись в стейте к новому имени без негативных последствий на реальную инфраструктуру [6, 21]. Синтаксис блока прост: hcl moved { from = <СТАРЫЙ_АДРЕС_РЕСУРСА> to = <НОВЫЙ_АДРЕС_РЕСУРСА> } 6.3. Основные сценарии использования В руководствах выделяют несколько ключевых сценариев, где блок moved становится незаменимым. Сценарий 1: Простое переименование ресурса Наиболее частый случай — приведение имен ресурсов к единому стандарту. # Было: resource "yandex_compute_instance" "vm" { ... } # Стало: resource "yandex_compute_instance" "app_server" { name = "production-app-01" platform_id = "standard-v3" # ... остальная конфигурация } moved { from = yandex_compute_instance.vm to = yandex_compute_instance.app_server } После применения terraform apply в стейте Terraform запись для ВМ будет обновлена, а сама ВМ в облаке останется нетронутой. Сценарий 2: Перемещение ресурса внутрь модуля В процессе развития проекта монолитный код разбивается на модули. Блок moved позволяет безопасно инкапсулировать ресурс. # В корневом main.tf (старая структура) resource "yandex_vpc_network" "this" { name = "my-network" } # В новом модуле modules/network/main.tf resource "yandex_vpc_network" "this" { name = var.network_name } # В корневом main.tf (новая структура) module "network" { source = "./modules/network" network_name = "my-network" } moved { from = yandex_vpc_network.this to = module.network.yandex_vpc_network.this } Сценарий 3: Рефакторинг вызова модулей Можно переименовывать не только ресурсы, но и вызовы самих модулей для улучшения читаемости кода. moved { from = module.obscure_name_xyz to = module.kubernetes_cluster } 6.4. Разница между moved и terraform state mv? До появления блоков moved использовали команду terraform state mv. Однако у декларативного подхода есть большие преимущества: Автоматизация для всей команды: Блок moved является частью кода. Когда разработчик обновляет репозиторий и запускает terraform apply, миграция состояния происходит автоматически. Не нужно запоминать и выполнять последовательность команд вручную [6, 21]. Безопасность и проверка через plan: Изменения стейта отображаются в выводе terraform plan. Вы можете заранее убедиться, что Terraform планирует именно перемещение (в логе будет указано # resource has moved), а не удаление и создание (# resource will be destroyed, # resource will be created) [6, 21]. История изменений в Git: Факт рефакторинга фиксируется в системе контроля версий. Любой член команды, просматривая историю коммитов, увидит, когда и почему ресурс был переименован, что делает эволюцию инфраструктуры полностью прозрачной [21]. 6.5. Интеграция рефакторинга в рабочий процесс с Terragrunt Terragrunt, выступая оркестратором, накладывает строгие ограничения на структуру проекта, что делает рефакторинг более предсказуемым и безопасным [27, 30]. 1. Изоляция состояния и минимизация Blast Radius Как уже было сказано, Terragrunt стимулирует дробление инфраструктуры на мелкие, независимые компоненты, каждый со своим файлом состояния [7, 20, 27]. Преимущество: Опять же, при ошибке в рефакторинге (например, ошибка в блоке moved) затронет состояние только одного компонента. Это предотвращает каскадные сбои во всей инфраструктуре [16, 24, 30]. Пример: При переименовании ресурса в модуле k8s-cluster состояние модулей network и database останутся полностью нетронутыми, что обеспечивает спокойный сон запускающего Terragrunt. 2. Согласованный рефакторинг в нескольких окружениях При использовании иерархической структуры live/ один и тот же модуль может использоваться в dev, stage и prod. Блок moved, добавленный в исходный код модуля, будет автоматически применен во всех окружениях при их следующем обновлении. Terragrunt позволяет выполнить массовую проверку с помощью terragrunt run-all plan и после аппрува terragrunt run-all apply, обеспечивая консистентность изменений во всех средах [7, 20]. 3. Управление зависимостями во время рефакторинга Если рефакторинг изменяет outputs модуля (например, переименовывает выходную переменную vpc_id в network_id), зависимые модули могут временно сломаться. В Terragrunt можно решить эту проблему через mock_outputs [29]. # В конфигурации модуля приложения, зависящего от network dependency "network" { config_path = "../network" mock_outputs = { # Предполагаем, что после рефакторинга vpc_id стал называться network_id network_id = "mock-temp-id" subnet_ids = ["mock-subnet-a", "mock-subnet-b"] } mock_outputs_allowed_terraform_commands = ["plan", "validate"] } inputs = { # Используем новое имя переменной vpc_id = dependency.network.outputs.network_id } Это позволяет выполнить terragrunt plan в модуле приложения до того, как рефакторинг в модуле сети будет применен, и проверить, корректно ли обновлены ссылки на выходные данные. 6.6. Рекомендации по использованию moved Не удалять блоки moved сразу: В отличие от блоков import, блоки moved рекомендуется оставлять в коде на длительный срок, особенно в публичных модулях. Это позволяет пользователям, которые обновляются нерегулярно, бесшовно мигрировать со старых версий [21]. Использовать модули публичные: Включение блоков moved в публичные и внутренние модули — это золотой стандарт, который обеспечивает обратную совместимость и беспроблемное обновление для консьюмеров модуля [6, 21]. Всегда внимательно проверять план (plan): Перед применением рефакторинга с помощью terraform apply или terragrunt apply обязательно нужно запустить terraform plan. Убедиться, что в выводе присутствует только запись о перемещении ресурсов (# ... has moved to ...) и отсутствуют операции destroy [16, 21]. 6.7. Практический пример: комплексный рефакторинг Рассмотрим сценарий, где необходимо переименовать несколько ресурсов и перенести конфигурацию сети в отдельный модуль. Исходная конфигурация: # main.tf (устаревший монолит) resource "yandex_vpc_network" "main" { name = "prod-net" } resource "yandex_vpc_subnet" "private" { name = "private-subnet" v4_cidr_blocks = ["10.0.1.0/24"] } resource "yandex_compute_instance" "app" { name = "app-old-name" # ... ссылка на subnet: yandex_vpc_subnet.private.id } Целевая конфигурация после рефакторинга: # Модуль: modules/network/main.tf resource "yandex_vpc_network" "this" { name = var.name } resource "yandex_vpc_subnet" "private" { name = "${var.name}-private" v4_cidr_blocks = var.private_subnets } # Корневой main.tf module "network" { source = "./modules/network" name = "prod-network" private_subnets = ["10.0.1.0/24"] } resource "yandex_compute_instance" "application_server" { name = "prod-app-01" # ... ссылка теперь на выход модуля: module.network.private_subnet_id } Блоки moved для безопасного перехода: # В корневом main.tf, рядом с вызовом модуля moved { from = yandex_vpc_network.main to = module.network.yandex_vpc_network.this } moved { from = yandex_vpc_subnet.private to = module.network.yandex_vpc_subnet.private } moved { from = yandex_compute_instance.app to = yandex_compute_instance.application_server } После выполнения terraform apply все существующие ресурсы будут корректно перенесены в стейт без их пересоздания в облаке.