feat(yc-s3): use bucket policy instead of folder-level IAM roles

- Remove yandex_resourcemanager_folder_iam_member with storage.editor
- Add yandex_storage_bucket_policy for bucket-level access control
- SA now has access only to its own bucket via bucket policy
- Add role variable (storage.uploader, storage.viewer, storage.editor)
- Add README.md with module documentation
- Remove comments from module files
This commit is contained in:
Work 2026-02-03 13:11:36 +03:00 committed by Kochetkov S
parent 6a141f8d51
commit fb3b9894d4
4 changed files with 284 additions and 26 deletions

236
README.md Normal file
View File

@ -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

View File

@ -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
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]
}

View File

@ -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,7 +15,6 @@ output "secret_key" {
sensitive = true
}
# Новые outputs - map всех бакетов
output "buckets" {
description = "Map of all buckets (name => data)"
value = {

View File

@ -3,6 +3,7 @@ variable "buckets" {
type = list(object({
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"
}