mirror of
https://gitlab.sarex.io/infra/terraform-contour-mirror.git
synced 2026-08-05 18:31:00 +03:00
add pulse terragrunt mvp
This commit is contained in:
parent
d86dc1b732
commit
465325a679
@ -1,6 +1,3 @@
|
||||
# Основной GitLab CI пайплайн для Terraform инфраструктуры
|
||||
# Использует downstream pipelines для динамической генерации джоб
|
||||
|
||||
stages:
|
||||
- generate
|
||||
- trigger
|
||||
|
||||
144
README.md
144
README.md
@ -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
|
||||
@ -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
|
||||
|
||||
15
live/backend.tf
Normal file
15
live/backend.tf
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
63
live/provider.tf
Normal file
63
live/provider.tf
Normal file
@ -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"
|
||||
}
|
||||
@ -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", "")
|
||||
}
|
||||
|
||||
@ -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", "")
|
||||
}
|
||||
|
||||
@ -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", "")
|
||||
}
|
||||
|
||||
@ -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
|
||||
for secret in local.secrets : {
|
||||
name = secret.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)
|
||||
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", "")
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,128 @@
|
||||
# Универсальный модуль для создания Kubernetes секрета
|
||||
# Не знает про конкретные сущности, работает с любыми данными
|
||||
|
||||
resource "kubernetes_secret" "this" {
|
||||
|
||||
locals {
|
||||
secrets_map = {
|
||||
for idx, secret in var.secrets : secret.name => secret
|
||||
}
|
||||
|
||||
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 = var.secret_name
|
||||
namespace = var.namespace
|
||||
labels = var.labels
|
||||
annotations = var.annotations
|
||||
name = each.value.name
|
||||
namespace = each.value.namespace
|
||||
labels = try(each.value.labels, {})
|
||||
annotations = try(each.value.annotations, {})
|
||||
}
|
||||
|
||||
type = var.secret_type
|
||||
type = each.value.secret_type
|
||||
|
||||
data = var.data
|
||||
data = each.value.data
|
||||
|
||||
# Защита от перезаписи если указано в lifecycle
|
||||
lifecycle {
|
||||
ignore_changes = var.ignore_changes ? [data] : []
|
||||
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]
|
||||
}
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
output "secret_name" {
|
||||
description = "Имя созданного секрета"
|
||||
value = kubernetes_secret.this.metadata[0].name
|
||||
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
|
||||
}
|
||||
|
||||
output "secret_id" {
|
||||
description = "ID созданного секрета"
|
||||
value = kubernetes_secret.this.id
|
||||
},
|
||||
{
|
||||
for name, secret in kubernetes_secret.without_ignore : name => {
|
||||
id = secret.id
|
||||
name = secret.metadata[0].name
|
||||
namespace = secret.metadata[0].namespace
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 "labels" {
|
||||
description = "Labels для секрета"
|
||||
variable "constants" {
|
||||
description = "Константные значения (CA сертификаты, endpoints и т.д.)"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "annotations" {
|
||||
description = "Annotations для секрета"
|
||||
variable "env_vars" {
|
||||
description = "Environment variables"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "ignore_changes" {
|
||||
description = "Игнорировать изменения после создания (не перезаписывать)"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
modules/k8s-secrets/main.tf
Normal file
21
modules/k8s-secrets/main.tf
Normal file
@ -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] : []
|
||||
}
|
||||
}
|
||||
10
modules/k8s-secrets/outputs.tf
Normal file
10
modules/k8s-secrets/outputs.tf
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
12
modules/k8s-secrets/variables.tf
Normal file
12
modules/k8s-secrets/variables.tf
Normal file
@ -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)
|
||||
}))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,3 +50,9 @@ variable "permissions" {
|
||||
}))
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "conn_limit" {
|
||||
description = "Лимит подключений для пользователя"
|
||||
type = number
|
||||
default = 10
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,10 +21,10 @@ 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
|
||||
|
||||
# Версионирование объектов
|
||||
@ -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]
|
||||
}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
yandex = {
|
||||
source = "yandex-cloud/yandex"
|
||||
version = "~> 0.100"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
925
theory.md
925
theory.md
@ -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 = <<EOF
|
||||
provider "yandex" {
|
||||
folder_id = "${local.folder_id}"
|
||||
zone = "ru-central1-a"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
В источниках отмечается, что такой подход, реализуемый Terragrunt, позволяет централизованно управлять настройками для сотен компонентов, обеспечивая согласованность и безопасность [27, 30].
|
||||
|
||||
|
||||
4. Управление состоянием (State Management)
|
||||
|
||||
Файл состояния (state) является критически важным компонентом Terraform, который выполняет роль «памяти» для IaC. Этот JSON-файл содержит точное сопоставление декларативных определений в файлах .tf и реальными физическими ресурсами в облачной среде [6, 16, 24]. Без корректного файла состояния Terraform не может определить, какие ресурсы уже развернуты, какие требуют обновления, а какие должны быть удалены, что ведет к катастрофическим последствиям для инфраструктуры [6, 16, 21].
|
||||
|
||||
4.1. Состав и структура файла состояния
|
||||
|
||||
В источниках подробно описывается, что файл состояния (обычно terraform.tfstate) содержит не только перечень ресурсов, но и метаданные, необходимые для отслеживания зависимостей между ними и поддержания целостности инфраструктурного графа [6, 16, 21].
|
||||
|
||||
{
|
||||
"version": 4,
|
||||
"terraform_version": "1.6.0",
|
||||
"serial": 5,
|
||||
"lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"outputs": {
|
||||
"vpc_id": {
|
||||
"value": "enp0s9d2v1abc",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "yandex_compute_instance",
|
||||
"name": "app_server",
|
||||
"provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
|
||||
"instances": [
|
||||
{
|
||||
"attributes": {
|
||||
"id": "fhm0b28lgm4qabcde",
|
||||
"name": "app-prod-01",
|
||||
"platform_id": "standard-v3",
|
||||
"resources": {
|
||||
"cores": 2,
|
||||
"memory": 4
|
||||
},
|
||||
"boot_disk": [
|
||||
{
|
||||
"disk_id": "fhm0b28lgm4qabcdefg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"private": "bnR5cGU6IHN0cmluZwp2YWx1ZTogaGFzaGljb3JwL2xpbnV4LWFtaQ=="
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Как разъясняется в источниках, ключевыми элементами структуры являются:
|
||||
|
||||
version: версия формата данных состояния, которая меняется при обновлении Terraform [21].
|
||||
|
||||
serial: возрастающий номер, увеличивающийся при каждом успешном применении изменений (terraform apply), что позволяет отслеживать историю изменений [6, 21].
|
||||
|
||||
lineage: уникальный идентификатор UUID, генерируемый при инициализации бэкенда и предотвращающий случайное смешивание разных состояний проекта [6, 16].
|
||||
|
||||
resources: детализированный список всех управляемых ресурсов с их полными атрибутами, включая чувствительные данные, которые могут храниться в открытом виде [6, 16, 24].
|
||||
|
||||
4.2. Remote Backends
|
||||
|
||||
Хранение файла состояния локально на ПК допустимо только в тестовых целях. В командной работе это неизбежно ведет к потере данных, конфликтам версий [3, 6, 16]. Рекомендуемой практикой является использование удаленных бэкендов (remote backends), которые обеспечивают безопасное хранение, шифрование данных, версионирование и совместный доступ к состоянию [6, 16, 23].
|
||||
Как показано в официальной документации Yandex Cloud и практических примерах, для надежного хранения состояния необходимо использовать Yandex Object Storage в сочетании с Yandex Database (YDB) для локов [10, 28].
|
||||
|
||||
terraform {
|
||||
backend "s3" {
|
||||
endpoint = "storage.yandexcloud.net"
|
||||
bucket = "company-name-terraform-state-prod"
|
||||
key = "global/network/terraform.tfstate"
|
||||
region = "ru-central1"
|
||||
|
||||
# Настройки для работы с Yandex Cloud S3-совместимым хранилищем
|
||||
skip_region_validation = true
|
||||
skip_credentials_validation = true
|
||||
skip_requesting_account_id = true
|
||||
skip_metadata_api_check = true
|
||||
force_path_style = true
|
||||
|
||||
# Блокировка состояния через Yandex Database (YDB)
|
||||
dynamodb_endpoint = "https://serverless.yandexcloud.net/ru-central1/b1g8..."
|
||||
dynamodb_table = "terraform-state-locks"
|
||||
encrypt = true
|
||||
}
|
||||
}
|
||||
|
||||
В источниках отмечается, что использование YDB для блокировок является обязательным для production-сред, так как предотвращает одновременное изменение состояния несколькими пользователями [10, 28].
|
||||
|
||||
4.3. Блокировка состояния (State Locking)
|
||||
|
||||
Механизм блокировки состояния предотвращает состояние race condition, когда несколько инженеров или CI/CD-пайплайнов одновременно пытаются изменить одну и ту же инфраструктуру [3, 16, 28]. Если процесс выполнения terraform apply прервется аварийно (например, из-за сетевого сбоя), блокировка может остаться активной. Для ее снятия вручную, согласно документации, используется команда:
|
||||
|
||||
terraform force-unlock <LOCK_ID>
|
||||
|
||||
В руководства=х предостерегают от частого использования этой команды и рекомендуют сначала убедиться, что ни один другой процесс не использует файл стейта [16, 21].
|
||||
|
||||
4.4. Изоляция окружений (Isolation)
|
||||
|
||||
Для обеспечения надежного разделения сред разработки (dev), тестирования (stage) и production (prod) в источниках рассматриваются два основных подхода:
|
||||
|
||||
Workspaces: Позволяют использовать одну и ту же конфигурацию кода для нескольких окружений, но хранят состояния всех окружений в одном бэкенде. Этот подход может подходить для небольших проектов, но не рекомендуется для production из-за риска случайного воздействия на несколько сред и отсутствия полной изоляции [6, 23].
|
||||
|
||||
Структура каталогов (Рекомендуемый подход): Обеспечивает полную изоляцию за счет использования разных директорий и, соответственно, разных файлов состояния для каждого окружения. В сочетании с Terragrunt этот подход автоматизируется с помощью функции path_relative_to_include() для динамического формирования ключей (key) в Object Storage [7, 20, 27].
|
||||
|
||||
Пример структуры каталогов с Terragrunt:
|
||||
|
||||
live/
|
||||
├── prod/
|
||||
│ ├── ru-central1/
|
||||
│ │ ├── vpc/
|
||||
│ │ │ └── terragrunt.hcl # key = "prod/ru-central1/vpc/terraform.tfstate"
|
||||
│ │ └── k8s/
|
||||
│ │ └── terragrunt.hcl # key = "prod/ru-central1/k8s/terraform.tfstate"
|
||||
│ └── _global/
|
||||
│ └── iam/
|
||||
│ └── terragrunt.hcl # key = "prod/_global/iam/terraform.tfstate"
|
||||
└── dev/
|
||||
└── ru-central1/
|
||||
└── vpc/
|
||||
└── terragrunt.hcl # key = "dev/ru-central1/vpc/terraform.tfstate"
|
||||
|
||||
Как подчеркивается в источниках Gruntwork, такая структура минимизирует «радиус поражения» (blast radius) и обеспечивает максимальную безопасность [7, 20].
|
||||
|
||||
4.5. Работа с данными из других стейтов
|
||||
|
||||
Для создания зависимостей между изолированными компонентами инфраструктуры (например, когда модулю приложений нужен ID VPC из модуля сети) в источниках описывается два подхода:
|
||||
|
||||
Нативный подход Terraform: Использование data source terraform_remote_state для чтения выходных значений из состояния другого модуля.
|
||||
|
||||
data "terraform_remote_state" "network" {
|
||||
backend = "s3"
|
||||
config = {
|
||||
endpoint = "storage.yandexcloud.net"
|
||||
bucket = "company-name-terraform-state-prod"
|
||||
key = "prod/ru-central1/vpc/terraform.tfstate"
|
||||
region = "ru-central1"
|
||||
skip_region_validation = true
|
||||
skip_credentials_validation = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "yandex_compute_instance" "app" {
|
||||
name = "app-server"
|
||||
platform_id = "standard-v3"
|
||||
zone = "ru-central1-a"
|
||||
|
||||
network_interface {
|
||||
subnet_id = data.terraform_remote_state.network.outputs.subnet_ids[0]
|
||||
}
|
||||
}
|
||||
|
||||
Однако этот подход требует жесткого указания путей к бэкенду и может создать скрытые зависимости [6, 16].
|
||||
|
||||
Подход с использованием Terragrunt: Использование блока dependency, который автоматически управляет зависимостями и порядком развертывания.
|
||||
|
||||
# terragrunt.hcl для модуля приложения
|
||||
dependency "vpc" {
|
||||
config_path = "../vpc"
|
||||
}
|
||||
|
||||
inputs = {
|
||||
subnet_id = dependency.vpc.outputs.subnet_ids[0]
|
||||
}
|
||||
|
||||
4.6. Манипуляции со стейтом через CLI и код
|
||||
|
||||
Никогда не следует редактировать файл состояния вручную с помощью текстового редактора, так как это почти гарантированно приведет к повреждению данных и неработоспособности инфры [3, 6, 16]. Для безопасного управления состоянием предназначены CLI-команды Terraform:
|
||||
|
||||
Импорт существующих ресурсов: Привязка ресурсов, созданных вручную или другим способом, под управление Terraform.
|
||||
|
||||
terraform import yandex_compute_instance.app fhm0b28lgm4qabcde
|
||||
|
||||
Как описывается в источниках, эта команда добавляет ресурс в состояние, но не создает код, который нужно написать отдельно [6, 21].
|
||||
|
||||
Безопасный рефакторинг с moved: Современный способ переименования ресурсов или изменения их адреса без физического пересоздания.
|
||||
|
||||
# В конфигурации Terraform
|
||||
moved {
|
||||
from = yandex_compute_instance.old_name
|
||||
to = yandex_compute_instance.new_name
|
||||
}
|
||||
|
||||
Этот блок в книге Robert Hafner'а указывает Terraform на необходимость перенести состояние, а не удалять и создавать заново [21].
|
||||
|
||||
Удаление ресурса из управления Terraform: Команда state rm удаляет ресурс из состояния, но не удаляет его физически в облаке.
|
||||
|
||||
bash
|
||||
|
||||
terraform state rm yandex_compute_instance.temporary
|
||||
|
||||
Эта операция должна выполняться с крайней осторожностью!! [16, 21].
|
||||
|
||||
|
||||
Downstream Pipelines и автоматизация в CI/CD
|
||||
|
||||
При управлении крупной инфраструктурой, состоящей из сотен компонентов, классический подход с единым CI/CD-пайплайном становится неприемлемым. Его выполнение превращается в многочасовую процедуру, а ошибка в одном компоненте приводит к остановке всего процесса и сложностям в локализации проблемы. Как показывают практические кейсы, Downstream (дочерние) Pipelines решают эту проблему, превращая монолитный деплой в набор независимых, параллельных задач, что сокращает время развертывания с часов до минут и кардинально упрощает операционную деятельность [5, 29].
|
||||
|
||||
5.1. Динамическая генерация пайплайна: принцип работы и практическая реализация
|
||||
|
||||
Суть динамической генерации заключается в том, что структура пайплайна не задается жестко в YAML-файле, а создается скриптом на лету на основе актуальной файловой структуры репозитория live/, как было уже указано выше. Это обеспечивает полное соответствие между инфраструктурным кодом и процессом его доставки.
|
||||
|
||||
Контекст и исходная структура:
|
||||
Предположим, инфраструктура организована по принципу, описанному в материале Gruntworks - Folder Structure, и структурно выглядит как Окружение/Регион/Компонент:
|
||||
|
||||
live/
|
||||
├── prod/
|
||||
│ ├── ru-central1-a/
|
||||
│ │ ├── network/ # Компонент A: VPC, subnets
|
||||
│ │ │ └── terragrunt.hcl
|
||||
│ │ └── k8s-cluster/ # Компонент B: k8s кластер
|
||||
│ │ └── terragrunt.hcl
|
||||
│ └── _global/
|
||||
│ └── object-storage/ # Компонент C: s3 buckets
|
||||
│ └── terragrunt.hcl
|
||||
└── dev/... # Аналогичная структура для других каталогов
|
||||
|
||||
Шаг 1: Создание скрипта-генератора
|
||||
Скрипт generate-pipeline.sh выполняет ключевую роль. Он:
|
||||
|
||||
Сканирует всю иерархию live/ в поисках файлов terragrunt.hcl, игнорируя служебные директории (например, .terragrunt-cache/).
|
||||
|
||||
Для каждого найденного компонента извлекает из пути его метаданные: окружение (prod, dev), регион (ru-central1-a, _global) и имя компонента (network).
|
||||
|
||||
Генерирует три стандартные job GitLab CI для каждого компонента: validate, plan и apply.
|
||||
|
||||
Автоматически назначает переменные окружения (например, TG_ROOT) и, что критически важно, настраивает правило when: manual для стадии apply в production-окружениях. Это обязательная мера безопасности, чтобы production-изменения не катились автоматически.
|
||||
|
||||
#!/bin/bash
|
||||
echo "stages: [validate, plan, apply]" > .gitlab-ci.generated.yml
|
||||
|
||||
find live -name "terragrunt.hcl" -not -path "*/.terragrunt-cache/*" | while read CONFIG_FILE; do
|
||||
COMPONENT_DIR=$(dirname "$CONFIG_FILE")
|
||||
ENV=$(echo "$COMPONENT_DIR" | cut -d'/' -f2)
|
||||
REGION=$(echo "$COMPONENT_DIR" | cut -d'/' -f3)
|
||||
COMPONENT_NAME=$(basename "$COMPONENT_DIR")
|
||||
|
||||
for STAGE in validate plan apply; do
|
||||
JOB_NAME="${STAGE}-${ENV}-${REGION}-${COMPONENT_NAME}"
|
||||
cat >> .gitlab-ci.generated.yml << EOF
|
||||
${JOB_NAME}:
|
||||
stage: ${STAGE}
|
||||
variables:
|
||||
TG_ROOT: "${COMPONENT_DIR}"
|
||||
ENVIRONMENT: "${ENV}"
|
||||
script:
|
||||
- cd \${TG_ROOT}
|
||||
- terragrunt init -reconfigure -input=false
|
||||
- terragrunt \${CI_JOB_STAGE} -input=false --terragrunt-non-interactive
|
||||
EOF
|
||||
# Ключевое правило безопасности для production
|
||||
if [[ "$STAGE" == "apply" && "$ENV" == "prod" ]]; then
|
||||
echo " when: manual" >> .gitlab-ci.generated.yml
|
||||
fi
|
||||
echo "" >> .gitlab-ci.generated.yml
|
||||
done
|
||||
done
|
||||
|
||||
Шаг 2: Оркестрация в основном пайплайне
|
||||
Основной файл .gitlab-ci.yml становится минимальным и выполняет лишь две функции — запуск генератора и триггер downstream пайплайна.
|
||||
|
||||
yaml
|
||||
|
||||
stages:
|
||||
- generate
|
||||
- trigger
|
||||
|
||||
generate-config:
|
||||
stage: generate
|
||||
script: ./generate-pipeline.sh
|
||||
artifacts:
|
||||
paths:
|
||||
- .gitlab-ci.generated.yml
|
||||
expire_in: 1 hour
|
||||
|
||||
trigger-downstream:
|
||||
stage: trigger
|
||||
trigger:
|
||||
include:
|
||||
- artifact: .gitlab-ci.generated.yml
|
||||
job: generate-config
|
||||
strategy: depend
|
||||
|
||||
Следовательно, при каждом пуше в репозиторий сначала выполняется джоба generate-config, которая создает актуальный файл .gitlab-ci.generated.yml и сохраняет его как артефакт. Затем job trigger-downstream запускает новый пайплайн, используя сгенерированный файл в качестве .gitlab-ci.yml.
|
||||
|
||||
Шаг 3: Что происходит в сгенерированном downstream пайплайне
|
||||
В результате для структуры из трех компонентов будет создано 9 независимых job, организованных в трех стейджах:
|
||||
|
||||
-=ВСЕ ДЖОБЫ ВЫПОЛНЯЮТСЯ ПАРАЛЛЕЛЬНО
|
||||
validate:
|
||||
• validate-prod-ru-central1-a-network
|
||||
• validate-prod-ru-central1-a-k8s-cluster
|
||||
• validate-prod-global-object-storage
|
||||
|
||||
plan (если validate завершился успешно):
|
||||
• plan-prod-ru-central1-a-network
|
||||
• plan-prod-ru-central1-a-k8s-cluster
|
||||
• plan-prod-global-object-storage
|
||||
|
||||
Стадия 'apply' (manual для prod или manual для всех apply):
|
||||
• apply-prod-ru-central1-a-network (manual)
|
||||
• apply-prod-ru-central1-a-k8s-cluster (manual)
|
||||
• apply-prod-global-object-storage (manual)
|
||||
|
||||
А вот так будет выглядеть сгенерированный gitlab-ci для компонента network:
|
||||
|
||||
validate-prod-ru-central1-a-network:
|
||||
stage: validate
|
||||
variables:
|
||||
TG_ROOT: "live/prod/ru-central1-a/network"
|
||||
ENVIRONMENT: "prod"
|
||||
YC_FOLDER_ID: "${YC_PROD_FOLDER_ID}"
|
||||
before_script:
|
||||
- cd ${TG_ROOT}
|
||||
script:
|
||||
- terragrunt init -reconfigure --terragrunt-non-interactive
|
||||
- terragrunt validate -input=false --terragrunt-non-interactive
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
tags:
|
||||
- yc
|
||||
interruptible: true
|
||||
|
||||
plan-prod-ru-central1-a-network:
|
||||
stage: plan
|
||||
variables:
|
||||
TG_ROOT: "live/prod/ru-central1-a/network"
|
||||
ENVIRONMENT: "prod"
|
||||
YC_FOLDER_ID: "${YC_PROD_FOLDER_ID}"
|
||||
before_script:
|
||||
- cd ${TG_ROOT}
|
||||
script:
|
||||
- terragrunt init -reconfigure --terragrunt-non-interactive
|
||||
- terragrunt plan -input=false --terragrunt-non-interactive -out=tfplan
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
tags:
|
||||
- yc
|
||||
interruptible: true
|
||||
artifacts:
|
||||
paths:
|
||||
- ${TG_ROOT}/*.plan
|
||||
when: always
|
||||
expire_in: 1 week
|
||||
|
||||
apply-prod-ru-central1-a-network:
|
||||
stage: apply
|
||||
variables:
|
||||
TG_ROOT: "live/prod/ru-central1-a/network"
|
||||
ENVIRONMENT: "prod"
|
||||
YC_FOLDER_ID: "${YC_PROD_FOLDER_ID}"
|
||||
before_script:
|
||||
- cd ${TG_ROOT}
|
||||
script:
|
||||
- terragrunt init -reconfigure --terragrunt-non-interactive
|
||||
- terragrunt apply -input=false --terragrunt-non-interactive tfplan
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
when: manual
|
||||
tags:
|
||||
- yc
|
||||
interruptible: true
|
||||
|
||||
Такой подход обеспечивает максимальный параллелизм. Например, пока выполняется plan для network, одновременно может идти validate для object-storage. Стадия apply для всего production-окружения блокируется и требует ручного нажатия кнопки деплоя для каждого компонента в отдельности, что дает полный контроль над разверткой.
|
||||
|
||||
5.2. Ключевые преимущества и операционные выгоды подхода
|
||||
|
||||
Экспоненциальный рост скорости: Вместо последовательного выполнения команд для всех компонентов, они обрабатываются параллельно. Это преобразует время деплоя из линейной зависимости O(n) в константную O(1). Яркий пример — кейс компании Selectel, где внедрение Downstream Pipelines сократило время полного развертывания инфраструктуры с 8 часов до 24 минут [5].
|
||||
|
||||
Минимизация «радиуса поражения» (Blast Radius): При использовании единого пайплайна с командой terragrunt run-all ошибка в любом модуле приводит к остановке всего процесса, а откат изменений может быть проблематичным занятием. При гранулярном подходе падение джобы типа apply-prod-ru-central1-a-k8s-cluster никак не повлияет на уже завершенное или еще не начатое развертывание компонентов network или object-storage. Можно точечно исправить ошибку в одном компоненте и перезапустить только его, не затрагивая остальные стабильные части системы [5, 16].
|
||||
|
||||
Прозрачность и простой траблшутинг: В интерфейсе GitLab CI/CD каждый компонент представлен отдельной джобой с собственным логом. Это кардинально отличается от гаргантюанского вывода run-all, где логи десятков модулей идут единым потоком. При возникновении проблемы инженер мгновенно видит, в каком именно компоненте и на каком стейдже произошел сбой, и вследствие может посмотреть его лог предметно. [5, 29].
|
||||
|
||||
<езопасность и контроль: Правило when: manual для apply в production является не просто рекомендацией, а обязательным guardrail, согласно истоичникам. Оно исключает возможность автоматического применения потенциально разрушительных изменений из-за ошибки в коде или сбоя в CI-системе. Каждое изменение подтвержадется руками, что соответствует принципам GitOps и compliance-требованиям для сред, критичных к аптайму.
|
||||
|
||||
Управление зависимостями и порядком выполнения: Важным аспектом, который решается уже на уровне конфигурации Terragrunt, а не самого пайплайна, являются зависимости между компонентами. Например, k8s-cluster зависит от выходных данных network (ID подсетей). В файле terragrunt.hcl компонента k8s-cluster это описывается через блок dependency. Во время выполнения terragrunt apply в сгенерированной джобе Terragrunt автоматически прочитает состояние network из remote стейта, получит необходимые выходные переменные и передаст их в модуль. Таким образом, оркестрация зависимостей и параллелизм не конфликтуют: пайплайн может запустить джобы параллельно, а Terragrunt внутри джобы будет ждать, пока состояние зависимого компонента (network) не станет актуальным [18, 27].
|
||||
|
||||
5.3. Статический анализ и валидация
|
||||
|
||||
Динамическую генерацию можно расширить для создания предварительных стадий проверки. Например, можно создать отдельную стадию lint, которая для каждого компонента запускает:
|
||||
|
||||
terragrunt validate — проверка синтаксиса Terraform.
|
||||
|
||||
terragrunt hclfmt --terragrunt-check — проверка соответствия формату.
|
||||
|
||||
checkov -d . — статический анализ безопасности на наличие уязвимых конфигураций (например, открытых security groups, незашифрованных дисков) [5, 23].
|
||||
|
||||
Эти проверки выполняются за секунды и позволяют «отсеять» очевидные проблемы до запуска длительных операций plan и apply, экономя время и ресурсы.
|
||||
|
||||
|
||||
6. Рефакторинг инфры
|
||||
|
||||
Рефакторинг в Terraform долгое время был доовльно рискованной операцией. Изменение идентификатора ресурса в коде (например, с aws_instance.web на aws_instance.server) воспринималось TF как удаление старого объекта и создание нового, что приводило к простоям и потере данных у ресурсов с состоянием (БД, S3-бакеты) [21, 24]. С выходом версии 1.1 Terraform представил блоки moved — декларативный способ управления состоянием прямо в коде, который пришел на замену ручным и опасным командам в CLI [6, 21].
|
||||
|
||||
6.1. Деструктивный рефакторинг
|
||||
|
||||
Terraform связывает идентификатор ресурса в файлах .tf с уникальным ID в облаке через стейт [16, 21]. Если мы просто переименуем ресурс в коде, Terraform интерпретирует это как удаление ресурса и создание другого. Это приводит к печальным последствиям — удалению работающего инстанса виртуальной машины, базы данных или бакета [16, 21, 24].
|
||||
|
||||
6.2. Блок moved
|
||||
|
||||
Блок moved сообщает Terraform, что ресурс сменил свое расположение или имя внутри кода. Это позволяет перепривязать существующую запись в стейте к новому имени без негативных последствий на реальную инфраструктуру [6, 21]. Синтаксис блока прост:
|
||||
|
||||
hcl
|
||||
|
||||
moved {
|
||||
from = <СТАРЫЙ_АДРЕС_РЕСУРСА>
|
||||
to = <НОВЫЙ_АДРЕС_РЕСУРСА>
|
||||
}
|
||||
|
||||
6.3. Основные сценарии использования
|
||||
|
||||
В руководствах выделяют несколько ключевых сценариев, где блок moved становится незаменимым.
|
||||
|
||||
Сценарий 1: Простое переименование ресурса
|
||||
Наиболее частый случай — приведение имен ресурсов к единому стандарту.
|
||||
|
||||
# Было: resource "yandex_compute_instance" "vm" { ... }
|
||||
# Стало:
|
||||
resource "yandex_compute_instance" "app_server" {
|
||||
name = "production-app-01"
|
||||
platform_id = "standard-v3"
|
||||
# ... остальная конфигурация
|
||||
}
|
||||
|
||||
moved {
|
||||
from = yandex_compute_instance.vm
|
||||
to = yandex_compute_instance.app_server
|
||||
}
|
||||
|
||||
После применения terraform apply в стейте Terraform запись для ВМ будет обновлена, а сама ВМ в облаке останется нетронутой.
|
||||
|
||||
Сценарий 2: Перемещение ресурса внутрь модуля
|
||||
В процессе развития проекта монолитный код разбивается на модули. Блок moved позволяет безопасно инкапсулировать ресурс.
|
||||
|
||||
# В корневом main.tf (старая структура)
|
||||
resource "yandex_vpc_network" "this" {
|
||||
name = "my-network"
|
||||
}
|
||||
|
||||
# В новом модуле modules/network/main.tf
|
||||
resource "yandex_vpc_network" "this" {
|
||||
name = var.network_name
|
||||
}
|
||||
|
||||
# В корневом main.tf (новая структура)
|
||||
module "network" {
|
||||
source = "./modules/network"
|
||||
network_name = "my-network"
|
||||
}
|
||||
|
||||
moved {
|
||||
from = yandex_vpc_network.this
|
||||
to = module.network.yandex_vpc_network.this
|
||||
}
|
||||
|
||||
Сценарий 3: Рефакторинг вызова модулей
|
||||
Можно переименовывать не только ресурсы, но и вызовы самих модулей для улучшения читаемости кода.
|
||||
|
||||
moved {
|
||||
from = module.obscure_name_xyz
|
||||
to = module.kubernetes_cluster
|
||||
}
|
||||
|
||||
6.4. Разница между moved и terraform state mv?
|
||||
|
||||
До появления блоков moved использовали команду terraform state mv. Однако у декларативного подхода есть большие преимущества:
|
||||
|
||||
Автоматизация для всей команды: Блок moved является частью кода. Когда разработчик обновляет репозиторий и запускает terraform apply, миграция состояния происходит автоматически. Не нужно запоминать и выполнять последовательность команд вручную [6, 21].
|
||||
|
||||
Безопасность и проверка через plan: Изменения стейта отображаются в выводе terraform plan. Вы можете заранее убедиться, что Terraform планирует именно перемещение (в логе будет указано # resource has moved), а не удаление и создание (# resource will be destroyed, # resource will be created) [6, 21].
|
||||
|
||||
История изменений в Git: Факт рефакторинга фиксируется в системе контроля версий. Любой член команды, просматривая историю коммитов, увидит, когда и почему ресурс был переименован, что делает эволюцию инфраструктуры полностью прозрачной [21].
|
||||
|
||||
6.5. Интеграция рефакторинга в рабочий процесс с Terragrunt
|
||||
|
||||
Terragrunt, выступая оркестратором, накладывает строгие ограничения на структуру проекта, что делает рефакторинг более предсказуемым и безопасным [27, 30].
|
||||
|
||||
1. Изоляция состояния и минимизация Blast Radius
|
||||
Как уже было сказано, Terragrunt стимулирует дробление инфраструктуры на мелкие, независимые компоненты, каждый со своим файлом состояния [7, 20, 27].
|
||||
|
||||
Преимущество: Опять же, при ошибке в рефакторинге (например, ошибка в блоке moved) затронет состояние только одного компонента. Это предотвращает каскадные сбои во всей инфраструктуре [16, 24, 30].
|
||||
|
||||
Пример: При переименовании ресурса в модуле k8s-cluster состояние модулей network и database останутся полностью нетронутыми, что обеспечивает спокойный сон запускающего Terragrunt.
|
||||
|
||||
2. Согласованный рефакторинг в нескольких окружениях
|
||||
При использовании иерархической структуры live/ один и тот же модуль может использоваться в dev, stage и prod. Блок moved, добавленный в исходный код модуля, будет автоматически применен во всех окружениях при их следующем обновлении. Terragrunt позволяет выполнить массовую проверку с помощью terragrunt run-all plan и после аппрува terragrunt run-all apply, обеспечивая консистентность изменений во всех средах [7, 20].
|
||||
|
||||
3. Управление зависимостями во время рефакторинга
|
||||
Если рефакторинг изменяет outputs модуля (например, переименовывает выходную переменную vpc_id в network_id), зависимые модули могут временно сломаться. В Terragrunt можно решить эту проблему через mock_outputs [29].
|
||||
|
||||
# В конфигурации модуля приложения, зависящего от network
|
||||
dependency "network" {
|
||||
config_path = "../network"
|
||||
|
||||
mock_outputs = {
|
||||
# Предполагаем, что после рефакторинга vpc_id стал называться network_id
|
||||
network_id = "mock-temp-id"
|
||||
subnet_ids = ["mock-subnet-a", "mock-subnet-b"]
|
||||
}
|
||||
mock_outputs_allowed_terraform_commands = ["plan", "validate"]
|
||||
}
|
||||
|
||||
inputs = {
|
||||
# Используем новое имя переменной
|
||||
vpc_id = dependency.network.outputs.network_id
|
||||
}
|
||||
|
||||
Это позволяет выполнить terragrunt plan в модуле приложения до того, как рефакторинг в модуле сети будет применен, и проверить, корректно ли обновлены ссылки на выходные данные.
|
||||
|
||||
6.6. Рекомендации по использованию moved
|
||||
|
||||
Не удалять блоки moved сразу: В отличие от блоков import, блоки moved рекомендуется оставлять в коде на длительный срок, особенно в публичных модулях. Это позволяет пользователям, которые обновляются нерегулярно, бесшовно мигрировать со старых версий [21].
|
||||
|
||||
Использовать модули публичные: Включение блоков moved в публичные и внутренние модули — это золотой стандарт, который обеспечивает обратную совместимость и беспроблемное обновление для консьюмеров модуля [6, 21].
|
||||
|
||||
Всегда внимательно проверять план (plan): Перед применением рефакторинга с помощью terraform apply или terragrunt apply обязательно нужно запустить terraform plan. Убедиться, что в выводе присутствует только запись о перемещении ресурсов (# ... has moved to ...) и отсутствуют операции destroy [16, 21].
|
||||
|
||||
6.7. Практический пример: комплексный рефакторинг
|
||||
|
||||
Рассмотрим сценарий, где необходимо переименовать несколько ресурсов и перенести конфигурацию сети в отдельный модуль.
|
||||
|
||||
Исходная конфигурация:
|
||||
|
||||
# main.tf (устаревший монолит)
|
||||
resource "yandex_vpc_network" "main" {
|
||||
name = "prod-net"
|
||||
}
|
||||
resource "yandex_vpc_subnet" "private" {
|
||||
name = "private-subnet"
|
||||
v4_cidr_blocks = ["10.0.1.0/24"]
|
||||
}
|
||||
resource "yandex_compute_instance" "app" {
|
||||
name = "app-old-name"
|
||||
# ... ссылка на subnet: yandex_vpc_subnet.private.id
|
||||
}
|
||||
|
||||
Целевая конфигурация после рефакторинга:
|
||||
|
||||
# Модуль: modules/network/main.tf
|
||||
resource "yandex_vpc_network" "this" {
|
||||
name = var.name
|
||||
}
|
||||
resource "yandex_vpc_subnet" "private" {
|
||||
name = "${var.name}-private"
|
||||
v4_cidr_blocks = var.private_subnets
|
||||
}
|
||||
|
||||
# Корневой main.tf
|
||||
module "network" {
|
||||
source = "./modules/network"
|
||||
name = "prod-network"
|
||||
private_subnets = ["10.0.1.0/24"]
|
||||
}
|
||||
|
||||
resource "yandex_compute_instance" "application_server" {
|
||||
name = "prod-app-01"
|
||||
# ... ссылка теперь на выход модуля: module.network.private_subnet_id
|
||||
}
|
||||
|
||||
Блоки moved для безопасного перехода:
|
||||
|
||||
# В корневом main.tf, рядом с вызовом модуля
|
||||
moved {
|
||||
from = yandex_vpc_network.main
|
||||
to = module.network.yandex_vpc_network.this
|
||||
}
|
||||
moved {
|
||||
from = yandex_vpc_subnet.private
|
||||
to = module.network.yandex_vpc_subnet.private
|
||||
}
|
||||
moved {
|
||||
from = yandex_compute_instance.app
|
||||
to = yandex_compute_instance.application_server
|
||||
}
|
||||
|
||||
После выполнения terraform apply все существующие ресурсы будут корректно перенесены в стейт без их пересоздания в облаке.
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user