Connect an App to RDS over VPC

Learn to connect an app to RDS over VPC in this AWS tutorial. Step-by-step, hands-on, with troubleshooting and next steps.

Focus: connect an app to rds over vpc

Sponsored

You've built an app, but it can't talk to your database. The psycopg2.OperationalError or MySQLdb._exceptions.OperationalError is a rite of passage for every AWS developer. The cause is almost never your code — it's the network. You haven't connected your app to RDS over the VPC properly. Security groups, subnets, and route tables feel like arcane magic until you see the simple mental model underneath. This lesson demystifies that network plumbing so you can connect any app to RDS in a secure, repeatable way.

The problem this lesson solves

Your app is in one place, and your database is in another. Even if you copy the correct hostname, port, and credentials into your connection string, the connection fails. Why?

Because AWS treats your database as a private resource. An RDS instance launched into a VPC (Virtual Private Cloud) has no public IP address by default. It lives on a network that only exists inside AWS, and your app — even if it's an EC2 instance in the same account — must be allowed into that network.

Think of the VPC as a gated community. Your RDS database is a house inside that community. The house has an address (DNS endpoint) and a front door (port 5432 for PostgreSQL, 3306 for MySQL). But you can't just knock — you need approval at the gate (security group inbound rules) and a valid path to the gate (subnet routing).

Without this lesson, you'll waste hours copy-pasting 'fixes' from Stack Overflow: rebooting the instance, checking credentials (they're fine), or even making the database public (a security nightmare). The real fix is understanding and configuring three things:

  1. Security groups — who is allowed to talk to whom.
  2. Subnets & route tables — how traffic flows to the database.
  3. Network ACLs (optional) — an extra layer of filtering at the subnet boundary.

Pro tip: If you're getting timeout errors, it's almost always a security group or subnet routing issue. If you're getting connection refused, the database is reachable but the port might be wrong or the DB isn't accepting connections.

Core concept / mental model

Let's build a simple analogy you can carry forever.

Imagine your VPC is a private apartment complex. Inside the complex, there are two types of units:

  • Public subnets — apartments with a door to the street (they have an internet gateway). Your app's EC2 instance lives here.
  • Private subnets — apartments with no street entrance (no internet gateway). Your RDS database lives here, safe from the internet.

Both units are in the same building (the VPC), so they could talk to each other over the internal hallway (the local route). But each unit has its own security guard (the security group).

The guard outside your database has a list of who may enter:

  • postgres on port 5432, but only from the app's security group — not from the world.

The guard outside your app decides who the app may talk to on the way out:

  • It can reach the database's security group on port 5432.

So connectivity is a two-way handshake of rules. Both sides must agree. The hallway (route tables) handles the path — normally the local route within the VPC makes this automatic.

Technical definitions you'll see in the AWS console:

  • VPC: a logically isolated section of the AWS cloud where you launch resources.
  • Subnet: a range of IP addresses in your VPC (e.g., 10.0.1.0/24).
  • Route table: a set of rules that determine where network traffic is directed.
  • Internet Gateway (IGW): the door to the public internet for public subnets.
  • NAT Gateway: a door for private subnets to reach the internet outbound without being reachable from outside.
  • Security Group: a virtual firewall controlling inbound/outbound traffic at the instance level.
  • Network ACL: a stateless firewall at the subnet level (optional layer).

Key insight: RDS instances are not launched into a specific subnet; they are launched into a subnet group — a set of subnets across Availability Zones. AWS automatically places the primary instance in one of those subnets and a standby in another for Multi-AZ.

How it works step by step

