From 52b568162421e94195ea974e9222070a72b431e1 Mon Sep 17 00:00:00 2001 From: Mina Abadir Date: Thu, 6 Aug 2026 17:53:12 -0400 Subject: [PATCH 1/7] add RDS Proxy module --- README.md | 5 +- database/aurora/.terraform.lock.hcl | 10 +- database/aurora/README.md | 15 + database/aurora/cluster.tf | 10 + database/aurora/locals.tf | 12 + database/aurora/outputs.tf | 19 + database/aurora/rds_proxy.tf | 43 ++ database/aurora/rvn-aurora-definition.yml | 130 +++++- database/aurora/security_group.tf | 12 +- database/aurora/variables.tf | 111 +++++ database/rds-proxy/.terraform.lock.hcl | 27 ++ database/rds-proxy/README.md | 149 +++++++ database/rds-proxy/data.tf | 11 + database/rds-proxy/db_proxy.tf | 83 ++++ database/rds-proxy/iam_role.tf | 61 +++ database/rds-proxy/locals.tf | 35 ++ database/rds-proxy/outputs.tf | 83 ++++ .../rds-proxy/rvn-rds-proxy-definition.yml | 385 ++++++++++++++++++ database/rds-proxy/security_group.tf | 50 +++ database/rds-proxy/variables.tf | 291 +++++++++++++ database/rds-proxy/versions.tf | 16 + database/rds/.terraform.lock.hcl | 10 +- database/rds/README.md | 15 + database/rds/locals.tf | 16 + database/rds/outputs.tf | 19 + database/rds/rds_instance.tf | 10 + database/rds/rds_proxy.tf | 39 ++ database/rds/rvn-rds-definition.yml | 148 ++++++- database/rds/security_group.tf | 12 +- database/rds/variables.tf | 111 +++++ 30 files changed, 1928 insertions(+), 10 deletions(-) create mode 100644 database/aurora/rds_proxy.tf create mode 100644 database/rds-proxy/.terraform.lock.hcl create mode 100644 database/rds-proxy/README.md create mode 100644 database/rds-proxy/data.tf create mode 100644 database/rds-proxy/db_proxy.tf create mode 100644 database/rds-proxy/iam_role.tf create mode 100644 database/rds-proxy/locals.tf create mode 100644 database/rds-proxy/outputs.tf create mode 100644 database/rds-proxy/rvn-rds-proxy-definition.yml create mode 100644 database/rds-proxy/security_group.tf create mode 100644 database/rds-proxy/variables.tf create mode 100644 database/rds-proxy/versions.tf create mode 100644 database/rds/rds_proxy.tf diff --git a/README.md b/README.md index 37a00cb0..ab63721e 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ This repository contains reusable infrastructure modules designed for enterprise | `database/` | `aurora` | AWS Aurora clusters (MySQL, PostgreSQL, Serverless v2, Global Database) (includes `rvn-aurora` module definition) | v1.0.0 | | `database/` | `dynamodb` | AWS DynamoDB tables | v1.0.0 | | `database/` | `rds` | AWS RDS instances | v1.0.0 | +| `database/` | `rds-proxy` | AWS RDS Proxy for connection pooling in front of RDS instances or Aurora clusters (standalone or via the `rds`/`aurora` modules) | v1.0.0 | | `hosting/` | `static_site` | Composite static site hosting (S3 + CloudFront + OAC, optional CloudFront Function / Lambda@Edge) | v1.0.0 | | `kubernetes/` | `eks_cluster` | AWS EKS clusters with OIDC, KMS-encrypted secrets, control plane logging, core add-ons, EBS CSI / Pod Identity Agent, LB Controller Pod Identity role, and access entries | v1.0.0 | | `kubernetes/` | `eks_node_group` | AWS EKS managed node groups (one per module) with IAM, optional launch template, taints, labels, and SPOT/ON_DEMAND capacity | v1.0.0 | @@ -59,7 +60,7 @@ sync by `node tools/ravion-modules/dist/src/cli.js readme` (enforced in CI, and | Definition | Name | Version | Module path | | ---------- | ---- | ------- | ----------- | | `rvn-acm-certificate` | ACM Certificate | v1.0.0 | `security/acm_certificate/` | -| `rvn-aurora` | Aurora Database | v1.0.0 | `database/aurora/` | +| `rvn-aurora` | Aurora Database | v1.1.0 | `database/aurora/` | | `rvn-aws-alb` | AWS Application Load Balancer | v1.0.0 | `networking/alb/` | | `rvn-aws-iam-policy` | AWS IAM Policy | v1.0.0 | `security/iam_policy/` | | `rvn-aws-iam-role` | AWS IAM Role | v1.0.0 | `security/iam/` | @@ -74,7 +75,7 @@ sync by `node tools/ravion-modules/dist/src/cli.js readme` (enforced in CI, and | `rvn-efs` | EFS File System | v1.0.0 | `storage/efs/` | | `rvn-elasticache` | ElastiCache | v1.0.0 | `cache/elasticache/` | | `rvn-lambda` | Lambda Function | v1.0.0 | `compute/lambda/` | -| `rvn-rds` | RDS Database | v1.0.0 | `database/rds/` | +| `rvn-rds` | RDS Database | v1.1.0 | `database/rds/` | | `rvn-route53` | Route 53 DNS | v1.0.0 | `networking/route53/` | | `rvn-s3` | S3 Bucket | v1.0.0 | `storage/s3/` | | `rvn-stack` | Terraform Stack | v1.2.4 | `stack/terraform/` | diff --git a/database/aurora/.terraform.lock.hcl b/database/aurora/.terraform.lock.hcl index f4f76e73..e3d141ec 100644 --- a/database/aurora/.terraform.lock.hcl +++ b/database/aurora/.terraform.lock.hcl @@ -3,9 +3,17 @@ provider "registry.opentofu.org/hashicorp/aws" { version = "6.31.0" - constraints = ">= 5.0.0" + constraints = ">= 6.0.0" hashes = [ + "h1:/NZ7N5KZ4AtbADMedSXuYDJ7UN1ID//Vm5Sg1FpJ01w=", "h1:GOVFUXeawJxB/a2xXeC8KysTDELEGo66HmHBnBiB9lk=", + "h1:MUX4jV+ZiFMjzUlOkWUbN8MBr0zTKHm+5HjKfvIUJVk=", + "h1:SQs/AJ+vD/NdenGjrwMv6lpVNvTtpkp2SDAVeo6QbGk=", + "h1:aVArahOuhTSr9mbzQjJ4TvHUl2u668P0/X8VJ4P47lg=", + "h1:d6Lvb7ZuKZkUXowkVDd3sNr3a/BoEFQe63YtfjtxD2U=", + "h1:jfvyLm1+0yRZGpXU9a1VAvPcq7DrZfADduCMmGkfMic=", + "h1:kPjuvs3kMhzZXw3X0bx3+AHgkWT+xRPz+vuL6BPhpA4=", + "h1:yNj5rY2CtV8331DHyQkCsBrgt8rAY/H8YESEm3df27c=", "zh:15038bcfac8cad8f1422d4e5e6d02aebed9b8cb67df5f2a9fec6823fbfefa6b1", "zh:5662e6801a6c5c0d9ac76b70ab77395702cf9ece871cd5c714038dfd97e58962", "zh:a4dbbe440e2bcc6d4770c96769963bf4b81a484ff9783251633a4564da2cf56b", diff --git a/database/aurora/README.md b/database/aurora/README.md index 7cb80eaa..d51e46be 100644 --- a/database/aurora/README.md +++ b/database/aurora/README.md @@ -422,6 +422,18 @@ module "aurora" { | activity_stream_mode | Activity stream mode: sync or async. | `string` | `"async"` | no | | activity_stream_kms_key_id | KMS key ARN for activity stream (required when enabled). | `string` | `null` | no | | iam_role_associations | Map of IAM role associations (S3_IMPORT, S3_EXPORT, LAMBDA_INVOKE, etc.). | `map(object)` | `{}` | no | +| proxy_creation_enabled | Whether to create an RDS Proxy in front of the cluster. | `bool` | `false` | no | +| proxy_auth_secret_arns | Secrets Manager secret ARNs for proxy auth (defaults to the managed master user secret). | `list(string)` | `[]` | no | +| proxy_secret_kms_key_arns | KMS key ARNs used to encrypt the proxy auth secrets (customer-managed keys). | `list(string)` | `[]` | no | +| proxy_iam_auth_enabled | Require IAM authentication for proxy connections. | `bool` | `false` | no | +| proxy_tls_requirement_enabled | Require TLS for proxy connections. | `bool` | `true` | no | +| proxy_debug_logging_enabled | Log detailed proxy connection information to CloudWatch Logs. | `bool` | `false` | no | +| proxy_idle_client_timeout | Seconds a client connection can be idle before the proxy disconnects it. | `number` | `1800` | no | +| proxy_connection_borrow_timeout | Seconds the proxy waits for an available connection in the pool. | `number` | `120` | no | +| proxy_init_query | SQL statements the proxy runs when opening each new database connection. | `string` | `null` | no | +| proxy_max_connections_percent | Max proxy connection pool size (% of database max_connections). | `number` | `100` | no | +| proxy_max_idle_connections_percent | Max idle proxy connections (% of database max_connections). | `number` | `50` | no | +| proxy_session_pinning_filters | Session pinning filters (EXCLUDE_VARIABLE_SETS). | `list(string)` | `[]` | no | ## Outputs @@ -458,6 +470,9 @@ module "aurora" { | activity_stream_kinesis_stream_name | The Kinesis data stream name for activity stream. | | activity_stream_kms_key_id | The KMS key ID for activity stream. | | autoscaling_target_arn | The Application Auto Scaling target ARN. | +| proxy_endpoint | The RDS Proxy endpoint (null when no proxy is created). | +| proxy_arn | The RDS Proxy ARN (null when no proxy is created). | +| proxy_security_group_id | The RDS Proxy security group ID (null when no proxy is created). | ## Security Considerations diff --git a/database/aurora/cluster.tf b/database/aurora/cluster.tf index c0db9873..8c516ffb 100644 --- a/database/aurora/cluster.tf +++ b/database/aurora/cluster.tf @@ -155,6 +155,16 @@ resource "aws_rds_cluster" "this" { condition = !var.activity_stream_enabled || var.activity_stream_kms_key_id != null error_message = "activity_stream_kms_key_id is required when activity_stream_enabled is true." } + + precondition { + condition = !var.proxy_creation_enabled || var.master_user_password_management_enabled || length(var.proxy_auth_secret_arns) > 0 + error_message = "proxy_auth_secret_arns is required when proxy_creation_enabled is true and master_user_password_management_enabled is false." + } + + precondition { + condition = !var.proxy_creation_enabled || length(var.subnet_ids) >= 2 + error_message = "At least 2 subnet_ids are required when proxy_creation_enabled is true." + } } depends_on = [ diff --git a/database/aurora/locals.tf b/database/aurora/locals.tf index 90c6adf6..06190171 100644 --- a/database/aurora/locals.tf +++ b/database/aurora/locals.tf @@ -101,4 +101,16 @@ locals { ) instances = length(var.instances) > 0 ? var.instances : local.generated_instances + + # RDS Proxy + create_proxy = var.proxy_creation_enabled + proxy_engine_family = local.is_mysql ? "MYSQL" : "POSTGRESQL" + proxy_auth_secret_arns = ( + length(var.proxy_auth_secret_arns) > 0 ? var.proxy_auth_secret_arns : + var.master_user_password_management_enabled ? [aws_rds_cluster.this.master_user_secret[0].secret_arn] : [] + ) + proxy_secret_kms_key_arns = distinct(concat( + var.proxy_secret_kms_key_arns, + try(startswith(var.master_user_secret_kms_key_id, "arn:"), false) ? [var.master_user_secret_kms_key_id] : [] + )) } diff --git a/database/aurora/outputs.tf b/database/aurora/outputs.tf index b798b557..5d19fd55 100644 --- a/database/aurora/outputs.tf +++ b/database/aurora/outputs.tf @@ -218,3 +218,22 @@ output "region" { description = "The AWS region where the resources are deployed." value = local.region } + +################################################################################ +# RDS Proxy Outputs +################################################################################ + +output "proxy_endpoint" { + description = "The endpoint of the RDS Proxy. Null when no proxy is created." + value = local.create_proxy ? module.proxy[0].endpoint : null +} + +output "proxy_arn" { + description = "The ARN of the RDS Proxy. Null when no proxy is created." + value = local.create_proxy ? module.proxy[0].proxy_arn : null +} + +output "proxy_security_group_id" { + description = "The ID of the RDS Proxy security group. Null when no proxy is created." + value = local.create_proxy ? module.proxy[0].security_group_id : null +} diff --git a/database/aurora/rds_proxy.tf b/database/aurora/rds_proxy.tf new file mode 100644 index 00000000..05338cf6 --- /dev/null +++ b/database/aurora/rds_proxy.tf @@ -0,0 +1,43 @@ +################################################################################ +# RDS Proxy (optional) +################################################################################ + +module "proxy" { + count = local.create_proxy ? 1 : 0 + + source = "../rds-proxy" + + name = var.name + engine_family = local.proxy_engine_family + vpc_id = var.vpc_id + subnet_ids = var.subnet_ids + port = local.port + tags = var.tags + + db_cluster_identifier = aws_rds_cluster.this.cluster_identifier + + auth = [ + for arn in local.proxy_auth_secret_arns : { + secret_arn = arn + iam_auth = var.proxy_iam_auth_enabled ? "REQUIRED" : "DISABLED" + } + ] + secret_kms_key_arns = local.proxy_secret_kms_key_arns + + tls_requirement_enabled = var.proxy_tls_requirement_enabled + debug_logging_enabled = var.proxy_debug_logging_enabled + idle_client_timeout = var.proxy_idle_client_timeout + + connection_borrow_timeout = var.proxy_connection_borrow_timeout + init_query = var.proxy_init_query + max_connections_percent = var.proxy_max_connections_percent + max_idle_connections_percent = var.proxy_max_idle_connections_percent + session_pinning_filters = var.proxy_session_pinning_filters + + allowed_security_group_ids = var.allowed_security_group_ids + allowed_cidr_blocks = var.allowed_cidr_blocks + + # Registering an Aurora cluster as a proxy target requires the cluster to + # already have at least one instance. + depends_on = [aws_rds_cluster_instance.this] +} diff --git a/database/aurora/rvn-aurora-definition.yml b/database/aurora/rvn-aurora-definition.yml index 35e3ce4d..dfacc888 100644 --- a/database/aurora/rvn-aurora-definition.yml +++ b/database/aurora/rvn-aurora-definition.yml @@ -4,8 +4,8 @@ definition: description: Creates and manages an Amazon Aurora cluster. Supports Aurora PostgreSQL and Aurora MySQL with provisioned or Serverless v2 capacity. release: - version: 1.0.0 - description: First stable release. + version: 1.1.0 + description: Add optional RDS Proxy connection pooling in front of the cluster. module: inputs: - id: network @@ -322,6 +322,115 @@ module: show_when: security_group_creation_enabled: true type: string_array + - id: section_proxy + label: Connection pooling + type: section + - default: false + description: Create an RDS Proxy in front of the cluster for connection pooling and improved failover handling. + id: proxy_creation_enabled + label: RDS Proxy + type: boolean + - add_button_label: Add secret ARN + collapsible: true + description: Secrets Manager secrets containing database credentials for the proxy. Leave blank to use the managed master user secret. + id: proxy_auth_secret_arns + label: Auth secret ARNs + placeholder: arn:aws:secretsmanager:... + required: false + show_when: + proxy_creation_enabled: true + type: string_array + - add_button_label: Add KMS key ARN + collapsible: true + description: KMS keys used to encrypt the auth secrets when using customer-managed keys. The master user secret KMS key is included automatically. + id: proxy_secret_kms_key_arns + label: Auth secret KMS key ARNs + placeholder: arn:aws:kms:... + required: false + show_when: + proxy_creation_enabled: true + type: string_array + - collapsible: true + default: false + description: Require IAM authentication for client connections to the proxy. + id: proxy_iam_auth_enabled + label: IAM authentication + show_when: + proxy_creation_enabled: true + type: boolean + - collapsible: true + default: true + description: Require TLS for client connections to the proxy. + id: proxy_tls_requirement_enabled + label: Require TLS + show_when: + proxy_creation_enabled: true + type: boolean + - collapsible: true + default: false + description: Log detailed connection information, including SQL statements, to CloudWatch Logs. + id: proxy_debug_logging_enabled + label: Debug logging + show_when: + proxy_creation_enabled: true + type: boolean + - collapsible: true + default: 1800 + id: proxy_idle_client_timeout + label: Idle client timeout (seconds) + max: 28800 + min: 1 + show_when: + proxy_creation_enabled: true + type: number + - collapsible: true + default: 120 + id: proxy_connection_borrow_timeout + label: Connection borrow timeout (seconds) + min: 0 + show_when: + proxy_creation_enabled: true + type: number + - collapsible: true + default: 100 + description: Maximum size of the connection pool as a percentage of the database max_connections setting. + id: proxy_max_connections_percent + label: Max connections (%) + max: 100 + min: 1 + show_when: + proxy_creation_enabled: true + type: number + - collapsible: true + default: 50 + description: Maximum idle connections kept open, as a percentage of the database max_connections setting. + id: proxy_max_idle_connections_percent + label: Max idle connections (%) + max: 100 + min: 0 + show_when: + proxy_creation_enabled: true + type: number + - add_button_label: Add filter + collapsible: true + id: proxy_session_pinning_filters + label: Session pinning filters + required: false + show_when: + proxy_creation_enabled: true + type: string_array + values: + - label: EXCLUDE_VARIABLE_SETS + value: EXCLUDE_VARIABLE_SETS + - collapsible: true + description: SQL statements the proxy runs when opening each new database connection. + id: proxy_init_query + label: Initialization query + placeholder: SET time_zone = 'UTC' + required: false + show_when: + proxy_creation_enabled: true + type: string - id: section_readers label: Readers type: section @@ -1003,6 +1112,18 @@ module: preferred_backup_window: << module.input.preferred_backup_window >> preferred_maintenance_window: << module.input.preferred_maintenance_window >> promotion_tier: << module.input.promotion_tier >> + proxy_auth_secret_arns: << module.input.proxy_auth_secret_arns >> + proxy_connection_borrow_timeout: << module.input.proxy_connection_borrow_timeout >> + proxy_creation_enabled: << module.input.proxy_creation_enabled >> + proxy_debug_logging_enabled: << module.input.proxy_debug_logging_enabled >> + proxy_iam_auth_enabled: << module.input.proxy_iam_auth_enabled >> + proxy_idle_client_timeout: << module.input.proxy_idle_client_timeout >> + proxy_init_query: << module.input.proxy_init_query >> + proxy_max_connections_percent: << module.input.proxy_max_connections_percent >> + proxy_max_idle_connections_percent: << module.input.proxy_max_idle_connections_percent >> + proxy_secret_kms_key_arns: << module.input.proxy_secret_kms_key_arns >> + proxy_session_pinning_filters: << module.input.proxy_session_pinning_filters >> + proxy_tls_requirement_enabled: << module.input.proxy_tls_requirement_enabled >> public_access_enabled: << module.input.public_access_enabled >> reader_count: << module.input.reader_count >> reader_instance_class: << module.input.reader_instance_class >> @@ -1275,6 +1396,10 @@ module: The Ravion UI charts Aurora CloudWatch metrics for CPU utilization, freeable memory, database connections, serverless capacity and ACU utilization, storage used, read and write latency, replica lag, buffer cache hit ratio, commit latency, deadlocks, network throughput, and volume IOPS. + ## Connection pooling + + Enable RDS Proxy to place a managed connection pool in front of the cluster. The proxy authenticates with the Secrets Manager master user secret by default, requires TLS, and allows the same security group and CIDR sources configured for the cluster. Pool sizing, idle timeouts, session pinning, IAM authentication, and custom auth secrets are configurable in the Connection pooling section. + ## Configuration | Field | Required | Default | Notes | @@ -1296,6 +1421,7 @@ module: | Deletion protection | No | true | Helps prevent accidental deletion. | | Performance insights | No | true | Enables AWS Performance Insights. | | CloudWatch alarms | No | true | Adds CPU, memory, and connection alarms. | + | RDS Proxy | No | false | Creates an RDS Proxy connection pool in front of the cluster. | | Global database | No | false | Create or join an Aurora global database. | | Activity stream | No | false | Enables Database Activity Streams with a required KMS key. | | Tags | No | Blank | Merged with Ravion standard tags. | diff --git a/database/aurora/security_group.tf b/database/aurora/security_group.tf index d834431a..126baf0c 100644 --- a/database/aurora/security_group.tf +++ b/database/aurora/security_group.tf @@ -33,7 +33,17 @@ module "security_group" { ip_protocol = "tcp" cidr_ipv4 = cidr } - ] + ], + # RDS Proxy source + local.create_proxy ? [ + { + description = "Allow ${var.engine} traffic from the RDS Proxy" + from_port = local.port + to_port = local.port + ip_protocol = "tcp" + referenced_security_group_id = module.proxy[0].security_group_id + } + ] : [] ) # Egress to VPC only. For ip_protocol="-1" (all protocols), AWS requires diff --git a/database/aurora/variables.tf b/database/aurora/variables.tf index 0bf076f3..9312225b 100644 --- a/database/aurora/variables.tf +++ b/database/aurora/variables.tf @@ -836,6 +836,117 @@ variable "iam_role_associations" { default = {} } +################################################################################ +# RDS Proxy +################################################################################ + +variable "proxy_creation_enabled" { + type = bool + description = "Whether to create an RDS Proxy in front of the cluster. Requires master_user_password_management_enabled or proxy_auth_secret_arns." + default = false +} + +variable "proxy_auth_secret_arns" { + type = list(string) + description = "List of Secrets Manager secret ARNs containing database credentials for the proxy. Defaults to the managed master user secret when master_user_password_management_enabled is true." + default = [] + + validation { + condition = alltrue([for arn in var.proxy_auth_secret_arns : startswith(arn, "arn:")]) + error_message = "All proxy_auth_secret_arns must be valid Secrets Manager secret ARNs." + } +} + +variable "proxy_secret_kms_key_arns" { + type = list(string) + description = "List of KMS key ARNs used to encrypt the proxy auth secrets when using customer-managed keys. The master user secret KMS key is included automatically." + default = [] + + validation { + condition = alltrue([for arn in var.proxy_secret_kms_key_arns : startswith(arn, "arn:")]) + error_message = "All proxy_secret_kms_key_arns must be valid KMS key ARNs." + } +} + +variable "proxy_iam_auth_enabled" { + type = bool + description = "Whether clients must use IAM authentication to connect to the proxy." + default = false +} + +variable "proxy_tls_requirement_enabled" { + type = bool + description = "Whether Transport Layer Security (TLS) encryption is required for connections to the proxy." + default = true +} + +variable "proxy_debug_logging_enabled" { + type = bool + description = "Whether the proxy logs detailed connection information, including SQL statements, to CloudWatch Logs." + default = false +} + +variable "proxy_idle_client_timeout" { + type = number + description = "The number of seconds a client connection to the proxy can be idle before the proxy disconnects it." + default = 1800 + + validation { + condition = var.proxy_idle_client_timeout >= 1 && var.proxy_idle_client_timeout <= 28800 + error_message = "The proxy_idle_client_timeout must be between 1 and 28800 seconds." + } +} + +variable "proxy_connection_borrow_timeout" { + type = number + description = "The number of seconds the proxy waits for a connection to become available in the connection pool before returning a timeout error." + default = 120 + + validation { + condition = var.proxy_connection_borrow_timeout >= 0 + error_message = "The proxy_connection_borrow_timeout must be greater than or equal to 0." + } +} + +variable "proxy_init_query" { + type = string + description = "One or more SQL statements for the proxy to run when opening each new database connection." + default = null +} + +variable "proxy_max_connections_percent" { + type = number + description = "The maximum size of the proxy connection pool as a percentage of the max_connections setting of the target database." + default = 100 + + validation { + condition = var.proxy_max_connections_percent >= 1 && var.proxy_max_connections_percent <= 100 + error_message = "The proxy_max_connections_percent must be between 1 and 100." + } +} + +variable "proxy_max_idle_connections_percent" { + type = number + description = "The maximum percentage of idle database connections the proxy keeps open, as a percentage of the max_connections setting of the target database." + default = 50 + + validation { + condition = var.proxy_max_idle_connections_percent >= 0 && var.proxy_max_idle_connections_percent <= 100 + error_message = "The proxy_max_idle_connections_percent must be between 0 and 100." + } +} + +variable "proxy_session_pinning_filters" { + type = list(string) + description = "Session pinning filters for the proxy. Valid value: EXCLUDE_VARIABLE_SETS." + default = [] + + validation { + condition = alltrue([for f in var.proxy_session_pinning_filters : f == "EXCLUDE_VARIABLE_SETS"]) + error_message = "The only valid session pinning filter is EXCLUDE_VARIABLE_SETS." + } +} + variable "region" { type = string description = "AWS region. When null, the provider's configured region is used." diff --git a/database/rds-proxy/.terraform.lock.hcl b/database/rds-proxy/.terraform.lock.hcl new file mode 100644 index 00000000..3b6cb03b --- /dev/null +++ b/database/rds-proxy/.terraform.lock.hcl @@ -0,0 +1,27 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "6.28.0" + constraints = ">= 6.0.0" + hashes = [ + "h1:HRIef5R96dZbCL7AtPbGsdJnr6bGjB0P3TnI+N/lEuU=", + "h1:JW/6X82H742ltNqdi1Z+o3Y+diA6fluOH805IOI7kqg=", + "h1:a04viexyZH7XCg13UIibwzkFMfQvMWA//8f9okDFKIs=", + "h1:gls3u4DZH97fapaPejImJ0ITIZo9epFFVhhJZUSuDbs=", + "h1:i0G7vt2sNy0oz84IiuG4gplonNVyOLRdprKLurU8pe4=", + "h1:k967tIoJHNgEt72UJDzlE6ngWN91Ke96zrpgnn2PvUA=", + "h1:kjmhRbLdowE0T9x6hGzO5fCM01Qz+8GntpsaXqe9HiE=", + "h1:tcau98fkhZ2RhbPHo8LdiiUk2RGpZUgT/t06sdMLids=", + "h1:wek8vEEZpTPulbLi9xCf2wnxvc97JXAN4qcOhduSg7k=", + "zh:38d58305206953783c150fb96d5c4f3ea5fe0b9e0987d927c884a6b0f2adf7a9", + "zh:43fd483251165f98b7a44360b41b437d309b007ef2bfff818eedcf3730e3f5cb", + "zh:4753decc5a718cb74b08244a02d00c150f0ddd6ebf2e1227f6a985c647c03ce9", + "zh:5956525650554bd3fbc4b695eb5250193f0ebf94c45862a7730457ab6a315069", + "zh:76d98fa1146750c01f607bae4421952ee9cd14ed3a4a59deb7136749adb9e0ae", + "zh:792c29e5ec91356baddb6219ac7f6f1df09c251cbe4ab6e089fc25d64270b22a", + "zh:856424380caa7c1536dc00515d12beac2693db1a8425da654eed5530abeb17d9", + "zh:e8982ec2bc692efa7236e3565e7094a09f52c5b71d8860a570a36fb31a40f27f", + "zh:f5e7ff825dc3f7356fb80936bfe7bb1b54a728ccf429cb753cfe590932f0403b", + ] +} diff --git a/database/rds-proxy/README.md b/database/rds-proxy/README.md new file mode 100644 index 00000000..99970dfb --- /dev/null +++ b/database/rds-proxy/README.md @@ -0,0 +1,149 @@ +# RDS Proxy + +Creates an Amazon RDS Proxy that sits in front of an RDS instance or Aurora cluster to provide connection pooling, improved failover handling, and optional IAM authentication for database connections. + +The module creates: + +- An RDS Proxy with Secrets Manager based authentication +- The default proxy target group with configurable connection pool settings +- A proxy target registration for an RDS instance or Aurora cluster +- An IAM role allowing the proxy to read credentials from Secrets Manager (optional, with least-privilege access to only the referenced secrets) +- A security group for the proxy (optional) + +This module can be used standalone, or deployed automatically by the `database/rds` and `database/aurora` modules by setting `proxy_creation_enabled = true` on those modules. + +## Usage + +### Standalone, in front of an existing RDS instance + +```hcl +module "rds_proxy" { + source = "git::https://github.com/flightcontrolhq/modules.git//database/rds-proxy?ref=v1.0.0" + + name = "myapp" + engine_family = "POSTGRESQL" + + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-11111111", "subnet-22222222"] + + db_instance_identifier = "myapp-db" + + auth = [ + { + secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp-db-credentials-AbCdEf" + } + ] + + allowed_security_group_ids = ["sg-12345678"] + + tags = { + Environment = "production" + } +} +``` + +### Standalone, in front of an Aurora cluster + +```hcl +module "rds_proxy" { + source = "git::https://github.com/flightcontrolhq/modules.git//database/rds-proxy?ref=v1.0.0" + + name = "myapp" + engine_family = "MYSQL" + + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-11111111", "subnet-22222222"] + + db_cluster_identifier = "myapp-aurora" + + auth = [ + { + secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp-db-credentials-AbCdEf" + iam_auth = "REQUIRED" + } + ] + + allowed_security_group_ids = ["sg-12345678"] +} +``` + +### Via the rds or aurora modules + +```hcl +module "rds" { + source = "git::https://github.com/flightcontrolhq/modules.git//database/rds?ref=v1.0.0" + + # ... regular rds inputs ... + + proxy_creation_enabled = true +} +``` + +When deployed through the `rds` or `aurora` modules, the proxy authenticates with the managed master user secret (requires `master_user_password_management_enabled = true`, the default) or with explicitly provided `proxy_auth_secret_arns`. The database security group automatically allows ingress from the proxy security group, and the proxy security group allows ingress from the module's `allowed_security_group_ids` and `allowed_cidr_blocks`. + +## Requirements + +| Name | Version | +| ------------------ | --------- | +| opentofu/terraform | >= 1.10.0 | +| aws | >= 6.0 | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|----------| +| name | Name prefix for all resources created by this module. Also used as the DB proxy name. | `string` | n/a | yes | +| engine_family | The kind of database engine the proxy connects to: MYSQL (also used for MariaDB), POSTGRESQL, or SQLSERVER. | `string` | n/a | yes | +| vpc_id | The ID of the VPC where the proxy will be created. | `string` | n/a | yes | +| subnet_ids | A list of subnet IDs for the proxy (minimum 2, in different AZs). | `list(string)` | n/a | yes | +| auth | List of authentication configurations. Each entry references a Secrets Manager secret and supports optional description, username, iam_auth (DISABLED, REQUIRED, ENABLED), and client_password_auth_type. | `list(object)` | n/a | yes | +| tags | A map of tags to assign to all resources. | `map(string)` | `{}` | no | +| tls_requirement_enabled | Require TLS for connections to the proxy. | `bool` | `true` | no | +| debug_logging_enabled | Log detailed connection information, including SQL statements, to CloudWatch Logs. | `bool` | `false` | no | +| idle_client_timeout | Seconds a client connection can be idle before the proxy disconnects it (1-28800). | `number` | `1800` | no | +| secret_kms_key_arns | KMS key ARNs used to encrypt the auth secrets (needed for customer-managed keys only). | `list(string)` | `[]` | no | +| db_instance_identifier | RDS instance identifier to register as the proxy target (exactly one of db_instance_identifier or db_cluster_identifier). | `string` | `null` | no | +| db_cluster_identifier | Aurora cluster identifier to register as the proxy target (exactly one of db_instance_identifier or db_cluster_identifier). | `string` | `null` | no | +| connection_borrow_timeout | Seconds the proxy waits for an available connection in the pool. | `number` | `120` | no | +| init_query | SQL statements the proxy runs when opening each new database connection. | `string` | `null` | no | +| max_connections_percent | Max connection pool size as a percentage of the database max_connections (1-100). | `number` | `100` | no | +| max_idle_connections_percent | Max idle connections as a percentage of the database max_connections (0-100). | `number` | `50` | no | +| session_pinning_filters | Session pinning filters. Valid value: EXCLUDE_VARIABLE_SETS. | `list(string)` | `[]` | no | +| port | The port used for security group rules (defaults per engine_family: 3306, 5432, 1433). | `number` | `null` | no | +| security_group_creation_enabled | Whether to create a security group for the proxy. | `bool` | `true` | no | +| security_group_id | Existing security group ID to use when creation is disabled. | `string` | `null` | no | +| allowed_security_group_ids | Security group IDs allowed to connect to the proxy. | `list(string)` | `[]` | no | +| allowed_cidr_blocks | CIDR blocks allowed to connect to the proxy. | `list(string)` | `[]` | no | +| iam_role_creation_enabled | Whether to create the IAM role the proxy uses to read secrets. | `bool` | `true` | no | +| iam_role_arn | Existing IAM role ARN to use when role creation is disabled. | `string` | `null` | no | +| region | AWS region. When null, the provider's configured region is used. | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| proxy_id | The ID of the RDS Proxy. | +| proxy_arn | The ARN of the RDS Proxy. | +| proxy_name | The name of the RDS Proxy. | +| endpoint | The endpoint that applications use to connect through the proxy. | +| port | The port on which the proxy accepts connections. | +| default_target_group_name | The name of the default proxy target group. | +| default_target_group_arn | The ARN of the default proxy target group. | +| iam_role_arn | The ARN of the IAM role the proxy uses to read Secrets Manager secrets. | +| security_group_id | The security group ID. | +| security_group_arn | The security group ARN. | +| aws_account_id | The AWS account ID where the resources are deployed. | +| region | The AWS region where the resources are deployed. | + +## Security Considerations + +- **Secrets Manager**: The proxy authenticates to the database using Secrets Manager secrets; no plaintext credentials. +- **Least privilege IAM**: The created IAM role can only read the specific secrets referenced in `auth` (plus `kms:Decrypt` scoped to the provided keys via Secrets Manager). +- **TLS**: Required by default for client connections to the proxy. +- **VPC only**: The proxy is deployed within your VPC; the security group only allows the sources you list. + +## Notes + +- RDS Proxy supports MySQL, MariaDB (via the MYSQL engine family), PostgreSQL, and SQL Server. Oracle engines are not supported. +- Registering an Aurora cluster as a proxy target requires the cluster to have at least one instance. +- The secrets referenced in `auth` must be in the same region as the proxy. diff --git a/database/rds-proxy/data.tf b/database/rds-proxy/data.tf new file mode 100644 index 00000000..af88cb63 --- /dev/null +++ b/database/rds-proxy/data.tf @@ -0,0 +1,11 @@ +################################################################################ +# Data Sources +################################################################################ + +data "aws_caller_identity" "current" {} + +data "aws_region" "current" {} + +data "aws_vpc" "this" { + id = var.vpc_id +} diff --git a/database/rds-proxy/db_proxy.tf b/database/rds-proxy/db_proxy.tf new file mode 100644 index 00000000..7f4f7228 --- /dev/null +++ b/database/rds-proxy/db_proxy.tf @@ -0,0 +1,83 @@ +################################################################################ +# RDS Proxy +################################################################################ + +resource "aws_db_proxy" "this" { + name = var.name + engine_family = var.engine_family + role_arn = local.iam_role_arn + vpc_subnet_ids = var.subnet_ids + + vpc_security_group_ids = [local.security_group_id] + + require_tls = var.tls_requirement_enabled + debug_logging = var.debug_logging_enabled + idle_client_timeout = var.idle_client_timeout + + dynamic "auth" { + for_each = var.auth + content { + auth_scheme = "SECRETS" + secret_arn = auth.value.secret_arn + description = auth.value.description + username = auth.value.username + iam_auth = auth.value.iam_auth + client_password_auth_type = auth.value.client_password_auth_type + } + } + + tags = merge(local.tags, { + Name = var.name + }) + + lifecycle { + precondition { + condition = var.security_group_creation_enabled || var.security_group_id != null + error_message = "security_group_id is required when security_group_creation_enabled is false." + } + + precondition { + condition = var.iam_role_creation_enabled || var.iam_role_arn != null + error_message = "iam_role_arn is required when iam_role_creation_enabled is false." + } + } + + depends_on = [ + aws_iam_role_policy.secrets_access + ] +} + +################################################################################ +# Default Target Group (connection pool configuration) +################################################################################ + +resource "aws_db_proxy_default_target_group" "this" { + db_proxy_name = aws_db_proxy.this.name + + connection_pool_config { + connection_borrow_timeout = var.connection_borrow_timeout + init_query = var.init_query + max_connections_percent = var.max_connections_percent + max_idle_connections_percent = var.max_idle_connections_percent + session_pinning_filters = var.session_pinning_filters + } +} + +################################################################################ +# Proxy Target +################################################################################ + +resource "aws_db_proxy_target" "this" { + db_proxy_name = aws_db_proxy.this.name + target_group_name = aws_db_proxy_default_target_group.this.name + + db_instance_identifier = var.db_instance_identifier + db_cluster_identifier = var.db_cluster_identifier + + lifecycle { + precondition { + condition = (var.db_instance_identifier != null) != (var.db_cluster_identifier != null) + error_message = "Exactly one of db_instance_identifier or db_cluster_identifier must be provided." + } + } +} diff --git a/database/rds-proxy/iam_role.tf b/database/rds-proxy/iam_role.tf new file mode 100644 index 00000000..a4a3a1d2 --- /dev/null +++ b/database/rds-proxy/iam_role.tf @@ -0,0 +1,61 @@ +################################################################################ +# Proxy IAM Role +# +# Allows the RDS Proxy service to read database credentials from Secrets +# Manager (and decrypt them when a customer-managed KMS key is used). +################################################################################ + +resource "aws_iam_role" "this" { + count = local.create_iam_role ? 1 : 0 + + name = "${var.name}-rds-proxy" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "rds.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = local.tags +} + +resource "aws_iam_role_policy" "secrets_access" { + count = local.create_iam_role ? 1 : 0 + + name = "secrets-access" + role = aws_iam_role.this[0].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = concat( + [ + { + Sid = "GetSecretValue" + Effect = "Allow" + Action = ["secretsmanager:GetSecretValue"] + Resource = local.auth_secret_arns + } + ], + length(var.secret_kms_key_arns) > 0 ? [ + { + Sid = "DecryptSecrets" + Effect = "Allow" + Action = ["kms:Decrypt"] + Resource = var.secret_kms_key_arns + Condition = { + StringEquals = { + "kms:ViaService" = "secretsmanager.${local.region}.amazonaws.com" + } + } + } + ] : [] + ) + }) +} diff --git a/database/rds-proxy/locals.tf b/database/rds-proxy/locals.tf new file mode 100644 index 00000000..739c8200 --- /dev/null +++ b/database/rds-proxy/locals.tf @@ -0,0 +1,35 @@ +locals { + region = coalesce(var.region, data.aws_region.current.region) +} + +################################################################################ +# Local Values +################################################################################ + +locals { + # Tags + default_tags = { + ManagedBy = "terraform" + Module = "database/rds-proxy" + } + tags = merge(local.default_tags, var.tags) + + # Port defaults based on engine family + default_port = ( + var.engine_family == "POSTGRESQL" ? 5432 : + var.engine_family == "SQLSERVER" ? 1433 : + 3306 + ) + port = coalesce(var.port, local.default_port) + + # Resource creation flags + create_security_group = var.security_group_creation_enabled + create_iam_role = var.iam_role_creation_enabled + + # Resolved resource references + security_group_id = local.create_security_group ? module.security_group[0].security_group_id : var.security_group_id + iam_role_arn = local.create_iam_role ? aws_iam_role.this[0].arn : var.iam_role_arn + + # Secrets the proxy IAM role must be able to read + auth_secret_arns = distinct([for a in var.auth : a.secret_arn]) +} diff --git a/database/rds-proxy/outputs.tf b/database/rds-proxy/outputs.tf new file mode 100644 index 00000000..fe11ff05 --- /dev/null +++ b/database/rds-proxy/outputs.tf @@ -0,0 +1,83 @@ +################################################################################ +# Proxy Outputs +################################################################################ + +output "proxy_id" { + description = "The ID of the RDS Proxy." + value = aws_db_proxy.this.id +} + +output "proxy_arn" { + description = "The ARN of the RDS Proxy." + value = aws_db_proxy.this.arn +} + +output "proxy_name" { + description = "The name of the RDS Proxy." + value = aws_db_proxy.this.name +} + +################################################################################ +# Connection Outputs +################################################################################ + +output "endpoint" { + description = "The endpoint that applications use to connect through the proxy." + value = aws_db_proxy.this.endpoint +} + +output "port" { + description = "The port on which the proxy accepts connections." + value = local.port +} + +################################################################################ +# Target Group Outputs +################################################################################ + +output "default_target_group_name" { + description = "The name of the default proxy target group." + value = aws_db_proxy_default_target_group.this.name +} + +output "default_target_group_arn" { + description = "The ARN of the default proxy target group." + value = aws_db_proxy_default_target_group.this.arn +} + +################################################################################ +# IAM Outputs +################################################################################ + +output "iam_role_arn" { + description = "The ARN of the IAM role the proxy uses to read Secrets Manager secrets." + value = local.iam_role_arn +} + +################################################################################ +# Security Group Outputs +################################################################################ + +output "security_group_id" { + description = "The ID of the security group." + value = local.security_group_id +} + +output "security_group_arn" { + description = "The ARN of the security group." + value = local.create_security_group ? module.security_group[0].security_group_arn : null +} + +################################################################################ +# Account & Region +################################################################################ + +output "aws_account_id" { + description = "The AWS account ID where the resources are deployed." + value = data.aws_caller_identity.current.account_id +} + +output "region" { + description = "The AWS region where the resources are deployed." + value = local.region +} diff --git a/database/rds-proxy/rvn-rds-proxy-definition.yml b/database/rds-proxy/rvn-rds-proxy-definition.yml new file mode 100644 index 00000000..4a45988e --- /dev/null +++ b/database/rds-proxy/rvn-rds-proxy-definition.yml @@ -0,0 +1,385 @@ +definition: + type: rvn-rds-proxy + name: RDS Proxy + description: Creates and manages an Amazon RDS Proxy that provides connection pooling in front of an RDS instance or Aurora cluster. + +release: + version: 0.1.0 + description: Initial release of the RDS Proxy module definition. +module: + inputs: + - id: network + immutable: true + label: VPC network + mapped_inputs: + - id: section_aws + label: AWS account & region + type: section + - default: << ref.input.aws_account_id >> + id: aws_account_id + immutable: true + label: AWS account + type: string + values: $values:ravion/aws_accounts + - default: << ref.input.aws_region >> + id: aws_region + immutable: true + label: Region + type: string + values: $values:aws/regions + - collapsible: true + default: << ref.input.execution_environment_id >> + description: Override the VPC, subnet, and security group for Pipeline Terraform runners. Must use the same AWS account as selected above. + id: execution_environment_id + label: Terraform execution environment + type: string + values: $values:ravion/execution_environments + - id: section_vpc + label: VPC + type: section + - default: <> + id: vpc_id + immutable: true + label: VPC ID + required: true + type: string + - add_button_label: Add private subnet ID + default: <> + description: Subnets for the RDS Proxy. Use private subnets for normal deployments. + id: subnet_ids + immutable: true + label: Private subnet IDs + required: true + type: string_array + required: true + type: $ref:rvn-aws-network + - id: section_proxy + label: Proxy + type: section + - default: <>-<>-proxy + description: Name prefix for all proxy resources. + id: name + immutable: true + label: Name slug + patterns: + - message: 1-40 lowercase letters, numbers, and hyphens. Start and end with a letter or number. + pattern: ^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$ + required: true + type: string + - default: POSTGRESQL + id: engine_family + immutable: true + label: Engine family + required: true + type: string + values: + - label: PostgreSQL + value: POSTGRESQL + - label: MySQL / MariaDB + value: MYSQL + - label: SQL Server + value: SQLSERVER + - id: section_target + label: Target database + type: section + - default: db_instance + id: target_type + label: Target type + required: true + type: string + values: + - label: RDS instance + value: db_instance + - label: Aurora cluster + value: db_cluster + - description: Identifier of the RDS instance to register as the proxy target. + id: db_instance_identifier + label: DB instance identifier + placeholder: my-db + required: true + show_when: + target_type: db_instance + type: string + - description: Identifier of the Aurora cluster to register as the proxy target. + id: db_cluster_identifier + label: DB cluster identifier + placeholder: my-aurora + required: true + show_when: + target_type: db_cluster + type: string + - id: section_auth + label: Authentication + type: section + - add_button_label: Add secret ARN + description: Secrets Manager secrets containing database credentials the proxy uses to connect to the target database. + id: auth_secret_arns + label: Auth secret ARNs + placeholder: arn:aws:secretsmanager:... + required: true + type: string_array + - add_button_label: Add KMS key ARN + collapsible: true + description: KMS keys used to encrypt the auth secrets when using customer-managed keys. + id: secret_kms_key_arns + label: Auth secret KMS key ARNs + placeholder: arn:aws:kms:... + required: false + type: string_array + - collapsible: true + default: false + description: Require IAM authentication for client connections to the proxy. + id: iam_auth_enabled + label: IAM authentication + type: boolean + - collapsible: true + default: true + description: Require TLS for client connections to the proxy. + id: tls_requirement_enabled + label: Require TLS + type: boolean + - collapsible: true + default: true + description: Create the IAM role the proxy uses to read credentials from Secrets Manager. Disable this only when you want to provide an existing role. + id: iam_role_creation_enabled + immutable: true + label: IAM role creation + type: boolean + - collapsible: true + id: iam_role_arn + label: Existing IAM role ARN + placeholder: arn:aws:iam::... + required: false + show_when: + iam_role_creation_enabled: false + type: string + - id: section_access + label: Network access + type: section + - collapsible: true + description: "Leave blank to use the engine family default port: 5432 for PostgreSQL, 3306 for MySQL, or 1433 for SQL Server." + id: port + label: Port + max: 65535 + min: 1 + required: false + type: number + - collapsible: true + default: true + id: security_group_creation_enabled + immutable: true + label: Security group creation + type: boolean + - collapsible: true + id: security_group_id + label: Existing security group ID + placeholder: sg-... + required: false + show_when: + security_group_creation_enabled: false + type: string + - add_button_label: Add security group ID + collapsible: true + id: allowed_security_group_ids + label: Allowed security groups + placeholder: sg-... + required: false + show_when: + security_group_creation_enabled: true + type: string_array + - add_button_label: Add CIDR block + collapsible: true + id: allowed_cidr_blocks + label: Allowed CIDR blocks + placeholder: 10.0.0.0/16 + required: false + show_when: + security_group_creation_enabled: true + type: string_array + - id: section_pool + label: Connection pool + type: section + - collapsible: true + default: 1800 + id: idle_client_timeout + label: Idle client timeout (seconds) + max: 28800 + min: 1 + type: number + - collapsible: true + default: 120 + id: connection_borrow_timeout + label: Connection borrow timeout (seconds) + min: 0 + type: number + - collapsible: true + default: 100 + description: Maximum size of the connection pool as a percentage of the database max_connections setting. + id: max_connections_percent + label: Max connections (%) + max: 100 + min: 1 + type: number + - collapsible: true + default: 50 + description: Maximum idle connections kept open, as a percentage of the database max_connections setting. + id: max_idle_connections_percent + label: Max idle connections (%) + max: 100 + min: 0 + type: number + - add_button_label: Add filter + collapsible: true + id: session_pinning_filters + label: Session pinning filters + required: false + type: string_array + values: + - label: EXCLUDE_VARIABLE_SETS + value: EXCLUDE_VARIABLE_SETS + - collapsible: true + description: SQL statements the proxy runs when opening each new database connection. + id: init_query + label: Initialization query + placeholder: SET time_zone = 'UTC' + required: false + type: string + - collapsible: true + default: false + description: Log detailed connection information, including SQL statements, to CloudWatch Logs. + id: debug_logging_enabled + label: Debug logging + type: boolean + - $include: ../../partials/inputs/misc-section.yml + - $include: ../../partials/inputs/tags.yml + - $include: ../../partials/inputs/terraform-settings.yml + stack: + $template: ../../partials/templates/opentofu-stack.yml + with: + base_path: database/rds-proxy + terraform_variables: + ...overrides: << module.input.advanced_terraform_variables >> + allowed_cidr_blocks: << module.input.allowed_cidr_blocks >> + allowed_security_group_ids: << module.input.allowed_security_group_ids >> + auth: '<< map(module.input.auth_secret_arns, {"secret_arn": #, "iam_auth": module.input.iam_auth_enabled ? "REQUIRED" : "DISABLED"}) >>' + connection_borrow_timeout: << module.input.connection_borrow_timeout >> + db_cluster_identifier: << module.input.db_cluster_identifier >> + db_instance_identifier: << module.input.db_instance_identifier >> + debug_logging_enabled: << module.input.debug_logging_enabled >> + engine_family: << module.input.engine_family >> + iam_role_arn: << module.input.iam_role_arn >> + iam_role_creation_enabled: << module.input.iam_role_creation_enabled >> + idle_client_timeout: << module.input.idle_client_timeout >> + init_query: << module.input.init_query >> + max_connections_percent: << module.input.max_connections_percent >> + max_idle_connections_percent: << module.input.max_idle_connections_percent >> + name: << module.input.name >> + port: << module.input.port >> + region: << module.input.aws_region >> + secret_kms_key_arns: << module.input.secret_kms_key_arns >> + security_group_creation_enabled: << module.input.security_group_creation_enabled >> + security_group_id: << module.input.security_group_id >> + session_pinning_filters: << module.input.session_pinning_filters >> + subnet_ids: << module.input.subnet_ids >> + tags: + $include: ../stack/ravion-tags.yml + tls_requirement_enabled: << module.input.tls_requirement_enabled >> + vpc_id: << module.input.vpc_id >> + ui: + metrics: + - id: client_connections + name: Client connections + source: + aws_account_id: << module.input.aws_account_id >> + dimensions: + ProxyName: << stack.output.proxy_name >> + name: ClientConnections + namespace: AWS/RDS + region: << stack.output.region >> + statistic: Average + type: cloudwatch + type: line + - id: database_connections + name: Database connections + source: + aws_account_id: << module.input.aws_account_id >> + dimensions: + ProxyName: << stack.output.proxy_name >> + name: DatabaseConnections + namespace: AWS/RDS + region: << stack.output.region >> + statistic: Average + type: cloudwatch + type: line + - id: query_requests + name: Query requests + source: + aws_account_id: << module.input.aws_account_id >> + dimensions: + ProxyName: << stack.output.proxy_name >> + name: QueryRequests + namespace: AWS/RDS + region: << stack.output.region >> + statistic: Sum + type: cloudwatch + type: line + readme: |- + Creates and manages an Amazon RDS Proxy that provides connection pooling in front of an RDS instance or Aurora cluster. + + ## Overview + + Use this module to place a managed connection pool in front of an existing database. The proxy holds a pool of database connections open and multiplexes application connections onto them, which protects the database from connection storms and makes failovers faster for clients. + + The RDS and Aurora modules can create a proxy directly with their RDS Proxy toggle. Use this standalone module when the target database is not managed by Ravion, or when you want to manage the proxy lifecycle separately from the database. + + ## Use cases + + | Scenario | Benefit | + | --- | --- | + | Serverless or many-worker applications | Reuse pooled connections instead of opening one per worker. | + | Existing database | Add pooling in front of a database created outside Ravion. | + | Faster failover | Clients stay connected to the proxy while the database fails over. | + | IAM authentication | Require IAM auth for clients without changing database credentials. | + + ## Networking + + Select a VPC network first. Ravion maps the network's AWS account, region, VPC ID, and private subnet IDs into this module. The proxy must be in the same VPC as the target database, and the database's security group must allow ingress from the proxy security group. + + Allowed security groups and CIDR blocks control which sources can connect to the proxy. + + ## Authentication + + The proxy reads database credentials from the Secrets Manager secrets listed in auth secret ARNs. For an RDS or Aurora database with managed master passwords, use the master user secret ARN. The module creates an IAM role that can read only those secrets; provide KMS key ARNs when the secrets use customer-managed keys. TLS is required by default, and IAM authentication for clients is optional. + + ## Configuration + + | Field | Required | Default | Notes | + | --- | --- | --- | --- | + | VPC network | Yes | None | Supplies AWS account, region, VPC ID, and private subnet IDs. | + | Name slug | Yes | Project and environment given IDs + proxy | Proxy name and prefix for related resources. | + | Engine family | Yes | PostgreSQL | PostgreSQL, MySQL / MariaDB, or SQL Server. Must match the target database engine. | + | Target type | Yes | RDS instance | Register an RDS instance or an Aurora cluster as the proxy target. | + | DB instance / cluster identifier | Yes | None | Identifier of the target database. | + | Auth secret ARNs | Yes | None | Secrets Manager secrets with database credentials. | + | Require TLS | No | true | Requires TLS for client connections. | + | IAM authentication | No | false | Requires IAM auth for client connections. | + | Security group creation | No | true | Creates ingress rules from allowed security groups and CIDRs. | + | Max connections (%) | No | 100 | Pool size as a percentage of the database max_connections. | + | Idle client timeout | No | 1800 | Seconds before idle client connections are closed. | + | Tags | No | Blank | Merged with Ravion standard tags. | + + ## Advanced configuration + + Connection pool settings control borrow timeouts, idle connection limits, session pinning filters, and an initialization query. Debug logging sends detailed connection information to CloudWatch Logs. Use advanced Terraform variables for one-off overrides not represented directly in the UI. + + Terraform settings let you override the OpenTofu version, Terraform execution environment inherited from the VPC network, and Ravion state backend workspace name. + + ## Design decisions + + The proxy is placed in private subnets by default through the VPC network reference. TLS enforcement and Secrets Manager based authentication default on. The created IAM role follows least privilege and can read only the listed auth secrets. + + ## Learn more + + - [Amazon RDS Proxy documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html) + - [RDS Proxy managing connections](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-managing.html) + - [Terraform source](https://github.com/flightcontrolhq/modules/tree/$local.module_tag/database/rds-proxy) diff --git a/database/rds-proxy/security_group.tf b/database/rds-proxy/security_group.tf new file mode 100644 index 00000000..512851bb --- /dev/null +++ b/database/rds-proxy/security_group.tf @@ -0,0 +1,50 @@ +################################################################################ +# Security Group +################################################################################ + +module "security_group" { + count = local.create_security_group ? 1 : 0 + + source = "../../networking/security-groups" + + name = var.name + name_suffix = "rds-proxy" + description = "Security group for ${var.name} RDS Proxy" + vpc_id = var.vpc_id + tags = var.tags + + ingress_rules = concat( + # Security group sources + [ + for sg_id in var.allowed_security_group_ids : { + description = "Allow database traffic from ${sg_id}" + from_port = local.port + to_port = local.port + ip_protocol = "tcp" + referenced_security_group_id = sg_id + } + ], + # IPv4 CIDR sources + [ + for cidr in var.allowed_cidr_blocks : { + description = "Allow database traffic from ${cidr}" + from_port = local.port + to_port = local.port + ip_protocol = "tcp" + cidr_ipv4 = cidr + } + ] + ) + + # Egress to VPC only. For ip_protocol="-1" (all protocols), AWS requires + # from_port/to_port to be omitted; use -1 here for caller clarity. + egress_rules = [ + { + description = "Allow outbound traffic within VPC" + from_port = -1 + to_port = -1 + ip_protocol = "-1" + cidr_ipv4 = data.aws_vpc.this.cidr_block + } + ] +} diff --git a/database/rds-proxy/variables.tf b/database/rds-proxy/variables.tf new file mode 100644 index 00000000..b2d2a27f --- /dev/null +++ b/database/rds-proxy/variables.tf @@ -0,0 +1,291 @@ +################################################################################ +# General +################################################################################ + +variable "name" { + type = string + description = "Name prefix for all resources created by this module. Also used as the DB proxy name." + + validation { + condition = length(var.name) > 0 && length(var.name) <= 60 + error_message = "The name must be between 1 and 60 characters." + } + + validation { + condition = can(regex("^[a-zA-Z][a-zA-Z0-9-]*$", var.name)) + error_message = "The name must start with a letter and contain only alphanumeric characters and hyphens." + } +} + +variable "tags" { + type = map(string) + description = "A map of tags to assign to all resources." + default = {} +} + +################################################################################ +# Proxy +################################################################################ + +variable "engine_family" { + type = string + description = "The kind of database engine the proxy connects to: MYSQL (also used for MariaDB), POSTGRESQL, or SQLSERVER." + + validation { + condition = contains(["MYSQL", "POSTGRESQL", "SQLSERVER"], var.engine_family) + error_message = "The engine_family must be one of: MYSQL, POSTGRESQL, SQLSERVER." + } +} + +variable "tls_requirement_enabled" { + type = bool + description = "Whether Transport Layer Security (TLS) encryption is required for connections to the proxy." + default = true +} + +variable "debug_logging_enabled" { + type = bool + description = "Whether the proxy logs detailed connection information, including SQL statements, to CloudWatch Logs." + default = false +} + +variable "idle_client_timeout" { + type = number + description = "The number of seconds a client connection can be idle before the proxy disconnects it." + default = 1800 + + validation { + condition = var.idle_client_timeout >= 1 && var.idle_client_timeout <= 28800 + error_message = "The idle_client_timeout must be between 1 and 28800 seconds." + } +} + +################################################################################ +# Authentication +################################################################################ + +variable "auth" { + type = list(object({ + secret_arn = string + description = optional(string) + username = optional(string) + iam_auth = optional(string, "DISABLED") + client_password_auth_type = optional(string) + })) + description = "List of authentication configurations for the proxy. Each entry references a Secrets Manager secret containing database credentials. iam_auth may be DISABLED, REQUIRED, or ENABLED." + + validation { + condition = length(var.auth) > 0 + error_message = "At least one auth configuration is required." + } + + validation { + condition = alltrue([for a in var.auth : startswith(a.secret_arn, "arn:")]) + error_message = "Each auth secret_arn must be a valid Secrets Manager secret ARN." + } + + validation { + condition = alltrue([for a in var.auth : contains(["DISABLED", "REQUIRED", "ENABLED"], a.iam_auth)]) + error_message = "Each auth iam_auth must be one of: DISABLED, REQUIRED, ENABLED." + } +} + +variable "secret_kms_key_arns" { + type = list(string) + description = "List of KMS key ARNs used to encrypt the auth secrets. Required for the proxy IAM role to decrypt secrets encrypted with a customer-managed key. Not needed for the default aws/secretsmanager key." + default = [] + + validation { + condition = alltrue([for arn in var.secret_kms_key_arns : startswith(arn, "arn:")]) + error_message = "All secret_kms_key_arns must be valid KMS key ARNs." + } +} + +################################################################################ +# Target +################################################################################ + +variable "db_instance_identifier" { + type = string + description = "The identifier of an RDS DB instance to register as the proxy target. Exactly one of db_instance_identifier or db_cluster_identifier must be provided." + default = null +} + +variable "db_cluster_identifier" { + type = string + description = "The identifier of an Aurora DB cluster to register as the proxy target. Exactly one of db_instance_identifier or db_cluster_identifier must be provided." + default = null +} + +################################################################################ +# Connection Pool +################################################################################ + +variable "connection_borrow_timeout" { + type = number + description = "The number of seconds the proxy waits for a connection to become available in the connection pool before returning a timeout error." + default = 120 + + validation { + condition = var.connection_borrow_timeout >= 0 + error_message = "The connection_borrow_timeout must be greater than or equal to 0." + } +} + +variable "init_query" { + type = string + description = "One or more SQL statements for the proxy to run when opening each new database connection." + default = null +} + +variable "max_connections_percent" { + type = number + description = "The maximum size of the connection pool as a percentage of the max_connections setting of the target database." + default = 100 + + validation { + condition = var.max_connections_percent >= 1 && var.max_connections_percent <= 100 + error_message = "The max_connections_percent must be between 1 and 100." + } +} + +variable "max_idle_connections_percent" { + type = number + description = "The maximum percentage of idle database connections the proxy keeps open, as a percentage of the max_connections setting of the target database." + default = 50 + + validation { + condition = var.max_idle_connections_percent >= 0 && var.max_idle_connections_percent <= 100 + error_message = "The max_idle_connections_percent must be between 0 and 100." + } + + validation { + condition = var.max_idle_connections_percent <= var.max_connections_percent + error_message = "The max_idle_connections_percent must be less than or equal to max_connections_percent." + } +} + +variable "session_pinning_filters" { + type = list(string) + description = "Each item in the list identifies a class of SQL operations that normally cause all later statements in a session to be pinned to the same database connection. Valid value: EXCLUDE_VARIABLE_SETS." + default = [] + + validation { + condition = alltrue([for f in var.session_pinning_filters : f == "EXCLUDE_VARIABLE_SETS"]) + error_message = "The only valid session pinning filter is EXCLUDE_VARIABLE_SETS." + } +} + +################################################################################ +# Network +################################################################################ + +variable "vpc_id" { + type = string + description = "The ID of the VPC where the proxy will be created." + + validation { + condition = can(regex("^vpc-", var.vpc_id)) + error_message = "The vpc_id must be a valid VPC ID starting with 'vpc-'." + } +} + +variable "subnet_ids" { + type = list(string) + description = "A list of subnet IDs for the proxy. At least two subnets in different AZs are required." + + validation { + condition = length(var.subnet_ids) >= 2 + error_message = "At least 2 subnet IDs in different availability zones are required." + } + + validation { + condition = alltrue([for s in var.subnet_ids : can(regex("^subnet-", s))]) + error_message = "All subnet_ids must be valid subnet IDs starting with 'subnet-'." + } +} + +variable "port" { + type = number + description = "The port on which the proxy accepts connections. If not specified, defaults based on engine_family (3306 for MYSQL, 5432 for POSTGRESQL, 1433 for SQLSERVER). Used for security group rules." + default = null + + validation { + condition = var.port == null || (var.port >= 1 && var.port <= 65535) + error_message = "The port must be between 1 and 65535." + } +} + +################################################################################ +# Security Group +################################################################################ + +variable "security_group_creation_enabled" { + type = bool + description = "Whether to create a new security group for the proxy." + default = true +} + +variable "security_group_id" { + type = string + description = "The ID of an existing security group to use for the proxy. Required if security_group_creation_enabled is false." + default = null + + validation { + condition = var.security_group_id == null || can(regex("^sg-", var.security_group_id)) + error_message = "The security_group_id must be a valid security group ID starting with 'sg-'." + } +} + +variable "allowed_security_group_ids" { + type = list(string) + description = "List of security group IDs allowed to connect to the proxy." + default = [] + + validation { + condition = alltrue([for sg in var.allowed_security_group_ids : can(regex("^sg-", sg))]) + error_message = "All allowed_security_group_ids must be valid security group IDs starting with 'sg-'." + } +} + +variable "allowed_cidr_blocks" { + type = list(string) + description = "List of CIDR blocks allowed to connect to the proxy." + default = [] + + validation { + condition = alltrue([for cidr in var.allowed_cidr_blocks : can(cidrhost(cidr, 0))]) + error_message = "All allowed_cidr_blocks must be valid CIDR blocks." + } +} + +################################################################################ +# IAM +################################################################################ + +variable "iam_role_creation_enabled" { + type = bool + description = "Whether to create the IAM role the proxy uses to read credentials from Secrets Manager." + default = true +} + +variable "iam_role_arn" { + type = string + description = "The ARN of an existing IAM role for the proxy to use. Required if iam_role_creation_enabled is false." + default = null + + validation { + condition = var.iam_role_arn == null || can(regex("^arn:aws[a-zA-Z-]*:iam::", var.iam_role_arn)) + error_message = "The iam_role_arn must be a valid IAM role ARN." + } +} + +################################################################################ +# Region +################################################################################ + +variable "region" { + type = string + description = "AWS region. When null, the provider's configured region is used." + default = null +} diff --git a/database/rds-proxy/versions.tf b/database/rds-proxy/versions.tf new file mode 100644 index 00000000..985850ec --- /dev/null +++ b/database/rds-proxy/versions.tf @@ -0,0 +1,16 @@ +################################################################################ +# OpenTofu/Terraform and Provider Requirements +################################################################################ + +terraform { + required_version = ">= 1.10.0" + + cloud {} + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + } +} diff --git a/database/rds/.terraform.lock.hcl b/database/rds/.terraform.lock.hcl index 5d64317a..3b6cb03b 100644 --- a/database/rds/.terraform.lock.hcl +++ b/database/rds/.terraform.lock.hcl @@ -3,8 +3,16 @@ provider "registry.opentofu.org/hashicorp/aws" { version = "6.28.0" - constraints = ">= 5.0.0" + constraints = ">= 6.0.0" hashes = [ + "h1:HRIef5R96dZbCL7AtPbGsdJnr6bGjB0P3TnI+N/lEuU=", + "h1:JW/6X82H742ltNqdi1Z+o3Y+diA6fluOH805IOI7kqg=", + "h1:a04viexyZH7XCg13UIibwzkFMfQvMWA//8f9okDFKIs=", + "h1:gls3u4DZH97fapaPejImJ0ITIZo9epFFVhhJZUSuDbs=", + "h1:i0G7vt2sNy0oz84IiuG4gplonNVyOLRdprKLurU8pe4=", + "h1:k967tIoJHNgEt72UJDzlE6ngWN91Ke96zrpgnn2PvUA=", + "h1:kjmhRbLdowE0T9x6hGzO5fCM01Qz+8GntpsaXqe9HiE=", + "h1:tcau98fkhZ2RhbPHo8LdiiUk2RGpZUgT/t06sdMLids=", "h1:wek8vEEZpTPulbLi9xCf2wnxvc97JXAN4qcOhduSg7k=", "zh:38d58305206953783c150fb96d5c4f3ea5fe0b9e0987d927c884a6b0f2adf7a9", "zh:43fd483251165f98b7a44360b41b437d309b007ef2bfff818eedcf3730e3f5cb", diff --git a/database/rds/README.md b/database/rds/README.md index 929b1989..1efaf0b0 100644 --- a/database/rds/README.md +++ b/database/rds/README.md @@ -381,6 +381,18 @@ module "mysql" { | option_group_engine_version | Option group major engine version. | `string` | `null` | no | | options | Options for the option group. | `list(object)` | `[]` | no | | blue_green_update | Blue/Green deployment configuration. | `object` | `null` | no | +| proxy_creation_enabled | Whether to create an RDS Proxy in front of the database. | `bool` | `false` | no | +| proxy_auth_secret_arns | Secrets Manager secret ARNs for proxy auth (defaults to the managed master user secret). | `list(string)` | `[]` | no | +| proxy_secret_kms_key_arns | KMS key ARNs used to encrypt the proxy auth secrets (customer-managed keys). | `list(string)` | `[]` | no | +| proxy_iam_auth_enabled | Require IAM authentication for proxy connections. | `bool` | `false` | no | +| proxy_tls_requirement_enabled | Require TLS for proxy connections. | `bool` | `true` | no | +| proxy_debug_logging_enabled | Log detailed proxy connection information to CloudWatch Logs. | `bool` | `false` | no | +| proxy_idle_client_timeout | Seconds a client connection can be idle before the proxy disconnects it. | `number` | `1800` | no | +| proxy_connection_borrow_timeout | Seconds the proxy waits for an available connection in the pool. | `number` | `120` | no | +| proxy_init_query | SQL statements the proxy runs when opening each new database connection. | `string` | `null` | no | +| proxy_max_connections_percent | Max proxy connection pool size (% of database max_connections). | `number` | `100` | no | +| proxy_max_idle_connections_percent | Max idle proxy connections (% of database max_connections). | `number` | `50` | no | +| proxy_session_pinning_filters | Session pinning filters (EXCLUDE_VARIABLE_SETS). | `list(string)` | `[]` | no | ## Outputs @@ -414,6 +426,9 @@ module "mysql" { | db_option_group_arn | The option group ARN. | | enhanced_monitoring_iam_role_arn | The Enhanced Monitoring IAM role ARN. | | cloudwatch_alarm_arns | Map of CloudWatch alarm ARNs. | +| proxy_endpoint | The RDS Proxy endpoint (null when no proxy is created). | +| proxy_arn | The RDS Proxy ARN (null when no proxy is created). | +| proxy_security_group_id | The RDS Proxy security group ID (null when no proxy is created). | ## Security Considerations diff --git a/database/rds/locals.tf b/database/rds/locals.tf index 31c14cf8..42d1324d 100644 --- a/database/rds/locals.tf +++ b/database/rds/locals.tf @@ -139,4 +139,20 @@ locals { # DB name handling - SQL Server doesn't support db_name at creation time db_name = local.is_sqlserver ? null : var.db_name + + # RDS Proxy + create_proxy = var.proxy_creation_enabled + proxy_engine_family = ( + local.is_postgres ? "POSTGRESQL" : + local.is_sqlserver ? "SQLSERVER" : + "MYSQL" + ) + proxy_auth_secret_arns = ( + length(var.proxy_auth_secret_arns) > 0 ? var.proxy_auth_secret_arns : + var.master_user_password_management_enabled ? [aws_db_instance.this.master_user_secret[0].secret_arn] : [] + ) + proxy_secret_kms_key_arns = distinct(concat( + var.proxy_secret_kms_key_arns, + try(startswith(var.master_user_secret_kms_key_id, "arn:"), false) ? [var.master_user_secret_kms_key_id] : [] + )) } diff --git a/database/rds/outputs.tf b/database/rds/outputs.tf index 9e91f430..19ffdf98 100644 --- a/database/rds/outputs.tf +++ b/database/rds/outputs.tf @@ -195,3 +195,22 @@ output "region" { description = "The AWS region where the resources are deployed." value = local.region } + +################################################################################ +# RDS Proxy Outputs +################################################################################ + +output "proxy_endpoint" { + description = "The endpoint of the RDS Proxy. Null when no proxy is created." + value = local.create_proxy ? module.proxy[0].endpoint : null +} + +output "proxy_arn" { + description = "The ARN of the RDS Proxy. Null when no proxy is created." + value = local.create_proxy ? module.proxy[0].proxy_arn : null +} + +output "proxy_security_group_id" { + description = "The ID of the RDS Proxy security group. Null when no proxy is created." + value = local.create_proxy ? module.proxy[0].security_group_id : null +} diff --git a/database/rds/rds_instance.tf b/database/rds/rds_instance.tf index ec4d645d..6514bab7 100644 --- a/database/rds/rds_instance.tf +++ b/database/rds/rds_instance.tf @@ -124,6 +124,16 @@ resource "aws_db_instance" "this" { condition = alltrue([for log in var.enabled_cloudwatch_logs_exports : contains(local.valid_log_exports[local.engine_log_type], log)]) error_message = "Invalid CloudWatch log export type for ${var.engine}. Valid types: ${join(", ", local.valid_log_exports[local.engine_log_type])}" } + + precondition { + condition = !var.proxy_creation_enabled || !local.is_oracle + error_message = "RDS Proxy is not supported for Oracle engines." + } + + precondition { + condition = !var.proxy_creation_enabled || var.master_user_password_management_enabled || length(var.proxy_auth_secret_arns) > 0 + error_message = "proxy_auth_secret_arns is required when proxy_creation_enabled is true and master_user_password_management_enabled is false." + } } depends_on = [ diff --git a/database/rds/rds_proxy.tf b/database/rds/rds_proxy.tf new file mode 100644 index 00000000..99dd6f15 --- /dev/null +++ b/database/rds/rds_proxy.tf @@ -0,0 +1,39 @@ +################################################################################ +# RDS Proxy (optional) +################################################################################ + +module "proxy" { + count = local.create_proxy ? 1 : 0 + + source = "../rds-proxy" + + name = var.name + engine_family = local.proxy_engine_family + vpc_id = var.vpc_id + subnet_ids = var.subnet_ids + port = local.port + tags = var.tags + + db_instance_identifier = aws_db_instance.this.identifier + + auth = [ + for arn in local.proxy_auth_secret_arns : { + secret_arn = arn + iam_auth = var.proxy_iam_auth_enabled ? "REQUIRED" : "DISABLED" + } + ] + secret_kms_key_arns = local.proxy_secret_kms_key_arns + + tls_requirement_enabled = var.proxy_tls_requirement_enabled + debug_logging_enabled = var.proxy_debug_logging_enabled + idle_client_timeout = var.proxy_idle_client_timeout + + connection_borrow_timeout = var.proxy_connection_borrow_timeout + init_query = var.proxy_init_query + max_connections_percent = var.proxy_max_connections_percent + max_idle_connections_percent = var.proxy_max_idle_connections_percent + session_pinning_filters = var.proxy_session_pinning_filters + + allowed_security_group_ids = var.allowed_security_group_ids + allowed_cidr_blocks = var.allowed_cidr_blocks +} diff --git a/database/rds/rvn-rds-definition.yml b/database/rds/rvn-rds-definition.yml index ad828939..59f2c63c 100644 --- a/database/rds/rvn-rds-definition.yml +++ b/database/rds/rvn-rds-definition.yml @@ -4,8 +4,8 @@ definition: description: Creates and manages an Amazon RDS database. Supports PostgreSQL, MySQL, MariaDB, Oracle, and SQL Server engines. release: - version: 1.0.0 - description: First stable release. + version: 1.1.0 + description: Add optional RDS Proxy connection pooling in front of the database. module: inputs: - id: network @@ -337,6 +337,133 @@ module: show_when: security_group_creation_enabled: true type: string_array + - id: section_proxy + label: Connection pooling + show_when: + engine: + - mysql + - postgres + - mariadb + - sqlserver-ee + - sqlserver-se + - sqlserver-ex + - sqlserver-web + type: section + - default: false + description: Create an RDS Proxy in front of the database for connection pooling and improved failover handling. + id: proxy_creation_enabled + label: RDS Proxy + show_when: + engine: + - mysql + - postgres + - mariadb + - sqlserver-ee + - sqlserver-se + - sqlserver-ex + - sqlserver-web + type: boolean + - add_button_label: Add secret ARN + collapsible: true + description: Secrets Manager secrets containing database credentials for the proxy. Leave blank to use the managed master user secret. + id: proxy_auth_secret_arns + label: Auth secret ARNs + placeholder: arn:aws:secretsmanager:... + required: false + show_when: + proxy_creation_enabled: true + type: string_array + - add_button_label: Add KMS key ARN + collapsible: true + description: KMS keys used to encrypt the auth secrets when using customer-managed keys. The master user secret KMS key is included automatically. + id: proxy_secret_kms_key_arns + label: Auth secret KMS key ARNs + placeholder: arn:aws:kms:... + required: false + show_when: + proxy_creation_enabled: true + type: string_array + - collapsible: true + default: false + description: Require IAM authentication for client connections to the proxy. + id: proxy_iam_auth_enabled + label: IAM authentication + show_when: + proxy_creation_enabled: true + type: boolean + - collapsible: true + default: true + description: Require TLS for client connections to the proxy. + id: proxy_tls_requirement_enabled + label: Require TLS + show_when: + proxy_creation_enabled: true + type: boolean + - collapsible: true + default: false + description: Log detailed connection information, including SQL statements, to CloudWatch Logs. + id: proxy_debug_logging_enabled + label: Debug logging + show_when: + proxy_creation_enabled: true + type: boolean + - collapsible: true + default: 1800 + id: proxy_idle_client_timeout + label: Idle client timeout (seconds) + max: 28800 + min: 1 + show_when: + proxy_creation_enabled: true + type: number + - collapsible: true + default: 120 + id: proxy_connection_borrow_timeout + label: Connection borrow timeout (seconds) + min: 0 + show_when: + proxy_creation_enabled: true + type: number + - collapsible: true + default: 100 + description: Maximum size of the connection pool as a percentage of the database max_connections setting. + id: proxy_max_connections_percent + label: Max connections (%) + max: 100 + min: 1 + show_when: + proxy_creation_enabled: true + type: number + - collapsible: true + default: 50 + description: Maximum idle connections kept open, as a percentage of the database max_connections setting. + id: proxy_max_idle_connections_percent + label: Max idle connections (%) + max: 100 + min: 0 + show_when: + proxy_creation_enabled: true + type: number + - add_button_label: Add filter + collapsible: true + id: proxy_session_pinning_filters + label: Session pinning filters + required: false + show_when: + proxy_creation_enabled: true + type: string_array + values: + - label: EXCLUDE_VARIABLE_SETS + value: EXCLUDE_VARIABLE_SETS + - collapsible: true + description: SQL statements the proxy runs when opening each new database connection. + id: proxy_init_query + label: Initialization query + placeholder: SET time_zone = 'UTC' + required: false + show_when: + proxy_creation_enabled: true + type: string - id: section_availability label: Availability & replicas type: section @@ -975,6 +1102,18 @@ module: performance_insights_kms_key_id: << module.input.performance_insights_kms_key_id >> performance_insights_retention_period: << module.input.performance_insights_retention_period >> port: << module.input.port >> + proxy_auth_secret_arns: << module.input.proxy_auth_secret_arns >> + proxy_connection_borrow_timeout: << module.input.proxy_connection_borrow_timeout >> + proxy_creation_enabled: << module.input.proxy_creation_enabled >> + proxy_debug_logging_enabled: << module.input.proxy_debug_logging_enabled >> + proxy_iam_auth_enabled: << module.input.proxy_iam_auth_enabled >> + proxy_idle_client_timeout: << module.input.proxy_idle_client_timeout >> + proxy_init_query: << module.input.proxy_init_query >> + proxy_max_connections_percent: << module.input.proxy_max_connections_percent >> + proxy_max_idle_connections_percent: << module.input.proxy_max_idle_connections_percent >> + proxy_secret_kms_key_arns: << module.input.proxy_secret_kms_key_arns >> + proxy_session_pinning_filters: << module.input.proxy_session_pinning_filters >> + proxy_tls_requirement_enabled: << module.input.proxy_tls_requirement_enabled >> public_access_enabled: << module.input.public_access_enabled >> read_replica_availability_zones: << module.input.read_replica_availability_zones >> read_replica_count: << module.input.read_replica_count >> @@ -1117,6 +1256,10 @@ module: CloudWatch alarms are optional. When enabled, the module can alarm on CPU utilization, free storage space, and database connections. The free storage alarm threshold is entered in GiB in Ravion and converted to bytes for Terraform. The Ravion UI also shows RDS CloudWatch metrics for CPU, free storage, connections, read IOPS, and write IOPS. + ## Connection pooling + + Enable RDS Proxy to place a managed connection pool in front of the database. The proxy authenticates with the Secrets Manager master user secret by default, requires TLS, and allows the same security group and CIDR sources configured for the database. Pool sizing, idle timeouts, session pinning, IAM authentication, and custom auth secrets are configurable in the Connection pooling section. RDS Proxy is not available for Oracle engines. + ## Configuration | Field | Required | Default | Notes | @@ -1142,6 +1285,7 @@ module: | Performance insights | No | true | Enables AWS Performance Insights. | | CloudWatch alarms | No | true | Adds CPU, free storage, and connection alarms. | | Free storage alarm threshold | No | 5 GiB | Converted to bytes before Terraform runs. | + | RDS Proxy | No | false | Creates an RDS Proxy connection pool in front of the database. Not available for Oracle engines. | | Tags | No | Blank | Merged with Ravion standard tags. | ## Advanced configuration diff --git a/database/rds/security_group.tf b/database/rds/security_group.tf index 3baaf8fa..96612062 100644 --- a/database/rds/security_group.tf +++ b/database/rds/security_group.tf @@ -33,7 +33,17 @@ module "security_group" { ip_protocol = "tcp" cidr_ipv4 = cidr } - ] + ], + # RDS Proxy source + local.create_proxy ? [ + { + description = "Allow ${var.engine} traffic from the RDS Proxy" + from_port = local.port + to_port = local.port + ip_protocol = "tcp" + referenced_security_group_id = module.proxy[0].security_group_id + } + ] : [] ) # Egress to VPC only. For ip_protocol="-1" (all protocols), AWS requires diff --git a/database/rds/variables.tf b/database/rds/variables.tf index 45be2502..e09d67d7 100644 --- a/database/rds/variables.tf +++ b/database/rds/variables.tf @@ -750,6 +750,117 @@ variable "blue_green_update" { default = null } +################################################################################ +# RDS Proxy +################################################################################ + +variable "proxy_creation_enabled" { + type = bool + description = "Whether to create an RDS Proxy in front of the database. Requires master_user_password_management_enabled or proxy_auth_secret_arns." + default = false +} + +variable "proxy_auth_secret_arns" { + type = list(string) + description = "List of Secrets Manager secret ARNs containing database credentials for the proxy. Defaults to the managed master user secret when master_user_password_management_enabled is true." + default = [] + + validation { + condition = alltrue([for arn in var.proxy_auth_secret_arns : startswith(arn, "arn:")]) + error_message = "All proxy_auth_secret_arns must be valid Secrets Manager secret ARNs." + } +} + +variable "proxy_secret_kms_key_arns" { + type = list(string) + description = "List of KMS key ARNs used to encrypt the proxy auth secrets when using customer-managed keys. The master user secret KMS key is included automatically." + default = [] + + validation { + condition = alltrue([for arn in var.proxy_secret_kms_key_arns : startswith(arn, "arn:")]) + error_message = "All proxy_secret_kms_key_arns must be valid KMS key ARNs." + } +} + +variable "proxy_iam_auth_enabled" { + type = bool + description = "Whether clients must use IAM authentication to connect to the proxy." + default = false +} + +variable "proxy_tls_requirement_enabled" { + type = bool + description = "Whether Transport Layer Security (TLS) encryption is required for connections to the proxy." + default = true +} + +variable "proxy_debug_logging_enabled" { + type = bool + description = "Whether the proxy logs detailed connection information, including SQL statements, to CloudWatch Logs." + default = false +} + +variable "proxy_idle_client_timeout" { + type = number + description = "The number of seconds a client connection to the proxy can be idle before the proxy disconnects it." + default = 1800 + + validation { + condition = var.proxy_idle_client_timeout >= 1 && var.proxy_idle_client_timeout <= 28800 + error_message = "The proxy_idle_client_timeout must be between 1 and 28800 seconds." + } +} + +variable "proxy_connection_borrow_timeout" { + type = number + description = "The number of seconds the proxy waits for a connection to become available in the connection pool before returning a timeout error." + default = 120 + + validation { + condition = var.proxy_connection_borrow_timeout >= 0 + error_message = "The proxy_connection_borrow_timeout must be greater than or equal to 0." + } +} + +variable "proxy_init_query" { + type = string + description = "One or more SQL statements for the proxy to run when opening each new database connection." + default = null +} + +variable "proxy_max_connections_percent" { + type = number + description = "The maximum size of the proxy connection pool as a percentage of the max_connections setting of the target database." + default = 100 + + validation { + condition = var.proxy_max_connections_percent >= 1 && var.proxy_max_connections_percent <= 100 + error_message = "The proxy_max_connections_percent must be between 1 and 100." + } +} + +variable "proxy_max_idle_connections_percent" { + type = number + description = "The maximum percentage of idle database connections the proxy keeps open, as a percentage of the max_connections setting of the target database." + default = 50 + + validation { + condition = var.proxy_max_idle_connections_percent >= 0 && var.proxy_max_idle_connections_percent <= 100 + error_message = "The proxy_max_idle_connections_percent must be between 0 and 100." + } +} + +variable "proxy_session_pinning_filters" { + type = list(string) + description = "Session pinning filters for the proxy. Valid value: EXCLUDE_VARIABLE_SETS." + default = [] + + validation { + condition = alltrue([for f in var.proxy_session_pinning_filters : f == "EXCLUDE_VARIABLE_SETS"]) + error_message = "The only valid session pinning filter is EXCLUDE_VARIABLE_SETS." + } +} + variable "region" { type = string description = "AWS region. When null, the provider's configured region is used." From 6dfb881327a2246141bb700d23ae2ce1cca062b5 Mon Sep 17 00:00:00 2001 From: Mina Abadir Date: Thu, 6 Aug 2026 17:53:19 -0400 Subject: [PATCH 2/7] update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ab63721e..5cd6015c 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ This repository contains reusable infrastructure modules designed for enterprise | `database/` | `aurora` | AWS Aurora clusters (MySQL, PostgreSQL, Serverless v2, Global Database) (includes `rvn-aurora` module definition) | v1.0.0 | | `database/` | `dynamodb` | AWS DynamoDB tables | v1.0.0 | | `database/` | `rds` | AWS RDS instances | v1.0.0 | -| `database/` | `rds-proxy` | AWS RDS Proxy for connection pooling in front of RDS instances or Aurora clusters (standalone or via the `rds`/`aurora` modules) | v1.0.0 | +| `database/` | `rds-proxy` | AWS RDS Proxy for connection pooling in front of RDS instances or Aurora clusters (standalone or via the `rds`/`aurora` modules) (includes `rvn-rds-proxy` module definition) | v1.0.0 | | `hosting/` | `static_site` | Composite static site hosting (S3 + CloudFront + OAC, optional CloudFront Function / Lambda@Edge) | v1.0.0 | | `kubernetes/` | `eks_cluster` | AWS EKS clusters with OIDC, KMS-encrypted secrets, control plane logging, core add-ons, EBS CSI / Pod Identity Agent, LB Controller Pod Identity role, and access entries | v1.0.0 | | `kubernetes/` | `eks_node_group` | AWS EKS managed node groups (one per module) with IAM, optional launch template, taints, labels, and SPOT/ON_DEMAND capacity | v1.0.0 | @@ -76,6 +76,7 @@ sync by `node tools/ravion-modules/dist/src/cli.js readme` (enforced in CI, and | `rvn-elasticache` | ElastiCache | v1.0.0 | `cache/elasticache/` | | `rvn-lambda` | Lambda Function | v1.0.0 | `compute/lambda/` | | `rvn-rds` | RDS Database | v1.1.0 | `database/rds/` | +| `rvn-rds-proxy` | RDS Proxy | v0.1.0 | `database/rds-proxy/` | | `rvn-route53` | Route 53 DNS | v1.0.0 | `networking/route53/` | | `rvn-s3` | S3 Bucket | v1.0.0 | `storage/s3/` | | `rvn-stack` | Terraform Stack | v1.2.4 | `stack/terraform/` | From d45817b5e8738b996d0106f762d34a6695434351 Mon Sep 17 00:00:00 2001 From: Mina Abadir Date: Thu, 6 Aug 2026 18:43:35 -0400 Subject: [PATCH 3/7] Add RDS Proxy test coverage and fix master secret handling Add tofu test suites for database/rds-proxy, and RDS Proxy integration tests for database/rds and database/aurora covering engine family mapping, auth secret resolution, and security group/IAM preconditions. Fix two bugs the new tests uncovered: - Hard-indexing the master user secret list errored under plan-time mocking; use a for expression that degrades to an empty list. - manage_master_user_password = false conflicted with a supplied password; omit the argument instead of setting it to false. --- .opencode/package-lock.json | 85 ++++--- database/aurora/cluster.tf | 2 +- database/aurora/locals.tf | 2 +- database/aurora/tests/proxy.tftest.hcl | 173 ++++++++++++++ database/rds-proxy/tests/basic.tftest.hcl | 263 ++++++++++++++++++++++ database/rds/locals.tf | 2 +- database/rds/rds_instance.tf | 2 +- database/rds/tests/proxy.tftest.hcl | 202 +++++++++++++++++ 8 files changed, 694 insertions(+), 37 deletions(-) create mode 100644 database/aurora/tests/proxy.tftest.hcl create mode 100644 database/rds-proxy/tests/basic.tftest.hcl create mode 100644 database/rds/tests/proxy.tftest.hcl diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json index d6a09d2b..cf8c2a5a 100644 --- a/.opencode/package-lock.json +++ b/.opencode/package-lock.json @@ -5,7 +5,19 @@ "packages": { "": { "dependencies": { - "@opencode-ai/plugin": "1.16.2" + "@opencode-ai/plugin": "1.18.14" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", + "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { @@ -87,19 +99,20 @@ ] }, "node_modules/@opencode-ai/plugin": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.16.2.tgz", - "integrity": "sha512-FaZhVXrbz93xsdGLCtarRDTeqFt8AkLfh8B34tFBj6G4HXVmKSgBwVXmtELKKC+08xMtawBC9hshiMbXryv6cg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.18.14.tgz", + "integrity": "sha512-M7gmzsQTE6z81fMZb8AaNd3u/FdQ8h7hhtQ59rWHnZMFMWusHXZM2S2BL1Z3E8kGWeveVluOVfTKtxJhCurXtw==", "license": "MIT", "dependencies": { - "@opencode-ai/sdk": "1.16.2", - "effect": "4.0.0-beta.74", + "@ai-sdk/provider": "3.0.8", + "@opencode-ai/sdk": "1.18.14", + "effect": "4.0.0-beta.83", "zod": "4.1.8" }, "peerDependencies": { - "@opentui/core": ">=0.3.2", - "@opentui/keymap": ">=0.3.2", - "@opentui/solid": ">=0.3.2" + "@opentui/core": ">=0.4.5", + "@opentui/keymap": ">=0.4.5", + "@opentui/solid": ">=0.4.5" }, "peerDependenciesMeta": { "@opentui/core": { @@ -114,9 +127,9 @@ } }, "node_modules/@opencode-ai/sdk": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.16.2.tgz", - "integrity": "sha512-Z/xZ7q79dYeE0afqIk/yFEcRNGEQFcE+H8ssYivUiy+xGZ1mGwT72jpaQZKBwPn3JH4sRCu4KA2lcktBQfcOjg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.14.tgz", + "integrity": "sha512-Yd8vPDT5DW++Hs2904Q/Vvk/m1U79aKFSRusgAfBQ+AGhfoT40LdeL5eiJtVy0xBUIZF/8DEZjkWaeof9Y59PA==", "license": "MIT", "dependencies": { "cross-spawn": "7.0.6" @@ -153,9 +166,9 @@ } }, "node_modules/effect": { - "version": "4.0.0-beta.74", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.74.tgz", - "integrity": "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA==", + "version": "4.0.0-beta.83", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.83.tgz", + "integrity": "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", @@ -171,9 +184,9 @@ } }, "node_modules/fast-check": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz", - "integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.9.0.tgz", + "integrity": "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==", "funding": [ { "type": "individual", @@ -213,6 +226,12 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/kubernetes-types": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", @@ -220,9 +239,9 @@ "license": "Apache-2.0" }, "node_modules/msgpackr": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.2.tgz", - "integrity": "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.5.tgz", + "integrity": "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==", "license": "MIT", "optionalDependencies": { "msgpackr-extract": "^3.0.4" @@ -251,9 +270,9 @@ } }, "node_modules/multipasta": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", - "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.8.tgz", + "integrity": "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==", "license": "MIT" }, "node_modules/node-gyp-build-optional-packages": { @@ -281,9 +300,9 @@ } }, "node_modules/pure-rand": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", - "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", "funding": [ { "type": "individual", @@ -318,18 +337,18 @@ } }, "node_modules/toml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", - "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz", + "integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==", "license": "MIT", "engines": { "node": ">=20" } }, "node_modules/uuid": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", - "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/database/aurora/cluster.tf b/database/aurora/cluster.tf index 8c516ffb..959206d6 100644 --- a/database/aurora/cluster.tf +++ b/database/aurora/cluster.tf @@ -39,7 +39,7 @@ resource "aws_rds_cluster" "this" { # Authentication master_username = var.master_username master_password = var.master_user_password_management_enabled ? null : var.master_password - manage_master_user_password = var.master_user_password_management_enabled + manage_master_user_password = var.master_user_password_management_enabled ? true : null master_user_secret_kms_key_id = var.master_user_password_management_enabled ? var.master_user_secret_kms_key_id : null iam_database_authentication_enabled = var.iam_database_authentication_enabled diff --git a/database/aurora/locals.tf b/database/aurora/locals.tf index 06190171..be12786e 100644 --- a/database/aurora/locals.tf +++ b/database/aurora/locals.tf @@ -107,7 +107,7 @@ locals { proxy_engine_family = local.is_mysql ? "MYSQL" : "POSTGRESQL" proxy_auth_secret_arns = ( length(var.proxy_auth_secret_arns) > 0 ? var.proxy_auth_secret_arns : - var.master_user_password_management_enabled ? [aws_rds_cluster.this.master_user_secret[0].secret_arn] : [] + var.master_user_password_management_enabled ? [for s in aws_rds_cluster.this.master_user_secret : s.secret_arn] : [] ) proxy_secret_kms_key_arns = distinct(concat( var.proxy_secret_kms_key_arns, diff --git a/database/aurora/tests/proxy.tftest.hcl b/database/aurora/tests/proxy.tftest.hcl new file mode 100644 index 00000000..951f593d --- /dev/null +++ b/database/aurora/tests/proxy.tftest.hcl @@ -0,0 +1,173 @@ +################################################################################ +# Aurora RDS Proxy Integration Tests +# +# Covers the optional RDS Proxy wiring: engine family mapping, auth secret +# resolution, preconditions, and that the combined plan graph (cluster +# security group ingress from the proxy security group, proxy targeting the +# cluster, proxy depending on cluster instances) resolves without dependency +# cycles. +################################################################################ + +mock_provider "aws" { + override_data { + target = data.aws_caller_identity.current + values = { + account_id = "123456789012" + } + } + + override_data { + target = data.aws_region.current + values = { + id = "us-east-1" + name = "us-east-1" + } + } + + override_data { + target = data.aws_vpc.this + values = { + cidr_block = "10.0.0.0/16" + } + } + + override_data { + target = module.proxy.data.aws_vpc.this + values = { + cidr_block = "10.0.0.0/16" + } + } + + # The proxy security group ID feeds the cluster security group's ingress + # rule validation, which requires an sg- prefix. + override_resource { + target = module.proxy.module.security_group.aws_security_group.this + values = { + id = "sg-proxy123456" + } + } + + override_resource { + target = module.proxy.aws_iam_role.this + values = { + arn = "arn:aws:iam::123456789012:role/test-aurora-rds-proxy" + } + } + + override_resource { + target = module.proxy.aws_db_proxy_default_target_group.this + values = { + name = "default" + } + } + + # The mock provider materializes computed lists as empty; provide the managed + # master user secret the proxy auth defaults to. + override_resource { + target = aws_rds_cluster.this + values = { + master_user_secret = [{ + kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" + secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:test-master-abc123" + secret_status = "active" + }] + } + } +} + +variables { + name = "test-aurora" + engine = "aurora-postgresql" + engine_version = "16.4" + instance_class = "db.t4g.medium" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-11111111", "subnet-22222222"] + master_username = "dbadmin" +} + +run "test_proxy_not_created_by_default" { + command = plan + + assert { + condition = local.create_proxy == false + error_message = "Proxy should not be created by default." + } +} + +# Full plan with the proxy enabled. This exercises the complete dependency +# graph, including the proxy module's depends_on on cluster instances. +run "test_proxy_created_when_enabled" { + command = plan + + variables { + proxy_creation_enabled = true + } + + assert { + condition = local.create_proxy == true + error_message = "Proxy should be created when proxy_creation_enabled is true." + } + + assert { + condition = local.proxy_engine_family == "POSTGRESQL" + error_message = "aurora-postgresql should map to the POSTGRESQL engine family." + } +} + +run "test_proxy_engine_family_mysql" { + command = plan + + variables { + engine = "aurora-mysql" + proxy_creation_enabled = true + } + + assert { + condition = local.proxy_engine_family == "MYSQL" + error_message = "aurora-mysql should map to the MYSQL engine family." + } +} + +run "test_proxy_requires_secret_when_password_management_disabled" { + command = plan + + variables { + proxy_creation_enabled = true + master_user_password_management_enabled = false + master_password = "dummy-password-123" + } + + expect_failures = [ + aws_rds_cluster.this, + ] +} + +run "test_proxy_explicit_auth_secrets" { + command = plan + + variables { + proxy_creation_enabled = true + master_user_password_management_enabled = false + master_password = "dummy-password-123" + proxy_auth_secret_arns = ["arn:aws:secretsmanager:us-east-1:123456789012:secret:custom-abc123"] + } + + assert { + condition = local.proxy_auth_secret_arns[0] == "arn:aws:secretsmanager:us-east-1:123456789012:secret:custom-abc123" + error_message = "Explicit proxy auth secrets should be used." + } +} + +run "test_proxy_master_secret_kms_key_passthrough" { + command = plan + + variables { + proxy_creation_enabled = true + master_user_secret_kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" + } + + assert { + condition = contains(local.proxy_secret_kms_key_arns, "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012") + error_message = "Master user secret KMS key ARN should be passed to the proxy." + } +} diff --git a/database/rds-proxy/tests/basic.tftest.hcl b/database/rds-proxy/tests/basic.tftest.hcl new file mode 100644 index 00000000..93b6a485 --- /dev/null +++ b/database/rds-proxy/tests/basic.tftest.hcl @@ -0,0 +1,263 @@ +################################################################################ +# RDS Proxy Module Unit Tests +################################################################################ + +# Mock AWS provider with overridden data sources +mock_provider "aws" { + override_data { + target = data.aws_caller_identity.current + values = { + account_id = "123456789012" + } + } + + override_data { + target = data.aws_region.current + values = { + id = "us-east-1" + name = "us-east-1" + } + } + + override_data { + target = data.aws_vpc.this + values = { + cidr_block = "10.0.0.0/16" + } + } + + override_resource { + target = aws_iam_role.this + values = { + arn = "arn:aws:iam::123456789012:role/test-proxy-rds-proxy" + } + } + + override_resource { + target = aws_db_proxy_default_target_group.this + values = { + name = "default" + } + } +} + +# Defaults shared by all runs; individual runs override as needed. +variables { + name = "test-proxy" + engine_family = "POSTGRESQL" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-11111111", "subnet-22222222"] + auth = [ + { secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:test-abc123" } + ] + db_instance_identifier = "test-db" +} + +#------------------------------------------------------------------------------- +# Basic Plan & Defaults +#------------------------------------------------------------------------------- + +run "test_basic_plan" { + command = plan + + assert { + condition = aws_db_proxy.this.name == "test-proxy" + error_message = "Proxy should use var.name as its name." + } + + assert { + condition = aws_db_proxy.this.require_tls == true + error_message = "TLS should be required by default." + } + + assert { + condition = aws_db_proxy.this.engine_family == "POSTGRESQL" + error_message = "Engine family should be passed through." + } + + assert { + condition = local.port == 5432 + error_message = "POSTGRESQL should default to port 5432." + } + + assert { + condition = local.tags["Module"] == "database/rds-proxy" + error_message = "Module default tag should be set." + } +} + +run "test_mysql_port_default" { + command = plan + + variables { + engine_family = "MYSQL" + } + + assert { + condition = local.port == 3306 + error_message = "MYSQL should default to port 3306." + } +} + +run "test_sqlserver_port_default" { + command = plan + + variables { + engine_family = "SQLSERVER" + } + + assert { + condition = local.port == 1433 + error_message = "SQLSERVER should default to port 1433." + } +} + +run "test_custom_port_override" { + command = plan + + variables { + port = 6543 + } + + assert { + condition = local.port == 6543 + error_message = "Custom port should override the engine family default." + } +} + +#------------------------------------------------------------------------------- +# Validation Tests +#------------------------------------------------------------------------------- + +run "test_invalid_engine_family" { + command = plan + + variables { + engine_family = "ORACLE" + } + + expect_failures = [ + var.engine_family, + ] +} + +run "test_auth_empty" { + command = plan + + variables { + auth = [] + } + + expect_failures = [ + var.auth, + ] +} + +run "test_auth_invalid_secret_arn" { + command = plan + + variables { + auth = [{ secret_arn = "not-an-arn" }] + } + + expect_failures = [ + var.auth, + ] +} + +run "test_max_idle_above_max_connections" { + command = plan + + variables { + max_connections_percent = 50 + max_idle_connections_percent = 80 + } + + expect_failures = [ + var.max_idle_connections_percent, + ] +} + +#------------------------------------------------------------------------------- +# Target Exclusivity Tests +#------------------------------------------------------------------------------- + +run "test_both_targets_rejected" { + command = plan + + variables { + db_cluster_identifier = "test-cluster" + } + + expect_failures = [ + aws_db_proxy_target.this, + ] +} + +run "test_no_target_rejected" { + command = plan + + variables { + db_instance_identifier = null + } + + expect_failures = [ + aws_db_proxy_target.this, + ] +} + +#------------------------------------------------------------------------------- +# Security Group & IAM Role Preconditions +#------------------------------------------------------------------------------- + +run "test_sg_disabled_requires_id" { + command = plan + + variables { + security_group_creation_enabled = false + } + + expect_failures = [ + aws_db_proxy.this, + ] +} + +run "test_sg_disabled_with_id" { + command = plan + + variables { + security_group_creation_enabled = false + security_group_id = "sg-12345678" + } + + assert { + condition = local.security_group_id == "sg-12345678" + error_message = "Provided security group ID should be used." + } +} + +run "test_iam_role_disabled_requires_arn" { + command = plan + + variables { + iam_role_creation_enabled = false + } + + expect_failures = [ + aws_db_proxy.this, + ] +} + +run "test_iam_role_disabled_with_arn" { + command = plan + + variables { + iam_role_creation_enabled = false + iam_role_arn = "arn:aws:iam::123456789012:role/custom-proxy-role" + } + + assert { + condition = local.iam_role_arn == "arn:aws:iam::123456789012:role/custom-proxy-role" + error_message = "Provided IAM role ARN should be used." + } +} diff --git a/database/rds/locals.tf b/database/rds/locals.tf index 42d1324d..cb42d9a1 100644 --- a/database/rds/locals.tf +++ b/database/rds/locals.tf @@ -149,7 +149,7 @@ locals { ) proxy_auth_secret_arns = ( length(var.proxy_auth_secret_arns) > 0 ? var.proxy_auth_secret_arns : - var.master_user_password_management_enabled ? [aws_db_instance.this.master_user_secret[0].secret_arn] : [] + var.master_user_password_management_enabled ? [for s in aws_db_instance.this.master_user_secret : s.secret_arn] : [] ) proxy_secret_kms_key_arns = distinct(concat( var.proxy_secret_kms_key_arns, diff --git a/database/rds/rds_instance.tf b/database/rds/rds_instance.tf index 6514bab7..970dd392 100644 --- a/database/rds/rds_instance.tf +++ b/database/rds/rds_instance.tf @@ -36,7 +36,7 @@ resource "aws_db_instance" "this" { # Authentication username = var.username password = var.master_user_password_management_enabled ? null : var.password - manage_master_user_password = var.master_user_password_management_enabled + manage_master_user_password = var.master_user_password_management_enabled ? true : null master_user_secret_kms_key_id = var.master_user_password_management_enabled ? var.master_user_secret_kms_key_id : null iam_database_authentication_enabled = local.iam_database_authentication_enabled diff --git a/database/rds/tests/proxy.tftest.hcl b/database/rds/tests/proxy.tftest.hcl new file mode 100644 index 00000000..5b03890d --- /dev/null +++ b/database/rds/tests/proxy.tftest.hcl @@ -0,0 +1,202 @@ +################################################################################ +# RDS Proxy Integration Tests +# +# Covers the optional RDS Proxy wiring: engine family mapping, auth secret +# resolution, preconditions, and that the combined plan graph (database +# security group ingress from the proxy security group, proxy targeting the +# database) resolves without dependency cycles. +################################################################################ + +mock_provider "aws" { + override_data { + target = data.aws_caller_identity.current + values = { + account_id = "123456789012" + } + } + + override_data { + target = data.aws_region.current + values = { + id = "us-east-1" + name = "us-east-1" + } + } + + override_data { + target = data.aws_vpc.this + values = { + cidr_block = "10.0.0.0/16" + } + } + + override_data { + target = module.proxy.data.aws_vpc.this + values = { + cidr_block = "10.0.0.0/16" + } + } + + # The proxy security group ID feeds the database security group's ingress + # rule validation, which requires an sg- prefix. + override_resource { + target = module.proxy.module.security_group.aws_security_group.this + values = { + id = "sg-proxy123456" + } + } + + override_resource { + target = module.proxy.aws_iam_role.this + values = { + arn = "arn:aws:iam::123456789012:role/test-db-rds-proxy" + } + } + + override_resource { + target = module.proxy.aws_db_proxy_default_target_group.this + values = { + name = "default" + } + } + + # The mock provider materializes computed lists as empty; provide the managed + # master user secret the proxy auth defaults to. + override_resource { + target = aws_db_instance.this + values = { + master_user_secret = [{ + kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" + secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:test-master-abc123" + secret_status = "active" + }] + } + } +} + +variables { + name = "test-db" + engine = "postgres" + instance_class = "db.t3.micro" + allocated_storage = 20 + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-11111111", "subnet-22222222"] + username = "admin" +} + +run "test_proxy_not_created_by_default" { + command = plan + + assert { + condition = local.create_proxy == false + error_message = "Proxy should not be created by default." + } +} + +# Full plan with the proxy enabled. This exercises the complete dependency +# graph: DB security group ingress references the proxy security group while +# the proxy targets the database. +run "test_proxy_created_when_enabled" { + command = plan + + variables { + proxy_creation_enabled = true + } + + assert { + condition = local.create_proxy == true + error_message = "Proxy should be created when proxy_creation_enabled is true." + } + + assert { + condition = local.proxy_engine_family == "POSTGRESQL" + error_message = "postgres should map to the POSTGRESQL engine family." + } +} + +run "test_proxy_engine_family_mariadb" { + command = plan + + variables { + engine = "mariadb" + proxy_creation_enabled = true + } + + assert { + condition = local.proxy_engine_family == "MYSQL" + error_message = "mariadb should map to the MYSQL engine family." + } +} + +run "test_proxy_engine_family_sqlserver" { + command = plan + + variables { + engine = "sqlserver-se" + license_model = "license-included" + proxy_creation_enabled = true + } + + assert { + condition = local.proxy_engine_family == "SQLSERVER" + error_message = "sqlserver-se should map to the SQLSERVER engine family." + } +} + +run "test_proxy_rejected_for_oracle" { + command = plan + + variables { + engine = "oracle-ee" + license_model = "bring-your-own-license" + proxy_creation_enabled = true + } + + expect_failures = [ + aws_db_instance.this, + ] +} + +run "test_proxy_requires_secret_when_password_management_disabled" { + command = plan + + variables { + proxy_creation_enabled = true + master_user_password_management_enabled = false + password = "dummy-password-123" + } + + expect_failures = [ + aws_db_instance.this, + ] +} + +run "test_proxy_explicit_auth_secrets" { + command = plan + + variables { + proxy_creation_enabled = true + master_user_password_management_enabled = false + password = "dummy-password-123" + proxy_auth_secret_arns = ["arn:aws:secretsmanager:us-east-1:123456789012:secret:custom-abc123"] + } + + assert { + condition = local.proxy_auth_secret_arns[0] == "arn:aws:secretsmanager:us-east-1:123456789012:secret:custom-abc123" + error_message = "Explicit proxy auth secrets should be used." + } +} + +run "test_proxy_master_secret_kms_key_passthrough" { + command = plan + + variables { + proxy_creation_enabled = true + master_user_secret_kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" + } + + assert { + condition = contains(local.proxy_secret_kms_key_arns, "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012") + error_message = "Master user secret KMS key ARN should be passed to the proxy." + } +} From e669423c44c122320a363fdaae98d41d7c8c70bb Mon Sep 17 00:00:00 2001 From: Mina Abadir Date: Sat, 8 Aug 2026 14:56:52 -0400 Subject: [PATCH 4/7] fix: bump aurora/rds definitions and break proxy SG cycle Publish dry-run failed because rvn-aurora and rvn-rds 1.1.1 already exist remotely. Bump to 1.2.0 for the proxy feature and move proxy ingress onto a standalone rule so the DB SG no longer cycles through the proxy target. --- README.md | 4 ++-- database/aurora/rvn-aurora-definition.yml | 4 ++-- database/aurora/security_group.tf | 29 ++++++++++++++--------- database/aurora/tests/proxy.tftest.hcl | 7 +++--- database/rds/rvn-rds-definition.yml | 4 ++-- database/rds/security_group.tf | 29 ++++++++++++++--------- database/rds/tests/proxy.tftest.hcl | 4 ++-- 7 files changed, 47 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 2ba02276..9916659d 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ sync by `node tools/ravion-modules/dist/src/cli.js readme` (enforced in CI, and | Definition | Name | Version | Module path | | ---------- | ---- | ------- | ----------- | | `rvn-acm-certificate` | ACM Certificate | v1.0.1 | `security/acm_certificate/` | -| `rvn-aurora` | Aurora Database | v1.1.1 | `database/aurora/` | +| `rvn-aurora` | Aurora Database | v1.2.0 | `database/aurora/` | | `rvn-aws-alb` | AWS Application Load Balancer | v1.0.1 | `networking/alb/` | | `rvn-aws-iam-policy` | AWS IAM Policy | v1.0.1 | `security/iam_policy/` | | `rvn-aws-iam-role` | AWS IAM Role | v1.0.1 | `security/iam/` | @@ -75,7 +75,7 @@ sync by `node tools/ravion-modules/dist/src/cli.js readme` (enforced in CI, and | `rvn-efs` | EFS File System | v1.0.1 | `storage/efs/` | | `rvn-elasticache` | ElastiCache | v1.0.1 | `cache/elasticache/` | | `rvn-lambda` | Lambda Function | v1.0.1 | `compute/lambda/` | -| `rvn-rds` | RDS Database | v1.1.1 | `database/rds/` | +| `rvn-rds` | RDS Database | v1.2.0 | `database/rds/` | | `rvn-rds-proxy` | RDS Proxy | v0.1.0 | `database/rds-proxy/` | | `rvn-route53` | Route 53 DNS | v1.0.1 | `networking/route53/` | | `rvn-s3` | S3 Bucket | v1.0.1 | `storage/s3/` | diff --git a/database/aurora/rvn-aurora-definition.yml b/database/aurora/rvn-aurora-definition.yml index 94b7eccf..cbb92e85 100644 --- a/database/aurora/rvn-aurora-definition.yml +++ b/database/aurora/rvn-aurora-definition.yml @@ -4,8 +4,8 @@ definition: description: Creates and manages an Amazon Aurora cluster. Supports Aurora PostgreSQL and Aurora MySQL with provisioned or Serverless v2 capacity. release: - version: 1.1.1 - description: Add optional RDS Proxy connection pooling and update Terraform module sources and documentation links to the renamed `ravionhq` GitHub organization. + version: 1.2.0 + description: Add optional RDS Proxy connection pooling. module: inputs: - id: network diff --git a/database/aurora/security_group.tf b/database/aurora/security_group.tf index 126baf0c..66c4217d 100644 --- a/database/aurora/security_group.tf +++ b/database/aurora/security_group.tf @@ -33,17 +33,7 @@ module "security_group" { ip_protocol = "tcp" cidr_ipv4 = cidr } - ], - # RDS Proxy source - local.create_proxy ? [ - { - description = "Allow ${var.engine} traffic from the RDS Proxy" - from_port = local.port - to_port = local.port - ip_protocol = "tcp" - referenced_security_group_id = module.proxy[0].security_group_id - } - ] : [] + ] ) # Egress to VPC only. For ip_protocol="-1" (all protocols), AWS requires @@ -58,3 +48,20 @@ module "security_group" { } ] } + +# Separate from the security group module so the cluster can depend on the SG +# without creating a cycle through the proxy target registration. +resource "aws_vpc_security_group_ingress_rule" "proxy" { + count = local.create_security_group && local.create_proxy ? 1 : 0 + + security_group_id = module.security_group[0].security_group_id + description = "Allow ${var.engine} traffic from the RDS Proxy" + from_port = local.port + to_port = local.port + ip_protocol = "tcp" + referenced_security_group_id = module.proxy[0].security_group_id + + tags = merge(local.tags, { + Name = "${var.name}-aurora-proxy-ingress" + }) +} diff --git a/database/aurora/tests/proxy.tftest.hcl b/database/aurora/tests/proxy.tftest.hcl index 951f593d..4244ec05 100644 --- a/database/aurora/tests/proxy.tftest.hcl +++ b/database/aurora/tests/proxy.tftest.hcl @@ -2,10 +2,9 @@ # Aurora RDS Proxy Integration Tests # # Covers the optional RDS Proxy wiring: engine family mapping, auth secret -# resolution, preconditions, and that the combined plan graph (cluster -# security group ingress from the proxy security group, proxy targeting the -# cluster, proxy depending on cluster instances) resolves without dependency -# cycles. +# resolution, preconditions, and that the combined plan graph (standalone +# proxy ingress rule, proxy targeting the cluster, proxy depending on cluster +# instances) resolves without dependency cycles. ################################################################################ mock_provider "aws" { diff --git a/database/rds/rvn-rds-definition.yml b/database/rds/rvn-rds-definition.yml index 63e261d3..b81a5327 100644 --- a/database/rds/rvn-rds-definition.yml +++ b/database/rds/rvn-rds-definition.yml @@ -4,8 +4,8 @@ definition: description: Creates and manages an Amazon RDS database. Supports PostgreSQL, MySQL, MariaDB, Oracle, and SQL Server engines. release: - version: 1.1.1 - description: Add optional RDS Proxy connection pooling and update Terraform module sources and documentation links to the renamed `ravionhq` GitHub organization. + version: 1.2.0 + description: Add optional RDS Proxy connection pooling. module: inputs: - id: network diff --git a/database/rds/security_group.tf b/database/rds/security_group.tf index 96612062..39cbef2d 100644 --- a/database/rds/security_group.tf +++ b/database/rds/security_group.tf @@ -33,17 +33,7 @@ module "security_group" { ip_protocol = "tcp" cidr_ipv4 = cidr } - ], - # RDS Proxy source - local.create_proxy ? [ - { - description = "Allow ${var.engine} traffic from the RDS Proxy" - from_port = local.port - to_port = local.port - ip_protocol = "tcp" - referenced_security_group_id = module.proxy[0].security_group_id - } - ] : [] + ] ) # Egress to VPC only. For ip_protocol="-1" (all protocols), AWS requires @@ -58,3 +48,20 @@ module "security_group" { } ] } + +# Separate from the security group module so the database can depend on the SG +# without creating a cycle through the proxy target registration. +resource "aws_vpc_security_group_ingress_rule" "proxy" { + count = local.create_security_group && local.create_proxy ? 1 : 0 + + security_group_id = module.security_group[0].security_group_id + description = "Allow ${var.engine} traffic from the RDS Proxy" + from_port = local.port + to_port = local.port + ip_protocol = "tcp" + referenced_security_group_id = module.proxy[0].security_group_id + + tags = merge(local.tags, { + Name = "${var.name}-rds-proxy-ingress" + }) +} diff --git a/database/rds/tests/proxy.tftest.hcl b/database/rds/tests/proxy.tftest.hcl index 5b03890d..7de00a7a 100644 --- a/database/rds/tests/proxy.tftest.hcl +++ b/database/rds/tests/proxy.tftest.hcl @@ -94,8 +94,8 @@ run "test_proxy_not_created_by_default" { } # Full plan with the proxy enabled. This exercises the complete dependency -# graph: DB security group ingress references the proxy security group while -# the proxy targets the database. +# graph: standalone proxy ingress rule references the proxy security group +# while the proxy targets the database. run "test_proxy_created_when_enabled" { command = plan From 149815fa753b755ae21affc0abb9c51c84c62bc5 Mon Sep 17 00:00:00 2001 From: Mina Abadir Date: Sat, 8 Aug 2026 15:07:34 -0400 Subject: [PATCH 5/7] feat: reference RDS/Aurora modules as RDS Proxy targets Add a target source toggle to rvn-rds-proxy so the proxy can target an existing rvn-rds or rvn-aurora module directly, inheriting its database identifier, master user secret, and port. Also document the connection string update needed after enabling proxy toggles on rvn-rds and rvn-aurora. --- database/aurora/rvn-aurora-definition.yml | 4 +- .../rds-proxy/rvn-rds-proxy-definition.yml | 108 ++++++++++++++++-- database/rds/rvn-rds-definition.yml | 4 +- 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/database/aurora/rvn-aurora-definition.yml b/database/aurora/rvn-aurora-definition.yml index cbb92e85..4f0f42c9 100644 --- a/database/aurora/rvn-aurora-definition.yml +++ b/database/aurora/rvn-aurora-definition.yml @@ -326,7 +326,7 @@ module: label: Connection pooling type: section - default: false - description: Create an RDS Proxy in front of the cluster for connection pooling and improved failover handling. + description: Create an RDS Proxy in front of the cluster for connection pooling and improved failover handling. Applications keep connecting to the cluster endpoint until you update their connection string to the proxy endpoint output. id: proxy_creation_enabled label: RDS Proxy type: boolean @@ -1415,6 +1415,8 @@ module: Enable RDS Proxy to place a managed connection pool in front of the cluster. The proxy authenticates with the Secrets Manager master user secret by default, requires TLS, and allows the same security group and CIDR sources configured for the cluster. Pool sizing, idle timeouts, session pinning, IAM authentication, and custom auth secrets are configurable in the Connection pooling section. + Creating the proxy does not change how applications reach the cluster. Update your application connection string to the proxy endpoint output to route traffic through the pool. + ## Configuration | Field | Required | Default | Notes | diff --git a/database/rds-proxy/rvn-rds-proxy-definition.yml b/database/rds-proxy/rvn-rds-proxy-definition.yml index 4a45988e..6483070f 100644 --- a/database/rds-proxy/rvn-rds-proxy-definition.yml +++ b/database/rds-proxy/rvn-rds-proxy-definition.yml @@ -67,6 +67,7 @@ module: required: true type: string - default: POSTGRESQL + description: Must match the engine of the target database. id: engine_family immutable: true label: Engine family @@ -83,6 +84,7 @@ module: label: Target database type: section - default: db_instance + description: Register an RDS instance or an Aurora cluster as the proxy target. id: target_type label: Target type required: true @@ -92,6 +94,71 @@ module: value: db_instance - label: Aurora cluster value: db_cluster + - default: module + description: Select an existing Ravion database module or enter the identifier of a database managed outside Ravion. + id: target_source + label: Target source + required: true + type: string + values: + - label: Ravion database module + value: module + - label: Database identifier + value: identifier + - description: RDS database module to register as the proxy target. Add this proxy's security group to that module's allowed security groups so the database accepts proxy connections. + id: rds_database + label: RDS database + required: false + show_when: + target_type: db_instance + target_source: module + type: $ref:rvn-rds + mapped_inputs: + - default: <> + description: Instance identifier from the selected RDS module. + id: rds_db_instance_identifier + label: DB instance identifier + required: true + type: string + - default: <> + description: Managed master user secret from the selected RDS module. Blank when that database does not manage its password in Secrets Manager; add auth secret ARNs instead. + id: rds_master_user_secret_arn + label: Master user secret ARN + required: false + type: string + - default: <> + description: Database port from the selected RDS module. + id: rds_port + label: Database port + required: false + type: number + - description: Aurora cluster module to register as the proxy target. Add this proxy's security group to that module's allowed security groups so the cluster accepts proxy connections. + id: aurora_database + label: Aurora cluster + required: false + show_when: + target_type: db_cluster + target_source: module + type: $ref:rvn-aurora + mapped_inputs: + - default: <> + description: Cluster identifier from the selected Aurora module. + id: aurora_cluster_identifier + label: DB cluster identifier + required: true + type: string + - default: <> + description: Managed master user secret from the selected Aurora module. Blank when that cluster does not manage its password in Secrets Manager; add auth secret ARNs instead. + id: aurora_master_user_secret_arn + label: Master user secret ARN + required: false + type: string + - default: <> + description: Cluster port from the selected Aurora module. + id: aurora_port + label: Database port + required: false + type: number - description: Identifier of the RDS instance to register as the proxy target. id: db_instance_identifier label: DB instance identifier @@ -99,6 +166,7 @@ module: required: true show_when: target_type: db_instance + target_source: identifier type: string - description: Identifier of the Aurora cluster to register as the proxy target. id: db_cluster_identifier @@ -107,16 +175,17 @@ module: required: true show_when: target_type: db_cluster + target_source: identifier type: string - id: section_auth label: Authentication type: section - add_button_label: Add secret ARN - description: Secrets Manager secrets containing database credentials the proxy uses to connect to the target database. + description: Secrets Manager secrets containing database credentials the proxy uses to connect to the target database. Leave blank to use the managed master user secret of the selected database module. id: auth_secret_arns label: Auth secret ARNs placeholder: arn:aws:secretsmanager:... - required: true + required: false type: string_array - add_button_label: Add KMS key ARN collapsible: true @@ -157,7 +226,7 @@ module: label: Network access type: section - collapsible: true - description: "Leave blank to use the engine family default port: 5432 for PostgreSQL, 3306 for MySQL, or 1433 for SQL Server." + description: "Leave blank to use the port of the selected database module, or the engine family default port: 5432 for PostgreSQL, 3306 for MySQL, or 1433 for SQL Server." id: port label: Port max: 65535 @@ -261,10 +330,10 @@ module: ...overrides: << module.input.advanced_terraform_variables >> allowed_cidr_blocks: << module.input.allowed_cidr_blocks >> allowed_security_group_ids: << module.input.allowed_security_group_ids >> - auth: '<< map(module.input.auth_secret_arns, {"secret_arn": #, "iam_auth": module.input.iam_auth_enabled ? "REQUIRED" : "DISABLED"}) >>' + auth: '<< map(len(module.input.auth_secret_arns) > 0 ? module.input.auth_secret_arns : ((module.input.rds_master_user_secret_arn || module.input.aurora_master_user_secret_arn) ? [module.input.rds_master_user_secret_arn || module.input.aurora_master_user_secret_arn] : []), {"secret_arn": #, "iam_auth": module.input.iam_auth_enabled ? "REQUIRED" : "DISABLED"}) >>' connection_borrow_timeout: << module.input.connection_borrow_timeout >> - db_cluster_identifier: << module.input.db_cluster_identifier >> - db_instance_identifier: << module.input.db_instance_identifier >> + db_cluster_identifier: '<< module.input.target_type == "db_cluster" ? (module.input.aurora_cluster_identifier || module.input.db_cluster_identifier) : null >>' + db_instance_identifier: '<< module.input.target_type == "db_instance" ? (module.input.rds_db_instance_identifier || module.input.db_instance_identifier) : null >>' debug_logging_enabled: << module.input.debug_logging_enabled >> engine_family: << module.input.engine_family >> iam_role_arn: << module.input.iam_role_arn >> @@ -274,7 +343,7 @@ module: max_connections_percent: << module.input.max_connections_percent >> max_idle_connections_percent: << module.input.max_idle_connections_percent >> name: << module.input.name >> - port: << module.input.port >> + port: << module.input.port || module.input.rds_port || module.input.aurora_port >> region: << module.input.aws_region >> secret_kms_key_arns: << module.input.secret_kms_key_arns >> security_group_creation_enabled: << module.input.security_group_creation_enabled >> @@ -332,6 +401,19 @@ module: The RDS and Aurora modules can create a proxy directly with their RDS Proxy toggle. Use this standalone module when the target database is not managed by Ravion, or when you want to manage the proxy lifecycle separately from the database. + ## Target database + + Target type selects whether the proxy registers an RDS instance or an Aurora cluster. Target source selects where the target comes from: + + | Target source | Behavior | + | --- | --- | + | Ravion database module | Select an existing RDS or Aurora module. Ravion reads its database identifier, managed master user secret, and port from that module's stack outputs. | + | Database identifier | Enter the identifier of a database managed outside Ravion, along with its auth secret ARNs. | + + When a Ravion module supplies the target, add this proxy's security group ID to that module's allowed security groups. The proxy connects to the database through the database security group, and the database module owns those ingress rules. + + Applications do not use the proxy until you point their connection string at the proxy endpoint output. + ## Use cases | Scenario | Benefit | @@ -349,7 +431,9 @@ module: ## Authentication - The proxy reads database credentials from the Secrets Manager secrets listed in auth secret ARNs. For an RDS or Aurora database with managed master passwords, use the master user secret ARN. The module creates an IAM role that can read only those secrets; provide KMS key ARNs when the secrets use customer-managed keys. TLS is required by default, and IAM authentication for clients is optional. + The proxy reads database credentials from the Secrets Manager secrets listed in auth secret ARNs. Leave that field blank when a selected database module manages its master password in Secrets Manager; the proxy then uses that module's master user secret. Add auth secret ARNs explicitly for databases identified by identifier, for databases whose password is managed outside Secrets Manager, or to authenticate as a different database user. + + The module creates an IAM role that can read only the resolved auth secrets; provide KMS key ARNs when the secrets use customer-managed keys. TLS is required by default, and IAM authentication for clients is optional. ## Configuration @@ -359,8 +443,10 @@ module: | Name slug | Yes | Project and environment given IDs + proxy | Proxy name and prefix for related resources. | | Engine family | Yes | PostgreSQL | PostgreSQL, MySQL / MariaDB, or SQL Server. Must match the target database engine. | | Target type | Yes | RDS instance | Register an RDS instance or an Aurora cluster as the proxy target. | - | DB instance / cluster identifier | Yes | None | Identifier of the target database. | - | Auth secret ARNs | Yes | None | Secrets Manager secrets with database credentials. | + | Target source | Yes | Ravion database module | Select an existing RDS or Aurora module, or enter a database identifier. | + | RDS database / Aurora cluster | Yes for module targets | None | Supplies the database identifier, master user secret, and port. | + | DB instance / cluster identifier | Yes for identifier targets | None | Identifier of a database managed outside Ravion. | + | Auth secret ARNs | No | Blank | Secrets Manager secrets with database credentials. Required when the target does not provide a managed master user secret. | | Require TLS | No | true | Requires TLS for client connections. | | IAM authentication | No | false | Requires IAM auth for client connections. | | Security group creation | No | true | Creates ingress rules from allowed security groups and CIDRs. | @@ -382,4 +468,4 @@ module: - [Amazon RDS Proxy documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html) - [RDS Proxy managing connections](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-managing.html) - - [Terraform source](https://github.com/flightcontrolhq/modules/tree/$local.module_tag/database/rds-proxy) + - [Terraform source](https://github.com/ravionhq/modules/tree/$local.module_tag/database/rds-proxy) diff --git a/database/rds/rvn-rds-definition.yml b/database/rds/rvn-rds-definition.yml index b81a5327..d9ec3814 100644 --- a/database/rds/rvn-rds-definition.yml +++ b/database/rds/rvn-rds-definition.yml @@ -350,7 +350,7 @@ module: - sqlserver-web type: section - default: false - description: Create an RDS Proxy in front of the database for connection pooling and improved failover handling. + description: Create an RDS Proxy in front of the database for connection pooling and improved failover handling. Applications keep connecting to the database endpoint until you update their connection string to the proxy endpoint output. id: proxy_creation_enabled label: RDS Proxy show_when: @@ -1275,6 +1275,8 @@ module: Enable RDS Proxy to place a managed connection pool in front of the database. The proxy authenticates with the Secrets Manager master user secret by default, requires TLS, and allows the same security group and CIDR sources configured for the database. Pool sizing, idle timeouts, session pinning, IAM authentication, and custom auth secrets are configurable in the Connection pooling section. RDS Proxy is not available for Oracle engines. + Creating the proxy does not change how applications reach the database. Update your application connection string to the proxy endpoint output to route traffic through the pool. + ## Configuration | Field | Required | Default | Notes | From 5fa09972956bbc8fe762fc537d5f8393713d6c2b Mon Sep 17 00:00:00 2001 From: Mina Abadir Date: Sat, 8 Aug 2026 15:15:33 -0400 Subject: [PATCH 6/7] fix: fold RDS Proxy target source into the target type toggle --- .../rds-proxy/rvn-rds-proxy-definition.yml | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/database/rds-proxy/rvn-rds-proxy-definition.yml b/database/rds-proxy/rvn-rds-proxy-definition.yml index 6483070f..e69e5038 100644 --- a/database/rds-proxy/rvn-rds-proxy-definition.yml +++ b/database/rds-proxy/rvn-rds-proxy-definition.yml @@ -83,35 +83,31 @@ module: - id: section_target label: Target database type: section - - default: db_instance - description: Register an RDS instance or an Aurora cluster as the proxy target. + - default: rds_module + description: Database this proxy pools connections for. id: target_type label: Target type required: true type: string values: - - label: RDS instance + - description: Use an existing Ravion RDS module as the proxy target. + label: RDS module + value: rds_module + - description: Use an existing Ravion Aurora module as the proxy target. + label: Aurora module + value: aurora_module + - description: Enter the identifier of an RDS instance managed outside Ravion. + label: RDS instance identifier value: db_instance - - label: Aurora cluster + - description: Enter the identifier of an Aurora cluster managed outside Ravion. + label: Aurora cluster identifier value: db_cluster - - default: module - description: Select an existing Ravion database module or enter the identifier of a database managed outside Ravion. - id: target_source - label: Target source - required: true - type: string - values: - - label: Ravion database module - value: module - - label: Database identifier - value: identifier - description: RDS database module to register as the proxy target. Add this proxy's security group to that module's allowed security groups so the database accepts proxy connections. id: rds_database label: RDS database required: false show_when: - target_type: db_instance - target_source: module + target_type: rds_module type: $ref:rvn-rds mapped_inputs: - default: <> @@ -137,8 +133,7 @@ module: label: Aurora cluster required: false show_when: - target_type: db_cluster - target_source: module + target_type: aurora_module type: $ref:rvn-aurora mapped_inputs: - default: <> @@ -166,7 +161,6 @@ module: required: true show_when: target_type: db_instance - target_source: identifier type: string - description: Identifier of the Aurora cluster to register as the proxy target. id: db_cluster_identifier @@ -175,7 +169,6 @@ module: required: true show_when: target_type: db_cluster - target_source: identifier type: string - id: section_auth label: Authentication @@ -332,8 +325,8 @@ module: allowed_security_group_ids: << module.input.allowed_security_group_ids >> auth: '<< map(len(module.input.auth_secret_arns) > 0 ? module.input.auth_secret_arns : ((module.input.rds_master_user_secret_arn || module.input.aurora_master_user_secret_arn) ? [module.input.rds_master_user_secret_arn || module.input.aurora_master_user_secret_arn] : []), {"secret_arn": #, "iam_auth": module.input.iam_auth_enabled ? "REQUIRED" : "DISABLED"}) >>' connection_borrow_timeout: << module.input.connection_borrow_timeout >> - db_cluster_identifier: '<< module.input.target_type == "db_cluster" ? (module.input.aurora_cluster_identifier || module.input.db_cluster_identifier) : null >>' - db_instance_identifier: '<< module.input.target_type == "db_instance" ? (module.input.rds_db_instance_identifier || module.input.db_instance_identifier) : null >>' + db_cluster_identifier: << module.input.aurora_cluster_identifier || module.input.db_cluster_identifier || null >> + db_instance_identifier: << module.input.rds_db_instance_identifier || module.input.db_instance_identifier || null >> debug_logging_enabled: << module.input.debug_logging_enabled >> engine_family: << module.input.engine_family >> iam_role_arn: << module.input.iam_role_arn >> @@ -343,7 +336,7 @@ module: max_connections_percent: << module.input.max_connections_percent >> max_idle_connections_percent: << module.input.max_idle_connections_percent >> name: << module.input.name >> - port: << module.input.port || module.input.rds_port || module.input.aurora_port >> + port: << module.input.port || module.input.rds_port || module.input.aurora_port || null >> region: << module.input.aws_region >> secret_kms_key_arns: << module.input.secret_kms_key_arns >> security_group_creation_enabled: << module.input.security_group_creation_enabled >> @@ -403,12 +396,14 @@ module: ## Target database - Target type selects whether the proxy registers an RDS instance or an Aurora cluster. Target source selects where the target comes from: + Target type selects the database this proxy pools connections for: - | Target source | Behavior | + | Target type | Behavior | | --- | --- | - | Ravion database module | Select an existing RDS or Aurora module. Ravion reads its database identifier, managed master user secret, and port from that module's stack outputs. | - | Database identifier | Enter the identifier of a database managed outside Ravion, along with its auth secret ARNs. | + | RDS module | Select an existing RDS module. Ravion reads its instance identifier, managed master user secret, and port from that module's stack outputs. | + | Aurora module | Select an existing Aurora module. Ravion reads its cluster identifier, managed master user secret, and port from that module's stack outputs. | + | RDS instance identifier | Enter the identifier of an RDS instance managed outside Ravion, along with its auth secret ARNs. | + | Aurora cluster identifier | Enter the identifier of an Aurora cluster managed outside Ravion, along with its auth secret ARNs. | When a Ravion module supplies the target, add this proxy's security group ID to that module's allowed security groups. The proxy connects to the database through the database security group, and the database module owns those ingress rules. @@ -442,8 +437,7 @@ module: | VPC network | Yes | None | Supplies AWS account, region, VPC ID, and private subnet IDs. | | Name slug | Yes | Project and environment given IDs + proxy | Proxy name and prefix for related resources. | | Engine family | Yes | PostgreSQL | PostgreSQL, MySQL / MariaDB, or SQL Server. Must match the target database engine. | - | Target type | Yes | RDS instance | Register an RDS instance or an Aurora cluster as the proxy target. | - | Target source | Yes | Ravion database module | Select an existing RDS or Aurora module, or enter a database identifier. | + | Target type | Yes | RDS module | Select an existing RDS or Aurora module, or enter a database identifier. | | RDS database / Aurora cluster | Yes for module targets | None | Supplies the database identifier, master user secret, and port. | | DB instance / cluster identifier | Yes for identifier targets | None | Identifier of a database managed outside Ravion. | | Auth secret ARNs | No | Blank | Secrets Manager secrets with database credentials. Required when the target does not provide a managed master user secret. | From 0ca58ed17234c556ec2bc15645eb4ff1fbd1773f Mon Sep 17 00:00:00 2001 From: Mina Abadir Date: Sat, 8 Aug 2026 15:28:29 -0400 Subject: [PATCH 7/7] fix: drop RDS Proxy manual target identifier inputs --- .../rds-proxy/rvn-rds-proxy-definition.yml | 67 ++++++------------- 1 file changed, 21 insertions(+), 46 deletions(-) diff --git a/database/rds-proxy/rvn-rds-proxy-definition.yml b/database/rds-proxy/rvn-rds-proxy-definition.yml index e69e5038..c7987afb 100644 --- a/database/rds-proxy/rvn-rds-proxy-definition.yml +++ b/database/rds-proxy/rvn-rds-proxy-definition.yml @@ -83,47 +83,41 @@ module: - id: section_target label: Target database type: section - - default: rds_module - description: Database this proxy pools connections for. + - default: db_instance + description: Register an RDS instance or an Aurora cluster as the proxy target. id: target_type label: Target type required: true type: string values: - - description: Use an existing Ravion RDS module as the proxy target. - label: RDS module - value: rds_module - - description: Use an existing Ravion Aurora module as the proxy target. - label: Aurora module - value: aurora_module - - description: Enter the identifier of an RDS instance managed outside Ravion. - label: RDS instance identifier + - label: RDS instance value: db_instance - - description: Enter the identifier of an Aurora cluster managed outside Ravion. - label: Aurora cluster identifier + - label: Aurora cluster value: db_cluster - description: RDS database module to register as the proxy target. Add this proxy's security group to that module's allowed security groups so the database accepts proxy connections. id: rds_database label: RDS database required: false show_when: - target_type: rds_module + target_type: db_instance type: $ref:rvn-rds mapped_inputs: - default: <> - description: Instance identifier from the selected RDS module. + description: Instance identifier from the selected RDS module, or the identifier of an existing RDS instance. id: rds_db_instance_identifier label: DB instance identifier + placeholder: my-db required: true type: string - default: <> description: Managed master user secret from the selected RDS module. Blank when that database does not manage its password in Secrets Manager; add auth secret ARNs instead. id: rds_master_user_secret_arn label: Master user secret ARN + placeholder: arn:aws:secretsmanager:... required: false type: string - default: <> - description: Database port from the selected RDS module. + description: Database port from the selected RDS module. Leave blank to use the engine family default port. id: rds_port label: Database port required: false @@ -133,43 +127,29 @@ module: label: Aurora cluster required: false show_when: - target_type: aurora_module + target_type: db_cluster type: $ref:rvn-aurora mapped_inputs: - default: <> - description: Cluster identifier from the selected Aurora module. + description: Cluster identifier from the selected Aurora module, or the identifier of an existing Aurora cluster. id: aurora_cluster_identifier label: DB cluster identifier + placeholder: my-aurora required: true type: string - default: <> description: Managed master user secret from the selected Aurora module. Blank when that cluster does not manage its password in Secrets Manager; add auth secret ARNs instead. id: aurora_master_user_secret_arn label: Master user secret ARN + placeholder: arn:aws:secretsmanager:... required: false type: string - default: <> - description: Cluster port from the selected Aurora module. + description: Cluster port from the selected Aurora module. Leave blank to use the engine family default port. id: aurora_port label: Database port required: false type: number - - description: Identifier of the RDS instance to register as the proxy target. - id: db_instance_identifier - label: DB instance identifier - placeholder: my-db - required: true - show_when: - target_type: db_instance - type: string - - description: Identifier of the Aurora cluster to register as the proxy target. - id: db_cluster_identifier - label: DB cluster identifier - placeholder: my-aurora - required: true - show_when: - target_type: db_cluster - type: string - id: section_auth label: Authentication type: section @@ -325,8 +305,8 @@ module: allowed_security_group_ids: << module.input.allowed_security_group_ids >> auth: '<< map(len(module.input.auth_secret_arns) > 0 ? module.input.auth_secret_arns : ((module.input.rds_master_user_secret_arn || module.input.aurora_master_user_secret_arn) ? [module.input.rds_master_user_secret_arn || module.input.aurora_master_user_secret_arn] : []), {"secret_arn": #, "iam_auth": module.input.iam_auth_enabled ? "REQUIRED" : "DISABLED"}) >>' connection_borrow_timeout: << module.input.connection_borrow_timeout >> - db_cluster_identifier: << module.input.aurora_cluster_identifier || module.input.db_cluster_identifier || null >> - db_instance_identifier: << module.input.rds_db_instance_identifier || module.input.db_instance_identifier || null >> + db_cluster_identifier: << module.input.aurora_cluster_identifier || null >> + db_instance_identifier: << module.input.rds_db_instance_identifier || null >> debug_logging_enabled: << module.input.debug_logging_enabled >> engine_family: << module.input.engine_family >> iam_role_arn: << module.input.iam_role_arn >> @@ -396,14 +376,9 @@ module: ## Target database - Target type selects the database this proxy pools connections for: + Target type selects whether the proxy registers an RDS instance or an Aurora cluster, and shows the matching database module selector. - | Target type | Behavior | - | --- | --- | - | RDS module | Select an existing RDS module. Ravion reads its instance identifier, managed master user secret, and port from that module's stack outputs. | - | Aurora module | Select an existing Aurora module. Ravion reads its cluster identifier, managed master user secret, and port from that module's stack outputs. | - | RDS instance identifier | Enter the identifier of an RDS instance managed outside Ravion, along with its auth secret ARNs. | - | Aurora cluster identifier | Enter the identifier of an Aurora cluster managed outside Ravion, along with its auth secret ARNs. | + Selecting an RDS or Aurora module fills the database identifier, managed master user secret, and port from that module's stack outputs. For a database managed outside Ravion, leave the module unselected and enter the identifier, and any auth secret ARNs, directly. When a Ravion module supplies the target, add this proxy's security group ID to that module's allowed security groups. The proxy connects to the database through the database security group, and the database module owns those ingress rules. @@ -437,9 +412,9 @@ module: | VPC network | Yes | None | Supplies AWS account, region, VPC ID, and private subnet IDs. | | Name slug | Yes | Project and environment given IDs + proxy | Proxy name and prefix for related resources. | | Engine family | Yes | PostgreSQL | PostgreSQL, MySQL / MariaDB, or SQL Server. Must match the target database engine. | - | Target type | Yes | RDS module | Select an existing RDS or Aurora module, or enter a database identifier. | - | RDS database / Aurora cluster | Yes for module targets | None | Supplies the database identifier, master user secret, and port. | - | DB instance / cluster identifier | Yes for identifier targets | None | Identifier of a database managed outside Ravion. | + | Target type | Yes | RDS instance | Register an RDS instance or an Aurora cluster as the proxy target. | + | RDS database / Aurora cluster | No | None | Fills the database identifier, master user secret, and port from the selected module. | + | DB instance / cluster identifier | Yes | None | Identifier of the target database. | | Auth secret ARNs | No | Blank | Secrets Manager secrets with database credentials. Required when the target does not provide a managed master user secret. | | Require TLS | No | true | Requires TLS for client connections. | | IAM authentication | No | false | Requires IAM auth for client connections. |