mirror of
https://gitlab.sarex.io/infra/terraform-contour-mirror.git
synced 2026-08-05 18:31:00 +03:00
feat: Add Terraform infrastructure for pulse project
- Add modules: yc-s3, yc-postgresql, k8s-namespace, k8s-secrets - Add live configuration for stage environment - Add GitLab CI with downstream pipelines - Implement best practices from theory.md
This commit is contained in:
commit
cd68e73e40
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Terraform
|
||||||
|
.terraform/
|
||||||
|
.terraform.lock.hcl
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.*
|
||||||
|
*.tfplan
|
||||||
|
*.tfplan.*
|
||||||
|
crash.log
|
||||||
|
crash.*.log
|
||||||
|
override.tf
|
||||||
|
override.tf.json
|
||||||
|
*_override.tf
|
||||||
|
*_override.tf.json
|
||||||
|
.terraformrc
|
||||||
|
terraform.rc
|
||||||
|
|
||||||
|
# Terragrunt
|
||||||
|
.terragrunt-cache/
|
||||||
|
terragrunt.hcl.backup
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.gitlab-ci.generated.yml
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
40
.gitlab-ci.yml
Normal file
40
.gitlab-ci.yml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Основной GitLab CI пайплайн для Terraform инфраструктуры
|
||||||
|
# Использует downstream pipelines для динамической генерации джоб
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- generate
|
||||||
|
- trigger
|
||||||
|
|
||||||
|
# Генерация динамического пайплайна на основе структуры live/
|
||||||
|
generate-pipeline:
|
||||||
|
stage: generate
|
||||||
|
image: alpine:latest
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache bash findutils
|
||||||
|
script:
|
||||||
|
- ./scripts/generate-pipeline.sh
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .gitlab-ci.generated.yml
|
||||||
|
expire_in: 1 hour
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"'
|
||||||
|
- if: '$CI_COMMIT_BRANCH =~ /^feature\/.*/'
|
||||||
|
tags:
|
||||||
|
- yc
|
||||||
|
|
||||||
|
# Запуск downstream пайплайна с сгенерированной конфигурацией
|
||||||
|
trigger-downstream:
|
||||||
|
stage: trigger
|
||||||
|
trigger:
|
||||||
|
include:
|
||||||
|
- artifact: .gitlab-ci.generated.yml
|
||||||
|
job: generate-pipeline
|
||||||
|
strategy: depend
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"'
|
||||||
|
- if: '$CI_COMMIT_BRANCH =~ /^feature\/.*/'
|
||||||
|
tags:
|
||||||
|
- yc
|
||||||
130
README.md
Normal file
130
README.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# Terraform Infrastructure for Pulse Project
|
||||||
|
|
||||||
|
Этот репозиторий содержит инфраструктуру как код (IaC) для проекта Pulse, развернутого в Yandex Cloud.
|
||||||
|
|
||||||
|
## Структура репозитория
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── modules/ # Переиспользуемые Terraform модули
|
||||||
|
│ ├── yc-s3/ # Модуль для создания S3 бакета
|
||||||
|
│ ├── yc-postgresql/ # Модуль для создания PostgreSQL кластера
|
||||||
|
│ ├── k8s-namespace/ # Модуль для создания Kubernetes namespace
|
||||||
|
│ └── k8s-secrets/ # Модуль для создания Kubernetes секретов
|
||||||
|
├── live/ # Живая инфраструктура
|
||||||
|
│ ├── terragrunt.hcl # Глобальная конфигурация Terragrunt
|
||||||
|
│ ├── stage/ # Окружение stage
|
||||||
|
│ │ ├── env.hcl # Конфигурация окружения
|
||||||
|
│ │ ├── namespace/ # Namespace pulse
|
||||||
|
│ │ ├── s3/ # S3 бакет pulse
|
||||||
|
│ │ ├── postgresql/ # PostgreSQL база данных
|
||||||
|
│ │ └── secrets/ # Kubernetes секреты
|
||||||
|
│ ├── prod/ # Окружение production
|
||||||
|
│ └── preprod/ # Окружение preprod
|
||||||
|
├── scripts/ # Вспомогательные скрипты
|
||||||
|
│ └── generate-pipeline.sh # Генератор GitLab CI пайплайна
|
||||||
|
└── .gitlab-ci.yml # Основной GitLab CI конфигурация
|
||||||
|
```
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Terraform >= 1.0
|
||||||
|
- Terragrunt >= 0.50.0
|
||||||
|
- Доступ к Yandex Cloud
|
||||||
|
- Доступ к Kubernetes кластеру
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
Для работы с инфраструктурой необходимо установить следующие переменные окружения:
|
||||||
|
|
||||||
|
### Yandex Cloud
|
||||||
|
- `YC_TOKEN` - токен для доступа к Yandex Cloud API
|
||||||
|
- `YC_CLOUD_ID` - ID облака
|
||||||
|
- `YC_FOLDER_ID` - ID каталога (или `YC_STAGE_FOLDER_ID`, `YC_PROD_FOLDER_ID`, `YC_PREPROD_FOLDER_ID`)
|
||||||
|
|
||||||
|
### Terraform State
|
||||||
|
- `TF_STATE_BUCKET` - имя S3 бакета для хранения состояния Terraform
|
||||||
|
- `TF_STATE_DYNAMODB_ENDPOINT` - endpoint YDB для блокировок состояния
|
||||||
|
- `TF_STATE_DYNAMODB_TABLE` - имя таблицы YDB для блокировок
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
- `KUBECONFIG` - путь к конфигурации Kubernetes (по умолчанию `~/.kube/config`)
|
||||||
|
- `KUBE_CONTEXT` - контекст Kubernetes (опционально)
|
||||||
|
|
||||||
|
### Docker Registry
|
||||||
|
- `DOCKER_REGISTRY_URL` - URL Docker registry (по умолчанию `cr.yandex`)
|
||||||
|
- `DOCKER_REGISTRY_USERNAME` - имя пользователя Docker registry
|
||||||
|
- `DOCKER_REGISTRY_PASSWORD` - пароль или токен Docker registry
|
||||||
|
|
||||||
|
### Сеть
|
||||||
|
- `YC_NETWORK_ID` - ID сети для PostgreSQL
|
||||||
|
- `YC_SUBNET_ID` - ID подсети для PostgreSQL
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Локальная разработка
|
||||||
|
|
||||||
|
1. Перейдите в директорию нужного компонента:
|
||||||
|
```bash
|
||||||
|
cd live/stage/namespace
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Инициализируйте Terragrunt:
|
||||||
|
```bash
|
||||||
|
terragrunt init
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Просмотрите план изменений:
|
||||||
|
```bash
|
||||||
|
terragrunt plan
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Примените изменения:
|
||||||
|
```bash
|
||||||
|
terragrunt apply
|
||||||
|
```
|
||||||
|
|
||||||
|
### Развертывание всех компонентов
|
||||||
|
|
||||||
|
Для развертывания всех компонентов окружения используйте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd live/stage
|
||||||
|
terragrunt run-all apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitLab CI
|
||||||
|
|
||||||
|
Проект использует динамическую генерацию GitLab CI пайплайнов через downstream pipelines. При каждом коммите:
|
||||||
|
|
||||||
|
1. Запускается джоба `generate-pipeline`, которая сканирует структуру `live/` и создает джобы для каждого компонента
|
||||||
|
2. Запускается downstream пайплайн с сгенерированными джобами
|
||||||
|
|
||||||
|
Для каждого компонента создаются три джобы:
|
||||||
|
- `validate-{env}-{component}` - валидация конфигурации
|
||||||
|
- `plan-{env}-{component}` - планирование изменений
|
||||||
|
- `apply-{env}-{component}` - применение изменений (manual для prod)
|
||||||
|
|
||||||
|
## Созданные ресурсы
|
||||||
|
|
||||||
|
### Stage окружение
|
||||||
|
|
||||||
|
- **Namespace**: `pulse` в Kubernetes
|
||||||
|
- **S3 бакет**: `pulse` с публичным доступом
|
||||||
|
- **PostgreSQL**: кластер `pulse-pg-stage` с базой `pulse_db` и пользователем `pulse`
|
||||||
|
- **Секреты Kubernetes**:
|
||||||
|
- `dockerhub` - доступ к Yandex Container Registry
|
||||||
|
- `pulse-s3-secret` - ключи доступа к S3
|
||||||
|
- `pulse-postgresql-secret` - параметры подключения к PostgreSQL
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
⚠️ **Важно**: Все секреты (пароли, ключи доступа) хранятся в Terraform state в открытом виде. Убедитесь, что:
|
||||||
|
|
||||||
|
- Бэкенд для state зашифрован (AES-256)
|
||||||
|
- Доступ к state строго ограничен
|
||||||
|
- Используется блокировка состояния через YDB
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Внутренний проект компании.
|
||||||
12
live/stage/env.hcl
Normal file
12
live/stage/env.hcl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Конфигурация для окружения stage
|
||||||
|
locals {
|
||||||
|
environment = "stage"
|
||||||
|
folder_id = get_env("YC_STAGE_FOLDER_ID", "")
|
||||||
|
|
||||||
|
# Общие параметры для всех компонентов в stage
|
||||||
|
common_tags = {
|
||||||
|
Environment = "stage"
|
||||||
|
Project = "pulse"
|
||||||
|
ManagedBy = "terraform"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
live/stage/namespace/terragrunt.hcl
Normal file
28
live/stage/namespace/terragrunt.hcl
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Включение корневой конфигурации
|
||||||
|
include "root" {
|
||||||
|
path = find_in_parent_folders()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Включение конфигурации окружения
|
||||||
|
include "env" {
|
||||||
|
path = find_in_parent_folders("env.hcl")
|
||||||
|
expose = true
|
||||||
|
merge_strategy = "deep"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Путь к модулю
|
||||||
|
terraform {
|
||||||
|
source = "${get_parent_terragrunt_dir()}/../../modules//k8s-namespace"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Входные переменные
|
||||||
|
inputs = {
|
||||||
|
namespace_name = "pulse"
|
||||||
|
labels = {
|
||||||
|
environment = local.environment
|
||||||
|
project = "pulse"
|
||||||
|
}
|
||||||
|
annotations = {
|
||||||
|
"managed-by" = "terraform"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
live/stage/postgresql/terragrunt.hcl
Normal file
33
live/stage/postgresql/terragrunt.hcl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Включение корневой конфигурации
|
||||||
|
include "root" {
|
||||||
|
path = find_in_parent_folders()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Включение конфигурации окружения
|
||||||
|
include "env" {
|
||||||
|
path = find_in_parent_folders("env.hcl")
|
||||||
|
expose = true
|
||||||
|
merge_strategy = "deep"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Путь к модулю
|
||||||
|
terraform {
|
||||||
|
source = "${get_parent_terragrunt_dir()}/../../modules//yc-postgresql"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Входные переменные
|
||||||
|
inputs = {
|
||||||
|
cluster_name = "pulse-pg-${local.environment}"
|
||||||
|
folder_id = local.folder_id
|
||||||
|
network_id = get_env("YC_NETWORK_ID", "")
|
||||||
|
subnet_id = get_env("YC_SUBNET_ID", "")
|
||||||
|
zone = "ru-central1-a"
|
||||||
|
environment = "PRODUCTION"
|
||||||
|
postgresql_version = "15"
|
||||||
|
resource_preset_id = "s2.micro"
|
||||||
|
disk_type_id = "network-ssd"
|
||||||
|
disk_size = 10
|
||||||
|
database_name = "pulse_db"
|
||||||
|
database_user = "pulse"
|
||||||
|
deletion_protection = false
|
||||||
|
}
|
||||||
23
live/stage/s3/terragrunt.hcl
Normal file
23
live/stage/s3/terragrunt.hcl
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Включение корневой конфигурации
|
||||||
|
include "root" {
|
||||||
|
path = find_in_parent_folders()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Включение конфигурации окружения
|
||||||
|
include "env" {
|
||||||
|
path = find_in_parent_folders("env.hcl")
|
||||||
|
expose = true
|
||||||
|
merge_strategy = "deep"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Путь к модулю
|
||||||
|
terraform {
|
||||||
|
source = "${get_parent_terragrunt_dir()}/../../modules//yc-s3"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Входные переменные
|
||||||
|
inputs = {
|
||||||
|
bucket_name = "pulse"
|
||||||
|
folder_id = local.folder_id
|
||||||
|
versioning_enabled = false
|
||||||
|
}
|
||||||
72
live/stage/secrets/terragrunt.hcl
Normal file
72
live/stage/secrets/terragrunt.hcl
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Включение корневой конфигурации
|
||||||
|
include "root" {
|
||||||
|
path = find_in_parent_folders()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Включение конфигурации окружения
|
||||||
|
include "env" {
|
||||||
|
path = find_in_parent_folders("env.hcl")
|
||||||
|
expose = true
|
||||||
|
merge_strategy = "deep"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Зависимости от других компонентов
|
||||||
|
dependency "namespace" {
|
||||||
|
config_path = "../namespace"
|
||||||
|
|
||||||
|
mock_outputs = {
|
||||||
|
name = "pulse"
|
||||||
|
}
|
||||||
|
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
|
||||||
|
}
|
||||||
|
|
||||||
|
dependency "s3" {
|
||||||
|
config_path = "../s3"
|
||||||
|
|
||||||
|
mock_outputs = {
|
||||||
|
bucket_name = "pulse"
|
||||||
|
access_key = "mock-access-key"
|
||||||
|
secret_key = "mock-secret-key"
|
||||||
|
}
|
||||||
|
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
|
||||||
|
}
|
||||||
|
|
||||||
|
dependency "postgresql" {
|
||||||
|
config_path = "../postgresql"
|
||||||
|
|
||||||
|
mock_outputs = {
|
||||||
|
host = "mock-host.example.com"
|
||||||
|
database_name = "pulse_db"
|
||||||
|
database_user = "pulse"
|
||||||
|
password = "mock-password"
|
||||||
|
}
|
||||||
|
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Путь к модулю
|
||||||
|
terraform {
|
||||||
|
source = "${get_parent_terragrunt_dir()}/../../modules//k8s-secrets"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Входные переменные
|
||||||
|
inputs = {
|
||||||
|
namespace = dependency.namespace.outputs.name
|
||||||
|
|
||||||
|
# Docker Hub (Yandex Registry) credentials
|
||||||
|
docker_registry_url = get_env("DOCKER_REGISTRY_URL", "cr.yandex")
|
||||||
|
docker_registry_username = get_env("DOCKER_REGISTRY_USERNAME", "")
|
||||||
|
docker_registry_password = get_env("DOCKER_REGISTRY_PASSWORD", "")
|
||||||
|
|
||||||
|
# S3 credentials from dependency
|
||||||
|
s3_access_key = dependency.s3.outputs.access_key
|
||||||
|
s3_secret_key = dependency.s3.outputs.secret_key
|
||||||
|
s3_bucket_name = dependency.s3.outputs.bucket_name
|
||||||
|
s3_endpoint = "https://storage.yandexcloud.net"
|
||||||
|
|
||||||
|
# PostgreSQL credentials from dependency
|
||||||
|
db_host = dependency.postgresql.outputs.host
|
||||||
|
db_port = dependency.postgresql.outputs.port
|
||||||
|
db_name = dependency.postgresql.outputs.database_name
|
||||||
|
db_user = dependency.postgresql.outputs.database_user
|
||||||
|
db_password = dependency.postgresql.outputs.password
|
||||||
|
}
|
||||||
63
live/terragrunt.hcl
Normal file
63
live/terragrunt.hcl
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Глобальная конфигурация Terragrunt
|
||||||
|
# Настройка удаленного бэкенда для хранения состояния
|
||||||
|
|
||||||
|
remote_state {
|
||||||
|
backend = "s3"
|
||||||
|
generate = {
|
||||||
|
path = "backend.tf"
|
||||||
|
if_exists = "overwrite_terragrunt"
|
||||||
|
}
|
||||||
|
config = {
|
||||||
|
endpoint = "storage.yandexcloud.net"
|
||||||
|
bucket = get_env("TF_STATE_BUCKET", "terraform-state-pulse")
|
||||||
|
key = "${path_relative_to_include()}/terraform.tfstate"
|
||||||
|
region = "ru-central1"
|
||||||
|
skip_region_validation = true
|
||||||
|
skip_credentials_validation = true
|
||||||
|
skip_requesting_account_id = true
|
||||||
|
skip_metadata_api_check = true
|
||||||
|
force_path_style = true
|
||||||
|
|
||||||
|
# Настройка блокировок состояний через Yandex Database (YDB)
|
||||||
|
dynamodb_endpoint = get_env("TF_STATE_DYNAMODB_ENDPOINT", "")
|
||||||
|
dynamodb_table = get_env("TF_STATE_DYNAMODB_TABLE", "terraform-locks")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Генерация общего провайдера для Yandex Cloud
|
||||||
|
generate "provider" {
|
||||||
|
path = "provider.tf"
|
||||||
|
if_exists = "overwrite_terragrunt"
|
||||||
|
contents = <<EOF
|
||||||
|
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 = get_env("YC_TOKEN", "")
|
||||||
|
cloud_id = get_env("YC_CLOUD_ID", "")
|
||||||
|
folder_id = get_env("YC_FOLDER_ID", "")
|
||||||
|
zone = "ru-central1-a"
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "kubernetes" {
|
||||||
|
config_path = get_env("KUBECONFIG", "~/.kube/config")
|
||||||
|
config_context = get_env("KUBE_CONTEXT", "")
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
13
modules/k8s-namespace/main.tf
Normal file
13
modules/k8s-namespace/main.tf
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Создание namespace в Kubernetes
|
||||||
|
resource "kubernetes_namespace" "pulse" {
|
||||||
|
metadata {
|
||||||
|
name = var.namespace_name
|
||||||
|
labels = merge(
|
||||||
|
{
|
||||||
|
name = var.namespace_name
|
||||||
|
},
|
||||||
|
var.labels
|
||||||
|
)
|
||||||
|
annotations = var.annotations
|
||||||
|
}
|
||||||
|
}
|
||||||
9
modules/k8s-namespace/outputs.tf
Normal file
9
modules/k8s-namespace/outputs.tf
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
output "name" {
|
||||||
|
description = "Name of the created namespace"
|
||||||
|
value = kubernetes_namespace.pulse.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "id" {
|
||||||
|
description = "ID of the created namespace"
|
||||||
|
value = kubernetes_namespace.pulse.id
|
||||||
|
}
|
||||||
16
modules/k8s-namespace/variables.tf
Normal file
16
modules/k8s-namespace/variables.tf
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
variable "namespace_name" {
|
||||||
|
description = "Name of the Kubernetes namespace"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "labels" {
|
||||||
|
description = "Labels for the namespace"
|
||||||
|
type = map(string)
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "annotations" {
|
||||||
|
description = "Annotations for the namespace"
|
||||||
|
type = map(string)
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
10
modules/k8s-namespace/versions.tf
Normal file
10
modules/k8s-namespace/versions.tf
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
kubernetes = {
|
||||||
|
source = "hashicorp/kubernetes"
|
||||||
|
version = "~> 2.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
modules/k8s-secrets/main.tf
Normal file
56
modules/k8s-secrets/main.tf
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Секрет для доступа к Docker Hub (Yandex Registry)
|
||||||
|
resource "kubernetes_secret" "dockerhub" {
|
||||||
|
metadata {
|
||||||
|
name = "dockerhub"
|
||||||
|
namespace = var.namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "kubernetes.io/dockerconfigjson"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
".dockerconfigjson" = jsonencode({
|
||||||
|
auths = {
|
||||||
|
"${var.docker_registry_url}" = {
|
||||||
|
username = var.docker_registry_username
|
||||||
|
password = var.docker_registry_password
|
||||||
|
auth = base64encode("${var.docker_registry_username}:${var.docker_registry_password}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Секрет для доступа к S3
|
||||||
|
resource "kubernetes_secret" "pulse_s3" {
|
||||||
|
metadata {
|
||||||
|
name = "pulse-s3-secret"
|
||||||
|
namespace = var.namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "Opaque"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
access_key = base64encode(var.s3_access_key)
|
||||||
|
secret_key = base64encode(var.s3_secret_key)
|
||||||
|
bucket = base64encode(var.s3_bucket_name)
|
||||||
|
endpoint = base64encode(var.s3_endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Секрет для доступа к PostgreSQL
|
||||||
|
resource "kubernetes_secret" "pulse_postgresql" {
|
||||||
|
metadata {
|
||||||
|
name = "pulse-postgresql-secret"
|
||||||
|
namespace = var.namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "Opaque"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
host = base64encode(var.db_host)
|
||||||
|
port = base64encode(tostring(var.db_port))
|
||||||
|
database = base64encode(var.db_name)
|
||||||
|
user = base64encode(var.db_user)
|
||||||
|
password = base64encode(var.db_password)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
modules/k8s-secrets/outputs.tf
Normal file
14
modules/k8s-secrets/outputs.tf
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
output "dockerhub_secret_name" {
|
||||||
|
description = "Name of the dockerhub secret"
|
||||||
|
value = kubernetes_secret.dockerhub.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "s3_secret_name" {
|
||||||
|
description = "Name of the S3 secret"
|
||||||
|
value = kubernetes_secret.pulse_s3.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "postgresql_secret_name" {
|
||||||
|
description = "Name of the PostgreSQL secret"
|
||||||
|
value = kubernetes_secret.pulse_postgresql.metadata[0].name
|
||||||
|
}
|
||||||
71
modules/k8s-secrets/variables.tf
Normal file
71
modules/k8s-secrets/variables.tf
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
variable "namespace" {
|
||||||
|
description = "Kubernetes namespace where secrets will be created"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "docker_registry_url" {
|
||||||
|
description = "Docker registry URL (e.g., cr.yandex)"
|
||||||
|
type = string
|
||||||
|
default = "cr.yandex"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "docker_registry_username" {
|
||||||
|
description = "Docker registry username"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "docker_registry_password" {
|
||||||
|
description = "Docker registry password or token"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "s3_access_key" {
|
||||||
|
description = "S3 access key"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "s3_secret_key" {
|
||||||
|
description = "S3 secret key"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "s3_bucket_name" {
|
||||||
|
description = "S3 bucket name"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "s3_endpoint" {
|
||||||
|
description = "S3 endpoint URL"
|
||||||
|
type = string
|
||||||
|
default = "https://storage.yandexcloud.net"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_host" {
|
||||||
|
description = "PostgreSQL host"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_port" {
|
||||||
|
description = "PostgreSQL port"
|
||||||
|
type = number
|
||||||
|
default = 6432
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_name" {
|
||||||
|
description = "PostgreSQL database name"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_user" {
|
||||||
|
description = "PostgreSQL user"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_password" {
|
||||||
|
description = "PostgreSQL password"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
10
modules/k8s-secrets/versions.tf
Normal file
10
modules/k8s-secrets/versions.tf
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
kubernetes = {
|
||||||
|
source = "hashicorp/kubernetes"
|
||||||
|
version = "~> 2.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
modules/yc-postgresql/main.tf
Normal file
55
modules/yc-postgresql/main.tf
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Генерация пароля для PostgreSQL (32 символа без специальных)
|
||||||
|
resource "random_password" "pg_password" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
upper = true
|
||||||
|
lower = true
|
||||||
|
numeric = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Создание кластера PostgreSQL
|
||||||
|
resource "yandex_mdb_postgresql_cluster" "pulse" {
|
||||||
|
name = var.cluster_name
|
||||||
|
description = "PostgreSQL cluster for pulse project"
|
||||||
|
environment = var.environment
|
||||||
|
network_id = var.network_id
|
||||||
|
folder_id = var.folder_id
|
||||||
|
|
||||||
|
config {
|
||||||
|
version = var.postgresql_version
|
||||||
|
|
||||||
|
resources {
|
||||||
|
resource_preset_id = var.resource_preset_id
|
||||||
|
disk_type_id = var.disk_type_id
|
||||||
|
disk_size = var.disk_size
|
||||||
|
}
|
||||||
|
|
||||||
|
postgresql_config = var.postgresql_config
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
name = var.database_name
|
||||||
|
owner = var.database_user
|
||||||
|
}
|
||||||
|
|
||||||
|
user {
|
||||||
|
name = var.database_user
|
||||||
|
password = random_password.pg_password.result
|
||||||
|
permission {
|
||||||
|
database_name = var.database_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host {
|
||||||
|
zone = var.zone
|
||||||
|
subnet_id = var.subnet_id
|
||||||
|
}
|
||||||
|
|
||||||
|
maintenance_window {
|
||||||
|
type = var.maintenance_window_type
|
||||||
|
day = var.maintenance_window_day
|
||||||
|
hour = var.maintenance_window_hour
|
||||||
|
}
|
||||||
|
|
||||||
|
deletion_protection = var.deletion_protection
|
||||||
|
}
|
||||||
35
modules/yc-postgresql/outputs.tf
Normal file
35
modules/yc-postgresql/outputs.tf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
output "cluster_id" {
|
||||||
|
description = "ID of the PostgreSQL cluster"
|
||||||
|
value = yandex_mdb_postgresql_cluster.pulse.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "cluster_fqdn" {
|
||||||
|
description = "FQDN of the PostgreSQL cluster"
|
||||||
|
value = yandex_mdb_postgresql_cluster.pulse.host[0].fqdn
|
||||||
|
}
|
||||||
|
|
||||||
|
output "host" {
|
||||||
|
description = "Host address of the PostgreSQL cluster"
|
||||||
|
value = yandex_mdb_postgresql_cluster.pulse.host[0].fqdn
|
||||||
|
}
|
||||||
|
|
||||||
|
output "database_name" {
|
||||||
|
description = "Name of the database"
|
||||||
|
value = var.database_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "database_user" {
|
||||||
|
description = "Name of the database user"
|
||||||
|
value = var.database_user
|
||||||
|
}
|
||||||
|
|
||||||
|
output "password" {
|
||||||
|
description = "Password for the database user"
|
||||||
|
value = random_password.pg_password.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "port" {
|
||||||
|
description = "Port of the PostgreSQL cluster"
|
||||||
|
value = 6432
|
||||||
|
}
|
||||||
97
modules/yc-postgresql/variables.tf
Normal file
97
modules/yc-postgresql/variables.tf
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
variable "cluster_name" {
|
||||||
|
description = "Name of the PostgreSQL cluster"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "folder_id" {
|
||||||
|
description = "Yandex Cloud folder ID"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "network_id" {
|
||||||
|
description = "Network ID for the PostgreSQL cluster"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "subnet_id" {
|
||||||
|
description = "Subnet ID for the PostgreSQL cluster"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "zone" {
|
||||||
|
description = "Availability zone"
|
||||||
|
type = string
|
||||||
|
default = "ru-central1-a"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "environment" {
|
||||||
|
description = "Environment (PRODUCTION, PRESTABLE)"
|
||||||
|
type = string
|
||||||
|
default = "PRODUCTION"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgresql_version" {
|
||||||
|
description = "PostgreSQL version"
|
||||||
|
type = string
|
||||||
|
default = "15"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "resource_preset_id" {
|
||||||
|
description = "Resource preset ID"
|
||||||
|
type = string
|
||||||
|
default = "s2.micro"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "disk_type_id" {
|
||||||
|
description = "Disk type ID"
|
||||||
|
type = string
|
||||||
|
default = "network-ssd"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "disk_size" {
|
||||||
|
description = "Disk size in GB"
|
||||||
|
type = number
|
||||||
|
default = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "database_name" {
|
||||||
|
description = "Name of the database"
|
||||||
|
type = string
|
||||||
|
default = "pulse_db"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "database_user" {
|
||||||
|
description = "Name of the database user"
|
||||||
|
type = string
|
||||||
|
default = "pulse"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgresql_config" {
|
||||||
|
description = "PostgreSQL configuration"
|
||||||
|
type = map(any)
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "maintenance_window_type" {
|
||||||
|
description = "Maintenance window type"
|
||||||
|
type = string
|
||||||
|
default = "WEEKLY"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "maintenance_window_day" {
|
||||||
|
description = "Maintenance window day"
|
||||||
|
type = string
|
||||||
|
default = "SAT"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "maintenance_window_hour" {
|
||||||
|
description = "Maintenance window hour"
|
||||||
|
type = number
|
||||||
|
default = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "deletion_protection" {
|
||||||
|
description = "Enable deletion protection"
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
14
modules/yc-postgresql/versions.tf
Normal file
14
modules/yc-postgresql/versions.tf
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
yandex = {
|
||||||
|
source = "yandex-cloud/yandex"
|
||||||
|
version = "~> 0.100"
|
||||||
|
}
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = "~> 3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
modules/yc-s3/main.tf
Normal file
44
modules/yc-s3/main.tf
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Создание сервисного аккаунта для S3
|
||||||
|
resource "yandex_iam_service_account" "sa" {
|
||||||
|
name = "${var.bucket_name}-sa"
|
||||||
|
description = "Service account for ${var.bucket_name} bucket"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Назначение роли storage.editor сервисному аккаунту
|
||||||
|
resource "yandex_resourcemanager_folder_iam_member" "storage_editor" {
|
||||||
|
folder_id = var.folder_id
|
||||||
|
role = "storage.editor"
|
||||||
|
member = "serviceAccount:${yandex_iam_service_account.sa.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Создание статического ключа доступа
|
||||||
|
resource "yandex_iam_service_account_static_access_key" "sa_key" {
|
||||||
|
service_account_id = yandex_iam_service_account.sa.id
|
||||||
|
description = "Static access key for ${var.bucket_name} bucket"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Создание S3 бакета с публичным доступом
|
||||||
|
resource "yandex_storage_bucket" "pulse" {
|
||||||
|
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 = "public-read"
|
||||||
|
|
||||||
|
# Настройка CORS для внешнего доступа
|
||||||
|
cors {
|
||||||
|
allowed_headers = ["*"]
|
||||||
|
allowed_methods = ["GET", "PUT", "POST", "DELETE", "HEAD"]
|
||||||
|
allowed_origins = ["*"]
|
||||||
|
expose_headers = ["ETag"]
|
||||||
|
max_age_seconds = 3600
|
||||||
|
}
|
||||||
|
|
||||||
|
# Версионирование объектов
|
||||||
|
versioning {
|
||||||
|
enabled = var.versioning_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [yandex_resourcemanager_folder_iam_member.storage_editor]
|
||||||
|
}
|
||||||
21
modules/yc-s3/outputs.tf
Normal file
21
modules/yc-s3/outputs.tf
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
output "bucket_name" {
|
||||||
|
description = "Name of the created bucket"
|
||||||
|
value = yandex_storage_bucket.pulse.bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
output "access_key" {
|
||||||
|
description = "Access key for S3 bucket"
|
||||||
|
value = yandex_iam_service_account_static_access_key.sa_key.access_key
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "secret_key" {
|
||||||
|
description = "Secret key for S3 bucket"
|
||||||
|
value = yandex_iam_service_account_static_access_key.sa_key.secret_key
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_account_id" {
|
||||||
|
description = "ID of the service account"
|
||||||
|
value = yandex_iam_service_account.sa.id
|
||||||
|
}
|
||||||
15
modules/yc-s3/variables.tf
Normal file
15
modules/yc-s3/variables.tf
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
variable "bucket_name" {
|
||||||
|
description = "Name of the S3 bucket"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "folder_id" {
|
||||||
|
description = "Yandex Cloud folder ID"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "versioning_enabled" {
|
||||||
|
description = "Enable versioning for the bucket"
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
10
modules/yc-s3/versions.tf
Normal file
10
modules/yc-s3/versions.tf
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
yandex = {
|
||||||
|
source = "yandex-cloud/yandex"
|
||||||
|
version = "~> 0.100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
scripts/generate-pipeline.sh
Executable file
182
scripts/generate-pipeline.sh
Executable file
@ -0,0 +1,182 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Скрипт для динамической генерации GitLab CI пайплайна
|
||||||
|
# Сканирует структуру live/ и создает джобы для каждого компонента
|
||||||
|
|
||||||
|
OUTPUT_FILE=".gitlab-ci.generated.yml"
|
||||||
|
|
||||||
|
# Функция для экранирования YAML
|
||||||
|
yaml_escape() {
|
||||||
|
echo "$1" | sed 's/"/\\"/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Начало файла
|
||||||
|
cat > "$OUTPUT_FILE" << 'EOF'
|
||||||
|
# Автоматически сгенерированный файл GitLab CI
|
||||||
|
# НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ! Этот файл генерируется скриптом scripts/generate-pipeline.sh
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- validate
|
||||||
|
- plan
|
||||||
|
- apply
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Поиск всех terragrunt.hcl файлов в live/
|
||||||
|
find live -name "terragrunt.hcl" -not -path "*/.terragrunt-cache/*" | sort | while read -r config_file; do
|
||||||
|
# Извлечение пути компонента
|
||||||
|
component_dir=$(dirname "$config_file")
|
||||||
|
relative_path=$(echo "$component_dir" | sed 's|^live/||')
|
||||||
|
|
||||||
|
# Парсинг окружения и компонента из пути
|
||||||
|
# Формат: live/{env}/{component}/terragrunt.hcl
|
||||||
|
env=$(echo "$relative_path" | cut -d'/' -f1)
|
||||||
|
component=$(echo "$relative_path" | cut -d'/' -f2)
|
||||||
|
|
||||||
|
# Пропускаем если это не компонент (например, env.hcl)
|
||||||
|
if [ -z "$component" ] || [ "$component" = "$env" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Формирование имени джобы
|
||||||
|
job_prefix="${env}-${component}"
|
||||||
|
|
||||||
|
# Определение переменных окружения для Yandex Cloud
|
||||||
|
case "$env" in
|
||||||
|
stage)
|
||||||
|
folder_var="YC_STAGE_FOLDER_ID"
|
||||||
|
;;
|
||||||
|
prod)
|
||||||
|
folder_var="YC_PROD_FOLDER_ID"
|
||||||
|
;;
|
||||||
|
preprod)
|
||||||
|
folder_var="YC_PREPROD_FOLDER_ID"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
folder_var="YC_FOLDER_ID"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Джоба validate
|
||||||
|
cat >> "$OUTPUT_FILE" << EOF
|
||||||
|
validate-${job_prefix}:
|
||||||
|
stage: validate
|
||||||
|
variables:
|
||||||
|
TG_ROOT: "${component_dir}"
|
||||||
|
ENVIRONMENT: "${env}"
|
||||||
|
YC_FOLDER_ID: "\${${folder_var}}"
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache curl unzip
|
||||||
|
- |
|
||||||
|
if [ ! -f /usr/local/bin/terragrunt ]; then
|
||||||
|
TERRAFORM_VERSION=1.6.0
|
||||||
|
TERRAGRUNT_VERSION=0.50.0
|
||||||
|
curl -fsSL https://releases.hashicorp.com/terraform/\${TERRAFORM_VERSION}/terraform_\${TERRAFORM_VERSION}_linux_amd64.zip -o terraform.zip
|
||||||
|
unzip terraform.zip -d /usr/local/bin/
|
||||||
|
chmod +x /usr/local/bin/terraform
|
||||||
|
curl -fsSL https://github.com/gruntwork-io/terragrunt/releases/download/v\${TERRAGRUNT_VERSION}/terragrunt_linux_amd64 -o /usr/local/bin/terragrunt
|
||||||
|
chmod +x /usr/local/bin/terragrunt
|
||||||
|
rm -f terraform.zip
|
||||||
|
fi
|
||||||
|
- cd \${TG_ROOT}
|
||||||
|
script:
|
||||||
|
- terragrunt init -reconfigure -input=false --terragrunt-non-interactive
|
||||||
|
- terragrunt validate-inputs --terragrunt-non-interactive
|
||||||
|
- terragrunt validate --terragrunt-non-interactive
|
||||||
|
rules:
|
||||||
|
- if: '\$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
- if: '\$CI_COMMIT_BRANCH == "master" || \$CI_COMMIT_BRANCH == "main"'
|
||||||
|
tags:
|
||||||
|
- yc
|
||||||
|
interruptible: true
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Джоба plan
|
||||||
|
cat >> "$OUTPUT_FILE" << EOF
|
||||||
|
plan-${job_prefix}:
|
||||||
|
stage: plan
|
||||||
|
variables:
|
||||||
|
TG_ROOT: "${component_dir}"
|
||||||
|
ENVIRONMENT: "${env}"
|
||||||
|
YC_FOLDER_ID: "\${${folder_var}}"
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache curl unzip
|
||||||
|
- |
|
||||||
|
if [ ! -f /usr/local/bin/terragrunt ]; then
|
||||||
|
TERRAFORM_VERSION=1.6.0
|
||||||
|
TERRAGRUNT_VERSION=0.50.0
|
||||||
|
curl -fsSL https://releases.hashicorp.com/terraform/\${TERRAFORM_VERSION}/terraform_\${TERRAFORM_VERSION}_linux_amd64.zip -o terraform.zip
|
||||||
|
unzip terraform.zip -d /usr/local/bin/
|
||||||
|
chmod +x /usr/local/bin/terraform
|
||||||
|
curl -fsSL https://github.com/gruntwork-io/terragrunt/releases/download/v\${TERRAGRUNT_VERSION}/terragrunt_linux_amd64 -o /usr/local/bin/terragrunt
|
||||||
|
chmod +x /usr/local/bin/terragrunt
|
||||||
|
rm -f terraform.zip
|
||||||
|
fi
|
||||||
|
- cd \${TG_ROOT}
|
||||||
|
script:
|
||||||
|
- terragrunt init -reconfigure -input=false --terragrunt-non-interactive
|
||||||
|
- terragrunt plan -input=false --terragrunt-non-interactive -out=tfplan
|
||||||
|
rules:
|
||||||
|
- if: '\$CI_COMMIT_BRANCH == "master" || \$CI_COMMIT_BRANCH == "main"'
|
||||||
|
- if: '\$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
tags:
|
||||||
|
- yc
|
||||||
|
interruptible: true
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "\${TG_ROOT}/tfplan"
|
||||||
|
- "\${TG_ROOT}/.terragrunt-cache/**/*"
|
||||||
|
expire_in: 1 week
|
||||||
|
when: always
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Джоба apply (manual для prod, автоматическая для остальных)
|
||||||
|
if [ "$env" = "prod" ]; then
|
||||||
|
when_clause="manual"
|
||||||
|
else
|
||||||
|
when_clause="on_success"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >> "$OUTPUT_FILE" << EOF
|
||||||
|
apply-${job_prefix}:
|
||||||
|
stage: apply
|
||||||
|
variables:
|
||||||
|
TG_ROOT: "${component_dir}"
|
||||||
|
ENVIRONMENT: "${env}"
|
||||||
|
YC_FOLDER_ID: "\${${folder_var}}"
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache curl unzip
|
||||||
|
- |
|
||||||
|
if [ ! -f /usr/local/bin/terragrunt ]; then
|
||||||
|
TERRAFORM_VERSION=1.6.0
|
||||||
|
TERRAGRUNT_VERSION=0.50.0
|
||||||
|
curl -fsSL https://releases.hashicorp.com/terraform/\${TERRAFORM_VERSION}/terraform_\${TERRAFORM_VERSION}_linux_amd64.zip -o terraform.zip
|
||||||
|
unzip terraform.zip -d /usr/local/bin/
|
||||||
|
chmod +x /usr/local/bin/terraform
|
||||||
|
curl -fsSL https://github.com/gruntwork-io/terragrunt/releases/download/v\${TERRAGRUNT_VERSION}/terragrunt_linux_amd64 -o /usr/local/bin/terragrunt
|
||||||
|
chmod +x /usr/local/bin/terragrunt
|
||||||
|
rm -f terraform.zip
|
||||||
|
fi
|
||||||
|
- cd \${TG_ROOT}
|
||||||
|
script:
|
||||||
|
- terragrunt init -reconfigure -input=false --terragrunt-non-interactive
|
||||||
|
- terragrunt apply -input=false --terragrunt-non-interactive tfplan
|
||||||
|
rules:
|
||||||
|
- if: '\$CI_COMMIT_BRANCH == "master" || \$CI_COMMIT_BRANCH == "main"'
|
||||||
|
when: ${when_clause}
|
||||||
|
tags:
|
||||||
|
- yc
|
||||||
|
interruptible: true
|
||||||
|
dependencies:
|
||||||
|
- plan-${job_prefix}
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Pipeline generated successfully: $OUTPUT_FILE"
|
||||||
|
echo "Found components:"
|
||||||
|
find live -name "terragrunt.hcl" -not -path "*/.terragrunt-cache/*" | sed 's|live/||; s|/terragrunt.hcl||' | sort
|
||||||
925
theory.md
Normal file
925
theory.md
Normal file
@ -0,0 +1,925 @@
|
|||||||
|
Архитектурные паттерны IaC на основе источников
|
||||||
|
|
||||||
|
1.1. Декларативный подход и идемпотентность
|
||||||
|
|
||||||
|
Terraform использует декларативный язык HCL, где инженер описывает целевое конечное состояние («что должно существовать»), в то время как инструмент самостоятельно вычисляет граф зависимостей и шаги для его достижения [22], [4]. Это обеспечивает идемпотентность: многократный запуск одного и того же кода приводит к идентичному результату, исключая риск дублирования ресурсов [21]. В отличие от процедурных инструментов (Ansible, Bash), где повторный запуск может потребовать дополнительных проверок, Terraform сравнивает текущее состояние (state) со свежим кодом и вносит только необходимые изменения [21], [16]. Начиная с версии 0.12 (HCL 2.0), синтаксис был упрощен: переменные больше не требуют обязательной интерполяции ${} везде, где они используются, что сделало код более читаемым для человека [1], [4]. Декларативный подход позволяет уйти от ручного управления («ClickOps») и превратить инфраструктуру в «живую» документацию [1], [5], [29].
|
||||||
|
|
||||||
|
1.2. Минимизация «радиуса поражения» (Blast Radius)
|
||||||
|
|
||||||
|
Монолитная инфраструктура, где все ресурсы описаны в одном файле или управляются единым файлом состояния, признана критическим антипаттерном [2]. Научное исследование SMELLS-SUS показывает, что «монолитный запах» (Monolithic Infrastructure smell) является наиболее распространенной проблемой и встречается в 9,67% проанализированных сценариев [2]. Ошибка в коде одного компонента (например, опечатка в Security Group) в монолитном стейте может привести к каскадному удалению или повреждению критически важных ресурсов, таких как базы данных всего ландшафта [15]. Эксперты рекомендуют разделять инфраструктуру на мелкие независимые стейты — «переборки» [16]. Опыт компании Selectel подтверждает, что распилка монолита на гранулярные компоненты позволяет ускорить деплой инфраструктуры в 20 раз за счет уменьшения времени опроса API облачного провайдера [5].
|
||||||
|
|
||||||
|
1.3 Слоистая архитектура (Layered Architecture)
|
||||||
|
|
||||||
|
Рекомендуется разделение ресурсов на независимые логические уровни (слои), каждый из которых имеет свой жизненный цикл и границы ответственности [17], [4609. «Владимир Дроздецкий — Эффективное управление инфрой»], [29]. Типичная иерархия включает следующие слои: Global (IAM, DNS), Network (VPC, подсети), Platform (K8s, базы данных) и Application (микросервисы) [17], [20]. Слой сети является фундаментальным, так как без него невозможна работа большинства других компонентов [29]. Связь между слоями осуществляется через передачу выходных данных (outputs), что в Terragrunt реализуется через блоки dependency, обеспечивающие автоматическую оркестрацию и соблюдение порядка развертывания [16], [18], [29]. Использование команды run-all позволяет развернуть всю иерархию модулей в правильной последовательности одним действием [1], [18], [27].
|
||||||
|
|
||||||
|
2. Организация и структура репо
|
||||||
|
|
||||||
|
1. Стратегия разделения: Modules и Live
|
||||||
|
Общепринятым корпоративным стандартом является разделение реализации инфраструктуры от ее непосредственного развертывания по разным репозиториям [20], [30]:
|
||||||
|
|
||||||
|
Repository «Modules»: Библиотека универсальных, версионируемых «чертежей» (blueprints). Они не содержат специфических данных окружения (IP-адресов, имен) и предназначены для многократного использования [20], [7].
|
||||||
|
|
||||||
|
Repository «Live»: Описание реальных «зданий», построенных по чертежам из модулей. Здесь фиксируются конкретные параметры для каждого ландшафта (dev, stage, prod) [7], [20].
|
||||||
|
|
||||||
|
2. Иерархическая структура каталогов (Live-репозиторий)
|
||||||
|
Для обеспечения изоляции и прозрачности рекомендуется древовидная структура, отражающая логику облака [20], [30]:
|
||||||
|
Account (Учетная запись) → Region (Регион) → Environment (Окружение) → Category (Категория) → Component (Компонент)
|
||||||
|
|
||||||
|
Account: На верхнем уровне разделяются облачные аккаунты (например, prod-account, stage-account), что обеспечивает максимальную изоляцию в рамках безопасности [7], [20].
|
||||||
|
|
||||||
|
Region: Внутри аккаунта создаются папки регионов (например, us-east-1, eu-central-1) [20]. Папка _global на этом уровне используется для ресурсов, доступных во всех регионах аккаунта (IAM, DNS) [7].
|
||||||
|
|
||||||
|
Environment: Разделение на dev, qa, stage и prod [20], [6]. Каждое окружение обычно соответствует отдельному VPC [7].
|
||||||
|
|
||||||
|
Category: Группировка ресурсов по назначению: networking (VPC, подсети), services (приложения, K8s), data-stores (БД, кэши) [20], [29].
|
||||||
|
|
||||||
|
Component: Конечная директория с файлом terragrunt.hcl или main.tf, управляющая конкретным ресурсом (например, mysql или vpc) [20].
|
||||||
|
|
||||||
|
3. Организация файлов внутри компонента
|
||||||
|
Для поддержания чистоты кода (принцип DRY) и удобства навигации рекомендуется разделение на функциональные файлы [6], [23]:
|
||||||
|
|
||||||
|
main.tf: Вызов модулей и описание основных ресурсов [6].
|
||||||
|
|
||||||
|
variables.tf: Объявление входных переменных с обязательным описанием (description) и типами [6].
|
||||||
|
|
||||||
|
outputs.tf: Описание выходных данных для передачи их между слоями или модулями [6].
|
||||||
|
|
||||||
|
providers.tf: Конфигурация провайдеров (AWS, Azure, Yandex) и их версий [23].
|
||||||
|
|
||||||
|
backend.tf: Настройка удаленного хранения стейта (S3, GCS) [6].
|
||||||
|
|
||||||
|
versions.tf: Фиксация версий Terraform и провайдеров для стабильности сборок [6].
|
||||||
|
|
||||||
|
4. Иерархическое управление переменными в Terragrunt
|
||||||
|
Terragrunt позволяет избежать дублирования за счет автоматического наследования конфигураций по иерархии папок [27], [30]:
|
||||||
|
|
||||||
|
root.hcl: Находится в корне и содержит общую конфигурацию remote_state и провайдеров для всех ландшафтов [27].
|
||||||
|
|
||||||
|
env.hcl / environment.yaml: Хранит переменные, общие для всего окружения (например, env = "prod") [18], [30].
|
||||||
|
|
||||||
|
region.hcl / region.yaml: Переменные конкретного региона (например, maintenance_window) [27], [30].
|
||||||
|
Для автоматического поиска этих файлов используется функция find_in_parent_folders() [30].
|
||||||
|
|
||||||
|
5. Изоляция состояний (State Isolation)
|
||||||
|
Иерархическая структура напрямую влияет на управление состоянием. Gruntwork и эксперты рекомендуют управлять стейтом на уровне каждого отдельного юнита (директории) [20], [30]. Использование Workspaces для Production-сред не рекомендуется, так как они используют один бэкенд и скрывают структуру в CLI [27]. Разделение по папкам гарантирует, что каждый компонент имеет свой изолированный файл состояния, что критически снижает «радиус поражения» (blast radius) в случае ошибки [16], [24].
|
||||||
|
|
||||||
|
6. Нововведения в структуре (Stacks)
|
||||||
|
В 2025 году появилась концепция Terragrunt Stacks, позволяющая упаковывать коллекции связанных юнитов в переиспользуемые стеки [27], [19]. Это переводит переиспользование с уровня отдельных модулей на уровень целых инфраструктурных паттернов (например, «App + DB + Monitoring»), полностью устраняя копипаст конфигураций между ландшафтами [27].
|
||||||
|
|
||||||
|
|
||||||
|
3. Модульность и стратегии переиспользования
|
||||||
|
|
||||||
|
Согласно исследованиям и best-practices, модульность является краеугольным камнем масштабируемой и поддерживаемой инфраструктуры. Она позволяет инкапсулировать ее сложность, обеспечивать переиспольщование и минимизировать дублирование кода, что напрямую снижает риск возникновения «монолитного запаха» (Monolithic Infrastructure smell) в проектах [2, 6].
|
||||||
|
|
||||||
|
3.1. Понятие и структура модуля
|
||||||
|
|
||||||
|
Как описывается в руководствах, в Terraform модулем может считаеться любая директория, содержащая конфигурационные файлы .tf [6]. Модули работают по принципу функций в программировании: они инкапсулируют логику создания определенного набора ресурсов (например, виртуальной машины или кластера БД), принимают входные параметры через переменные и возвращают результаты через выходные значения [21]. Это позволяет абстрагироваться от деталей реализации.
|
||||||
|
Как показано в примерах, для создания переиспользуемого компонента его необходимо выделить в отдельную директорию. Например:
|
||||||
|
|
||||||
|
modules/object-storage/bucket/main.tf:
|
||||||
|
|
||||||
|
resource "yandex_storage_bucket" "this" {
|
||||||
|
bucket = var.bucket_name
|
||||||
|
acl = var.acl
|
||||||
|
|
||||||
|
|
||||||
|
dynamic "server_side_encryption_configuration" {
|
||||||
|
for_each = var.kms_key_id != null ? [1] : []
|
||||||
|
content {
|
||||||
|
rule {
|
||||||
|
apply_server_side_encryption_by_default {
|
||||||
|
kms_master_key_id = var.kms_key_id
|
||||||
|
sse_algorithm = "aws:kms"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
lifecycle_rule {
|
||||||
|
enabled = true
|
||||||
|
expiration {
|
||||||
|
days = var.object_expiration_days
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Дабы вызвать данный модуль и корневого каталога, можно сделать следующее:
|
||||||
|
|
||||||
|
module "static_assets_bucket" {
|
||||||
|
source = "./modules/object-storage/bucket"
|
||||||
|
bucket_name = "myapp-static-assets-prod"
|
||||||
|
acl = "private"
|
||||||
|
kms_key_id = yandex_kms_symmetric_key.bucket_key.id
|
||||||
|
}
|
||||||
|
|
||||||
|
Источники подчеркивают, что такой подход превращает инфраструктуру в набор переиспользуемых «кирпичиков лего» [6, 21].
|
||||||
|
|
||||||
|
3.2. Использование публичных и готовых модулей
|
||||||
|
|
||||||
|
Для ускорения разработки и следования лучшим практикам настоятельно рекомендуется использовать готовые модули. Хотя публичный реестр Terraform для Yandex Cloud менее обширен, чем для AWS, существуют как официальные модули от Yandex, так и проверенные сообществом, которые можно найти на GitHub. Использование таких модулей, как отмечается в источниках, позволяет избежать написания сотен строк стандартного кода для настройки, например, виртуальных частных облаков (VPC) или групп безопасности [6, 23]. Тем самым нам не нужно в большинстве случаев изобретать вилосипед.
|
||||||
|
|
||||||
|
Адаптированный пример того, как можно использовать структурированный модуль для быстрого развертывания сети:
|
||||||
|
|
||||||
|
module "vpc" {
|
||||||
|
source = "terraform-yc-modules/vpc/yc"
|
||||||
|
version = "~> 1.0"
|
||||||
|
|
||||||
|
name = "production-network"
|
||||||
|
description = "VPC for production environment"
|
||||||
|
|
||||||
|
# Определение подсетей в разных зонах доступности
|
||||||
|
subnets = {
|
||||||
|
"ru-central1-a" = "10.0.1.0/24",
|
||||||
|
"ru-central1-b" = "10.0.2.0/24",
|
||||||
|
"ru-central1-c" = "10.0.3.0/24"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Включение NAT для выхода в интернет из приватных подсетей
|
||||||
|
enable_nat = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Использование версионированных модулей из проверенных источников является всеобщей стандартной практикой [6, 23].
|
||||||
|
|
||||||
|
3.3. Стратегии проектирования
|
||||||
|
|
||||||
|
Сложные инфраструктурные решения строятся по принципу композиции («Lego bricks»). Как описывается в литературе, выходные данные одного модуля (например, идентификаторы подсетей) должны передаваться на вход другому (например, модулю создания виртуальной машины), формируя явные и управляемые зависимости [6, 30]. Это обеспечивает слабую, но хоть какую-то связанность компонентов.
|
||||||
|
|
||||||
|
# 1. Модуль создания кластера Managed PostgreSQL
|
||||||
|
module "prod_postgresql" {
|
||||||
|
source = "../../../modules/data-stores/postgresql"
|
||||||
|
|
||||||
|
cluster_name = "app-db-prod"
|
||||||
|
network_id = data.yandex_vpc_network.default.id
|
||||||
|
subnet_ids = [yandex_vpc_subnet.private_a.id]
|
||||||
|
host_class = "s2.micro"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Модуль развертывания приложения, использующий вывод модуля БД
|
||||||
|
module "backend_app" {
|
||||||
|
source = "../../../modules/services/backend-app"
|
||||||
|
|
||||||
|
name = "backend-api"
|
||||||
|
subnet_id = yandex_vpc_subnet.public_a.id
|
||||||
|
image_id = "blahblahblah"
|
||||||
|
db_host = module.prod_postgresql.cluster_hosts_fqdn[0]
|
||||||
|
db_name = module.prod_postgresql.database_name
|
||||||
|
}
|
||||||
|
|
||||||
|
Такой подход исключает хардкод значений и делает инфраструктурный код декларативным и прозрачным [30].
|
||||||
|
|
||||||
|
3.4. Динамическое создание ресурсов (итерирование)
|
||||||
|
|
||||||
|
Для массового создания однотипных ресурсов без копирования кода используются count и for_each. В руководствах отмечается, что for_each предпочтительнее для работы с коллекциями (map, set), так как он создает ресурсы с уникальными идентификаторами в стейте, что делает их безопасными для последующих изменений [6, 10].
|
||||||
|
|
||||||
|
# variables.tf
|
||||||
|
variable "vm_instances" {
|
||||||
|
description = "Конфигурация создаваемых виртуальных машин"
|
||||||
|
type = map(object({
|
||||||
|
cores = number
|
||||||
|
memory = number
|
||||||
|
boot_disk_gb = number
|
||||||
|
image_family = string
|
||||||
|
user_data = string # Cloud-init
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
# main.tf
|
||||||
|
resource "yandex_compute_instance" "vm" {
|
||||||
|
for_each = var.vm_instances
|
||||||
|
|
||||||
|
name = each.key
|
||||||
|
platform_id = "standard-v3"
|
||||||
|
zone = "ru-central1-a"
|
||||||
|
|
||||||
|
resources {
|
||||||
|
cores = each.value.cores
|
||||||
|
memory = each.value.memory
|
||||||
|
}
|
||||||
|
|
||||||
|
boot_disk {
|
||||||
|
initialize_params {
|
||||||
|
size = each.value.boot_disk_gb
|
||||||
|
image_id = data.yandex_compute_image.this[each.key].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
subnet_id = yandex_vpc_subnet.app.id
|
||||||
|
nat = true # Для ВМ, которым нужен внешний IP
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
user-data = each.value.user_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Использование data source для поиска актуального образа по семейству
|
||||||
|
data "yandex_compute_image" "this" {
|
||||||
|
for_each = var.vm_instances
|
||||||
|
family = each.value.image_family
|
||||||
|
}
|
||||||
|
|
||||||
|
Это позволяет позволяет гибко управлять множеством ресурсов через изменение одной переменной [10].
|
||||||
|
Как это работает?
|
||||||
|
|
||||||
|
Допустим, в файле terraform.tfvars или в Terragrunt inputs мы передадим
|
||||||
|
|
||||||
|
vm_instances = {
|
||||||
|
"web-server-01" = {
|
||||||
|
cores = 2
|
||||||
|
memory = 4
|
||||||
|
boot_disk_gb = 30
|
||||||
|
image_family = "ubuntu-2204-lts"
|
||||||
|
user_data = "#cloud-config\npackage_update: true"
|
||||||
|
},
|
||||||
|
"database-01" = {
|
||||||
|
cores = 4
|
||||||
|
memory = 8
|
||||||
|
boot_disk_gb = 100
|
||||||
|
image_family = "centos-7"
|
||||||
|
user_data = "#cloud-config\ndisable_root: false"
|
||||||
|
},
|
||||||
|
"monitoring-01" = {
|
||||||
|
cores = 2
|
||||||
|
memory = 2
|
||||||
|
boot_disk_gb = 20
|
||||||
|
image_family = "debian-11"
|
||||||
|
user_data = "#cloud-config\npackages: ['prometheus']"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Terraform возьмет каждый элемент этой мапы и создаст отдельный ресурс yandex_compute_instance:
|
||||||
|
|
||||||
|
Итерация 1:
|
||||||
|
|
||||||
|
Создается ВМ с именем web-server-01, 2 ядрами, 4 ГБ памяти
|
||||||
|
|
||||||
|
Итерация 2:
|
||||||
|
|
||||||
|
Создается ВМ с именем database-01, 4 ядрами, 8 ГБ памяти
|
||||||
|
|
||||||
|
Итерация 3:
|
||||||
|
|
||||||
|
Создается ВМ с именем monitoring-01 и т.д
|
||||||
|
|
||||||
|
3.5. Версионирование как гарантия стабильности
|
||||||
|
|
||||||
|
Для надежности production-сред в особненности, источники категорически настаивают на фиксировании версии как внешних провайдеров, так и собственных модулей. Это защищает от непреднамеренных изменений, которые могут быть внесены в апстрим репозитории [6, 21]. Семантическое версионирование (SemVer) позволяет контролировать процесс обновлений.
|
||||||
|
|
||||||
|
module "network" {
|
||||||
|
source = "git::https://gitlab.sarex.io/terraform/yc-network-module.git?ref=v2.5.1"
|
||||||
|
|
||||||
|
vpc_name = "production"
|
||||||
|
cidr = "10.100.0.0/16"
|
||||||
|
}
|
||||||
|
|
||||||
|
3.6. Оркестрация и соблюдение принципа DRY через Terragrunt
|
||||||
|
|
||||||
|
Для управления сложными, многоуровневыми деплойментами в облаках и соблюдения принципа DRY рекомендуется использовать инструменты оркестрации, такие как Terragrunt [27, 30]. Он позволяет устранить дублирование в конфигурациях бэкенда и провайдеров, а также явно управлять зависимостями между компонентами.
|
||||||
|
|
||||||
|
# Автоматическое включение конфигураций из родительских папок
|
||||||
|
include "root" {
|
||||||
|
path = find_in_parent_folders("root.hcl")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Объявление зависимости от модуля сети
|
||||||
|
dependency "vpc" {
|
||||||
|
config_path = "../network"
|
||||||
|
|
||||||
|
# Mock-выходы для команд `plan` без доступа к реальному state
|
||||||
|
mock_outputs = {
|
||||||
|
subnet_ids = ["mock-subnet-id-a", "mock-subnet-id-b"]
|
||||||
|
network_id = "mock-network-id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Локальные переменные для удобства
|
||||||
|
locals {
|
||||||
|
common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl")).locals
|
||||||
|
}
|
||||||
|
|
||||||
|
# Входные переменные для Terraform-модуля
|
||||||
|
inputs = {
|
||||||
|
environment = local.common_vars.environment
|
||||||
|
region = local.common_vars.region
|
||||||
|
subnet_id = dependency.vpc.outputs.subnet_ids[0]
|
||||||
|
# ... другие переменные
|
||||||
|
}
|
||||||
|
|
||||||
|
Файл root.hcl:
|
||||||
|
|
||||||
|
hcl
|
||||||
|
|
||||||
|
# Настройка удаленного бэкенда в Yandex Object Storage с блокировкой через YDB
|
||||||
|
remote_state {
|
||||||
|
backend = "s3"
|
||||||
|
config = {
|
||||||
|
endpoint = "storage.yandexcloud.net"
|
||||||
|
bucket = "company-terraform-state-prod"
|
||||||
|
key = "${path_relative_to_include()}/terraform.tfstate"
|
||||||
|
region = "ru-central1"
|
||||||
|
skip_region_validation = true
|
||||||
|
skip_credentials_validation = true
|
||||||
|
skip_requesting_account_id = true
|
||||||
|
skip_metadata_api_check = true
|
||||||
|
force_path_style = true
|
||||||
|
|
||||||
|
# Настройка блокировок состояний через Yandex Database (YDB)
|
||||||
|
dynamodb_endpoint = "https://docapi.serverless.yandexcloud.net/ru-central1/b1gxxxxxxxx/etn03j3j45678"
|
||||||
|
dynamodb_table = "terraform_locks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Генерация общего провайдера для Yandex Cloud
|
||||||
|
generate "provider" {
|
||||||
|
path = "provider.tf"
|
||||||
|
if_exists = "overwrite_terragrunt"
|
||||||
|
contents = <<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