MIKAEELS

Explore the blog

AWS CDK RDS PostgreSQL Example with EC2 (TypeScript)

This AWS CDK v2 example creates a private RDS PostgreSQL database and an EC2 client in the same VPC. The TypeScript stack generates credentials in Secrets Manager and permits PostgreSQL connections from the EC2 security group on port 5432.

Since both EC2 and RDS services require a Virtual Private Cloud (VPC), we'll also cover setting up a custom VPC with appropriate subnets. Specifically, the RDS instance will reside in an ISOLATED (private) subnet, while the EC2 instance will be deployed within a PUBLIC subnet.

Scope: This is a disposable learning stack: backups and deletion protection are disabled. The revised snippets have not been deployed to AWS for this update. The original project repository provides historical context; use the corrected configuration below for this revision.

Prerequisites

  1. AWS account with appropriate permissions
  2. AWS CLI installed and configured
  3. Node.js and AWS CDK v2 installed
  4. Basic TypeScript knowledge
  5. An EC2 key pair named ec2-demo-key-pair in the deployment region, with its private key available locally

Use a sandbox account and check aws sts get-caller-identity before deployment. EC2, RDS, storage, public IPv4 addresses, and Secrets Manager can incur charges. These examples use CDK v2 APIs; preserve the generated package lock file.

Project Setup

First, create a new CDK project:

mkdir deploy-rds-ec2 cd deploy-rds-ec2 cdk init app --language typescript npm install --save-exact aws-cdk-lib@2.272.0 constructs@10.5.0

Download the complete revised stack and save it as lib/deploy-rds-ec2-stack.ts, or follow the two stages below.

The complete stack was type-checked with TypeScript 5.9.3 against the pinned packages above and synthesized locally with example account, region, Availability Zone, and SSH CIDR values. These checks do not confirm a deployment or engine availability in your account.

The code is split into two stages: keep the VPC and EC2 definitions from the first block, then insert the database definitions from the second block inside the same constructor.

Let's start by setting up our Virtual Private Cloud (VPC) and an EC2 instance. Open the file lib/deploy-rds-ec2-stack.ts and add the following implementation:

