diff --git a/README.md b/README.md new file mode 100644 index 0000000..87e8c95 --- /dev/null +++ b/README.md @@ -0,0 +1,236 @@ +# Terraform Infrastructure + +Infrastructure as Code для управления ресурсами Yandex Cloud и Kubernetes. + +## Структура + +``` +terraform/ +├── infrastructure.yaml # Конфигурация всех ресурсов (единый источник правды) +├── live/ # Terragrunt конфигурации по окружениям +│ ├── terragrunt.hcl # Общие настройки (backend, providers) +│ └── stage/ # Stage окружение +│ ├── env.hcl +│ ├── namespace/ +│ ├── database/ +│ ├── s3/ +│ └── secrets/ +└── modules/ # Terraform модули + ├── k8s-namespace/ # Создание Kubernetes namespaces + ├── k8s-secret/ # Создание Kubernetes secrets (с зависимостями) + ├── k8s-secrets/ # Простое создание secrets + ├── yc-database/ # PostgreSQL users и databases в Yandex Cloud + └── yc-s3/ # S3 бакеты с изолированным доступом +``` + +## Принцип работы + +### infrastructure.yaml + +Единый файл конфигурации для всех ресурсов. Структура: + +```yaml +environments: + stage: # Имя окружения + namespaces: # Kubernetes namespaces + - name: pulse + labels: {} + annotations: {} + + buckets: # S3 бакеты + - name: pulse-stage + acl: private + role: storage.uploader # опционально + versioning: + enabled: false + cors: + enabled: true + allowed_methods: [GET, PUT] + + databases: # PostgreSQL databases + - cluster_id: "xxx" + database: + name: mydb + extensions: [pg_stat_statements] + user: + name: myuser + password_length: 32 + conn_limit: 10 + + secrets: # Kubernetes secrets + - name: my-secret + namespace: pulse + type: database # database | s3 | dockerconfigjson | opaque + dependencies: + cluster: "xxx" + db: mydb + user: myuser +``` + +### Terragrunt + +Terragrunt читает `infrastructure.yaml` и передаёт данные в модули: + +```hcl +locals { + infra_config = yamldecode(file("${local.repo_root}/infrastructure.yaml")) + env_config = local.infra_config.environments[local.env_name] +} + +inputs = { + buckets = local.env_config.buckets +} +``` + +### Порядок применения + +Модули имеют зависимости и применяются в порядке: + +``` +namespace → database → s3 → secrets +``` + +## Модули + +### yc-s3 + +Создание S3 бакетов с изолированным доступом. + +**Особенности:** +- Каждый бакет получает отдельный Service Account +- SA не имеет глобальных IAM ролей на storage +- Доступ к бакету только через bucket policy +- SA может работать только со своим бакетом + +**Роли:** +- `storage.uploader` (default) — PutObject, DeleteObject, GetObject +- `storage.viewer` — GetObject, ListBucket +- `storage.editor` — все операции + +**Пример:** +```yaml +buckets: + - name: my-bucket + acl: private + role: storage.uploader # опционально +``` + +### yc-database + +Создание PostgreSQL users и databases в существующем кластере. + +**Особенности:** +- Пароль генерируется автоматически +- Поддержка extensions +- Поддержка permissions на другие БД +- `ignore_changes` на password + +**Пример:** +```yaml +databases: + - cluster_id: "c9qa2coo5ukgcg93fldm" + database: + name: mydb + extensions: [pg_stat_statements] + user: + name: myuser + password_length: 32 + conn_limit: 10 + permissions: [other_db] +``` + +### k8s-secret + +Создание Kubernetes secrets с поддержкой зависимостей. + +**Типы секретов:** +- `database` — host, port, database, username, password, ca.crt +- `s3` — access_key, secret_key, bucket, endpoint +- `dockerconfigjson` — credentials для container registry +- `opaque` — произвольные данные + +**Особенности:** +- Автоматическое получение данных из database/s3 модулей +- Поддержка `random_keys` для генерации паролей +- Поддержка `custom_keys` для статических значений +- `ignore_changes` для предотвращения перезаписи + +**Пример:** +```yaml +secrets: + - name: my-db-secret + namespace: pulse + type: database + dependencies: + cluster: "c9qa2coo5ukgcg93fldm" + db: mydb + user: myuser + random_keys: + session_key: + length: 32 + special: false + lifecycle: + ignore_changes: true +``` + +### k8s-namespace + +Создание Kubernetes namespaces. + +**Пример:** +```yaml +namespaces: + - name: pulse + labels: + environment: stage + annotations: + managed-by: terraform +``` + +## Использование + +### Локальный запуск + +```bash +cd terraform/live/stage/s3 +terragrunt init +terragrunt plan +terragrunt apply +``` + +### Применение всех модулей + +```bash +cd terraform/live/stage +terragrunt run-all plan +terragrunt run-all apply +``` + +### Переменные окружения + +```bash +export YC_TOKEN="..." # IAM токен (или YC_SERVICE_ACCOUNT_KEY_FILE) +export YC_CLOUD_ID="..." +export YC_FOLDER_ID="..." # Default folder +export YC_STAGE_FOLDER_ID="..." # Stage folder (опционально) +export KUBECONFIG="~/.kube/config" +export KUBE_CONTEXT="stage" +export S3_ACCESS_KEY="..." # Для terraform state +export S3_SECRET_KEY="..." +export DOCKER_REGISTRY_USERNAME="..." # Для dockerconfigjson secrets +export DOCKER_REGISTRY_PASSWORD="..." +``` + +## CI/CD + +Pipeline автоматически: +1. Читает изменённые файлы +2. Определяет затронутые модули +3. Запускает `terragrunt plan` / `terragrunt apply` + +## Требования + +- Terraform >= 1.0 +- Terragrunt >= 0.45 +- Yandex Cloud провайдер ~> 0.100 +- IAM токен с правами `storage.admin` для создания бакетов и bucket policy diff --git a/modules/yc-s3/main.tf b/modules/yc-s3/main.tf index 85d2605..1a62e7c 100644 --- a/modules/yc-s3/main.tf +++ b/modules/yc-s3/main.tf @@ -1,27 +1,20 @@ -# Универсальный модуль для создания S3 бакетов - locals { buckets_map = { for bucket in var.buckets : bucket.name => bucket } + + role_actions = { + "storage.uploader" = ["s3:PutObject", "s3:DeleteObject", "s3:GetObject"] + "storage.viewer" = ["s3:GetObject", "s3:ListBucket"] + "storage.editor" = ["s3:*"] + } } -# Создание сервисного аккаунта для каждого бакета resource "yandex_iam_service_account" "sa" { for_each = local.buckets_map name = "${each.key}-sa" - description = "Service account for ${each.key} bucket" + description = "Service account for ${each.key} bucket (access via bucket policy only)" } -# Назначение роли storage.editor сервисному аккаунту -resource "yandex_resourcemanager_folder_iam_member" "storage_editor" { - for_each = local.buckets_map - - folder_id = var.folder_id - role = "storage.editor" - member = "serviceAccount:${yandex_iam_service_account.sa[each.key].id}" -} - -# Создание статического ключа доступа resource "yandex_iam_service_account_static_access_key" "sa_key" { for_each = local.buckets_map @@ -29,14 +22,13 @@ resource "yandex_iam_service_account_static_access_key" "sa_key" { description = "Static access key for ${each.key} bucket" } -# Создание S3 бакета resource "yandex_storage_bucket" "this" { for_each = local.buckets_map - bucket = each.key - acl = each.value.acl + bucket = each.key + acl = each.value.acl + folder_id = var.folder_id - # Версионирование объектов dynamic "versioning" { for_each = try(each.value.versioning.enabled, false) ? [1] : [] content { @@ -44,7 +36,6 @@ resource "yandex_storage_bucket" "this" { } } - # Настройка CORS dynamic "cors_rule" { for_each = try(each.value.cors.enabled, false) ? [1] : [] content { @@ -56,3 +47,29 @@ resource "yandex_storage_bucket" "this" { } } } + +resource "yandex_storage_bucket_policy" "this" { + for_each = local.buckets_map + + bucket = yandex_storage_bucket.this[each.key].bucket + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowServiceAccountAccess" + Effect = "Allow" + Principal = { + CanonicalUser = [yandex_iam_service_account.sa[each.key].id] + } + Action = local.role_actions[coalesce(each.value.role, var.default_bucket_role)] + Resource = [ + "arn:aws:s3:::${each.key}", + "arn:aws:s3:::${each.key}/*" + ] + } + ] + }) + + depends_on = [yandex_storage_bucket.this] +} diff --git a/modules/yc-s3/outputs.tf b/modules/yc-s3/outputs.tf index 0a95b7c..258f62d 100644 --- a/modules/yc-s3/outputs.tf +++ b/modules/yc-s3/outputs.tf @@ -1,4 +1,3 @@ -# Для обратной совместимости - первый бакет output "bucket_name" { description = "Name of the first bucket (for backward compatibility)" value = length(var.buckets) > 0 ? yandex_storage_bucket.this[var.buckets[0].name].bucket : "" @@ -16,14 +15,13 @@ output "secret_key" { sensitive = true } -# Новые outputs - map всех бакетов output "buckets" { description = "Map of all buckets (name => data)" value = { for name, bucket in yandex_storage_bucket.this : name => { - bucket_name = bucket.bucket - access_key = yandex_iam_service_account_static_access_key.sa_key[name].access_key - secret_key = yandex_iam_service_account_static_access_key.sa_key[name].secret_key + bucket_name = bucket.bucket + access_key = yandex_iam_service_account_static_access_key.sa_key[name].access_key + secret_key = yandex_iam_service_account_static_access_key.sa_key[name].secret_key service_account_id = yandex_iam_service_account.sa[name].id } } diff --git a/modules/yc-s3/variables.tf b/modules/yc-s3/variables.tf index 10b3b48..b82d6be 100644 --- a/modules/yc-s3/variables.tf +++ b/modules/yc-s3/variables.tf @@ -1,8 +1,9 @@ variable "buckets" { description = "List of S3 buckets to create" type = list(object({ - name = string - acl = optional(string, "private") + name = string + acl = optional(string, "private") + role = optional(string, null) versioning = optional(object({ enabled = bool }), { enabled = false }) @@ -21,3 +22,9 @@ variable "folder_id" { description = "Yandex Cloud folder ID" type = string } + +variable "default_bucket_role" { + description = "Default role for SA on bucket level (storage.uploader, storage.viewer, storage.editor)" + type = string + default = "storage.uploader" +}