Skip to content

Latest commit

Β 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🟧 AWS RDS Proxy Terraform Module

A secure-by-default Amazon RDS Proxy β€” TLS required in transit, IAM client auth on by default, debug logging off, private-subnet-only β€” that fronts an RDS instance or Aurora cluster with a managed connection pool, its default target group, target registration, and optional read/write-split endpoints, all from a single composite call. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • πŸ”Œ Provisions an Amazon RDS Proxy (aws_db_proxy) β€” AWS's fully-managed database connection pooler β€” as the keystone resource, smoothing connection storms from serverless/containerized fleets and adding failover speed-up in front of RDS and Aurora.
  • πŸŽ›οΈ Owns the proxy's default target group (aws_db_proxy_default_target_group) for connection-pool tuning β€” max connections percent, borrow timeout, idle reaping, init query, session-pinning filters.
  • 🎯 Registers the backing database (aws_db_proxy_target) β€” an RDS instance or an Aurora cluster (mutually exclusive) β€” via a churn-free for_each, or lets you register out of band by leaving both identifiers null.
  • πŸͺ’ Creates additional proxy endpoints (aws_db_proxy_endpoint) as a for_each map keyed by a stable caller name, so you can carve READ_ONLY endpoints fanned out to Aurora readers for read/write splitting.
  • πŸ”’ Secure by default: require_tls = true, per-user iam_auth = "REQUIRED", debug_logging = false, a conservative 1800s idle timeout, and private subnets only β€” there is no public option.
  • πŸ”‘ Credentials are never module inputs. The proxy reads {username, password} at runtime from AWS Secrets Manager using the caller-supplied IAM role β€” nothing secret ever lands in Terraform state.

πŸ’‘ Why it matters: the proxy sits directly in the path of PII under privacy-regulation. A proxy that accepts plaintext connections or skips IAM auth widens the blast radius far beyond the convenience saved β€” so this module ships locked down and makes you opt out explicitly.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


πŸ—ΊοΈ Where this fits in the family

flowchart LR
 VPC["terraform-aws-vpc"]
 SG["terraform-aws-security-group"]
 IAM["terraform-aws-iam-role"]
 SM["terraform-aws-secrets-manager"]
 KMS["terraform-aws-kms"]
 RDS["terraform-aws-rds"]
 AUR["terraform-aws-rds-aurora"]
 PROXY["terraform-aws-rds-proxy"]
 APP["Application tier<br/>(ECS / EKS / Lambda / EC2)"]

 VPC -->|vpc_subnet_ids| PROXY
 SG -->|vpc_security_group_ids| PROXY
 IAM -->|role_arn| PROXY
 SM -->|auth secret_arn| PROXY
 KMS -.->|encrypts secret| SM
 RDS -->|db_instance_identifier| PROXY
 AUR -->|db_cluster_identifier| PROXY
 PROXY -->|endpoint / proxy_endpoints| APP

 style PROXY fill:#FF9900,color:#fff
Loading

RDS Proxy sits at the data-access tier β€” downstream of the networking, security-group, IAM, and Secrets Manager foundations, in front of the terraform-aws-rds / terraform-aws-rds-aurora backing database, and upstream of the application fleet that connects to its endpoints instead of to the database directly.


🧬 What this module builds

flowchart TD
 subgraph caller["Caller-supplied (by reference)"]
 SUBNETS["vpc_subnet_ids<br/>(terraform-aws-vpc)"]
 SGS["vpc_security_group_ids<br/>(terraform-aws-security-group)"]
 ROLE["role_arn<br/>(terraform-aws-iam-role)"]
 SECRET["auth[*].secret_arn<br/>(terraform-aws-secrets-manager)"]
 DB["db_instance_identifier /<br/>db_cluster_identifier<br/>(terraform-aws-rds / _aurora)"]
 end

 subgraph mod["terraform-aws-rds-proxy"]
 P["aws_db_proxy.this<br/>keystone β€” TLS required, IAM auth"]
 TG["aws_db_proxy_default_target_group.this<br/>connection-pool config"]
 T["aws_db_proxy_target.this<br/>for_each β€” registers backing DB"]
 EP["aws_db_proxy_endpoint.this<br/>for_each β€” read/write split"]
 end

 SUBNETS --> P
 SGS --> P
 ROLE --> P
 SECRET --> P
 P --> TG
 TG --> T
 DB --> T
 P --> EP

 style P fill:#FF9900,color:#fff
