DEV Community

Cover image for Building a Production Grade AWS Infrastructure Project (Part 5): RDS
KithupaG
KithupaG

Posted on

Building a Production Grade AWS Infrastructure Project (Part 5): RDS

Continuing the Build

In my previous post, I set up the security group module that acts as the network firewall for everything in the VPC. With networking and permissions sorted, the next logical piece is the database.

The architecture plan is coming together:

VPC → IAM → Security Groups → RDS → ALB → EC2 ASG → S3+CDN → Monitoring → CI/CD

The backend needs a database, and RDS is the managed choice. It's relatively self-contained once it has a subnet group and security group reference, both of which we now have.

Why RDS Now?

You might wonder why not ALB or EC2 first. The dependency chain is straightforward: RDS needs a VPC (for subnets) and a security group (for network access). Both are done. The ALB needs the VPC and SG too, but the ASG also needs the ALB target group to attach to. RDS is the leaf node in the dependency tree, nothing else depends on it being ready first, so it unblocks the database layer early.

The Module Structure

I created a dedicated modules/rds directory. The module interface is intentionally minimal:

variable "security_group" {}
variable "subnet_ids" {}
Enter fullscreen mode Exit fullscreen mode

That's it. Two inputs, the security group ID (wrapped in a list because AWS expects a list) and the private subnet IDs for the DB subnet group.

Configuring the Database Instance

First, the basics: identifier, engine, and storage.

identifier     = "postgres-db-dev"
db_name        = "notetaker"
engine         = "postgres"
engine_version = "16.3"
allocated_storage = 20
Enter fullscreen mode Exit fullscreen mode

Pin the minor version. I learned this the hard way in other projects — if you don't pin 16.3, Terraform will pick up 16.4 or 16.5 on a fresh apply, and minor version upgrades can introduce subtle behavior changes. In production, you want explicit control.

username = "postgres"
port     = 5432
Enter fullscreen mode Exit fullscreen mode

Standard PostgreSQL defaults. The username postgres is the default superuser, in production you'd create a dedicated application user with limited privileges, but for this project the default is fine.

Parameter Groups: The "Advanced Settings" Nobody Talks About

By default, AWS launches RDS with stock configuration. Parameter groups let you tune the database engine behavior. The community module I'm using (terraform-aws-modules/rds/aws) defaults to MySQL parameters, so I had to swap them out.

Original (MySQL defaults):

parameters = [
  { name = "character_set_client",  value = "utf8mb4" },
  { name = "character_set_server",  value = "utf8mb4" },
]
Enter fullscreen mode Exit fullscreen mode

Changed to PostgreSQL-appropriate logging:

parameters = [
  { name = "log_connections",   value = "1" },
  { name = "log_disconnections", value = "1" },
]
Enter fullscreen mode Exit fullscreen mode

Why these? Connection logging is invaluable for debugging connection pool exhaustion, leaked connections, or unexpected traffic patterns. These parameters are PostgreSQL-specific, MySQL uses different parameter names entirely. The module's defaults assumed MySQL because that's what the example configuration shipped with.

Key insight: Parameter groups are engine-specific. If you switch from MySQL to PostgreSQL (or upgrade major versions), you need a new parameter group family (postgres16 vs mysql8.0).

Option Groups: The Audit Plugin Situation

The module also included a MariaDB audit plugin by default:

options = [
  {
    option_name = "MARIADB_AUDIT_PLUGIN"
    option_settings = [
      { name = "SERVER_AUDIT_EVENTS",       value = "CONNECT" },
      { name = "SERVER_AUDIT_FILE_ROTATIONS", value = "37" },
    ]
  },
]
Enter fullscreen mode Exit fullscreen mode

I removed this entirely. It's a MariaDB-specific plugin. PostgreSQL has its own auditing extensions (pgaudit), but they require a different setup — you enable them via shared_preload_libraries in the parameter group, not through an option group. For this project, connection logging via parameter group is sufficient.

Lesson: Don't blindly copy example configurations. The community module's defaults are MySQL/MariaDB-centric.

Monitoring Interval: Integer, Not String

