diff --git a/infrastructure.yaml b/infrastructure.yaml index 4618282..d5fe1ad 100644 --- a/infrastructure.yaml +++ b/infrastructure.yaml @@ -30,6 +30,51 @@ environments: min.insync.replicas: "1" deletionPolicy: orphan + rabbitmq: + management_endpoint: http://default-rabbit-cluster.rabbitmq.svc.cluster.local:15672 + amqp_host: default-rabbit-cluster.rabbitmq.svc.cluster.local + amqp_port: 5672 + policy: + allow_create: true + allow_delete: false + vhosts: + - name: iac-smoke-prod + users: + - name: iac-smoke-prod + permissions: + - vhost: iac-smoke-prod + user: iac-smoke-prod + configure: "^iac\\.smoke\\..*" + write: "^iac\\.smoke\\..*" + read: "^iac\\.smoke\\..*" + exchanges: + - vhost: iac-smoke-prod + name: iac.smoke.events + type: topic + durable: true + auto_delete: false + queues: + - vhost: iac-smoke-prod + name: iac.smoke.queue + durable: true + auto_delete: false + arguments: + x-queue-type: quorum + bindings: + - vhost: iac-smoke-prod + source: iac.smoke.events + destination: iac.smoke.queue + destination_type: queue + routing_key: iac.smoke.created + policies: + - vhost: iac-smoke-prod + name: iac-smoke-queue-ttl + pattern: "^iac\\.smoke\\..*" + apply_to: queues + priority: 1 + definition: + message-ttl: 600000 + namespaces: - name: block-analyzer labels: @@ -190,6 +235,15 @@ environments: lifecycle: ignore_changes: true + - name: rabbitmq-iac-smoke-secret + namespace: pulse + type: rabbitmq + dependencies: + rabbitmq_vhost: iac-smoke-prod + rabbitmq_user: iac-smoke-prod + lifecycle: + ignore_changes: true + - name: iams-secret namespace: iam type: opaque diff --git a/live/prod/rabbitmq/terragrunt.hcl b/live/prod/rabbitmq/terragrunt.hcl new file mode 100644 index 0000000..2fdf8d6 --- /dev/null +++ b/live/prod/rabbitmq/terragrunt.hcl @@ -0,0 +1,49 @@ +include "root" { + path = find_in_parent_folders() +} + +include "env" { + path = find_in_parent_folders("env.hcl") + expose = true + merge_strategy = "deep" +} + +locals { + repo_root = try(get_repo_root(), "${get_terragrunt_dir()}/../../..") + infra_config = yamldecode(file("${local.repo_root}/infrastructure.yaml")) + env_name = basename(dirname(get_terragrunt_dir())) + env_var_prefix = upper(replace(local.env_name, "-", "_")) + env_config = local.infra_config.environments[local.env_name] + rabbitmq_config = try(local.env_config.rabbitmq, {}) +} + +terraform { + source = "${get_terragrunt_dir()}/../../../modules//rabbitmq" +} + +inputs = { + environment = local.env_name + + management_endpoint = get_env("RABBITMQ_${local.env_var_prefix}_MANAGEMENT_ENDPOINT", try(local.rabbitmq_config.management_endpoint, "")) + management_username = get_env("RABBITMQ_${local.env_var_prefix}_MANAGEMENT_USERNAME", "") + management_password = get_env("RABBITMQ_${local.env_var_prefix}_MANAGEMENT_PASSWORD", "") + + amqp_host = get_env("RABBITMQ_${local.env_var_prefix}_AMQP_HOST", try(local.rabbitmq_config.amqp_host, "rabbitmq.rabbitmq.svc.cluster.local")) + amqp_port = tonumber(get_env("RABBITMQ_${local.env_var_prefix}_AMQP_PORT", tostring(try(local.rabbitmq_config.amqp_port, 5672)))) + + rabbitmq_policy = try(local.rabbitmq_config.policy, {}) + vhosts = try(local.rabbitmq_config.vhosts, []) + users = try(local.rabbitmq_config.users, []) + permissions = try(local.rabbitmq_config.permissions, []) + exchanges = try(local.rabbitmq_config.exchanges, []) + queues = try(local.rabbitmq_config.queues, []) + bindings = try(local.rabbitmq_config.bindings, []) + policies = try(local.rabbitmq_config.policies, []) + + yc_token = get_env("YC_TOKEN", "") + yc_service_account_key_file = get_env("YC_SERVICE_ACCOUNT_KEY_FILE", "") + yc_cloud_id = get_env("YC_CLOUD_ID", "") + yc_folder_id = get_env("YC_PROD_FOLDER_ID", get_env("YC_FOLDER_ID", "")) + kubeconfig_path = get_env("KUBECONFIG", "") + kube_context = get_env("KUBE_CONTEXT", "") +} diff --git a/live/prod/secrets/terragrunt.hcl b/live/prod/secrets/terragrunt.hcl index 19218e1..73478ff 100644 --- a/live/prod/secrets/terragrunt.hcl +++ b/live/prod/secrets/terragrunt.hcl @@ -40,6 +40,15 @@ dependency "database" { skip_outputs = get_env("TG_SKIP_DEPENDENCY_OUTPUTS", "false") == "true" } +dependency "rabbitmq" { + config_path = "../rabbitmq" + mock_outputs = { + rabbitmq_outputs_map = {} + } + mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"] + skip_outputs = get_env("TG_SKIP_DEPENDENCY_OUTPUTS", "false") == "true" +} + dependency "valkey_users" { config_path = "../valkey-users" mock_outputs = { @@ -150,6 +159,7 @@ inputs = { yc_service_accounts_map = try(dependency.s3.outputs.service_accounts, {}) database_outputs_map = dependency.database.outputs.database_outputs_map + rabbitmq_outputs_map = dependency.rabbitmq.outputs.rabbitmq_outputs_map valkey_outputs_map = dependency.valkey_users.outputs.valkey_outputs_map constants = { diff --git a/live/terragrunt.hcl b/live/terragrunt.hcl index b7a3674..9d381b9 100644 --- a/live/terragrunt.hcl +++ b/live/terragrunt.hcl @@ -40,6 +40,10 @@ terraform { source = "hashicorp/random" version = "~> 3.1" } + rabbitmq = { + source = "cyrilgdn/rabbitmq" + version = "~> 1.8" + } } } diff --git a/modules/k8s-secret/main.tf b/modules/k8s-secret/main.tf index 500c07b..b351b0d 100644 --- a/modules/k8s-secret/main.tf +++ b/modules/k8s-secret/main.tf @@ -9,6 +9,7 @@ locals { "s3" = "Opaque" "valkey" = "Opaque" "yc_sa" = "Opaque" + "rabbitmq" = "Opaque" "opaque" = "Opaque" } @@ -54,6 +55,20 @@ locals { ) } + rabbitmq_outputs_by_secret = { + for name, secret in local.secrets_map : name => try( + var.rabbitmq_outputs_map[ + format( + "%s:%s:%s", + try(secret.dependencies.rabbitmq_ref, "default"), + try(secret.dependencies.rabbitmq_vhost, try(secret.dependencies.vhost, "")), + try(secret.dependencies.rabbitmq_user, try(secret.dependencies.user, "")) + ) + ], + null + ) + } + yc_sa_sources = { for name, secret in local.secrets_map : name => try( var.yc_service_accounts_map[try(secret.dependencies.service_account, "")], @@ -155,6 +170,25 @@ locals { { for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result } + ) : secret.secret_type == "rabbitmq" && local.rabbitmq_outputs_by_secret[name] != null ? merge( + { + host = local.rabbitmq_outputs_by_secret[name].host + hostname = "${local.rabbitmq_outputs_by_secret[name].host}:${local.rabbitmq_outputs_by_secret[name].port}" + port = local.rabbitmq_outputs_by_secret[name].port + vhost = local.rabbitmq_outputs_by_secret[name].vhost + user = local.rabbitmq_outputs_by_secret[name].username + username = local.rabbitmq_outputs_by_secret[name].username + password = local.rabbitmq_outputs_by_secret[name].password + uri = local.rabbitmq_outputs_by_secret[name].uri + management_endpoint = local.rabbitmq_outputs_by_secret[name].management_endpoint + }, + try(secret.custom_keys, {}), + { + for key, constant_name in try(secret.constant_keys, {}) : key => try(var.constants[constant_name], "") + }, + { + for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result + } ) : merge( try(secret.custom_keys, {}), { diff --git a/modules/k8s-secret/variables.tf b/modules/k8s-secret/variables.tf index bb10658..2dea32b 100644 --- a/modules/k8s-secret/variables.tf +++ b/modules/k8s-secret/variables.tf @@ -13,6 +13,9 @@ variable "secrets" { valkey_user = optional(string, "") bucket = optional(string, "") # Имя бакета для s3 секретов service_account = optional(string, "") # Имя YC Service Account для yc_sa секретов + rabbitmq_ref = optional(string, "default") + rabbitmq_vhost = optional(string, "") + rabbitmq_user = optional(string, "") }), {}) yc_sa_key_names = optional(object({ access_key = optional(string, "access_key") @@ -90,6 +93,20 @@ variable "valkey_outputs_map" { default = {} } +variable "rabbitmq_outputs_map" { + description = "Outputs от модуля RabbitMQ, ключ = ref:vhost:user" + type = map(object({ + host = string + port = string + vhost = string + username = string + password = string + uri = string + management_endpoint = string + })) + default = {} +} + variable "constants" { description = "Константные значения (CA сертификаты, endpoints и т.д.)" type = map(string) diff --git a/modules/k8s-secrets/main.tf b/modules/k8s-secrets/main.tf index b461cc6..719b555 100644 --- a/modules/k8s-secrets/main.tf +++ b/modules/k8s-secrets/main.tf @@ -5,9 +5,9 @@ resource "kubernetes_secret" "this" { } metadata { - name = each.value.name - namespace = each.value.namespace - labels = try(each.value.labels, {}) + name = each.value.name + namespace = each.value.namespace + labels = try(each.value.labels, {}) annotations = try(each.value.annotations, {}) } diff --git a/modules/k8s-secrets/variables.tf b/modules/k8s-secrets/variables.tf index 79531e6..228f3b4 100644 --- a/modules/k8s-secrets/variables.tf +++ b/modules/k8s-secrets/variables.tf @@ -1,12 +1,12 @@ variable "secrets" { description = "Список секретов для создания" type = list(object({ - name = string - namespace = string - secret_type = string - data = map(string) - labels = optional(map(string)) - annotations = optional(map(string)) + name = string + namespace = string + secret_type = string + data = map(string) + labels = optional(map(string)) + annotations = optional(map(string)) ignore_changes = optional(bool, false) })) } diff --git a/modules/rabbitmq/main.tf b/modules/rabbitmq/main.tf new file mode 100644 index 0000000..572cba3 --- /dev/null +++ b/modules/rabbitmq/main.tf @@ -0,0 +1,208 @@ +locals { + rabbitmq_enabled = length(var.vhosts) + length(var.users) + length(var.permissions) + length(var.exchanges) + length(var.queues) + length(var.bindings) + length(var.policies) > 0 + + management_endpoint = local.rabbitmq_enabled ? var.management_endpoint : "http://127.0.0.1:15672" + management_username = local.rabbitmq_enabled ? var.management_username : "noop" + management_password = local.rabbitmq_enabled ? var.management_password : "noop" + + vhosts = { + for vhost in var.vhosts : vhost.name => { + name = vhost.name + } + } + + users = { + for user in var.users : user.name => { + name = user.name + tags = try(user.tags, []) + password_length = try(tonumber(user.password_length), var.default_user_password_length) + password_special = try(user.password_special, false) + } + } + + permissions = { + for permission in var.permissions : "${permission.vhost}:${permission.user}" => { + vhost = permission.vhost + user = permission.user + configure = tostring(try(permission.configure, "")) + write = tostring(try(permission.write, "")) + read = tostring(try(permission.read, "")) + } + } + + exchanges = { + for exchange in var.exchanges : "${exchange.vhost}:${exchange.name}" => { + name = exchange.name + vhost = exchange.vhost + type = try(exchange.type, "topic") + durable = try(exchange.durable, true) + auto_delete = try(exchange.auto_delete, false) + arguments = try(exchange.arguments, {}) + } + } + + queues = { + for queue in var.queues : "${queue.vhost}:${queue.name}" => { + name = queue.name + vhost = queue.vhost + durable = try(queue.durable, true) + auto_delete = try(queue.auto_delete, false) + arguments = try(queue.arguments, {}) + } + } + + bindings = { + for idx, binding in var.bindings : "${binding.vhost}:${binding.source}:${binding.destination_type}:${binding.destination}:${try(binding.routing_key, "")}:${idx}" => { + vhost = binding.vhost + source = binding.source + destination = binding.destination + destination_type = binding.destination_type + routing_key = try(binding.routing_key, "") + arguments = try(binding.arguments, {}) + } + } + + policies = { + for policy in var.policies : "${policy.vhost}:${policy.name}" => { + name = policy.name + vhost = policy.vhost + pattern = policy.pattern + apply_to = try(policy.apply_to, "all") + priority = try(tonumber(policy.priority), 0) + definition = policy.definition + } + } + + user_vhosts = { + for item in distinct([ + for permission in values(local.permissions) : "${permission.vhost}:${permission.user}" + ]) : item => { + vhost = split(":", item)[0] + user = split(":", item)[1] + } + } +} + +provider "rabbitmq" { + endpoint = local.management_endpoint + username = local.management_username + password = local.management_password +} + +resource "rabbitmq_vhost" "this" { + for_each = local.vhosts + + name = each.value.name + + lifecycle { + prevent_destroy = true + } +} + +resource "random_password" "user" { + for_each = local.users + + length = each.value.password_length + special = each.value.password_special + upper = true + lower = true + numeric = true +} + +resource "rabbitmq_user" "this" { + for_each = local.users + + name = each.value.name + password = random_password.user[each.key].result + tags = each.value.tags + + lifecycle { + prevent_destroy = true + ignore_changes = [password] + } +} + +resource "rabbitmq_permissions" "this" { + for_each = local.permissions + + user = rabbitmq_user.this[each.value.user].name + vhost = rabbitmq_vhost.this[each.value.vhost].name + + permissions { + configure = each.value.configure + write = each.value.write + read = each.value.read + } + + lifecycle { + prevent_destroy = true + } +} + +resource "rabbitmq_exchange" "this" { + for_each = local.exchanges + + name = each.value.name + vhost = rabbitmq_vhost.this[each.value.vhost].name + + settings { + type = each.value.type + durable = each.value.durable + auto_delete = each.value.auto_delete + arguments = each.value.arguments + } + + lifecycle { + prevent_destroy = true + } +} + +resource "rabbitmq_queue" "this" { + for_each = local.queues + + name = each.value.name + vhost = rabbitmq_vhost.this[each.value.vhost].name + + settings { + durable = each.value.durable + auto_delete = each.value.auto_delete + arguments = each.value.arguments + } + + lifecycle { + prevent_destroy = true + } +} + +resource "rabbitmq_binding" "this" { + for_each = local.bindings + + source = rabbitmq_exchange.this["${each.value.vhost}:${each.value.source}"].name + vhost = rabbitmq_vhost.this[each.value.vhost].name + destination = each.value.destination + destination_type = each.value.destination_type + routing_key = each.value.routing_key + arguments = each.value.arguments + + lifecycle { + prevent_destroy = true + } +} + +resource "rabbitmq_policy" "this" { + for_each = local.policies + + name = each.value.name + vhost = rabbitmq_vhost.this[each.value.vhost].name + + policy { + pattern = each.value.pattern + priority = each.value.priority + apply_to = each.value.apply_to + definition = each.value.definition + } + + lifecycle { + prevent_destroy = true + } +} diff --git a/modules/rabbitmq/outputs.tf b/modules/rabbitmq/outputs.tf new file mode 100644 index 0000000..aabfadc --- /dev/null +++ b/modules/rabbitmq/outputs.tf @@ -0,0 +1,25 @@ +output "vhosts" { + description = "RabbitMQ vhosts managed by this component." + value = keys(rabbitmq_vhost.this) +} + +output "users" { + description = "RabbitMQ users managed by this component." + value = keys(rabbitmq_user.this) +} + +output "rabbitmq_outputs_map" { + description = "Map for k8s-secret module. Key format: default:vhost:user." + value = { + for key, item in local.user_vhosts : "default:${item.vhost}:${item.user}" => { + host = var.amqp_host + port = tostring(var.amqp_port) + vhost = item.vhost + username = rabbitmq_user.this[item.user].name + password = random_password.user[item.user].result + uri = "amqp://${rabbitmq_user.this[item.user].name}:${urlencode(random_password.user[item.user].result)}@${var.amqp_host}:${var.amqp_port}/${urlencode(item.vhost)}" + management_endpoint = var.management_endpoint + } + } + sensitive = true +} diff --git a/modules/rabbitmq/variables.tf b/modules/rabbitmq/variables.tf new file mode 100644 index 0000000..fc796d8 --- /dev/null +++ b/modules/rabbitmq/variables.tf @@ -0,0 +1,90 @@ +variable "environment" { + description = "Target environment name." + type = string +} + +variable "management_endpoint" { + description = "RabbitMQ Management API endpoint, for example http://rabbitmq.rabbitmq.svc.cluster.local:15672." + type = string +} + +variable "management_username" { + description = "RabbitMQ admin username for Management API." + type = string + sensitive = true +} + +variable "management_password" { + description = "RabbitMQ admin password for Management API." + type = string + sensitive = true +} + +variable "amqp_host" { + description = "RabbitMQ AMQP host published to generated Kubernetes secrets." + type = string + default = "rabbitmq.rabbitmq.svc.cluster.local" +} + +variable "amqp_port" { + description = "RabbitMQ AMQP port published to generated Kubernetes secrets." + type = number + default = 5672 +} + +variable "rabbitmq_policy" { + description = "Platform policy for RabbitMQ declarations." + type = object({ + allow_create = optional(bool, true) + allow_delete = optional(bool, false) + }) + default = {} +} + +variable "vhosts" { + description = "RabbitMQ vhosts declared in infrastructure.yaml." + type = list(any) + default = [] +} + +variable "users" { + description = "RabbitMQ users declared in infrastructure.yaml." + type = list(any) + default = [] +} + +variable "permissions" { + description = "RabbitMQ permissions declared in infrastructure.yaml." + type = list(any) + default = [] +} + +variable "exchanges" { + description = "RabbitMQ exchanges declared in infrastructure.yaml." + type = list(any) + default = [] +} + +variable "queues" { + description = "RabbitMQ queues declared in infrastructure.yaml." + type = list(any) + default = [] +} + +variable "bindings" { + description = "RabbitMQ bindings declared in infrastructure.yaml." + type = list(any) + default = [] +} + +variable "policies" { + description = "RabbitMQ policies declared in infrastructure.yaml." + type = list(any) + default = [] +} + +variable "default_user_password_length" { + description = "Default generated RabbitMQ user password length." + type = number + default = 32 +} diff --git a/modules/rabbitmq/versions.tf b/modules/rabbitmq/versions.tf new file mode 100644 index 0000000..fd126d1 --- /dev/null +++ b/modules/rabbitmq/versions.tf @@ -0,0 +1,3 @@ +terraform { + required_version = ">= 1.5.0" +} diff --git a/scripts/generate-pipeline.sh b/scripts/generate-pipeline.sh index 41b8f02..c13ab8a 100755 --- a/scripts/generate-pipeline.sh +++ b/scripts/generate-pipeline.sh @@ -38,7 +38,7 @@ find live -name "terragrunt.hcl" -not -path "*/.terragrunt-cache/*" | sort | whi secrets_dependency_components=() needs_section="" if [ "$component" = "secrets" ]; then - for dep_component in namespace s3 database valkey-users; do + for dep_component in namespace s3 database rabbitmq valkey-users; do if [ -f "live/${env}/${dep_component}/terragrunt.hcl" ]; then secrets_dependency_components+=("$dep_component") fi