Loading
Resource Role
aws_db_proxy.this Keystone β€” the proxy itself: subnets, SGs, TLS, auth entries, idle timeout
aws_db_proxy_default_target_group.this Connection-pool config (max connections %, borrow timeout, init query, pinning filters)
aws_db_proxy_target.this Registers the backing RDS instance or Aurora cluster (for_each, 0 or 1)
aws_db_proxy_endpoint.this Optional additional read/write or read-only endpoints (for_each over var.endpoints)

βœ… Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws >= 6.0, < 7.0

No provider {} block is declared inside the module β€” it inherits the caller's configured provider (and credential chain / Region). See the AWS Prerequisites Region note below.


πŸ”‘ Required IAM Permissions

RDS Proxy is managed through the rds: IAM action namespace (it shares the RDS control plane). The two critical entries are iam:PassRole (to hand the proxy its Secrets-reading role) and the proxy role's runtime secretsmanager:GetSecretValue. The Terraform identity needs (least-privilege):

Action Required for Notes
rds:CreateDBProxy, rds:DeleteDBProxy, rds:ModifyDBProxy, rds:DescribeDBProxies Proxy lifecycle Keystone resource
rds:ModifyDBProxyTargetGroup, rds:DescribeDBProxyTargetGroups Default target group Connection-pool config (created implicitly with the proxy)
rds:RegisterDBProxyTargets, rds:DeregisterDBProxyTargets, rds:DescribeDBProxyTargets Target registration Only when db_instance_identifier / db_cluster_identifier is set
rds:CreateDBProxyEndpoint, rds:DeleteDBProxyEndpoint, rds:ModifyDBProxyEndpoint, rds:DescribeDBProxyEndpoints Extra endpoints One per endpoints map entry
rds:AddTagsToResource, rds:RemoveTagsFromResource, rds:ListTagsForResource Tagging All taggable resources
iam:PassRole Pass the proxy's IAM role (role_arn) to rds.amazonaws.com Critical. Scope to the specific role ARN
secretsmanager:GetSecretValue, secretsmanager:DescribeSecret Reading the credential secret(s) Needed by the proxy role at runtime, on each auth[*].secret_arn
kms:Decrypt Decrypting a CMK-encrypted secret Granted to the proxy role, only when the secret uses a customer-managed key
ec2:CreateNetworkInterface, ec2:DeleteNetworkInterface, ec2:DescribeNetworkInterfaces ENIs the proxy plants in your subnets Proxy-managed; describe is typically already present

⚠️ secretsmanager:GetSecretValue and kms:Decrypt belong to the proxy's IAM role (the principal rds.amazonaws.com assumes), not to the Terraform deploy identity. The Terraform identity only needs iam:PassRole on that role. Scope resource ARNs to arn:aws:rds:<region>:<account>:db-proxy:* and the specific secret/role ARNs rather than "*" where governance allows.


πŸ“‹ AWS Prerequisites

  • No dedicated service-linked role. RDS Proxy does not use an SLR β€” it authenticates to Secrets Manager via the caller-supplied role_arn. That IAM role must trust rds.amazonaws.com and be allowed secretsmanager:GetSecretValue (plus kms:Decrypt for a CMK-encrypted secret) on every auth[*].secret_arn. Wire it from terraform-aws-iam-role. See Setting up network prerequisites and adding an IAM role and Configuring IAM authentication for RDS Proxy.
  • Credential secret. Each auth entry references a Secrets Manager secret holding the database user's { "username": "...", "password": "..." } JSON. Wire from terraform-aws-secrets-manager. The proxy reads it at runtime β€” it is never a module variable.
  • Backing database, same VPC. The RDS instance or Aurora cluster you register must already exist, be in the same VPC as the proxy, and be reachable on its DB port. RDS Proxy supports specific engines/versions only (Aurora MySQL/PostgreSQL, RDS MySQL/MariaDB/PostgreSQL/SQL Server) β€” confirm with RDS Proxy region and version availability.
  • Networking. Provide subnets in at least two Availability Zones (vpc_subnet_ids, β‰₯ 2 β€” enforced by validation). The proxy plants ENIs into those subnets. Use AZs that support DB proxies β€” subnets in unsupported AZs can produce perpetual plan drift. The security group must permit the DB port from clients.
  • Region model. The module relies on provider inheritance β€” there is no region variable. The caller's provider block (or alias) sets the Region. RDS Proxy is a regional service β€” none of the us-east-1 global-service rules (CloudFront / WAF / ACM) apply.
  • Service quotas (Quotas and limitations for RDS Proxy): default soft limit of 20 DB proxies per Region (raisable via Service Quotas), with per-proxy limits on target groups, endpoints, and registered targets. Plan endpoint fan-out within those limits.