Let's trace the journey of a connection request from your app to RDS. This is the mental model you'll apply every time.

  1. App initiates connection — your code opens a TCP socket to the RDS endpoint (e.g., mydb.c7gxq8k4n9rk.us-east-1.rds.amazonaws.com:5432).
  2. DNS resolution — AWS Route 53 resolves that hostname to a private IP address inside your VPC (e.g., 10.0.2.45). This IP belongs to a network interface in one of your private subnets.
  3. Outbound rule check — The EC2 instance's security group (let's call it app-sg) checks its outbound rules. If there's a rule allowing traffic to db-sg on port 5432, the packet is allowed to leave the instance.
  4. Route table lookup — The subnet's route table determines where the packet goes. Since the destination IP is within your VPC's CIDR range (e.g., 10.0.0.0/16), the route is local — traffic stays inside the VPC.
  5. Inbound rule check — The RDS instance's security group (db-sg) reviews inbound rules. If there's a rule allowing postgres from app-sg (or from the app's private IP), the packet is admitted.
  6. Database accepts connection — MySQL/PostgreSQL listens on its configured port and authenticates using the credentials you provided.

Why not just make RDS public?

When you enable "Public accessibility" on RDS, it gets a public IP and sits behind an internet gateway. This works but is a security liability — your database is exposed to the whole internet, protected only by a username/password. One leaked credential and your data is gone. Staying private over VPC is the gold standard.

Hands-on walkthrough

Let's do it. I'll assume you have a VPC with at least one public and one private subnet (the default VPC is fine for practice, but let's build a custom one to see the pieces).

Step 1: Identify or create your VPC

# List your VPCs
theory: aws ec2 describe-vpcs --region us-east-1

# Get their CIDR and ID
theory: aws ec2 describe-vpcs --region us-east-1 --query 'Vpcs[*].{ID:VpcId,CIDR:CidrBlock}'

Look for the default VPC (it has IsDefault: true). If you're practicing, you can use it — it already has public and private subnets, an internet gateway, and a local route. If you want isolation, create a new VPC with a /16 CIDR like 10.1.0.0/16.

Step 2: Create your subnets

You need at least two subnets in different Availability Zones (for RDS). One can be private, one public. For simplicity, we'll keep both private — your EC2 can be in the same private subnet or in a public subnet.

# Create a public subnet
aws ec2 create-subnet --vpc-id vpc-0a1b2c3d --cidr-block 10.1.0.0/24

# Create a private subnet (for RDS)
aws ec2 create-subnet --vpc-id vpc-0a1b2c3d --cidr-block 10.1.1.0/24

Pro tip: Always design your subnets with high availability in mind. RDS requires at least two subnets in different AZs if you enable Multi-AZ.

Step 3: Create security groups

First, create a security group for your EC2 instance (the app):

aws ec2 create-security-group --group-name app-sg --description "App security group" --vpc-id vpc-0a1b2c3d

Then create one for RDS:

aws ec2 create-security-group --group-name db-sg --description "RDS security group" --vpc-id vpc-0a1b2c3d

Now, on the app security group, add an outbound rule that allows traffic to the DB security group on port 5432 (or 3306 for MySQL):

aws ec2 authorize-security-group-egress --group-id sg-app123 --protocol tcp --port 5432 --source-group sg-db456

On the DB security group, add an inbound rule that allows PostgreSQL (or MySQL) from the app security group:

aws ec2 authorize-security-group-ingress --group-id sg-db456 --protocol tcp --port 5432 --source-group sg-app123

Why --source-group instead of an IP?

Using a security group as a source means the rule is dynamic. If your EC2 instance gets a new private IP (e.g., after restart), the rule still works because AWS resolves the group membership — much better than hardcoding an IP that can change.

Step 4: Launch your RDS instance

Now create your RDS instance in the private subnets:

aws rds create-db-instance \
  --db-instance-identifier mydb \
  --db-instance-class db.t3.micro \
  --engine postgres \
  --master-username admin \
  --master-user-password YourPassword123! \
  --allocated-storage 20 \
  --vpc-security-group-ids sg-db456 \
  --db-subnet-group-name my-db-subnet-group \
  --no-publicly-accessible

Before that, create a DB subnet group — RDS needs it to know which subnets it can use:

aws rds create-db-subnet-group \
  --db-subnet-group-name my-db-subnet-group \
  --db-subnet-group-description "Subnets for RDS" \
  --subnet-ids subnet-private1 subnet-private2

Step 5: Launch your EC2 instance (the app)

Launch an EC2 instance into a public subnet (so you can SSH in for testing) with the app-sg security group:

aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t2.micro \
  --key-name your-keypair \
  --security-group-ids sg-app123 \
  --subnet-id subnet-public1

Step 6: Test the connection

SSH into your EC2 instance and try connecting to RDS using psql (install it first if needed):

sudo apt update && sudo apt install postgresql-client -y
psql "host=mydb.c7gxq8k4n9rk.us-east-1.rds.amazonaws.com port=5432 user=admin password=YourPassword123! dbname=mydb"

If it works, you'll see the psql prompt. If not, check your security groups and route tables — see Troubleshooting below.

From your local machine (for development): If you want to test from your laptop, you can either use a bastion host (SSH tunnel) or temporarily add your IP to the DB security group. Never make RDS public for production.

Compare options / when to choose what

You have a few ways to give an app access to RDS. Here's a quick comparison:

Option Pros Cons Use when
Same VPC, security group reference Secure, dynamic, easy to manage Requires app and DB in same VPC (or peered) Most common — best for EC2, Lambda same region
Same VPC, IP-based rule Simpler to trace logs IP can change, more maintenance Static IP on a private subnet, or a single instance
VPC peering Allows cross-VPC communication Adds complexity; need to update route tables & security groups Apps in different VPCs (e.g., separate accounts or environments)
Public RDS (internet accessible) Simple to configure, works from anywhere quickly Security risk, not recommended for production Prototyping only, or if absolutely no VPC access possible
RDS Proxy Reduces connection overhead, handles failover gracefully Extra cost, adds a layer High connection count, serverless apps, Lambda

Recommendation: For most production scenarios, use a security-group reference in the same VPC. If your app lives in a different VPC or account, use VPC peering. Avoid public RDS completely.

Troubleshooting & edge cases

Here are the exact errors you'll likely see and how to fix them.

Connection timed out

This means your app can't reach the database at all — packets are being dropped.

  • Check the DB security group inbound rule. Is it allowing traffic from the app's security group or IP on the right port? Common mistake: you added a rule for SSH (22) instead of PostgreSQL (5432).
  • Check the app's security group outbound rule. Outbound default is often allow all, but if you restricted it, add an allow rule for the DB port to the DB security group.
  • Check subnet route tables. If your app is in a private subnet with no route to the VPC local CIDR (rare but possible after custom route edits), fix by adding a local route to the route table.

Connection refused

This means the network path is fine, but the database isn't accepting connections (port closed or DB not listening).

  • Is the RDS instance available? Check in the console — it takes ~10 minutes to create.
  • Is the port correct? PostgreSQL uses 5432, MySQL 3306. You can see the port in the endpoint URL.
  • Is the DB inside a public subnet? If publicly-accessible is true, it has a separate path. If false, ensure you're on the VPC network.

password authentication failed

Not a network issue! Check your master username/password. If you're using an IAM user, you'd need to enable IAM authentication.

Security group reference not working across VPCs

If you peer VPCs, security group references don't work across peer connections unless you specify the full CIDR of the other VPC. Use IP-based rules for cross-VPC access.

Pro tip: Use the nc -zv <endpoint> <port> command to test if the port is open — it's faster than launching a full DB client. For example: nc -zv mydb.c7gxq8k4n9rk.us-east-1.rds.amazonaws.com 5432.

What you learned & what's next

You now understand how to connect an app to RDS over VPC — the security-group dance, subnet placement, and the mental model of network firewalls. You can configure this from scratch using the AWS CLI, and you know where to look when something goes wrong.

In the next lesson, you'll take this further and learn how to harden your RDS setup with encryption, automated backups, and multi-AZ deployments — the production-grade finish that turns a working connection into a reliable one.

Now go test that connection, and may your psql prompts always be green.

Practice recap

Create a small test VPC with one public and one private subnet. Launch an RDS PostgreSQL instance (free tier) in the private subnets and an EC2 instance in the public subnet. Configure security groups using the pattern above, then SSH into the EC2 instance and run psql to verify the connection. If it fails, walk through the troubleshooting steps until it works.

Common mistakes

  • Making RDS publicly accessible when you don't need to — it's a security risk and often a workaround that hides the real VPC misconfiguration.
  • Hardcoding an IP address in the DB security group instead of using the app's security group ID — when the IP changes, your app breaks silently.
  • Forgetting to check both inbound on the DB security group and outbound on the app security group — a one-way rule is not enough.
  • Putting RDS in a public subnet with a public IP, then wondering why it's slow or vulnerable — the traffic goes through the Internet Gateway instead of staying internal.
  • Assuming the default VPC is configured for you — it often is, but if you create a custom VPC and forget to add an internet gateway or NAT, your app can't reach anything.

Variations

  1. Use RDS Proxy to pool connections, especially with serverless apps — it simplifies security by managing IPs and adds resilience.
  2. Use VPC peering to connect an app in a different VPC — you'll need to update route tables and use IP-based security group rules.
  3. Use AWS PrivateLink to access RDS from another account or VPC without exposing the DB to the internet — more advanced but fully private.

Real-world use cases

  • A Python web app on EC2 using SQLAlchemy connects to a private RDS PostgreSQL database securely within the same VPC.
  • A microservices architecture across multiple VPCs uses VPC peering to let a backend service query an RDS MySQL database.
  • A serverless Lambda function uses RDS Proxy to manage connections to an RDS database without exposing it publicly.

Key takeaways

  • Connecting to RDS over VPC requires two-way security group rules: outbound on the app and inbound on the DB.
  • Keep RDS private — don't enable public accessibility in production; use subnet placement and security groups instead.
  • Use security group IDs as sources instead of IP addresses to avoid breakage when instances change.
  • Understand the role of subnets and route tables: RDS lives in a subnet group, and traffic stays internal via the local route.
  • Always test with nc -zv and psql to isolate network vs. database issues.
  • For cross-VPC scenarios, remember that security group references don't work across peering — use IP rules.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.