commit cd68e73e401bf4d8edca584c3df8ec0e77d4339c Author: kochetkov.s Date: Mon Jan 19 15:15:14 2026 +0300 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b21fa44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Terraform +.terraform/ +.terraform.lock.hcl +*.tfstate +*.tfstate.* +*.tfplan +*.tfplan.* +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.terraformrc +terraform.rc + +# Terragrunt +.terragrunt-cache/ +terragrunt.hcl.backup + +# Generated files +.gitlab-ci.generated.yml + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..18b1dc5 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,40 @@ +# Основной GitLab CI пайплайн для Terraform инфраструктуры +# Использует downstream pipelines для динамической генерации джоб + +stages: + - generate + - trigger + +# Генерация динамического пайплайна на основе структуры live/ +generate-pipeline: + stage: generate + image: alpine:latest + before_script: + - apk add --no-cache bash findutils + script: + - ./scripts/generate-pipeline.sh + artifacts: + paths: + - .gitlab-ci.generated.yml + expire_in: 1 hour + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"' + - if: '$CI_COMMIT_BRANCH =~ /^feature\/.*/' + tags: + - yc + +# Запуск downstream пайплайна с сгенерированной конфигурацией +trigger-downstream: + stage: trigger + trigger: + include: + - artifact: .gitlab-ci.generated.yml + job: generate-pipeline + strategy: depend + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"' + - if: '$CI_COMMIT_BRANCH =~ /^feature\/.*/' + tags: + - yc diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a87a8a --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# Terraform Infrastructure for Pulse Project + +Этот репозиторий содержит инфраструктуру как код (IaC) для проекта Pulse, развернутого в Yandex Cloud. + +## Структура репозитория + +``` +. +├── modules/ # Переиспользуемые Terraform модули +│ ├── yc-s3/ # Модуль для создания S3 бакета +│ ├── yc-postgresql/ # Модуль для создания PostgreSQL кластера +│ ├── k8s-namespace/ # Модуль для создания Kubernetes namespace +│ └── k8s-secrets/ # Модуль для создания Kubernetes секретов +├── live/ # Живая инфраструктура +│ ├── terragrunt.hcl # Глобальная конфигурация Terragrunt +│ ├── stage/ # Окружение stage +│ │ ├── env.hcl # Конфигурация окружения +│ │ ├── namespace/ # Namespace pulse +│ │ ├── s3/ # S3 бакет pulse +│ │ ├── postgresql/ # PostgreSQL база данных +│ │ └── secrets/ # Kubernetes секреты +│ ├── prod/ # Окружение production +│ └── preprod/ # Окружение preprod +├── scripts/ # Вспомогательные скрипты +│ └── generate-pipeline.sh # Генератор GitLab CI пайплайна +└── .gitlab-ci.yml # Основной GitLab CI конфигурация +``` + +## Требования + +- Terraform >= 1.0 +- Terragrunt >= 0.50.0 +- Доступ к Yandex Cloud +- Доступ к Kubernetes кластеру + +## Переменные окружения + +Для работы с инфраструктурой необходимо установить следующие переменные окружения: + +### Yandex Cloud +- `YC_TOKEN` - токен для доступа к Yandex Cloud API +- `YC_CLOUD_ID` - ID облака +- `YC_FOLDER_ID` - ID каталога (или `YC_STAGE_FOLDER_ID`, `YC_PROD_FOLDER_ID`, `YC_PREPROD_FOLDER_ID`) + +### Terraform State +- `TF_STATE_BUCKET` - имя S3 бакета для хранения состояния Terraform +- `TF_STATE_DYNAMODB_ENDPOINT` - endpoint YDB для блокировок состояния +- `TF_STATE_DYNAMODB_TABLE` - имя таблицы YDB для блокировок + +### Kubernetes +- `KUBECONFIG` - путь к конфигурации Kubernetes (по умолчанию `~/.kube/config`) +- `KUBE_CONTEXT` - контекст Kubernetes (опционально) + +### Docker Registry +- `DOCKER_REGISTRY_URL` - URL Docker registry (по умолчанию `cr.yandex`) +- `DOCKER_REGISTRY_USERNAME` - имя пользователя Docker registry +- `DOCKER_REGISTRY_PASSWORD` - пароль или токен Docker registry + +### Сеть +- `YC_NETWORK_ID` - ID сети для PostgreSQL +- `YC_SUBNET_ID` - ID подсети для PostgreSQL + +## Использование + +### Локальная разработка + +1. Перейдите в директорию нужного компонента: +```bash +cd live/stage/namespace +``` + +2. Инициализируйте Terragrunt: +```bash +terragrunt init +``` + +3. Просмотрите план изменений: +```bash +terragrunt plan +``` + +4. Примените изменения: +```bash +terragrunt apply +``` + +### Развертывание всех компонентов + +Для развертывания всех компонентов окружения используйте: + +```bash +cd live/stage +terragrunt run-all apply +``` + +## GitLab CI + +Проект использует динамическую генерацию GitLab CI пайплайнов через downstream pipelines. При каждом коммите: + +1. Запускается джоба `generate-pipeline`, которая сканирует структуру `live/` и создает джобы для каждого компонента +2. Запускается downstream пайплайн с сгенерированными джобами + +Для каждого компонента создаются три джобы: +- `validate-{env}-{component}` - валидация конфигурации +- `plan-{env}-{component}` - планирование изменений +- `apply-{env}-{component}` - применение изменений (manual для prod) + +## Созданные ресурсы + +### Stage окружение + +- **Namespace**: `pulse` в Kubernetes +- **S3 бакет**: `pulse` с публичным доступом +- **PostgreSQL**: кластер `pulse-pg-stage` с базой `pulse_db` и пользователем `pulse` +- **Секреты Kubernetes**: + - `dockerhub` - доступ к Yandex Container Registry + - `pulse-s3-secret` - ключи доступа к S3 + - `pulse-postgresql-secret` - параметры подключения к PostgreSQL + +## Безопасность + +⚠️ **Важно**: Все секреты (пароли, ключи доступа) хранятся в Terraform state в открытом виде. Убедитесь, что: + +- Бэкенд для state зашифрован (AES-256) +- Доступ к state строго ограничен +- Используется блокировка состояния через YDB + +## Лицензия + +Внутренний проект компании. diff --git a/live/stage/env.hcl b/live/stage/env.hcl new file mode 100644 index 0000000..324e221 --- /dev/null +++ b/live/stage/env.hcl @@ -0,0 +1,12 @@ +# Конфигурация для окружения stage +locals { + environment = "stage" + folder_id = get_env("YC_STAGE_FOLDER_ID", "") + + # Общие параметры для всех компонентов в stage + common_tags = { + Environment = "stage" + Project = "pulse" + ManagedBy = "terraform" + } +} diff --git a/live/stage/namespace/terragrunt.hcl b/live/stage/namespace/terragrunt.hcl new file mode 100644 index 0000000..1b3cc1f --- /dev/null +++ b/live/stage/namespace/terragrunt.hcl @@ -0,0 +1,28 @@ +# Включение корневой конфигурации +include "root" { + path = find_in_parent_folders() +} + +# Включение конфигурации окружения +include "env" { + path = find_in_parent_folders("env.hcl") + expose = true + merge_strategy = "deep" +} + +# Путь к модулю +terraform { + source = "${get_parent_terragrunt_dir()}/../../modules//k8s-namespace" +} + +# Входные переменные +inputs = { + namespace_name = "pulse" + labels = { + environment = local.environment + project = "pulse" + } + annotations = { + "managed-by" = "terraform" + } +} diff --git a/live/stage/postgresql/terragrunt.hcl b/live/stage/postgresql/terragrunt.hcl new file mode 100644 index 0000000..77641ed --- /dev/null +++ b/live/stage/postgresql/terragrunt.hcl @@ -0,0 +1,33 @@ +# Включение корневой конфигурации +include "root" { + path = find_in_parent_folders() +} + +# Включение конфигурации окружения +include "env" { + path = find_in_parent_folders("env.hcl") + expose = true + merge_strategy = "deep" +} + +# Путь к модулю +terraform { + source = "${get_parent_terragrunt_dir()}/../../modules//yc-postgresql" +} + +# Входные переменные +inputs = { + cluster_name = "pulse-pg-${local.environment}" + folder_id = local.folder_id + network_id = get_env("YC_NETWORK_ID", "") + subnet_id = get_env("YC_SUBNET_ID", "") + zone = "ru-central1-a" + environment = "PRODUCTION" + postgresql_version = "15" + resource_preset_id = "s2.micro" + disk_type_id = "network-ssd" + disk_size = 10 + database_name = "pulse_db" + database_user = "pulse" + deletion_protection = false +} diff --git a/live/stage/s3/terragrunt.hcl b/live/stage/s3/terragrunt.hcl new file mode 100644 index 0000000..6c272de --- /dev/null +++ b/live/stage/s3/terragrunt.hcl @@ -0,0 +1,23 @@ +# Включение корневой конфигурации +include "root" { + path = find_in_parent_folders() +} + +# Включение конфигурации окружения +include "env" { + path = find_in_parent_folders("env.hcl") + expose = true + merge_strategy = "deep" +} + +# Путь к модулю +terraform { + source = "${get_parent_terragrunt_dir()}/../../modules//yc-s3" +} + +# Входные переменные +inputs = { + bucket_name = "pulse" + folder_id = local.folder_id + versioning_enabled = false +} diff --git a/live/stage/secrets/terragrunt.hcl b/live/stage/secrets/terragrunt.hcl new file mode 100644 index 0000000..4897c6c --- /dev/null +++ b/live/stage/secrets/terragrunt.hcl @@ -0,0 +1,72 @@ +# Включение корневой конфигурации +include "root" { + path = find_in_parent_folders() +} + +# Включение конфигурации окружения +include "env" { + path = find_in_parent_folders("env.hcl") + expose = true + merge_strategy = "deep" +} + +# Зависимости от других компонентов +dependency "namespace" { + config_path = "../namespace" + + mock_outputs = { + name = "pulse" + } + mock_outputs_allowed_terraform_commands = ["validate", "plan"] +} + +dependency "s3" { + config_path = "../s3" + + mock_outputs = { + bucket_name = "pulse" + access_key = "mock-access-key" + secret_key = "mock-secret-key" + } + mock_outputs_allowed_terraform_commands = ["validate", "plan"] +} + +dependency "postgresql" { + config_path = "../postgresql" + + mock_outputs = { + host = "mock-host.example.com" + database_name = "pulse_db" + database_user = "pulse" + password = "mock-password" + } + mock_outputs_allowed_terraform_commands = ["validate", "plan"] +} + +# Путь к модулю +terraform { + source = "${get_parent_terragrunt_dir()}/../../modules//k8s-secrets" +} + +# Входные переменные +inputs = { + namespace = dependency.namespace.outputs.name + + # Docker Hub (Yandex Registry) credentials + docker_registry_url = get_env("DOCKER_REGISTRY_URL", "cr.yandex") + docker_registry_username = get_env("DOCKER_REGISTRY_USERNAME", "") + docker_registry_password = get_env("DOCKER_REGISTRY_PASSWORD", "") + + # S3 credentials from dependency + s3_access_key = dependency.s3.outputs.access_key + s3_secret_key = dependency.s3.outputs.secret_key + s3_bucket_name = dependency.s3.outputs.bucket_name + s3_endpoint = "https://storage.yandexcloud.net" + + # PostgreSQL credentials from dependency + db_host = dependency.postgresql.outputs.host + db_port = dependency.postgresql.outputs.port + db_name = dependency.postgresql.outputs.database_name + db_user = dependency.postgresql.outputs.database_user + db_password = dependency.postgresql.outputs.password +} diff --git a/live/terragrunt.hcl b/live/terragrunt.hcl new file mode 100644 index 0000000..824207a --- /dev/null +++ b/live/terragrunt.hcl @@ -0,0 +1,63 @@ +# Глобальная конфигурация Terragrunt +# Настройка удаленного бэкенда для хранения состояния + +remote_state { + backend = "s3" + generate = { + path = "backend.tf" + if_exists = "overwrite_terragrunt" + } + config = { + endpoint = "storage.yandexcloud.net" + bucket = get_env("TF_STATE_BUCKET", "terraform-state-pulse") + 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 = get_env("TF_STATE_DYNAMODB_ENDPOINT", "") + dynamodb_table = get_env("TF_STATE_DYNAMODB_TABLE", "terraform-locks") + } +} + +# Генерация общего провайдера для Yandex Cloud +generate "provider" { + path = "provider.tf" + if_exists = "overwrite_terragrunt" + contents = < "$OUTPUT_FILE" << 'EOF' +# Автоматически сгенерированный файл GitLab CI +# НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ! Этот файл генерируется скриптом scripts/generate-pipeline.sh + +stages: + - validate + - plan + - apply + +EOF + +# Поиск всех terragrunt.hcl файлов в live/ +find live -name "terragrunt.hcl" -not -path "*/.terragrunt-cache/*" | sort | while read -r config_file; do + # Извлечение пути компонента + component_dir=$(dirname "$config_file") + relative_path=$(echo "$component_dir" | sed 's|^live/||') + + # Парсинг окружения и компонента из пути + # Формат: live/{env}/{component}/terragrunt.hcl + env=$(echo "$relative_path" | cut -d'/' -f1) + component=$(echo "$relative_path" | cut -d'/' -f2) + + # Пропускаем если это не компонент (например, env.hcl) + if [ -z "$component" ] || [ "$component" = "$env" ]; then + continue + fi + + # Формирование имени джобы + job_prefix="${env}-${component}" + + # Определение переменных окружения для Yandex Cloud + case "$env" in + stage) + folder_var="YC_STAGE_FOLDER_ID" + ;; + prod) + folder_var="YC_PROD_FOLDER_ID" + ;; + preprod) + folder_var="YC_PREPROD_FOLDER_ID" + ;; + *) + folder_var="YC_FOLDER_ID" + ;; + esac + + # Джоба validate + cat >> "$OUTPUT_FILE" << EOF +validate-${job_prefix}: + stage: validate + variables: + TG_ROOT: "${component_dir}" + ENVIRONMENT: "${env}" + YC_FOLDER_ID: "\${${folder_var}}" + before_script: + - apk add --no-cache curl unzip + - | + if [ ! -f /usr/local/bin/terragrunt ]; then + TERRAFORM_VERSION=1.6.0 + TERRAGRUNT_VERSION=0.50.0 + curl -fsSL https://releases.hashicorp.com/terraform/\${TERRAFORM_VERSION}/terraform_\${TERRAFORM_VERSION}_linux_amd64.zip -o terraform.zip + unzip terraform.zip -d /usr/local/bin/ + chmod +x /usr/local/bin/terraform + curl -fsSL https://github.com/gruntwork-io/terragrunt/releases/download/v\${TERRAGRUNT_VERSION}/terragrunt_linux_amd64 -o /usr/local/bin/terragrunt + chmod +x /usr/local/bin/terragrunt + rm -f terraform.zip + fi + - cd \${TG_ROOT} + script: + - terragrunt init -reconfigure -input=false --terragrunt-non-interactive + - terragrunt validate-inputs --terragrunt-non-interactive + - terragrunt validate --terragrunt-non-interactive + rules: + - if: '\$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '\$CI_COMMIT_BRANCH == "master" || \$CI_COMMIT_BRANCH == "main"' + tags: + - yc + interruptible: true + +EOF + + # Джоба plan + cat >> "$OUTPUT_FILE" << EOF +plan-${job_prefix}: + stage: plan + variables: + TG_ROOT: "${component_dir}" + ENVIRONMENT: "${env}" + YC_FOLDER_ID: "\${${folder_var}}" + before_script: + - apk add --no-cache curl unzip + - | + if [ ! -f /usr/local/bin/terragrunt ]; then + TERRAFORM_VERSION=1.6.0 + TERRAGRUNT_VERSION=0.50.0 + curl -fsSL https://releases.hashicorp.com/terraform/\${TERRAFORM_VERSION}/terraform_\${TERRAFORM_VERSION}_linux_amd64.zip -o terraform.zip + unzip terraform.zip -d /usr/local/bin/ + chmod +x /usr/local/bin/terraform + curl -fsSL https://github.com/gruntwork-io/terragrunt/releases/download/v\${TERRAGRUNT_VERSION}/terragrunt_linux_amd64 -o /usr/local/bin/terragrunt + chmod +x /usr/local/bin/terragrunt + rm -f terraform.zip + fi + - cd \${TG_ROOT} + script: + - terragrunt init -reconfigure -input=false --terragrunt-non-interactive + - terragrunt plan -input=false --terragrunt-non-interactive -out=tfplan + rules: + - if: '\$CI_COMMIT_BRANCH == "master" || \$CI_COMMIT_BRANCH == "main"' + - if: '\$CI_PIPELINE_SOURCE == "merge_request_event"' + tags: + - yc + interruptible: true + artifacts: + paths: + - "\${TG_ROOT}/tfplan" + - "\${TG_ROOT}/.terragrunt-cache/**/*" + expire_in: 1 week + when: always + +EOF + + # Джоба apply (manual для prod, автоматическая для остальных) + if [ "$env" = "prod" ]; then + when_clause="manual" + else + when_clause="on_success" + fi + + cat >> "$OUTPUT_FILE" << EOF +apply-${job_prefix}: + stage: apply + variables: + TG_ROOT: "${component_dir}" + ENVIRONMENT: "${env}" + YC_FOLDER_ID: "\${${folder_var}}" + before_script: + - apk add --no-cache curl unzip + - | + if [ ! -f /usr/local/bin/terragrunt ]; then + TERRAFORM_VERSION=1.6.0 + TERRAGRUNT_VERSION=0.50.0 + curl -fsSL https://releases.hashicorp.com/terraform/\${TERRAFORM_VERSION}/terraform_\${TERRAFORM_VERSION}_linux_amd64.zip -o terraform.zip + unzip terraform.zip -d /usr/local/bin/ + chmod +x /usr/local/bin/terraform + curl -fsSL https://github.com/gruntwork-io/terragrunt/releases/download/v\${TERRAGRUNT_VERSION}/terragrunt_linux_amd64 -o /usr/local/bin/terragrunt + chmod +x /usr/local/bin/terragrunt + rm -f terraform.zip + fi + - cd \${TG_ROOT} + script: + - terragrunt init -reconfigure -input=false --terragrunt-non-interactive + - terragrunt apply -input=false --terragrunt-non-interactive tfplan + rules: + - if: '\$CI_COMMIT_BRANCH == "master" || \$CI_COMMIT_BRANCH == "main"' + when: ${when_clause} + tags: + - yc + interruptible: true + dependencies: + - plan-${job_prefix} + +EOF + +done + +echo "Pipeline generated successfully: $OUTPUT_FILE" +echo "Found components:" +find live -name "terragrunt.hcl" -not -path "*/.terragrunt-cache/*" | sed 's|live/||; s|/terragrunt.hcl||' | sort diff --git a/theory.md b/theory.md new file mode 100644 index 0000000..f5d9bf2 --- /dev/null +++ b/theory.md @@ -0,0 +1,925 @@ +Архитектурные паттерны 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 все существующие ресурсы будут корректно перенесены в стейт без их пересоздания в облаке. + + +