πŸ“ Module Structure

terraform-aws-rds-proxy/
β”œβ”€β”€ providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); no provider{} block
β”œβ”€β”€ variables.tf # typed inputs: identity β†’ required β†’ optional β†’ tags β†’ timeouts
β”œβ”€β”€ main.tf # proxy (this), default target group, target registration, extra endpoints
β”œβ”€β”€ outputs.tf # id + arn, endpoint(s), target, target-group + endpoint maps, tags_all
β”œβ”€β”€ SCOPE.md # boundary, IAM, prerequisites, gotchas, secure defaults
└── README.md # this file

βš™οΈ Quick Start

The smallest working call β€” networking, security, IAM, the credential secret, and the backing database all wired from upstream modules:

module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-core-proxy"
  engine_family = "POSTGRESQL"          # FORCE-NEW
  role_arn      = module.proxy_role.arn # trusts rds.amazonaws.com, reads the secret

  vpc_subnet_ids         = module.vpc.private_subnet_ids # >= 2 AZs
  vpc_security_group_ids = [module.proxy_sg.id]

  auth = [{
    secret_arn = module.db_secret.arn # {username, password} in Secrets Manager
    # iam_auth defaults to "REQUIRED" (secure baseline)
  }]

  db_instance_identifier = module.rds.id # register the backing RDS instance

  tags = {
    Environment = "prod"
    DataClass   = "PII"
    CostCenter  = "platform-data"
  }
}

Everything not shown inherits the secure baseline: TLS required, IAM client auth required, debug logging off, 1800s idle timeout, private subnets only.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
role_arn string (IAM role ARN) terraform-aws-iam-role
auth[*].secret_arn string (Secrets Manager secret ARN) terraform-aws-secrets-manager
vpc_subnet_ids list(string) (β‰₯ 2 AZs) terraform-aws-vpc
vpc_security_group_ids list(string) terraform-aws-security-group
db_instance_identifier string (optional) terraform-aws-rds
db_cluster_identifier string (optional) terraform-aws-rds-aurora
(CMK encrypting the secret) by reference via the secret terraform-aws-kms

Emits

Output Description Consumed by
id Proxy id (name / ARN per the provider) references
arn Proxy ARN β€” cross-resource reference type IAM policies, monitoring, AWS Backup
name Proxy name CLI / console
endpoint Default proxy endpoint host application connection string
engine_family Engine family the proxy speaks wiring / validation
default_target_group_arn / default_target_group_name Default target group references
target Registered backing target (endpoint/port/rds_resource_id/type), or null monitoring, DR tooling
proxy_endpoints Map of extra endpoint key β†’ host read/write splitting
proxy_endpoint_arns / proxy_endpoint_ids Maps of extra endpoint key β†’ ARN / id references
tags_all All tags incl. provider default_tags governance / audit

πŸ“š Example Library

1 Β· Minimal proxy fronting an RDS instance (secure defaults)
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-min"
  engine_family = "MYSQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]

  auth = [{ secret_arn = module.db_secret.arn }]

  db_instance_identifier = module.rds.id
}
2 Β· Aurora cluster target + read/write-split endpoints
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-aurora"
  engine_family = "POSTGRESQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]

  auth = [{ secret_arn = module.db_secret.arn }]

  db_cluster_identifier = module.aurora.cluster_identifier # Aurora cluster, not an instance

  endpoints = {
    readonly = { target_role = "READ_ONLY" } # READ_ONLY requires an Aurora cluster target
  }
}
# module.rds_proxy.endpoint => writer (default RW) endpoint
# module.rds_proxy.proxy_endpoints.readonly => reader endpoint

target_role = "READ_ONLY" is only valid against an Aurora cluster target β€” an RDS instance has no reader role.

3 Β· Tags (merge with provider default_tags)
# Provider-level default_tags is the CALLER's concern (root module / pipeline):
provider "aws" {
  default_tags { tags = { ManagedBy = "terraform", Org = "" } }
}

