terraform-contour-mirror/theory.md
kochetkov.s cd68e73e40 feat: Add Terraform infrastructure for pulse project
- Add modules: yc-s3, yc-postgresql, k8s-namespace, k8s-secrets
- Add live configuration for stage environment
- Add GitLab CI with downstream pipelines
- Implement best practices from theory.md
2026-01-19 15:15:14 +03:00

926 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Архитектурные паттерны 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 = <<EOF
provider "yandex" {
folder_id = "${local.folder_id}"
zone = "ru-central1-a"
}
EOF
}
В источниках отмечается, что такой подход, реализуемый Terragrunt, позволяет централизованно управлять настройками для сотен компонентов, обеспечивая согласованность и безопасность [27, 30].
4. Управление состоянием (State Management)
Файл состояния (state) является критически важным компонентом Terraform, который выполняет роль «памяти» для IaC. Этот JSON-файл содержит точное сопоставление декларативных определений в файлах .tf и реальными физическими ресурсами в облачной среде [6, 16, 24]. Без корректного файла состояния Terraform не может определить, какие ресурсы уже развернуты, какие требуют обновления, а какие должны быть удалены, что ведет к катастрофическим последствиям для инфраструктуры [6, 16, 21].
4.1. Состав и структура файла состояния
В источниках подробно описывается, что файл состояния (обычно terraform.tfstate) содержит не только перечень ресурсов, но и метаданные, необходимые для отслеживания зависимостей между ними и поддержания целостности инфраструктурного графа [6, 16, 21].
{
"version": 4,
"terraform_version": "1.6.0",
"serial": 5,
"lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"outputs": {
"vpc_id": {
"value": "enp0s9d2v1abc",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "yandex_compute_instance",
"name": "app_server",
"provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
"instances": [
{
"attributes": {
"id": "fhm0b28lgm4qabcde",
"name": "app-prod-01",
"platform_id": "standard-v3",
"resources": {
"cores": 2,
"memory": 4
},
"boot_disk": [
{
"disk_id": "fhm0b28lgm4qabcdefg"
}
]
},
"private": "bnR5cGU6IHN0cmluZwp2YWx1ZTogaGFzaGljb3JwL2xpbnV4LWFtaQ=="
}
]
}
]
}
Как разъясняется в источниках, ключевыми элементами структуры являются:
version: версия формата данных состояния, которая меняется при обновлении Terraform [21].
serial: возрастающий номер, увеличивающийся при каждом успешном применении изменений (terraform apply), что позволяет отслеживать историю изменений [6, 21].
lineage: уникальный идентификатор UUID, генерируемый при инициализации бэкенда и предотвращающий случайное смешивание разных состояний проекта [6, 16].
resources: детализированный список всех управляемых ресурсов с их полными атрибутами, включая чувствительные данные, которые могут храниться в открытом виде [6, 16, 24].
4.2. Remote Backends
Хранение файла состояния локально на ПК допустимо только в тестовых целях. В командной работе это неизбежно ведет к потере данных, конфликтам версий [3, 6, 16]. Рекомендуемой практикой является использование удаленных бэкендов (remote backends), которые обеспечивают безопасное хранение, шифрование данных, версионирование и совместный доступ к состоянию [6, 16, 23].
Как показано в официальной документации Yandex Cloud и практических примерах, для надежного хранения состояния необходимо использовать Yandex Object Storage в сочетании с Yandex Database (YDB) для локов [10, 28].
terraform {
backend "s3" {
endpoint = "storage.yandexcloud.net"
bucket = "company-name-terraform-state-prod"
key = "global/network/terraform.tfstate"
region = "ru-central1"
# Настройки для работы с Yandex Cloud S3-совместимым хранилищем
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://serverless.yandexcloud.net/ru-central1/b1g8..."
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
В источниках отмечается, что использование YDB для блокировок является обязательным для production-сред, так как предотвращает одновременное изменение состояния несколькими пользователями [10, 28].
4.3. Блокировка состояния (State Locking)
Механизм блокировки состояния предотвращает состояние race condition, когда несколько инженеров или CI/CD-пайплайнов одновременно пытаются изменить одну и ту же инфраструктуру [3, 16, 28]. Если процесс выполнения terraform apply прервется аварийно (например, из-за сетевого сбоя), блокировка может остаться активной. Для ее снятия вручную, согласно документации, используется команда:
terraform force-unlock <LOCK_ID>
В руководства=х предостерегают от частого использования этой команды и рекомендуют сначала убедиться, что ни один другой процесс не использует файл стейта [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 все существующие ресурсы будут корректно перенесены в стейт без их пересоздания в облаке.