Small but frustrating gotcha:

monitoring_interval = 30  # integer, not "30"
Enter fullscreen mode Exit fullscreen mode

The Terraform provider expects a number. Passing a string causes a type mismatch error at apply time. The example in the module docs showed it as a string, which cost me a debug cycle.

Subnet Groups and Security Groups: The Wiring

This is where the module connects to the rest of the infrastructure:

vpc_security_group_ids = [var.security_group]
subnet_ids             = var.subnet_ids
Enter fullscreen mode Exit fullscreen mode

The security group variable is a string, but vpc_security_group_ids expects a list. Wrapping it in [var.security_group] satisfies the type requirement.

The subnet_ids comes from the VPC module's private subnets output — these are the subnets without a route to the internet gateway, which is exactly where a database should live.

Module Version Pinning

module "rds" {
  source  = "terraform-aws-modules/rds/aws"
  version = "6.0.0"
  # ...
}
Enter fullscreen mode Exit fullscreen mode

Always pin module versions. If you don't specify a version, Terraform pulls the latest on every init. A module update could introduce breaking changes — renamed variables, changed defaults, removed outputs, and your pipeline would fail mysteriously. Pin to a specific version (or use a version constraint like ~> 6.0) and upgrade intentionally.

Wiring It Into the Environment

In environments/dev/main.tf:

module "dev_rds" {
  source         = "../../modules/rds"
  security_group = module.dev_sg.main_sg
  subnet_ids     = module.dev_app_vpc.private_subnets
}
Enter fullscreen mode Exit fullscreen mode

The security group comes from the security group module output (main_sg), and the private subnets come from the VPC module. This is the dependency chain in action, the RDS module can't be applied until both of those exist.

What I Learned

  • Parameter groups are engine-specific. Don't copy MySQL configs for PostgreSQL.
  • Option groups ≠ parameter groups. Options are for engine features (audit plugins, TDE, etc.); parameters are for engine configuration (logging, memory, timeouts).
  • Pin module versions. Always. No exceptions.
  • Security group IDs need list wrapping. vpc_security_group_ids expects list(string), not string.
  • Minor version pinning matters. engine_version = "16.3" not "16".
  • Monitoring interval is a number. Not a string. The provider will yell at you.

What's Next: The ALB Module

With the database ready, the next piece is the Application Load Balancer. The ALB sits in front of the compute layer (EC2 Auto Scaling Group or ECS), terminates TLS, and routes traffic to healthy targets. It needs the VPC, subnets, and security group, all of which are now in place. That's coming up next.


Want to follow along? The full code is available here:

GitHub logo KithupaG / aws-production-infra

A complete AWS themed production infrastructure

CloudNotes - Production Grade AWS Infrastructure

A full-stack Note Taker application designed as a sandbox for building production-grade AWS infrastructure with Terraform. The app is fully containerized with Docker and orchestrated with Docker Compose, ready to be deployed across a high-availability AWS architecture.

Architecture

                        ┌──────────────────────────────────┐
                        │         AWS Cloud (Target)       │
                        │                                  │
                        │   Route53 ─── CloudFront ─── S3  │
                        │                    │             │
                        │                    ▼             │
                        │        ┌─────── ALB ───────┐     │
                        │        │                   │     │
                        │   ┌────▼────┐        ┌────▼────┐ │
                        │   │ EC2 ASG │        │ EC2 ASG │ │
                        │   │(Backend)│        │(Backend)│ │
                        │   └────┬────┘        └────┬────┘ │
                        │        │                  │      │
                        │        └────────┬─────────┘      │
                        │                 ▼                │
                        │           ┌──── RDS ────┐        │
                        │           │  PostgreSQL │        │
                        │           └─────────────┘        │
                        └──────────────────────────────────┘
Local (Docker Compose):
┌──────────┐    ┌────────────┐    ┌──────────────┐
│ Frontend │───▶│  Backend  │───▶│  PostgreSQL  │
│ (Nginx)  │    │ (Node.js)  │    │ (15-alpine)  │
│  :3000   │    │  :5001     │    │   :5432

Top comments (0)