module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-tagged"
  engine_family = "MYSQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  auth                   = [{ secret_arn = module.db_secret.arn }]
  db_instance_identifier = module.rds.id

  tags = {
    Environment = "prod"
    DataClass   = "PII"
    Org         = "-Data" # resource tag wins over default_tags on key conflict
  }
}
# module.rds_proxy.tags_all => { ManagedBy, Org=-Data, Environment, DataClass }
4 Β· Customer-managed KMS (CMK) for the credential secret
# The CMK encrypts the SECRET β€” the proxy module references the secret, not the key.
# The proxy ROLE must be allowed kms:Decrypt on this key.
module "kms" {
  source      = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  description = "CMK for RDS Proxy credential secret"
  alias       = "alias/casey-proxy-secret"
}

module "db_secret" {
  source      = "git::https://github.com/microsoftexpert/terraform-aws-secrets-manager?ref=v1.0.0"
  name        = "casey/proxy/db-creds"
  kms_key_arn = module.kms.arn # CMK-encrypted secret
  # secret_string carries {"username":"...","password":"..."}
}

module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-cmk"
  engine_family = "POSTGRESQL"
  role_arn      = module.proxy_role.arn # role policy includes kms:Decrypt on module.kms.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  auth                   = [{ secret_arn = module.db_secret.arn }]
  db_instance_identifier = module.rds.id
}
5 Β· Connection-pool tuning (default target group)
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-pool"
  engine_family = "MYSQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  auth                   = [{ secret_arn = module.db_secret.arn }]
  db_instance_identifier = module.rds.id

  connection_pool_config = {
    max_connections_percent      = 75
    max_idle_connections_percent = 50
    connection_borrow_timeout    = 120
    init_query                   = "SET time_zone = 'UTC'"
    session_pinning_filters      = ["EXCLUDE_VARIABLE_SETS"] # MySQL only
  }
}

EXCLUDE_VARIABLE_SETS is the only valid pinning filter, and it applies to MySQL engines only.

6 Β· Multiple auth entries (several DB users / secrets)
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-multiuser"
  engine_family = "POSTGRESQL"
  role_arn      = module.proxy_role.arn # must read BOTH secrets

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  db_cluster_identifier  = module.aurora.cluster_identifier

  auth = [
    {
      secret_arn  = module.app_secret.arn
      username    = "app_rw"
      description = "Application read/write user"
    },
    {
      secret_arn  = module.report_secret.arn
      username    = "report_ro"
      description = "Reporting read-only user"
    },
  ]
}
7 Β· IPv6 / dual-stack endpoint
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-ipv6"
  engine_family = "MYSQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.ipv6_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  auth                   = [{ secret_arn = module.db_secret.arn }]
  db_instance_identifier = module.rds.id

  endpoint_network_type          = "IPV6" # subnets must be IPv6-only
  target_connection_network_type = "IPV6"
}
8 Β· Custom idle timeout + provider-default auth scheme (IAM)
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-iam"
  engine_family = "POSTGRESQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  auth                   = [{ secret_arn = module.db_secret.arn, iam_auth = "REQUIRED" }]
  db_instance_identifier = module.rds.id

  default_auth_scheme = "IAM_AUTH" # default the proxy to IAM auth
  idle_client_timeout = 900        # 15 minutes
}
9 Β· Per-endpoint subnets, security groups, and tags
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-endpoints"
  engine_family = "POSTGRESQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  auth                   = [{ secret_arn = module.db_secret.arn }]
  db_cluster_identifier  = module.aurora.cluster_identifier

  endpoints = {
    analytics-ro = {
      target_role            = "READ_ONLY"
      vpc_subnet_ids         = module.vpc.analytics_subnet_ids
      vpc_security_group_ids = [module.analytics_sg.id]
      tags                   = { Team = "analytics" }
    }
  }
}
10 Β· Register the target out of band (proxy only)
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-notarget"
  engine_family = "MYSQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  auth                   = [{ secret_arn = module.db_secret.arn }]

  # Both db_instance_identifier and db_cluster_identifier omitted β†’
  # no aws_db_proxy_target created; register the backing DB elsewhere.
}

Leaving both identifiers null skips target registration entirely (target output is null).