import { App, CfnOutput, Duration, RemovalPolicy, Stack, StackProps, } from "aws-cdk-lib"; import { MachineImage, Instance, InstanceClass, InstanceSize, InstanceType, IpAddresses, KeyPair, Peer, Port, SecurityGroup, SubnetType, Vpc, } from "aws-cdk-lib/aws-ec2"; import { Credentials, DatabaseInstance, DatabaseInstanceEngine, PostgresEngineVersion, } from "aws-cdk-lib/aws-rds"; export class DeployRDSEC2Stack extends Stack { constructor(scope: App, id: string, props?: StackProps) { super(scope, id, props); // create a vpc const vpc = new Vpc(this, "main-vpc", { ipAddresses: IpAddresses.cidr("10.0.0.0/16"), natGateways: 0, maxAzs: 2, subnetConfiguration: [ { name: "public-subnet-1", subnetType: SubnetType.PUBLIC, cidrMask: 24, }, { name: "isolated-subnet-1", subnetType: SubnetType.PRIVATE_ISOLATED, cidrMask: 28, }, ], }); // create a security group for the EC2 instance const ec2InstanceSG = new SecurityGroup(this, "ec2-instance-sg", { vpc, }); ec2InstanceSG.addIngressRule( Peer.ipv4(this.node.tryGetContext("sshCidr")), Port.tcp(22), "allow SSH from the configured client IPv4 CIDR" ); // importing your SSH key const keyPair = KeyPair.fromKeyPairName(this, "key-pair", "ec2-demo-key-pair"); // create the EC2 instance const ec2Instance = new Instance(this, "ec2-instance", { vpc, vpcSubnets: { subnetType: SubnetType.PUBLIC, }, securityGroup: ec2InstanceSG, instanceType: InstanceType.of( InstanceClass.BURSTABLE3, InstanceSize.MICRO ), machineImage: MachineImage.latestAmazonLinux2023(), associatePublicIpAddress: true, keyPair, }); new CfnOutput(this, "Ec2PublicIp", { value: ec2Instance.instancePublicIp, }); } }

Let's review the above code snippet:

  1. We've configured our VPC with both PUBLIC and ISOLATED subnet groups.

    A PUBLIC subnet has a route to an internet gateway. An EC2 instance also needs a public IP and appropriate security rules to communicate over the internet. This setup is suitable for resources like our EC2 instance, which we'll deploy in the PUBLIC subnet.

    In this newly created VPC, isolated subnets have no internet route. RDS uses them because this exercise only needs connections from the EC2 client within the VPC. Other private connectivity, such as VPN or peering, would require additional network configuration.

  2. We set up a Security Group specifically for our EC2 instance. Its inbound SSH rule uses the sshCidr context value supplied at deployment. Set it to your current public IPv4 address followed by /32; do not open SSH to every address.

  3. We provisioned a t3.micro EC2 instance using the Amazon Linux 2023 AMI and placed it in the PUBLIC subnet.

    Note that we included the keyPair property when creating the EC2 instance. This key pair will enable SSH access, allowing us to interact directly with our RDS database from the EC2 instance. Ensure that a key pair matching the specified name already exists in your default AWS region, otherwise the deployment process will fail.

    Note: Before proceeding to set up the RDS instance, verify that a key pair named ec2-demo-key-pair already exists in your default AWS region. This step is essential to ensure successful deployment and SSH access to the EC2 instance.

    Let's now create a key pair named ec2-demo-key-pair in your default AWS region. Alternatively, you can replace this key name with an existing one already set up in your AWS account.

    Follow these steps to create the key pair:

    1. Open the EC2 Management Console in AWS.

    2. From the navigation pane, select Key Pairs.

    3. Click on Create key pair.

    4. Provide the key pair name as ec2-demo-key-pair.

    5. Select the file format suitable for your operating system:

      • .pem for Mac or Linux systems.
      • .ppk for Windows systems (commonly used with PuTTY).

    Download and securely store the key file, as you'll need it to SSH into your EC2 instance.

    After the key pair has been created, change to the directory it was downloaded in and change its permissions:

chmod 400 ec2-demo-key-pair.pem

Now, let's add the RDS instance to our stack. The imports below repeat those from the first block for reference. Insert the constructor body below after the EC2 definition; keep the earlier VPC, EC2, and Ec2PublicIp output. The // ... marker is an insertion point, not a complete standalone stack:

import { App, CfnOutput, Duration, RemovalPolicy, Stack, StackProps, } from "aws-cdk-lib"; import { MachineImage, Instance, InstanceClass, InstanceSize, InstanceType, IpAddresses, KeyPair, Peer, Port, SecurityGroup, SubnetType, Vpc, } from "aws-cdk-lib/aws-ec2"; import { Credentials, DatabaseInstance, DatabaseInstanceEngine, PostgresEngineVersion, } from "aws-cdk-lib/aws-rds"; export class DeployRDSEC2Stack extends Stack { constructor(scope: App, id: string, props?: StackProps) { super(scope, id, props); // ... rest of the code // create RDS Instance const dbInstance = new DatabaseInstance(this, "db-instance", { vpc, vpcSubnets: { subnetType: SubnetType.PRIVATE_ISOLATED, }, engine: DatabaseInstanceEngine.postgres({ version: PostgresEngineVersion.VER_16, }), instanceType: InstanceType.of( InstanceClass.BURSTABLE3, InstanceSize.MICRO ), credentials: Credentials.fromGeneratedSecret("postgres"), multiAz: false, allocatedStorage: 100, storageEncrypted: true, maxAllocatedStorage: 120, allowMajorVersionUpgrade: false, autoMinorVersionUpgrade: true, backupRetention: Duration.days(0), deleteAutomatedBackups: true, removalPolicy: RemovalPolicy.DESTROY, deletionProtection: false, databaseName: "testdb", publiclyAccessible: false, }); dbInstance.connections.allowFrom(ec2Instance, Port.tcp(5432)); new CfnOutput(this, "DbEndpoint", { value: dbInstance.instanceEndpoint.hostname, }); new CfnOutput(this, "SecretName", { value: dbInstance.secret?.secretName!, }); } }

Let's review the above code snippet:

  1. We've created an RDS database by instantiating the DatabaseInstance class from the AWS CDK.
  2. The props we provided to the constructor are as follows:
SettingValue and purpose in this example
vpcSubnetsIsolated subnets spanning two Availability Zones; see CDK subnet selection
enginePostgreSQL 16; check engine and instance-class availability in your region before deployment
instanceTypet3.micro, a burstable demonstration instance
credentialsGenerated password stored in Secrets Manager; no password in source code
multiAzfalse; this example has no Multi-AZ standby
allocatedStorage100 GiB initially
storageEncryptedtrue; enable encryption at rest for the database storage
maxAllocatedStorage120 GiB storage autoscaling ceiling
backupRetentionZero days; automated backups are disabled for this disposable example
deleteAutomatedBackupstrue; remove automated backups on instance deletion
removalPolicyDESTROY; deleting the stack deletes the database without a final snapshot
deletionProtectionfalse, allowing cleanup of the exercise
databaseNametestdb, the initial application database
publiclyAccessiblefalse; connect through the EC2 instance within the VPC

These options follow the CDK DatabaseInstance reference. Before reusing the stack for real data, enable backups and deletion protection, choose an appropriate retention policy, and review availability requirements.

  1. Next, we allowed connections to our RDS instance, on port 5432, from the security group of the EC2 instance
  2. The complete stack creates three outputs:
  • The EC2 public IP for SSH
  • The database hostname that we'll use to connect to our RDS instance
  • The name of the secret that stores the password of the postgres user

Deploying the RDS and EC2 Instance

Let's deploy the stack and test our RDS instance:

npx tsc --noEmit npx aws-cdk synth -c sshCidr=YOUR_PUBLIC_IPV4/32 npx aws-cdk diff -c sshCidr=YOUR_PUBLIC_IPV4/32 npx aws-cdk deploy -c sshCidr=YOUR_PUBLIC_IPV4/32 --outputs-file ./stack-outputs.json

Replace YOUR_PUBLIC_IPV4 with your current public IPv4 address, and pass the same context value to every command. Confirm that the intended region supports PostgreSQL 16 on the selected instance class. If the account/region has not been bootstrapped, run npx aws-cdk bootstrap for that environment first; see what CDK bootstrap does.

We have directed the outputs into a file named stack-outputs.json located in the root directory.

Wait for CloudFormation to report a successful deployment. Provisioning time varies; check stack events if creation fails. The JSON output file nests Ec2PublicIp, DbEndpoint, and SecretName under your stack name.

Upon inspecting the RDS instance's security group, you'll notice it permits inbound connections on port 5432 specifically from the security group assigned to our EC2 instance. This configuration ensures our EC2 instance can securely communicate with the RDS database after we establish an SSH session.

Before initiating an SSH connection to the EC2 instance, you'll need the database user's password, which is securely stored as a secret in AWS Secrets Manager.

To retrieve this secret:

  • Using the AWS Management Console:
    1. Open the Secrets Manager service.
    2. Select your secret, and click Retrieve Secret Value to reveal the password.
  • Using AWS CLI:
    1. Replace YOUR_SECRET_NAME with the SecretName output value from the stack-outputs.json file, and run this command to retrieve the secret:
    2. aws secretsmanager get-secret-value \ --secret-id YOUR_SECRET_NAME --output yaml
      Copy and store the returned password safely, as you'll need it to access your database from the EC2 instance.

Connecting to an RDS from EC2 instance

Let’s now SSH into our EC2 instance and connect to the RDS database.

  1. Open your terminal and navigate to the directory where you saved the ec2-demo-key-pair private key file.

  2. Set the correct permissions on the key file (if needed):

    chmod 400 ec2-demo-key-pair.pem
  3. SSH into the EC2 instance using the public IP address from the stack-outputs.json file:

    ssh -i ec2-demo-key-pair.pem ec2-user@<EC2_PUBLIC_IP>

    Replace <EC2_PUBLIC_IP> with the actual public IP address of your EC2 instance.

Once connected, you'll be inside the EC2 instance, ready to install a PostgreSQL client and connect to your RDS database.

sudo dnf install -y postgresql16 psql --version

Now we can connect to the RDS instance. Replace YOUR_DB_ENDPOINT with the value of DbEndpoint from the stack-outputs.json file, alternatively grab the Endpoint value from the RDS management console.

psql "host=YOUR_DB_ENDPOINT port=5432 dbname=testdb user=postgres sslmode=require"

You will be prompted for the password of the postgres user. Paste the value you grabbed from Secrets Manager and you should be connected to the RDS instance.

Let's list the databases:

\l

The database list should include testdb. Installing a PostgreSQL server on EC2 is unnecessary: RDS runs the server, and EC2 only needs the client. AWS documents client installation and connection requirements. sslmode=require encrypts the connection; use the RDS CA bundle and verify-full when you also need server certificate verification.

Let's connect to it.

-- get current database SELECT current_database(); -- connect to the application database \c testdb

Let's create a table and insert a few rows in it.

CREATE TABLE IF NOT EXISTS demotable (id SERIAL PRIMARY KEY, text TEXT NOT NULL); INSERT INTO demotable (text) VALUES ('hello world');

Finally, let's print the records from the demotable table of our RDS instance.

SELECT * FROM demotable;

A successful query should return the inserted hello world row. This checks the EC2-to-RDS network path, credentials, and database selection. It is an expected result, not a recorded deployment result for this revision.

Note: Don't forget to delete the resources you have provisioned, to avoid incurring charges.

Cleanup

npx aws-cdk destroy -c sshCidr=YOUR_PUBLIC_IPV4/32

Cleanup: Review the destroy proposal only for the disposable exercise stack. Verify EC2 and RDS deletion, and inspect Secrets Manager, retained snapshots, and storage for anything still billable. Keep backups you need; do not delete snapshots simply because they exist. The imported EC2 key pair is not owned or removed by this stack.

Troubleshooting Common Issues

SymptomWhat to inspect
Missing or invalid SSH CIDR during synthesisPass -c sshCidr=YOUR_PUBLIC_IPV4/32 to synth, diff, deploy, and destroy
EC2 key pair not foundCreate ec2-demo-key-pair in the deployment region or consistently replace the name in the code and SSH command
SSH timeoutEC2 public IP, internet-gateway route, network ACLs, and whether your public IP changed since deployment
PostgreSQL connection timeoutRDS availability, endpoint, DNS, and the database security group's TCP 5432 rule from the EC2 security group
Password authentication failedThe current SecretName output and its generated password, using username postgres
Database does not existUse dbname=testdb; the initial database name must match the stack
Engine or instance class rejectedRegional PostgreSQL engine availability and supported DB instance classes

Production differences and next steps

The example disables recovery safeguards to make a temporary exercise removable. For production, configure backup retention, test restores, retain or snapshot data on deletion, enable deletion protection, and decide whether Multi-AZ availability is required. Restrict administrative access and review TLS certificate verification, credential rotation, and monitoring.

Check current regional pricing rather than assuming a micro instance is free. RDS storage, EC2, public IPv4, and secrets are separate costs; stopping a workload does not remove every charge.

If your network already exists, use Vpc.fromLookup to import an existing VPC instead of creating a new one, then verify its subnet layout. Compare the CDK workflow with getting started with Pulumi on AWS and GCP if you are choosing a TypeScript infrastructure tool.

Quick Recap

Why does the RDS instance go in an isolated subnet while the EC2 instance goes in a public subnet?

This demo uses SSH to a public EC2 instance and keeps RDS in isolated subnets without an internet route. Public subnet placement alone does not grant access: EC2 also needs a public IP and an allowed security group rule.

How do you generate and store RDS credentials securely with CDK?

Use Credentials.fromGeneratedSecret("postgres") on the DatabaseInstance construct. CDK auto-generates a password and stores it in AWS Secrets Manager rather than hardcoding it.

How do you allow an EC2 instance to connect to an RDS instance in CDK?

Call dbInstance.connections.allowFrom(ec2Instance, Port.tcp(5432)), which adds an ingress rule to the RDS security group permitting traffic from the EC2 instance's security group on the database port.

How do you retrieve the RDS password after deployment?

Open the secret in AWS Secrets Manager via the console (Retrieve Secret Value) or the CLI with aws secretsmanager get-secret-value --secret-id YOUR_SECRET_NAME, using the secret name from the stack's CfnOutput.

Add Mikaeels Blog as a preferred source on Google

Comments

Share your thoughts and questions below