add SA creation logic

This commit is contained in:
Kochetkov S 2026-02-19 12:12:40 +03:00
parent 31c3bf338a
commit 3286b26777
8 changed files with 221 additions and 88 deletions

View File

@ -59,6 +59,19 @@ environments:
# lifecycle: # lifecycle:
# ignore_changes: false # 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: buckets:
- name: pulse-prod - name: pulse-prod
acl: private acl: private
@ -66,8 +79,14 @@ environments:
enabled: false enabled: false
cors: cors:
enabled: false enabled: false
# yc_service_accounts:
# - name: dwg-sa
# description: "DWG SA Uploader"
# folder_roles:
# - storage.uploader
# create_static_access_key: true
databases: databases:
- cluster_id: "c9quu72dj2ibu8dk54q5" - cluster_id: "c9quu72dj2ibu8dk54q5"
@ -81,4 +100,3 @@ environments:
password_special: false password_special: false
conn_limit: 10 conn_limit: 10

View File

@ -20,8 +20,9 @@ terraform {
} }
inputs = { inputs = {
buckets = local.env_config.buckets buckets = local.env_config.buckets
folder_id = get_env("YC_PROD_FOLDER_ID", get_env("YC_FOLDER_ID", "")) 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_token = get_env("YC_TOKEN", "")
yc_service_account_key_file = get_env("YC_SERVICE_ACCOUNT_KEY_FILE", "") yc_service_account_key_file = get_env("YC_SERVICE_ACCOUNT_KEY_FILE", "")

View File

@ -21,10 +21,11 @@ dependency "namespace" {
dependency "s3" { dependency "s3" {
config_path = "../s3" config_path = "../s3"
mock_outputs = { mock_outputs = {
bucket_name = "mock-bucket" bucket_name = "mock-bucket"
access_key = "mock-access-key" access_key = "mock-access-key"
secret_key = "mock-secret-key" secret_key = "mock-secret-key"
buckets = {} buckets = {}
service_accounts = {}
} }
mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"] mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"]
skip_outputs = get_env("TG_SKIP_DEPENDENCY_OUTPUTS", "false") == "true" skip_outputs = get_env("TG_SKIP_DEPENDENCY_OUTPUTS", "false") == "true"
@ -115,16 +116,17 @@ terraform {
inputs = { inputs = {
secrets = [ secrets = [
for secret in local.env_config.secrets : { for secret in local.env_config.secrets : {
name = secret.name name = secret.name
namespace = secret.namespace namespace = secret.namespace
secret_type = secret.type secret_type = secret.type
registry_url = try(secret.registry_url, "") registry_url = try(secret.registry_url, "")
dependencies = try(secret.dependencies, {}) dependencies = try(secret.dependencies, {})
custom_keys = try(secret.custom_keys, {}) yc_sa_key_names = try(secret.yc_sa_key_names, {})
random_keys = try(secret.random_keys, {}) custom_keys = try(secret.custom_keys, {})
labels = try(secret.labels, {}) random_keys = try(secret.random_keys, {})
annotations = try(secret.annotations, {}) labels = try(secret.labels, {})
ignore_changes = try(secret.lifecycle.ignore_changes, false) annotations = try(secret.annotations, {})
ignore_changes = try(secret.lifecycle.ignore_changes, false)
} }
] ]
@ -134,7 +136,8 @@ inputs = {
secret_key = dependency.s3.outputs.secret_key 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 database_outputs_map = dependency.database.outputs.database_outputs_map

View File

@ -6,13 +6,20 @@ locals {
"dockerconfigjson" = "kubernetes.io/dockerconfigjson" "dockerconfigjson" = "kubernetes.io/dockerconfigjson"
"database" = "Opaque" "database" = "Opaque"
"s3" = "Opaque" "s3" = "Opaque"
"yc_sa" = "Opaque"
"opaque" = "Opaque" "opaque" = "Opaque"
} }
default_yc_sa_key_names = {
access_key = "access_key"
secret_key = "secret_key"
service_account_id = "service_account_id"
}
secrets_map = { secrets_map = {
for idx, secret in var.secrets : secret.name => secret for idx, secret in var.secrets : secret.name => secret
} }
random_keys_flat = merge([ random_keys_flat = merge([
for secret_name, secret in local.secrets_map : { for secret_name, secret in local.secrets_map : {
for key, config in try(secret.random_keys, {}) : "${secret_name}:${key}" => { for key, config in try(secret.random_keys, {}) : "${secret_name}:${key}" => {
@ -23,56 +30,82 @@ locals {
} }
} }
]...) ]...)
database_outputs_by_secret = { database_outputs_by_secret = {
for name, secret in local.secrets_map : name => try( 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, "")}"], var.database_outputs_map["${try(secret.dependencies.cluster, "")}:${try(secret.dependencies.db, "")}:${try(secret.dependencies.user, "")}"],
null null
) )
} }
secrets_data = { yc_sa_sources = {
for name, secret in local.secrets_map : name => for name, secret in local.secrets_map : name => try(
secret.secret_type == "dockerconfigjson" ? { var.yc_service_accounts_map[try(secret.dependencies.service_account, "")],
".dockerconfigjson" = jsonencode({ null
auths = { )
"${secret.registry_url}" = { if secret.secret_type == "yc_sa"
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_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 = { secrets_with_data = {
for name, secret in local.secrets_map : name => { for name, secret in local.secrets_map : name => {
name = secret.name name = secret.name
@ -85,7 +118,7 @@ locals {
data = try(local.secrets_data[name], {}) data = try(local.secrets_data[name], {})
} }
} }
secrets_with_ignore = { secrets_with_ignore = {
for name, secret in local.secrets_with_data : name => secret for name, secret in local.secrets_with_data : name => secret
if secret.ignore_changes if secret.ignore_changes
@ -98,7 +131,7 @@ locals {
resource "random_password" "secrets" { resource "random_password" "secrets" {
for_each = local.random_keys_flat for_each = local.random_keys_flat
length = each.value.length length = each.value.length
special = each.value.special special = each.value.special
upper = true upper = true

View File

@ -1,23 +1,29 @@
variable "secrets" { variable "secrets" {
description = "Список секретов для создания" description = "Список секретов для создания"
type = list(object({ type = list(object({
name = string name = string
namespace = string namespace = string
secret_type = string secret_type = string
registry_url = optional(string, "") registry_url = optional(string, "")
dependencies = optional(object({ dependencies = optional(object({
cluster = optional(string, "") cluster = optional(string, "")
db = optional(string, "") db = optional(string, "")
user = optional(string, "") user = optional(string, "")
bucket = optional(string, "") # Имя бакета для s3 секретов bucket = optional(string, "") # Имя бакета для s3 секретов
}), {}) service_account = optional(string, "") # Имя YC Service Account для yc_sa секретов
custom_keys = optional(map(string), {}) }), {})
random_keys = optional(map(object({ 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) length = optional(number, 32)
special = optional(bool, false) special = optional(bool, false)
})), {}) # Случайные ключи })), {}) # Случайные ключи
labels = optional(map(string), {}) labels = optional(map(string), {})
annotations = optional(map(string), {}) annotations = optional(map(string), {})
ignore_changes = optional(bool, false) ignore_changes = optional(bool, false)
})) }))
} }
@ -47,25 +53,36 @@ variable "s3_buckets_map" {
default = {} 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" { variable "database_outputs_map" {
description = "Outputs от модулей Database, ключ = cluster_id:database_name:user_name" description = "Outputs от модулей Database, ключ = cluster_id:database_name:user_name"
type = map(object({ type = map(object({
host = string host = string
database_name = string database_name = string
user_name = string user_name = string
password = string password = string
})) }))
default = {} default = {}
} }
variable "constants" { variable "constants" {
description = "Константные значения (CA сертификаты, endpoints и т.д.)" description = "Константные значения (CA сертификаты, endpoints и т.д.)"
type = map(string) type = map(string)
default = {} default = {}
} }
variable "env_vars" { variable "env_vars" {
description = "Environment variables" description = "Environment variables"
type = map(string) type = map(string)
default = {} default = {}
} }

View File

@ -1,5 +1,20 @@
locals { 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" { resource "yandex_iam_service_account" "sa" {
@ -54,3 +69,25 @@ resource "yandex_storage_bucket_iam_binding" "uploader" {
depends_on = [yandex_storage_bucket.this] 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}"
}

View File

@ -27,3 +27,16 @@ output "buckets" {
} }
sensitive = true 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
}

View File

@ -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" { variable "folder_id" {
description = "Yandex Cloud folder ID" description = "Yandex Cloud folder ID"
type = string type = string