11 Β· SQL Server engine family
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-mssql"
  engine_family = "SQLSERVER"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  db_instance_identifier = module.rds_mssql.id

  auth = [{
    secret_arn                = module.db_secret.arn
    client_password_auth_type = "SQL_SERVER_AUTHENTICATION"
  }]
}
12 Β· Operation timeouts
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-timeouts"
  engine_family = "MYSQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  auth                   = [{ secret_arn = module.db_secret.arn }]
  db_instance_identifier = module.rds.id

  timeouts = {
    create = "30m"
    update = "30m"
    delete = "60m"
  }
}
13 Β· Disposable dev proxy (secure defaults relaxed)
module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-proxy-dev"
  engine_family = "MYSQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]
  db_instance_identifier = module.rds_dev.id

  # OPT-OUTS β€” dev only, never in prod / PII:
  require_tls   = false # plaintext client connections (discouraged)
  debug_logging = true  # logs SQL text β€” can expose PII
  auth          = [{ secret_arn = module.db_secret.arn, iam_auth = "DISABLED" }]

  tags = { Environment = "dev", DataClass = "synthetic" }
}

Even disposable proxies still live in private subnets only β€” there is no public exposure to relax.

14 Β· 🏁 End-to-end composition (VPC β†’ SG β†’ IAM β†’ KMS β†’ Secret β†’ RDS β†’ Proxy)
module "vpc" {
  source   = "git::https://github.com/microsoftexpert/terraform-aws-vpc?ref=v1.0.0"
  name     = "casey-data"
  vpc_cidr = "10.40.0.0/16"
  #... produces private_subnet_ids across >= 2 AZs
}

module "kms" {
  source      = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  description = "CMK for RDS Proxy credential secret"
  alias       = "alias/casey-proxy"
}

module "db_secret" {
  source      = "git::https://github.com/microsoftexpert/terraform-aws-secrets-manager?ref=v1.0.0"
  name        = "casey/proxy/db-creds"
  kms_key_arn = module.kms.arn
  # secret_string => {"username":"caseyadmin","password":"..."}
}

module "proxy_role" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"
  name   = "casey-rds-proxy-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "rds.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })

  inline_policies = {
    read-secret = jsonencode({
      Version = "2012-10-17"
      Statement = [
        { Effect = "Allow", Action = ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"], Resource = module.db_secret.arn },
        { Effect = "Allow", Action = ["kms:Decrypt"], Resource = module.kms.arn },
      ]
    })
  }
}

module "proxy_sg" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-security-group?ref=v1.0.0"
  name   = "casey-proxy-sg"
  vpc_id = module.vpc.id

  ingress_rules = {
    pg = {
      from_port                    = 5432
      to_port                      = 5432
      ip_protocol                  = "tcp"
      referenced_security_group_id = module.app_sg.id # app tier only
      description                  = "Postgres from application tier"
    }
  }
}

module "rds" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds?ref=v1.0.0"
  #... a Postgres instance in module.vpc, reachable by module.proxy_sg
}

module "rds_proxy" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-rds-proxy?ref=v1.0.0"

  name          = "casey-core-proxy"
  engine_family = "POSTGRESQL"
  role_arn      = module.proxy_role.arn

  vpc_subnet_ids         = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.proxy_sg.id]

  auth                   = [{ secret_arn = module.db_secret.arn }] # iam_auth REQUIRED by default
  db_instance_identifier = module.rds.id

  connection_pool_config = { max_connections_percent = 80 }

  tags = {
    Environment = "prod"
    DataClass   = "PII"
    Compliance  = "privacy-regulation"
  }
}

# The application points its connection string at the proxy, not the database:
output "proxy_endpoint" { value = module.rds_proxy.endpoint }

πŸ“₯ Inputs

