diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 18b1dc5..1eff7f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,3 @@ -# Основной GitLab CI пайплайн для Terraform инфраструктуры -# Использует downstream pipelines для динамической генерации джоб - stages: - generate - trigger diff --git a/README.md b/README.md deleted file mode 100644 index 32db5b1..0000000 --- a/README.md +++ /dev/null @@ -1,144 +0,0 @@ -# Terraform Infrastructure for Pulse Project - -Этот репозиторий содержит инфраструктуру как код (IaC) для проекта Pulse, развернутого в Yandex Cloud. - -## Архитектура - -Проект использует **единый конфигурационный файл** `infrastructure.yaml` (аналог `values.yaml` в Helm), который описывает всю инфраструктуру декларативно. Модули Terraform универсальны и не знают про конкретные сущности - они используются для создания ресурсов, описанных в конфиге. - -## Структура репозитория - -``` -. -├── infrastructure.yaml # Единый конфигурационный файл (аналог values.yaml) -├── modules/ # Универсальные Terraform модули -│ ├── yc-s3/ # Модуль для создания S3 бакета -│ ├── yc-database/ # Модуль для работы с существующим PostgreSQL -│ ├── k8s-namespace/ # Модуль для создания Kubernetes namespace -│ └── k8s-secret/ # Модуль для создания Kubernetes секретов -└── live/ # Живая инфраструктура - ├── terragrunt.hcl # Глобальная конфигурация Terragrunt - └── stage/ # Окружение stage - ├── env.hcl # Конфигурация окружения - ├── namespace/ # Namespace из конфига - ├── s3/ # S3 бакет из конфига - ├── database/ # База данных из конфига - └── secrets/ # Секреты из конфига -``` - -## Конфигурационный файл - -Все ресурсы описываются в `infrastructure.yaml`: - -```yaml -environments: - stage: - namespaces: - - name: pulse - labels: {} - annotations: {} - - secrets: - - name: dockerhub - namespace: pulse - type: dockerconfigjson - # ... - - buckets: - - name: pulse - acl: public-read - # ... - - databases: - - cluster_id: pg-stage # ID существующего кластера - database: - name: pulse_db - user: - name: pulse - password_length: 32 -``` - -Каждый `terragrunt.hcl` читает этот конфиг через `yamldecode(file(...))` и извлекает нужные параметры. - -## Модули - -Все модули **универсальны** и не содержат хардкода про конкретные сущности: - -- **yc-s3** - создает S3 бакет с любыми параметрами из конфига -- **yc-database** - работает с существующим PostgreSQL кластером, создает БД и пользователя -- **k8s-namespace** - создает namespace с любыми labels/annotations -- **k8s-secret** - создает секреты с поддержкой `lifecycle.ignore_changes` - -## Особенности - -### Секреты с защитой от перезаписи - -Секреты могут быть настроены с `lifecycle.ignore_changes: true`, чтобы не перезаписываться после создания: - -```yaml -secrets: - - name: pulse-s3-secret - lifecycle: - ignore_changes: true -``` - -### Использование существующего PostgreSQL - -Модуль `yc-database` работает с **существующим** кластером PostgreSQL в Yandex Cloud. Он только создает базу данных и пользователя, не создавая новый кластер. - -### Зависимости между ресурсами - -Terragrunt автоматически управляет зависимостями через блоки `dependency`. Например, секреты зависят от namespace, s3 и database, и получают их outputs. - -## Использование - -### Локальная разработка - -1. Отредактируйте `infrastructure.yaml` для добавления/изменения ресурсов -2. Перейдите в директорию компонента: -```bash -cd live/stage/namespace -``` - -3. Инициализируйте Terragrunt: -```bash -terragrunt init -``` - -4. Просмотрите план: -```bash -terragrunt plan -``` - -5. Примените изменения: -```bash -terragrunt apply -``` - -### Развертывание всех компонентов - -```bash -cd live/stage -terragrunt run-all apply -``` - -## Переменные окружения - -- `YC_TOKEN` - токен Yandex Cloud -- `YC_CLOUD_ID` - ID облака -- `YC_FOLDER_ID` / `YC_STAGE_FOLDER_ID` - ID каталога -- `DOCKER_REGISTRY_USERNAME` / `DOCKER_REGISTRY_PASSWORD` - для секрета dockerhub -- `TF_STATE_BUCKET` - бакет для хранения state -- `TF_STATE_DYNAMODB_ENDPOINT` / `TF_STATE_DYNAMODB_TABLE` - для блокировок state - -## GitLab CI - -Проект использует динамическую генерацию GitLab CI пайплайнов через downstream pipelines. Для каждого компонента создаются джобы: validate, plan, apply. - -## Принципы - -1. **DRY** - вся конфигурация в одном файле `infrastructure.yaml` -2. **Универсальность** - модули не знают про конкретные сущности -3. **Декларативность** - описание желаемого состояния, а не шагов -4. **Идемпотентность** - повторный запуск безопасен -5. **Изоляция** - каждый компонент имеет свой state diff --git a/infrastructure.yaml b/infrastructure.yaml index 8ad26e6..a8ea989 100644 --- a/infrastructure.yaml +++ b/infrastructure.yaml @@ -1,6 +1,3 @@ -# Единый конфигурационный файл инфраструктуры -# Аналог values.yaml в Helm - environments: stage: namespaces: @@ -16,33 +13,50 @@ environments: namespace: pulse type: dockerconfigjson registry_url: cr.yandex - # username и password из переменных окружения - name: pulse-s3-secret namespace: pulse - type: opaque - keys: - access_key: "" # Заполнится из outputs модуля s3 - secret_key: "" # Заполнится из outputs модуля s3 - bucket: "" # Заполнится из outputs модуля s3 - endpoint: https://storage.yandexcloud.net + type: s3 lifecycle: - ignore_changes: true + ignore_changes: false - name: pulse-postgresql-secret + namespace: pulse + type: database + dependencies: + cluster: "c9qa2coo5ukgcg93fldm" + db: pulse_db + user: pulse + lifecycle: + ignore_changes: false + + - name: session-secret namespace: pulse type: opaque - keys: - host: "" # Заполнится из outputs модуля database - port: "6432" - database: "" # Заполнится из outputs модуля database - user: "" # Заполнится из outputs модуля database - password: "" # Заполнится из outputs модуля database + random_keys: + key: + length: 32 + special: false lifecycle: ignore_changes: true + # Пример секрета для другой базы данных (test_db/test) + - name: test-db-secret + namespace: pulse + type: database + dependencies: + cluster: "c9qa2coo5ukgcg93fldm" + db: test_db + user: test + random_keys: + session_key: + length: 32 + special: false + lifecycle: + ignore_changes: false + buckets: - - name: pulse + - name: pulse-stage acl: public-read versioning: enabled: false @@ -55,10 +69,11 @@ environments: max_age_seconds: 3600 databases: - - cluster_id: "c9q..." # ID существующего кластера PostgreSQL в YC (заменить на реальный) + - cluster_id: "c9qa2coo5ukgcg93fldm" database: name: pulse_db user: name: pulse password_length: 32 password_special: false + conn_limit: 10 diff --git a/live/backend.tf b/live/backend.tf new file mode 100644 index 0000000..31854d0 --- /dev/null +++ b/live/backend.tf @@ -0,0 +1,15 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + backend "s3" { + access_key = "" + bucket = "tfstate-terragrunt-stage" + endpoint = "storage.yandexcloud.net" + force_path_style = true + key = "./terraform.tfstate" + region = "ru-central1" + secret_key = "" + skip_credentials_validation = true + skip_metadata_api_check = true + skip_region_validation = true + } +} diff --git a/live/provider.tf b/live/provider.tf new file mode 100644 index 0000000..7e6317f --- /dev/null +++ b/live/provider.tf @@ -0,0 +1,63 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +terraform { + required_version = ">= 1.0" + + required_providers { + yandex = { + source = "yandex-cloud/yandex" + version = "~> 0.100" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.23" + } + random = { + source = "hashicorp/random" + version = "~> 3.1" + } + } +} + +provider "yandex" { + token = var.yc_token != "" ? var.yc_token : null + cloud_id = var.yc_cloud_id != "" ? var.yc_cloud_id : null + folder_id = var.yc_folder_id != "" ? var.yc_folder_id : null + zone = "ru-central1-a" +} + +provider "kubernetes" { + # Если kubeconfig_path не указан, провайдер использует дефолтный путь ~/.kube/config + # Если указан, берем только первый путь (KUBECONFIG может содержать несколько путей через :) + config_path = var.kubeconfig_path != "" ? split(":", var.kubeconfig_path)[0] : null + config_context = var.kube_context != "" ? var.kube_context : null +} + +variable "yc_token" { + type = string + default = "" + description = "Yandex Cloud token" +} + +variable "yc_cloud_id" { + type = string + default = "" + description = "Yandex Cloud ID" +} + +variable "yc_folder_id" { + type = string + default = "" + description = "Yandex Cloud folder ID" +} + +variable "kubeconfig_path" { + type = string + default = "" + description = "Path to kubeconfig file" +} + +variable "kube_context" { + type = string + default = "" + description = "Kubernetes context" +} diff --git a/live/stage/database/terragrunt.hcl b/live/stage/database/terragrunt.hcl index 6b3e943..0ba0a4d 100644 --- a/live/stage/database/terragrunt.hcl +++ b/live/stage/database/terragrunt.hcl @@ -1,37 +1,37 @@ -# Включение корневой конфигурации + include "root" { path = find_in_parent_folders() } -# Включение конфигурации окружения include "env" { path = find_in_parent_folders("env.hcl") expose = true merge_strategy = "deep" } -# Чтение конфигурации инфраструктуры locals { - infra_config = yamldecode(file("${get_parent_terragrunt_dir()}/../../infrastructure.yaml")) - env_config = local.infra_config.environments[local.environment] + repo_root = try(get_repo_root(), "${get_terragrunt_dir()}/../../..") + infra_config = yamldecode(file("${local.repo_root}/infrastructure.yaml")) + env_name = basename(dirname(get_terragrunt_dir())) + env_config = local.infra_config.environments[local.env_name] db_config = local.env_config.databases[0] # Первая БД из списка } -# Путь к модулю terraform { - source = "${get_parent_terragrunt_dir()}/../../modules//yc-database" + source = "${get_terragrunt_dir()}/../../../modules//yc-database" } -# Входные переменные из конфига inputs = { cluster_id = local.db_config.cluster_id database_name = local.db_config.database.name user_name = local.db_config.user.name password_length = try(local.db_config.user.password_length, 32) password_special = try(local.db_config.user.password_special, false) - permissions = [ - { - database_name = local.db_config.database.name - } - ] + conn_limit = try(local.db_config.user.conn_limit, 10) + + yc_token = get_env("YC_TOKEN", "") + yc_cloud_id = get_env("YC_CLOUD_ID", "") + yc_folder_id = get_env("YC_STAGE_FOLDER_ID", get_env("YC_FOLDER_ID", "")) + kubeconfig_path = get_env("KUBECONFIG", "") + kube_context = get_env("KUBE_CONTEXT", "") } diff --git a/live/stage/namespace/terragrunt.hcl b/live/stage/namespace/terragrunt.hcl index d5909ff..df1509c 100644 --- a/live/stage/namespace/terragrunt.hcl +++ b/live/stage/namespace/terragrunt.hcl @@ -1,30 +1,34 @@ -# Включение корневой конфигурации + include "root" { path = find_in_parent_folders() } -# Включение конфигурации окружения include "env" { path = find_in_parent_folders("env.hcl") expose = true merge_strategy = "deep" } -# Чтение конфигурации инфраструктуры locals { - infra_config = yamldecode(file("${get_parent_terragrunt_dir()}/../../infrastructure.yaml")) - env_config = local.infra_config.environments[local.environment] + repo_root = try(get_repo_root(), "${get_terragrunt_dir()}/../../..") + infra_config = yamldecode(file("${local.repo_root}/infrastructure.yaml")) + env_name = basename(dirname(get_terragrunt_dir())) + env_config = local.infra_config.environments[local.env_name] namespace = local.env_config.namespaces[0] # Первый namespace из списка } -# Путь к модулю terraform { - source = "${get_parent_terragrunt_dir()}/../../modules//k8s-namespace" + source = "${get_terragrunt_dir()}/../../../modules//k8s-namespace" } -# Входные переменные из конфига inputs = { namespace_name = local.namespace.name labels = local.namespace.labels annotations = local.namespace.annotations + + yc_token = get_env("YC_TOKEN", "") + yc_cloud_id = get_env("YC_CLOUD_ID", "") + yc_folder_id = get_env("YC_STAGE_FOLDER_ID", get_env("YC_FOLDER_ID", "")) + kubeconfig_path = get_env("KUBECONFIG", "") + kube_context = get_env("KUBE_CONTEXT", "") } diff --git a/live/stage/s3/terragrunt.hcl b/live/stage/s3/terragrunt.hcl index c83fcc7..6288365 100644 --- a/live/stage/s3/terragrunt.hcl +++ b/live/stage/s3/terragrunt.hcl @@ -1,32 +1,38 @@ -# Включение корневой конфигурации + include "root" { path = find_in_parent_folders() } -# Включение конфигурации окружения include "env" { path = find_in_parent_folders("env.hcl") expose = true merge_strategy = "deep" } -# Чтение конфигурации инфраструктуры locals { - infra_config = yamldecode(file("${get_parent_terragrunt_dir()}/../../infrastructure.yaml")) - env_config = local.infra_config.environments[local.environment] + repo_root = try(get_repo_root(), "${get_terragrunt_dir()}/../../..") + infra_config = yamldecode(file("${local.repo_root}/infrastructure.yaml")) + env_name = basename(dirname(get_terragrunt_dir())) + env_config = local.infra_config.environments[local.env_name] bucket = local.env_config.buckets[0] # Первый bucket из списка } -# Путь к модулю + terraform { - source = "${get_parent_terragrunt_dir()}/../../modules//yc-s3" + source = "${get_terragrunt_dir()}/../../../modules//yc-s3" } -# Входные переменные из конфига inputs = { bucket_name = local.bucket.name - folder_id = local.folder_id + folder_id = get_env("YC_STAGE_FOLDER_ID", get_env("YC_FOLDER_ID", "")) + acl = try(local.bucket.acl, "private") versioning_enabled = try(local.bucket.versioning.enabled, false) cors_enabled = try(local.bucket.cors.enabled, false) cors_config = try(local.bucket.cors, {}) + + yc_token = get_env("YC_TOKEN", "") + yc_cloud_id = get_env("YC_CLOUD_ID", "") + yc_folder_id = get_env("YC_STAGE_FOLDER_ID", get_env("YC_FOLDER_ID", "")) + kubeconfig_path = get_env("KUBECONFIG", "") + kube_context = get_env("KUBE_CONTEXT", "") } diff --git a/live/stage/secrets/terragrunt.hcl b/live/stage/secrets/terragrunt.hcl index 9dac919..16a1773 100644 --- a/live/stage/secrets/terragrunt.hcl +++ b/live/stage/secrets/terragrunt.hcl @@ -17,16 +17,18 @@ dependency "namespace" { name = "pulse" } mock_outputs_allowed_terraform_commands = ["validate", "plan"] + # При apply используем реальные outputs, не mock } dependency "s3" { config_path = "../s3" mock_outputs = { - bucket_name = "pulse" + bucket_name = "pulse-stage" access_key = "mock-access-key" secret_key = "mock-secret-key" } mock_outputs_allowed_terraform_commands = ["validate", "plan"] + # При apply используем реальные outputs, не mock } dependency "database" { @@ -38,67 +40,139 @@ dependency "database" { password = "mock-password" } mock_outputs_allowed_terraform_commands = ["validate", "plan"] + # При apply используем реальные outputs, не mock } -# Чтение конфигурации инфраструктуры +# Сертификат Yandex Cloud PostgreSQL (константа, не копируется в infrastructure.yaml) locals { - infra_config = yamldecode(file("${get_parent_terragrunt_dir()}/../../infrastructure.yaml")) - env_config = local.infra_config.environments[local.environment] + yc_postgresql_ca_cert = <<-EOT +-----BEGIN CERTIFICATE----- +MIIE3TCCAsWgAwIBAgIKPxb5sAAAAAAAFzANBgkqhkiG9w0BAQ0FADAfMR0wGwYD +VQQDExRZYW5kZXhJbnRlcm5hbFJvb3RDQTAeFw0xNzA2MjAxNjQ0MzdaFw0yNzA2 +MjAxNjU0MzdaMFUxEjAQBgoJkiaJk/IsZAEZFgJydTEWMBQGCgmSJomT8ixkARkW +BnlhbmRleDESMBAGCgmSJomT8ixkARkWAmxkMRMwEQYDVQQDEwpZYW5kZXhDTENB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqgNnjk0JKPcbsk1+KG2t +eM1AfMnEe5RkAJuBBuwVV49snhcvO1jhKBx/pCnjr6biICc1/oAFDVgU8yVYYPwp +WZ2vH3ZtscjJ/RAT/NS9OKKG7kKknhFhVYxua5xhoIQmm6usBNYYiTcWoFm1eHC8 +I9oddOLSscZYbh3unVRvt+3V+drVmUx9oSUKpqMgfysiv1MN6zB3vq9TFkbhz53E +k0tEcV+W2NnDaeFhLKy284FDKLvOdTDj1EDsSAihxl7sNEKpupNuhgyy2siOqUb+ +d5mO/CRfaAKGg3E6hDM3pEi48E506dJdjPXWfHKSvuguMLRlb2RWdVocRZuyWxOh +0QIDAQABo4HkMIHhMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRMU5uItjx+ +TOicX1+ovC1Xq2PSnzAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8E +BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSrucX/oe/mUx0zOSKE +0XbUN04tajBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vY3Jscy55YW5kZXgucnUv +WWFuZGV4SW50ZXJuYWxSb290Q0EvWWFuZGV4SW50ZXJuYWxSb290Q0EuY3JsMA0G +CSqGSIb3DQEBDQUAA4ICAQAsR5Lb4Pv2FD0Kk+4oc1GEOnehxKLsQtdV81nrU+IV +l9pr2oNMdi8lwIolvHZRllLM4Ba5AcRH6YJ5fe7AjKm+5EdSkhqVWo2UOllRCbtS +wmL50+erOAkxstSlRkO6b8x1L0MOBKv54E5YcQ/Wwt27ldSb6RkEmJBGvmxObAaf +5zc51pqSqao9tnldYaCblEQ/Zmy43FliIpa2eUJoh8DqK8bVo2gcI3wbQ32tWs9u +wvKk8fo4lAdhCwhv+QHuqau1VAY9hPU106bsFIDUmijTMxjAobKBi6CkIX6EbNHU +Jv4DzYVLlDd2y0CADdn2F6I70xpCBn5cquSGuvFbqZjQDmIHwb7WQSxadkiGRWfc +zVTnmiHjJONJJIpE2t+FOV3hc+8o98OzOtNaH2QQ9j6dnKvtIGKGFeNSDp0vXPOi +QhHiIyuB7eWx+g2whktQ74UCpGDSXYnEW3s8w5wezVWIEmouq7q4rCEkTNvJ7Ico +43AgUdPzAFS2zYktw1C+cbUALM8smvXbXrXOBzMmscjIhtXvLMrpPeh23VfdJfQB +0rN2BmRCLUE8JOV+o0k98XMm83oN+lGkL1l+hyoj3ok1uI3JrsWOcDyjOds3ptcN +KimJLm27ndjcxDNo/iA6gefMJuCxFRaqI+eF4P0jSkMgnnQqZkvLGFuHCw8eRDhm +bw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFGTCCAwGgAwIBAgIQJMM7ZIy2SYxCBgK7WcFwnjANBgkqhkiG9w0BAQ0FADAf +MR0wGwYDVQQDExRZYW5kZXhJbnRlcm5hbFJvb3RDQTAeFw0xMzAyMTExMzQxNDNa +Fw0zMzAyMTExMzUxNDJaMB8xHTAbBgNVBAMTFFlhbmRleEludGVybmFsUm9vdENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgb4xoQjBQ7oEFk8EHVGy +1pDEmPWw0Wgw5nX9RM7LL2xQWyUuEq+Lf9Dgh+O725aZ9+SO2oEs47DHHt81/fne +5N6xOftRrCpy8hGtUR/A3bvjnQgjs+zdXvcO9cTuuzzPTFSts/iZATZsAruiepMx +SGj9S1fGwvYws/yiXWNoNBz4Tu1Tlp0g+5fp/ADjnxc6DqNk6w01mJRDbx+6rlBO +aIH2tQmJXDVoFdrhmBK9qOfjxWlIYGy83TnrvdXwi5mKTMtpEREMgyNLX75UjpvO +NkZgBvEXPQq+g91wBGsWIE2sYlguXiBniQgAJOyRuSdTxcJoG8tZkLDPRi5RouWY +gxXr13edn1TRDGco2hkdtSUBlajBMSvAq+H0hkslzWD/R+BXkn9dh0/DFnxVt4XU +5JbFyd/sKV/rF4Vygfw9ssh1ZIWdqkfZ2QXOZ2gH4AEeoN/9vEfUPwqPVzL0XEZK +r4s2WjU9mE5tHrVsQOZ80wnvYHYi2JHbl0hr5ghs4RIyJwx6LEEnj2tzMFec4f7o +dQeSsZpgRJmpvpAfRTxhIRjZBrKxnMytedAkUPguBQwjVCn7+EaKiJfpu42JG8Mm ++/dHi+Q9Tc+0tX5pKOIpQMlMxMHw8MfPmUjC3AAd9lsmCtuybYoeN2IRdbzzchJ8 +l1ZuoI3gH7pcIeElfVSqSBkCAwEAAaNRME8wCwYDVR0PBAQDAgGGMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFKu5xf+h7+ZTHTM5IoTRdtQ3Ti1qMBAGCSsGAQQB +gjcVAQQDAgEAMA0GCSqGSIb3DQEBDQUAA4ICAQAVpyJ1qLjqRLC34F1UXkC3vxpO +nV6WgzpzA+DUNog4Y6RhTnh0Bsir+I+FTl0zFCm7JpT/3NP9VjfEitMkHehmHhQK +c7cIBZSF62K477OTvLz+9ku2O/bGTtYv9fAvR4BmzFfyPDoAKOjJSghD1p/7El+1 +eSjvcUBzLnBUtxO/iYXRNo7B3+1qo4F5Hz7rPRLI0UWW/0UAfVCO2fFtyF6C1iEY +/q0Ldbf3YIaMkf2WgGhnX9yH/8OiIij2r0LVNHS811apyycjep8y/NkG4q1Z9jEi +VEX3P6NEL8dWtXQlvlNGMcfDT3lmB+tS32CPEUwce/Ble646rukbERRwFfxXojpf +C6ium+LtJc7qnK6ygnYF4D6mz4H+3WaxJd1S1hGQxOb/3WVw63tZFnN62F6/nc5g +6T44Yb7ND6y3nVcygLpbQsws6HsjX65CoSjrrPn0YhKxNBscF7M7tLTW/5LK9uhk +yjRCkJ0YagpeLxfV1l1ZJZaTPZvY9+ylHnWHhzlq0FzcrooSSsp4i44DB2K7O2ID +87leymZkKUY6PMDa4GkDJx0dG4UXDhRETMf+NkYgtLJ+UIzMNskwVDcxO4kVL+Hi +Pj78bnC5yCw8P5YylR45LdxLzLO68unoXOyFz1etGXzszw8lJI9LNubYxk77mK8H +LpuQKbSbIERsmR+QqQ== +-----END CERTIFICATE----- +EOT + + repo_root = try(get_repo_root(), "${get_terragrunt_dir()}/../../..") + infra_config = yamldecode(file("${local.repo_root}/infrastructure.yaml")) + env_name = basename(dirname(get_terragrunt_dir())) + env_config = local.infra_config.environments[local.env_name] secrets = local.env_config.secrets } # Путь к модулю terraform { - source = "${get_parent_terragrunt_dir()}/../../modules//k8s-secret" + source = "${get_terragrunt_dir()}/../../../modules//k8s-secret" } -# Входные переменные из конфига с подстановкой значений из dependencies +# Входные переменные из конфига - модуль сам формирует data из зависимостей inputs = { + # Передаем секреты как есть из infrastructure.yaml, модуль сам обработает type и dependencies secrets = [ - # dockerhub секрет - { - name = local.secrets[0].name - namespace = dependency.namespace.outputs.name - secret_type = local.secrets[0].type - data = { - ".dockerconfigjson" = jsonencode({ - auths = { - "${local.secrets[0].registry_url}" = { - username = get_env("DOCKER_REGISTRY_USERNAME", "") - password = get_env("DOCKER_REGISTRY_PASSWORD", "") - auth = base64encode("${get_env("DOCKER_REGISTRY_USERNAME", "")}:${get_env("DOCKER_REGISTRY_PASSWORD", "")}") - } - } - }) - } - ignore_changes = false - }, - # pulse-s3-secret - { - name = local.secrets[1].name - namespace = dependency.namespace.outputs.name - secret_type = local.secrets[1].type - data = { - access_key = base64encode(dependency.s3.outputs.access_key) - secret_key = base64encode(dependency.s3.outputs.secret_key) - bucket = base64encode(dependency.s3.outputs.bucket_name) - endpoint = base64encode(local.secrets[1].keys.endpoint) - } - ignore_changes = try(local.secrets[1].lifecycle.ignore_changes, false) - }, - # pulse-postgresql-secret - { - name = local.secrets[2].name - namespace = dependency.namespace.outputs.name - secret_type = local.secrets[2].type - data = { - host = base64encode(dependency.database.outputs.host) - port = base64encode(local.secrets[2].keys.port) - database = base64encode(dependency.database.outputs.database_name) - user = base64encode(dependency.database.outputs.user_name) - password = base64encode(dependency.database.outputs.password) - } - ignore_changes = try(local.secrets[2].lifecycle.ignore_changes, false) + for secret in local.secrets : { + name = secret.name + namespace = dependency.namespace.outputs.name + secret_type = secret.type + registry_url = try(secret.registry_url, "") + dependencies = try(secret.dependencies, {}) + custom_keys = try(secret.custom_keys, {}) + random_keys = try(secret.random_keys, {}) + labels = try(secret.labels, {}) + annotations = try(secret.annotations, {}) + ignore_changes = try(secret.lifecycle.ignore_changes, false) } ] + + # Зависимости от других модулей + s3_outputs = { + bucket_name = dependency.s3.outputs.bucket_name + access_key = dependency.s3.outputs.access_key + secret_key = dependency.s3.outputs.secret_key + } + + # Database outputs как map, ключ = "cluster_id:database_name:user_name" + # Собираем все базы данных из infrastructure.yaml + database_outputs_map = { + # Текущая база из dependency + "${local.env_config.databases[0].cluster_id}:${local.env_config.databases[0].database.name}:${local.env_config.databases[0].user.name}" = { + host = dependency.database.outputs.host + database_name = dependency.database.outputs.database_name + user_name = dependency.database.outputs.user_name + password = dependency.database.outputs.password + } + } + + # Константы + constants = { + s3_endpoint = "https://storage.yandexcloud.net" + postgres_port = "6432" + postgres_ca = local.yc_postgresql_ca_cert + } + + # Environment variables + env_vars = { + DOCKER_REGISTRY_USERNAME = get_env("DOCKER_REGISTRY_USERNAME", "") + DOCKER_REGISTRY_PASSWORD = get_env("DOCKER_REGISTRY_PASSWORD", "") + } + + # Провайдеры (передаются в сгенерированный provider.tf) + yc_token = get_env("YC_TOKEN", "") + yc_cloud_id = get_env("YC_CLOUD_ID", "") + yc_folder_id = get_env("YC_STAGE_FOLDER_ID", get_env("YC_FOLDER_ID", "")) + kubeconfig_path = get_env("KUBECONFIG", "") + kube_context = get_env("KUBE_CONTEXT", "") } diff --git a/live/terragrunt.hcl b/live/terragrunt.hcl index 824207a..f23c988 100644 --- a/live/terragrunt.hcl +++ b/live/terragrunt.hcl @@ -9,22 +9,19 @@ remote_state { } config = { endpoint = "storage.yandexcloud.net" - bucket = get_env("TF_STATE_BUCKET", "terraform-state-pulse") + bucket = "tfstate-terragrunt-stage" key = "${path_relative_to_include()}/terraform.tfstate" region = "ru-central1" + access_key = get_env("S3_ACCESS_KEY", get_env("AWS_ACCESS_KEY_ID", "")) + secret_key = get_env("S3_SECRET_KEY", get_env("AWS_SECRET_ACCESS_KEY", "")) 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" @@ -49,15 +46,47 @@ terraform { } provider "yandex" { - token = get_env("YC_TOKEN", "") - cloud_id = get_env("YC_CLOUD_ID", "") - folder_id = get_env("YC_FOLDER_ID", "") + token = var.yc_token != "" ? var.yc_token : null + cloud_id = var.yc_cloud_id != "" ? var.yc_cloud_id : null + folder_id = var.yc_folder_id != "" ? var.yc_folder_id : null zone = "ru-central1-a" } provider "kubernetes" { - config_path = get_env("KUBECONFIG", "~/.kube/config") - config_context = get_env("KUBE_CONTEXT", "") + # Если kubeconfig_path не указан, провайдер использует дефолтный путь ~/.kube/config + # Если указан, берем только первый путь (KUBECONFIG может содержать несколько путей через :) + config_path = var.kubeconfig_path != "" ? split(":", var.kubeconfig_path)[0] : null + config_context = var.kube_context != "" ? var.kube_context : null +} + +variable "yc_token" { + type = string + default = "" + description = "Yandex Cloud token" +} + +variable "yc_cloud_id" { + type = string + default = "" + description = "Yandex Cloud ID" +} + +variable "yc_folder_id" { + type = string + default = "" + description = "Yandex Cloud folder ID" +} + +variable "kubeconfig_path" { + type = string + default = "" + description = "Path to kubeconfig file" +} + +variable "kube_context" { + type = string + default = "" + description = "Kubernetes context" } EOF } diff --git a/modules/k8s-namespace/versions.tf b/modules/k8s-namespace/versions.tf index ec92e81..0d15f94 100644 --- a/modules/k8s-namespace/versions.tf +++ b/modules/k8s-namespace/versions.tf @@ -1,10 +1,4 @@ + terraform { required_version = ">= 1.0" - - required_providers { - kubernetes = { - source = "hashicorp/kubernetes" - version = "~> 2.23" - } - } } diff --git a/modules/k8s-secret/main.tf b/modules/k8s-secret/main.tf index 4eee411..d5ea9e1 100644 --- a/modules/k8s-secret/main.tf +++ b/modules/k8s-secret/main.tf @@ -1,20 +1,128 @@ -# Универсальный модуль для создания Kubernetes секрета -# Не знает про конкретные сущности, работает с любыми данными -resource "kubernetes_secret" "this" { - metadata { - name = var.secret_name - namespace = var.namespace - labels = var.labels - annotations = var.annotations + +locals { + secrets_map = { + for idx, secret in var.secrets : secret.name => secret } - - type = var.secret_type - - data = var.data - - # Защита от перезаписи если указано в lifecycle - lifecycle { - ignore_changes = var.ignore_changes ? [data] : [] + + random_keys_flat = merge([ + for secret_name, secret in local.secrets_map : { + for key, config in try(secret.random_keys, {}) : "${secret_name}:${key}" => { + secret_name = secret_name + key = key + length = try(config.length, 32) + special = try(config.special, false) + } + } + ]...) + + database_outputs_by_secret = { + for name, secret in local.secrets_map : name => try( + var.database_outputs_map["${try(secret.dependencies.cluster, "")}:${try(secret.dependencies.db, "")}:${try(secret.dependencies.user, "")}"], + null + ) + } + + secrets_data = { + for name, secret in local.secrets_map : name => + secret.secret_type == "dockerconfigjson" ? { + ".dockerconfigjson" = jsonencode({ + auths = { + "${secret.registry_url}" = { + username = try(var.env_vars.DOCKER_REGISTRY_USERNAME, "") + password = try(var.env_vars.DOCKER_REGISTRY_PASSWORD, "") + auth = base64encode("${try(var.env_vars.DOCKER_REGISTRY_USERNAME, "")}:${try(var.env_vars.DOCKER_REGISTRY_PASSWORD, "")}") + } + } + }) + } : secret.secret_type == "s3" ? { + access_key = var.s3_outputs.access_key + secret_key = var.s3_outputs.secret_key + bucket = var.s3_outputs.bucket_name + endpoint = try(var.constants.s3_endpoint, "https://storage.yandexcloud.net") + } : secret.secret_type == "database" && local.database_outputs_by_secret[name] != null ? merge({ + host = local.database_outputs_by_secret[name].host + port = try(var.constants.postgres_port, "6432") + database = local.database_outputs_by_secret[name].database_name + user = local.database_outputs_by_secret[name].user_name + password = local.database_outputs_by_secret[name].password + "ca.crt" = try(var.constants.postgres_ca, "") + }, { + for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result + }) : merge( + try(secret.custom_keys, {}), + { + for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result + } + ) + } + + secrets_with_data = { + for name, secret in local.secrets_map : name => { + name = secret.name + namespace = secret.namespace + secret_type = secret.secret_type + labels = try(secret.labels, {}) + annotations = try(secret.annotations, {}) + ignore_changes = try(secret.ignore_changes, false) + data = try(local.secrets_data[name], {}) + } + } + + secrets_with_ignore = { + for name, secret in local.secrets_with_data : name => secret + if secret.ignore_changes + } + secrets_without_ignore = { + for name, secret in local.secrets_with_data : name => secret + if !secret.ignore_changes } } + +resource "random_password" "secrets" { + for_each = local.random_keys_flat + + length = each.value.length + special = each.value.special + upper = true + lower = true + numeric = true +} + +resource "kubernetes_secret" "with_ignore" { + for_each = local.secrets_with_ignore + + metadata { + name = each.value.name + namespace = each.value.namespace + labels = try(each.value.labels, {}) + annotations = try(each.value.annotations, {}) + } + + type = each.value.secret_type + + data = each.value.data + + lifecycle { + ignore_changes = [data] + } + + depends_on = [random_password.secrets] +} + +resource "kubernetes_secret" "without_ignore" { + for_each = local.secrets_without_ignore + + metadata { + name = each.value.name + namespace = each.value.namespace + labels = try(each.value.labels, {}) + annotations = try(each.value.annotations, {}) + } + + type = each.value.secret_type + + data = each.value.data + + depends_on = [random_password.secrets] +} diff --git a/modules/k8s-secret/outputs.tf b/modules/k8s-secret/outputs.tf index 6649706..29f7576 100644 --- a/modules/k8s-secret/outputs.tf +++ b/modules/k8s-secret/outputs.tf @@ -1,9 +1,19 @@ -output "secret_name" { - description = "Имя созданного секрета" - value = kubernetes_secret.this.metadata[0].name -} - -output "secret_id" { - description = "ID созданного секрета" - value = kubernetes_secret.this.id +output "secrets" { + description = "Информация о созданных секретах" + value = merge( + { + for name, secret in kubernetes_secret.with_ignore : name => { + id = secret.id + name = secret.metadata[0].name + namespace = secret.metadata[0].namespace + } + }, + { + for name, secret in kubernetes_secret.without_ignore : name => { + id = secret.id + name = secret.metadata[0].name + namespace = secret.metadata[0].namespace + } + } + ) } diff --git a/modules/k8s-secret/variables.tf b/modules/k8s-secret/variables.tf index 4df2ac6..d919482 100644 --- a/modules/k8s-secret/variables.tf +++ b/modules/k8s-secret/variables.tf @@ -1,38 +1,59 @@ -variable "secret_name" { - description = "Имя секрета" - type = string +variable "secrets" { + description = "Список секретов для создания" + type = list(object({ + name = string + namespace = string + secret_type = string + registry_url = optional(string, "") + dependencies = optional(object({ + cluster = optional(string, "") + db = optional(string, "") + user = optional(string, "") + }), {}) + custom_keys = optional(map(string), {}) + random_keys = optional(map(object({ + length = optional(number, 32) + special = optional(bool, false) + })), {}) # Случайные ключи + labels = optional(map(string), {}) + annotations = optional(map(string), {}) + ignore_changes = optional(bool, false) + })) } -variable "namespace" { - description = "Kubernetes namespace" - type = string +variable "s3_outputs" { + description = "Outputs от модуля S3" + type = object({ + bucket_name = optional(string, "") + access_key = optional(string, "") + secret_key = optional(string, "") + }) + default = { + bucket_name = "" + access_key = "" + secret_key = "" + } } -variable "secret_type" { - description = "Тип секрета (Opaque, kubernetes.io/dockerconfigjson, etc.)" - type = string - default = "Opaque" +variable "database_outputs_map" { + description = "Outputs от модулей Database, ключ = cluster_id:database_name:user_name" + type = map(object({ + host = string + database_name = string + user_name = string + password = string + })) + default = {} } -variable "data" { - description = "Данные секрета (map строк)" - type = map(string) +variable "constants" { + description = "Константные значения (CA сертификаты, endpoints и т.д.)" + type = map(string) + default = {} } -variable "labels" { - description = "Labels для секрета" - type = map(string) - default = {} -} - -variable "annotations" { - description = "Annotations для секрета" - type = map(string) - default = {} -} - -variable "ignore_changes" { - description = "Игнорировать изменения после создания (не перезаписывать)" - type = bool - default = false +variable "env_vars" { + description = "Environment variables" + type = map(string) + default = {} } diff --git a/modules/k8s-secret/versions.tf b/modules/k8s-secret/versions.tf index ec92e81..0d15f94 100644 --- a/modules/k8s-secret/versions.tf +++ b/modules/k8s-secret/versions.tf @@ -1,10 +1,4 @@ + terraform { required_version = ">= 1.0" - - required_providers { - kubernetes = { - source = "hashicorp/kubernetes" - version = "~> 2.23" - } - } } diff --git a/modules/k8s-secrets/main.tf b/modules/k8s-secrets/main.tf new file mode 100644 index 0000000..b461cc6 --- /dev/null +++ b/modules/k8s-secrets/main.tf @@ -0,0 +1,21 @@ + +resource "kubernetes_secret" "this" { + for_each = { + for idx, secret in var.secrets : secret.name => secret + } + + metadata { + name = each.value.name + namespace = each.value.namespace + labels = try(each.value.labels, {}) + annotations = try(each.value.annotations, {}) + } + + type = each.value.secret_type + + data = each.value.data + + lifecycle { + ignore_changes = each.value.ignore_changes ? [data] : [] + } +} diff --git a/modules/k8s-secrets/outputs.tf b/modules/k8s-secrets/outputs.tf new file mode 100644 index 0000000..e8e1d1e --- /dev/null +++ b/modules/k8s-secrets/outputs.tf @@ -0,0 +1,10 @@ +output "secrets" { + description = "Map созданных секретов" + value = { + for k, v in kubernetes_secret.this : k => { + name = v.metadata[0].name + namespace = v.metadata[0].namespace + id = v.id + } + } +} diff --git a/modules/k8s-secrets/variables.tf b/modules/k8s-secrets/variables.tf new file mode 100644 index 0000000..79531e6 --- /dev/null +++ b/modules/k8s-secrets/variables.tf @@ -0,0 +1,12 @@ +variable "secrets" { + description = "Список секретов для создания" + type = list(object({ + name = string + namespace = string + secret_type = string + data = map(string) + labels = optional(map(string)) + annotations = optional(map(string)) + ignore_changes = optional(bool, false) + })) +} diff --git a/modules/yc-database/main.tf b/modules/yc-database/main.tf index 822b2fe..41b4cd2 100644 --- a/modules/yc-database/main.tf +++ b/modules/yc-database/main.tf @@ -1,7 +1,4 @@ -# Модуль для работы с существующим PostgreSQL кластером -# Создает базу данных и пользователя в существующем кластере -# Генерация пароля для пользователя resource "random_password" "user_password" { length = var.password_length special = var.password_special @@ -10,12 +7,24 @@ resource "random_password" "user_password" { numeric = true } -# Получение информации о существующем кластере data "yandex_mdb_postgresql_cluster" "existing" { cluster_id = var.cluster_id } -# Создание базы данных +resource "yandex_mdb_postgresql_user" "this" { + cluster_id = var.cluster_id + name = var.user_name + password = random_password.user_password.result + conn_limit = var.conn_limit + + dynamic "permission" { + for_each = length(var.permissions) > 0 ? var.permissions : [] + content { + database_name = permission.value.database_name + } + } +} + resource "yandex_mdb_postgresql_database" "this" { cluster_id = var.cluster_id name = var.database_name @@ -23,6 +32,8 @@ resource "yandex_mdb_postgresql_database" "this" { lc_collate = var.lc_collate lc_type = var.lc_type + depends_on = [yandex_mdb_postgresql_user.this] + dynamic "extension" { for_each = var.extensions content { @@ -30,17 +41,3 @@ resource "yandex_mdb_postgresql_database" "this" { } } } - -# Создание пользователя -resource "yandex_mdb_postgresql_user" "this" { - cluster_id = var.cluster_id - name = var.user_name - password = random_password.user_password.result - - dynamic "permission" { - for_each = var.permissions - content { - database_name = permission.value.database_name - } - } -} diff --git a/modules/yc-database/variables.tf b/modules/yc-database/variables.tf index b006a8f..231be1d 100644 --- a/modules/yc-database/variables.tf +++ b/modules/yc-database/variables.tf @@ -50,3 +50,9 @@ variable "permissions" { })) default = [] } + +variable "conn_limit" { + description = "Лимит подключений для пользователя" + type = number + default = 10 +} diff --git a/modules/yc-database/versions.tf b/modules/yc-database/versions.tf index 9dedcc0..0d15f94 100644 --- a/modules/yc-database/versions.tf +++ b/modules/yc-database/versions.tf @@ -1,14 +1,4 @@ + terraform { required_version = ">= 1.0" - - required_providers { - yandex = { - source = "yandex-cloud/yandex" - version = "~> 0.100" - } - random = { - source = "hashicorp/random" - version = "~> 3.1" - } - } } diff --git a/modules/yc-s3/main.tf b/modules/yc-s3/main.tf index 057618c..0ddb8cf 100644 --- a/modules/yc-s3/main.tf +++ b/modules/yc-s3/main.tf @@ -21,11 +21,11 @@ resource "yandex_iam_service_account_static_access_key" "sa_key" { } # Создание S3 бакета +# Bucket создается от имени текущего пользователя (YC_TOKEN), не сервисного аккаунта +# Ключи сервисного аккаунта используются только для доступа к bucket после создания resource "yandex_storage_bucket" "this" { - bucket = var.bucket_name - access_key = yandex_iam_service_account_static_access_key.sa_key.access_key - secret_key = yandex_iam_service_account_static_access_key.sa_key.secret_key - acl = var.acl + bucket = var.bucket_name + acl = var.acl # Версионирование объектов dynamic "versioning" { @@ -46,6 +46,4 @@ resource "yandex_storage_bucket" "this" { max_age_seconds = try(var.cors_config.max_age_seconds, 3600) } } - - depends_on = [yandex_resourcemanager_folder_iam_member.storage_editor] } diff --git a/modules/yc-s3/versions.tf b/modules/yc-s3/versions.tf index d2ba21f..0d15f94 100644 --- a/modules/yc-s3/versions.tf +++ b/modules/yc-s3/versions.tf @@ -1,10 +1,4 @@ + terraform { required_version = ">= 1.0" - - required_providers { - yandex = { - source = "yandex-cloud/yandex" - version = "~> 0.100" - } - } } diff --git a/theory.md b/theory.md deleted file mode 100644 index f5d9bf2..0000000 --- a/theory.md +++ /dev/null @@ -1,925 +0,0 @@ -Архитектурные паттерны 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 все существующие ресурсы будут корректно перенесены в стейт без их пересоздания в облаке. - - -