From 3286b26777bd8819e8369bcceb45cca85d474212 Mon Sep 17 00:00:00 2001 From: Kochetkov S Date: Thu, 19 Feb 2026 12:12:40 +0300 Subject: [PATCH] add SA creation logic --- infrastructure.yaml | 24 +++++- live/prod/s3/terragrunt.hcl | 5 +- live/prod/secrets/terragrunt.hcl | 33 ++++---- modules/k8s-secret/main.tf | 125 +++++++++++++++++++------------ modules/k8s-secret/variables.tf | 59 +++++++++------ modules/yc-s3/main.tf | 39 +++++++++- modules/yc-s3/outputs.tf | 13 ++++ modules/yc-s3/variables.tf | 11 +++ 8 files changed, 221 insertions(+), 88 deletions(-) diff --git a/infrastructure.yaml b/infrastructure.yaml index b7e4804..c42319b 100644 --- a/infrastructure.yaml +++ b/infrastructure.yaml @@ -59,6 +59,19 @@ environments: # lifecycle: # ignore_changes: false + # # Пример секрета из SA ключей + # - name: dwg-sa-secret + # namespace: pulse + # type: yc_sa + # dependencies: + # service_account: dwg-sa + # yc_sa_key_names: # как назвать ключи в секрете + # access_key: access_key + # secret_key: secret_key + # service_account_id: sa_id + # lifecycle: + # ignore_changes: false + buckets: - name: pulse-prod acl: private @@ -66,8 +79,14 @@ environments: enabled: false cors: enabled: false - - + + # yc_service_accounts: + # - name: dwg-sa + # description: "DWG SA Uploader" + # folder_roles: + # - storage.uploader + # create_static_access_key: true + databases: - cluster_id: "c9quu72dj2ibu8dk54q5" @@ -81,4 +100,3 @@ environments: password_special: false conn_limit: 10 - diff --git a/live/prod/s3/terragrunt.hcl b/live/prod/s3/terragrunt.hcl index 09db01f..37037d2 100644 --- a/live/prod/s3/terragrunt.hcl +++ b/live/prod/s3/terragrunt.hcl @@ -20,8 +20,9 @@ terraform { } inputs = { - buckets = local.env_config.buckets - folder_id = get_env("YC_PROD_FOLDER_ID", get_env("YC_FOLDER_ID", "")) + buckets = local.env_config.buckets + yc_service_accounts = try(local.env_config.yc_service_accounts, []) + folder_id = get_env("YC_PROD_FOLDER_ID", get_env("YC_FOLDER_ID", "")) yc_token = get_env("YC_TOKEN", "") yc_service_account_key_file = get_env("YC_SERVICE_ACCOUNT_KEY_FILE", "") diff --git a/live/prod/secrets/terragrunt.hcl b/live/prod/secrets/terragrunt.hcl index ca950b4..38e1302 100644 --- a/live/prod/secrets/terragrunt.hcl +++ b/live/prod/secrets/terragrunt.hcl @@ -21,10 +21,11 @@ dependency "namespace" { dependency "s3" { config_path = "../s3" mock_outputs = { - bucket_name = "mock-bucket" - access_key = "mock-access-key" - secret_key = "mock-secret-key" - buckets = {} + bucket_name = "mock-bucket" + access_key = "mock-access-key" + secret_key = "mock-secret-key" + buckets = {} + service_accounts = {} } mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"] skip_outputs = get_env("TG_SKIP_DEPENDENCY_OUTPUTS", "false") == "true" @@ -115,16 +116,17 @@ terraform { inputs = { secrets = [ for secret in local.env_config.secrets : { - name = secret.name - namespace = secret.namespace - secret_type = secret.type - registry_url = try(secret.registry_url, "") - dependencies = try(secret.dependencies, {}) - custom_keys = try(secret.custom_keys, {}) - random_keys = try(secret.random_keys, {}) - labels = try(secret.labels, {}) - annotations = try(secret.annotations, {}) - ignore_changes = try(secret.lifecycle.ignore_changes, false) + name = secret.name + namespace = secret.namespace + secret_type = secret.type + registry_url = try(secret.registry_url, "") + dependencies = try(secret.dependencies, {}) + yc_sa_key_names = try(secret.yc_sa_key_names, {}) + custom_keys = try(secret.custom_keys, {}) + random_keys = try(secret.random_keys, {}) + labels = try(secret.labels, {}) + annotations = try(secret.annotations, {}) + ignore_changes = try(secret.lifecycle.ignore_changes, false) } ] @@ -134,7 +136,8 @@ inputs = { secret_key = dependency.s3.outputs.secret_key } - s3_buckets_map = dependency.s3.outputs.buckets + s3_buckets_map = dependency.s3.outputs.buckets + yc_service_accounts_map = try(dependency.s3.outputs.service_accounts, {}) database_outputs_map = dependency.database.outputs.database_outputs_map diff --git a/modules/k8s-secret/main.tf b/modules/k8s-secret/main.tf index f254df0..aecda4c 100644 --- a/modules/k8s-secret/main.tf +++ b/modules/k8s-secret/main.tf @@ -6,13 +6,20 @@ locals { "dockerconfigjson" = "kubernetes.io/dockerconfigjson" "database" = "Opaque" "s3" = "Opaque" + "yc_sa" = "Opaque" "opaque" = "Opaque" } + default_yc_sa_key_names = { + access_key = "access_key" + secret_key = "secret_key" + service_account_id = "service_account_id" + } + secrets_map = { for idx, secret in var.secrets : secret.name => secret } - + random_keys_flat = merge([ for secret_name, secret in local.secrets_map : { for key, config in try(secret.random_keys, {}) : "${secret_name}:${key}" => { @@ -23,56 +30,82 @@ locals { } } ]...) - + database_outputs_by_secret = { for name, secret in local.secrets_map : name => try( var.database_outputs_map["${try(secret.dependencies.cluster, "")}:${try(secret.dependencies.db, "")}:${try(secret.dependencies.user, "")}"], null ) } - - secrets_data = { - for name, secret in local.secrets_map : name => - secret.secret_type == "dockerconfigjson" ? { - ".dockerconfigjson" = jsonencode({ - auths = { - "${secret.registry_url}" = { - username = try(var.env_vars.DOCKER_REGISTRY_USERNAME, "") - password = try(var.env_vars.DOCKER_REGISTRY_PASSWORD, "") - auth = base64encode("${try(var.env_vars.DOCKER_REGISTRY_USERNAME, "")}:${try(var.env_vars.DOCKER_REGISTRY_PASSWORD, "")}") - } - } - }) - } : secret.secret_type == "s3" ? { - # Если указан конкретный bucket в dependencies - используем его, иначе fallback на s3_outputs - access_key = try(var.s3_buckets_map[secret.dependencies.bucket].access_key, var.s3_outputs.access_key) - secret_key = try(var.s3_buckets_map[secret.dependencies.bucket].secret_key, var.s3_outputs.secret_key) - bucket = try(var.s3_buckets_map[secret.dependencies.bucket].bucket_name, var.s3_outputs.bucket_name) - endpoint = try(var.constants.s3_endpoint, "https://storage.yandexcloud.net") - } : secret.secret_type == "database" && local.database_outputs_by_secret[name] != null ? merge( - # Базовые поля из database outputs - { - host = local.database_outputs_by_secret[name].host - port = try(var.constants.postgres_port, "6432") - database = local.database_outputs_by_secret[name].database_name - username = local.database_outputs_by_secret[name].user_name - password = local.database_outputs_by_secret[name].password - "ca.crt" = try(var.constants.postgres_ca, "") - }, - # Кастомные статические поля - try(secret.custom_keys, {}), - # Рандомные поля - { - for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result - } - ) : merge( - try(secret.custom_keys, {}), - { - for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result - } - ) + + yc_sa_sources = { + for name, secret in local.secrets_map : name => try( + var.yc_service_accounts_map[try(secret.dependencies.service_account, "")], + null + ) + if secret.secret_type == "yc_sa" } - + + yc_sa_key_names_by_secret = { + for name, secret in local.secrets_map : name => merge( + local.default_yc_sa_key_names, + try(secret.yc_sa_key_names, {}) + ) + if secret.secret_type == "yc_sa" + } + + secrets_data = { + for name, secret in local.secrets_map : name => + secret.secret_type == "dockerconfigjson" ? { + ".dockerconfigjson" = jsonencode({ + auths = { + "${secret.registry_url}" = { + username = try(var.env_vars.DOCKER_REGISTRY_USERNAME, "") + password = try(var.env_vars.DOCKER_REGISTRY_PASSWORD, "") + auth = base64encode("${try(var.env_vars.DOCKER_REGISTRY_USERNAME, "")}:${try(var.env_vars.DOCKER_REGISTRY_PASSWORD, "")}") + } + } + }) + } : secret.secret_type == "s3" ? { + # Если указан конкретный bucket в dependencies - используем его, иначе fallback на s3_outputs + access_key = try(var.s3_buckets_map[secret.dependencies.bucket].access_key, var.s3_outputs.access_key) + secret_key = try(var.s3_buckets_map[secret.dependencies.bucket].secret_key, var.s3_outputs.secret_key) + bucket = try(var.s3_buckets_map[secret.dependencies.bucket].bucket_name, var.s3_outputs.bucket_name) + endpoint = try(var.constants.s3_endpoint, "https://storage.yandexcloud.net") + } : secret.secret_type == "yc_sa" ? merge( + { + (local.yc_sa_key_names_by_secret[name].access_key) = try(local.yc_sa_sources[name].access_key, "") + (local.yc_sa_key_names_by_secret[name].secret_key) = try(local.yc_sa_sources[name].secret_key, "") + (local.yc_sa_key_names_by_secret[name].service_account_id) = try(local.yc_sa_sources[name].service_account_id, "") + }, + try(secret.custom_keys, {}), + { + for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result + } + ) : secret.secret_type == "database" && local.database_outputs_by_secret[name] != null ? merge( + # Базовые поля из database outputs + { + host = local.database_outputs_by_secret[name].host + port = try(var.constants.postgres_port, "6432") + database = local.database_outputs_by_secret[name].database_name + username = local.database_outputs_by_secret[name].user_name + password = local.database_outputs_by_secret[name].password + "ca.crt" = try(var.constants.postgres_ca, "") + }, + # Кастомные статические поля + try(secret.custom_keys, {}), + # Рандомные поля + { + for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result + } + ) : merge( + try(secret.custom_keys, {}), + { + for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result + } + ) + } + secrets_with_data = { for name, secret in local.secrets_map : name => { name = secret.name @@ -85,7 +118,7 @@ locals { data = try(local.secrets_data[name], {}) } } - + secrets_with_ignore = { for name, secret in local.secrets_with_data : name => secret if secret.ignore_changes @@ -98,7 +131,7 @@ locals { resource "random_password" "secrets" { for_each = local.random_keys_flat - + length = each.value.length special = each.value.special upper = true diff --git a/modules/k8s-secret/variables.tf b/modules/k8s-secret/variables.tf index 05149da..f7a42ec 100644 --- a/modules/k8s-secret/variables.tf +++ b/modules/k8s-secret/variables.tf @@ -1,23 +1,29 @@ variable "secrets" { description = "Список секретов для создания" type = list(object({ - name = string - namespace = string - secret_type = string - registry_url = optional(string, "") - dependencies = optional(object({ - cluster = optional(string, "") - db = optional(string, "") - user = optional(string, "") - bucket = optional(string, "") # Имя бакета для s3 секретов - }), {}) - custom_keys = optional(map(string), {}) - random_keys = optional(map(object({ + name = string + namespace = string + secret_type = string + registry_url = optional(string, "") + dependencies = optional(object({ + cluster = optional(string, "") + db = optional(string, "") + user = optional(string, "") + bucket = optional(string, "") # Имя бакета для s3 секретов + service_account = optional(string, "") # Имя YC Service Account для yc_sa секретов + }), {}) + yc_sa_key_names = optional(object({ + access_key = optional(string, "access_key") + secret_key = optional(string, "secret_key") + service_account_id = optional(string, "service_account_id") + }), {}) + custom_keys = optional(map(string), {}) + random_keys = optional(map(object({ length = optional(number, 32) special = optional(bool, false) })), {}) # Случайные ключи - labels = optional(map(string), {}) - annotations = optional(map(string), {}) + labels = optional(map(string), {}) + annotations = optional(map(string), {}) ignore_changes = optional(bool, false) })) } @@ -47,25 +53,36 @@ variable "s3_buckets_map" { default = {} } +variable "yc_service_accounts_map" { + description = "Map YC service accounts (name => data)" + type = map(object({ + service_account_id = string + access_key = optional(string, "") + secret_key = optional(string, "") + folder_roles = optional(list(string), []) + })) + default = {} +} + variable "database_outputs_map" { description = "Outputs от модулей Database, ключ = cluster_id:database_name:user_name" type = map(object({ - host = string + host = string database_name = string - user_name = string - password = string + user_name = string + password = string })) default = {} } variable "constants" { description = "Константные значения (CA сертификаты, endpoints и т.д.)" - type = map(string) - default = {} + type = map(string) + default = {} } variable "env_vars" { description = "Environment variables" - type = map(string) - default = {} + type = map(string) + default = {} } diff --git a/modules/yc-s3/main.tf b/modules/yc-s3/main.tf index 0bb0aea..343fc64 100644 --- a/modules/yc-s3/main.tf +++ b/modules/yc-s3/main.tf @@ -1,5 +1,20 @@ locals { - buckets_map = { for bucket in var.buckets : bucket.name => bucket } + buckets_map = { for bucket in var.buckets : bucket.name => bucket } + yc_service_accounts_map = { for sa in var.yc_service_accounts : sa.name => sa } + + yc_service_account_folder_roles = length(local.yc_service_accounts_map) > 0 ? merge([ + for sa_name, sa in local.yc_service_accounts_map : { + for role in try(sa.folder_roles, []) : "${sa_name}:${role}" => { + sa_name = sa_name + role = role + } + } + ]...) : {} + + yc_service_accounts_with_static_key = { + for sa_name, sa in local.yc_service_accounts_map : sa_name => sa + if try(sa.create_static_access_key, true) + } } resource "yandex_iam_service_account" "sa" { @@ -54,3 +69,25 @@ resource "yandex_storage_bucket_iam_binding" "uploader" { depends_on = [yandex_storage_bucket.this] } + +resource "yandex_iam_service_account" "custom_sa" { + for_each = local.yc_service_accounts_map + + name = each.value.name + description = try(each.value.description, "") +} + +resource "yandex_resourcemanager_folder_iam_member" "custom_sa_role" { + for_each = local.yc_service_account_folder_roles + + folder_id = var.folder_id + role = each.value.role + member = "serviceAccount:${yandex_iam_service_account.custom_sa[each.value.sa_name].id}" +} + +resource "yandex_iam_service_account_static_access_key" "custom_sa_key" { + for_each = local.yc_service_accounts_with_static_key + + service_account_id = yandex_iam_service_account.custom_sa[each.key].id + description = "Static access key for ${each.key}" +} diff --git a/modules/yc-s3/outputs.tf b/modules/yc-s3/outputs.tf index 258f62d..dd00fd8 100644 --- a/modules/yc-s3/outputs.tf +++ b/modules/yc-s3/outputs.tf @@ -27,3 +27,16 @@ output "buckets" { } sensitive = true } + +output "service_accounts" { + description = "Map of additional YC service accounts (name => data)" + value = { + for name, sa in yandex_iam_service_account.custom_sa : name => { + service_account_id = sa.id + access_key = try(yandex_iam_service_account_static_access_key.custom_sa_key[name].access_key, "") + secret_key = try(yandex_iam_service_account_static_access_key.custom_sa_key[name].secret_key, "") + folder_roles = try(local.yc_service_accounts_map[name].folder_roles, []) + } + } + sensitive = true +} diff --git a/modules/yc-s3/variables.tf b/modules/yc-s3/variables.tf index b82d6be..61bd6ce 100644 --- a/modules/yc-s3/variables.tf +++ b/modules/yc-s3/variables.tf @@ -18,6 +18,17 @@ variable "buckets" { })) } +variable "yc_service_accounts" { + description = "Additional Yandex Cloud service accounts with optional folder roles and static keys" + type = list(object({ + name = string + description = optional(string, "") + folder_roles = optional(list(string), []) + create_static_access_key = optional(bool, true) + })) + default = [] +} + variable "folder_id" { description = "Yandex Cloud folder ID" type = string