Name Type Default Description
name string β€” (required) Proxy identifier. FORCE-NEW. Letter-led, ASCII letters/digits/hyphens
engine_family string β€” (required) MYSQL / POSTGRESQL / SQLSERVER. FORCE-NEW
role_arn string β€” (required) IAM role the proxy assumes to read the secret (trusts rds.amazonaws.com)
vpc_subnet_ids list(string) β€” (required) Proxy subnets, β‰₯ 2 AZs. FORCE-NEW
auth list(object) β€” (required) Auth entries β€” at least one; each references a secret_arn
vpc_security_group_ids list(string) [] Security groups to attach
endpoint_network_type string "IPV4" IPV4 / IPV6 / DUAL
target_connection_network_type string "IPV4" IPV4 / IPV6
require_tls bool true Require TLS for client connections (secure default)
default_auth_scheme string "NONE" NONE / IAM_AUTH default scheme
idle_client_timeout number 1800 Idle seconds before disconnect (1–28800)
debug_logging bool false Log SQL statement detail (can expose PII)
connection_pool_config object {} Default target group pool tuning
db_instance_identifier string null Backing RDS instance to register. FORCE-NEW on registration
db_cluster_identifier string null Backing Aurora cluster to register. FORCE-NEW on registration
endpoints map(object) {} Additional proxy endpoints keyed by stable name
tags map(string) {} Tags merged onto all taggable resources
timeouts object {} create / update / delete timeouts

ℹ️ Full type schemas and per-field descriptions live in variables.tf;


🧾 Outputs

See the Emits table above. Primary outputs are id and arn; endpoint is the host you put in the application connection string; target carries the registered backing-database details (or null when none is registered); proxy_endpoints / proxy_endpoint_arns / proxy_endpoint_ids are maps keyed by your endpoints keys; tags_all reflects the merged tag set. No secrets are emitted.


🧠 Architecture Notes

  • ARN / ID formats.
  • Proxy arn: arn:aws:rds:<region>:<account>:db-proxy:prx-<hash>. The provider's id for aws_db_proxy is the proxy name.
  • Proxy endpoint arn: arn:aws:rds:<region>:<account>:db-proxy-endpoint:prx-endpoint-<hash>; its id is DB-PROXY-NAME/DB-PROXY-ENDPOINT-NAME.
  • target.rds_resource_id is the backing database's stable resource id (db-XXXX / cluster-XXXX) β€” use it (not the name) for IAM rds-db:connect policy conditions when clients use IAM database auth.
  • rds: namespace. RDS Proxy rides the RDS control plane β€” every API call, ARN service segment, and IAM action uses rds, not a proxy namespace. Write least-privilege policies accordingly.
  • FORCE-NEW (immutable) fields. name, engine_family, and vpc_subnet_ids recreate the proxy. db_instance_identifier / db_cluster_identifier force replacement of the target registration (and the two are mutually exclusive β€” a validation blocks setting both). The module's aws_db_proxy_default_target_group and aws_db_proxy_target carry replace_triggered_by = [aws_db_proxy.this.id] so they track a proxy replacement cleanly.
  • tags ↔ tags_all ↔ default_tags. The module sets only resource-level tags (and merges merge(var.tags, each.value.tags) onto each extra endpoint). The provider's default_tags is the caller's concern (never set inside a module). On a key collision, the resource tag wins. tags_all (output) is the computed union AWS actually applied β€” use it for drift checks and audit.
  • Credentials never in state. Unlike a database module's master password, the proxy holds no secret material. It references auth[*].secret_arn and reads {username, password} at runtime via role_arn. Nothing sensitive is a variable or an output.
  • Eventual consistency / ordering. Target registration requires the proxy and the backing DB to be available β€” register after the DB is healthy or you can hit transient DBProxyTargetNotFound/InvalidDBInstanceState errors. Endpoint and target creation is asynchronous; endpoint is a stable DNS name that follows failover.
  • Destroy ordering. Terraform tears down extra endpoints and the target registration before the proxy, then the proxy releases its ENIs from your subnets. ENI cleanup is asynchronous on the AWS side β€” a security group or subnet that still has proxy-owned ENIs attached can briefly resist deletion until RDS reclaims them. Subnets/SGs are consumed by reference, so the module does not delete them.
  • No us-east-1 constraint. RDS Proxy is a regional service β€” none of the us-east-1 global-service rules (CloudFront / WAF / ACM) apply.

🧱 Design Principles

Secure by default; every weakening is an explicit, documented opt-out.

Posture Default How to opt out
In-transit TLS require_tls = true require_tls = false (discouraged)
Client IAM auth auth[*].iam_auth = "REQUIRED" "DISABLED" (falls back to secret-password client auth)
Debug (SQL) logging debug_logging = false true (temporary, documented β€” can expose PII in logs)
Idle client timeout idle_client_timeout = 1800 (30 min) tune 1–28800s via the variable
Public exposure private subnets only β€” no public option n/a (cannot be relaxed)
Credentials in state never β€” read at runtime from Secrets Manager n/a (no plaintext-credential path exists)

Other principles: exactly four .tf files; single keystone aws_db_proxy.this; child collections via for_each over map(object) / a keyed toset (never count); deeply-typed object schemas with optional defaults; validation {} on every closed value set (engine family, auth scheme, IAM auth, pinning filters, percent ranges, instance/cluster mutual exclusion); no credential or region variables; primary outputs id + arn; tags_all surfaced.


πŸš€ Runbook

# Validate (no credentials needed)
terraform init -backend=false
terraform validate
terraform fmt -check

# Plan / apply (requires AWS credentials + Region)
# credentials via AWS_PROFILE / SSO / OIDC; Region via the provider block
terraform plan -out tfplan
terraform apply tfplan

⚠️ Pin the module with ?ref=v1.0.0 β€” never a branch. plan/apply require a valid credential chain (profile / SSO / OIDC web-identity) and a configured Region. The module declares no provider {} block β€” supply it (and any assume_role) at the root.


πŸ§ͺ Testing

  • terraform init -backend=false && terraform validate β€” schema and reference integrity.
  • terraform fmt -check β€” canonical formatting.
  • terraform plan against a sandbox account β€” confirm secure defaults render (require_tls = true, per-user iam_auth = "REQUIRED", debug_logging = false, 1800s idle timeout) and that omitting both DB identifiers produces no target.
  • Post-apply smoke test: from a client in an allowed security group, connect to endpoint over TLS and run a trivial query (SELECT 1); confirm pooling under load and that a READ_ONLY endpoint (if defined) routes to Aurora readers.

πŸ’¬ Example Output

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

arn = "arn:aws:rds:us-east-2:123456789012:db-proxy:prx-0a1b2c3d4e5f67890"
endpoint = "casey-core-proxy.proxy-abc123.us-east-2.rds.amazonaws.com"
id = "casey-core-proxy"
proxy_endpoints = {
 "readonly" = "casey-core-proxy-readonly.endpoint.proxy-abc123.us-east-2.rds.amazonaws.com"
}
 "endpoint" = "casey-core-pg.abc123.us-east-2.rds.amazonaws.com"
 "port" = 5432
 "rds_resource_id" = "db-ABCDEFGHIJKLMNOP"
 "type" = "RDS_INSTANCE"
}

πŸ” Troubleshooting

Symptom Likely cause Fix
Tag drift on every plan default_tags overlaps a key the module also sets Drop the duplicate from one side; resource tags win β€” reconcile in the root module
AccessDenied on rds:CreateDBProxy Identity lacks the rds: actions (RDS Proxy uses rds, not a proxy namespace) Attach the Required IAM Permissions
AccessDenied / iam:PassRole on create Terraform identity can't pass role_arn to RDS Grant iam:PassRole on the specific role ARN to the deploy identity
Proxy creates but stays unavailable / can't reach secret Proxy role doesn't trust rds.amazonaws.com or lacks secretsmanager:GetSecretValue (or kms:Decrypt) Fix the role's trust policy and grant the secret/KMS permissions on each auth[*].secret_arn
DBProxyTargetNotFound / InvalidDBInstanceState on register Backing DB not yet available, or in a different VPC Register after the DB is healthy; ensure same-VPC reachability
Client connection refused / TLS handshake fails require_tls = true but client not using TLS Connect over TLS (or, dev only, set require_tls = false)
Perpetual diff on vpc_subnet_ids A subnet sits in an AZ that doesn't support DB proxies Restrict vpc_subnet_ids to supported AZs
Set only one of db_instance_identifier or db_cluster_identifier Both target identifiers supplied Pick instance or cluster β€” they're mutually exclusive
READ_ONLY endpoint rejected Endpoint target_role = "READ_ONLY" against an RDS instance READ_ONLY requires an Aurora cluster target
Security group / subnet won't delete on destroy Proxy ENIs not yet reclaimed by RDS Wait for RDS to release the ENIs, then retry the destroy
DBProxyQuotaExceeded Region soft limit (default 20 proxies) hit Raise the quota via Service Quotas
Credential-chain errors on plan/apply No profile/SSO/OIDC resolved, or wrong Region Set AWS_PROFILE / assume the role; confirm the provider Region

πŸ”— Related Docs


🧑 "Infrastructure as Code should be standardized, consistent, and secure."