RDS Example in AWS CDK - Complete Guide

avatar

Borislav Hadzhiev

Sun May 09 20217 min read

banner

Photo by Milan Popovic

A complete example of provisioning an RDS instance with AWS CDK and connecting to it from an EC2 instance.

Creating an RDS Instance in AWS CDK #

In this article we are going to create an RDS instance and connect to it from an EC2 instance.

We will also create a VPC, as RDS databases and EC2 instances must be launched in a VPC. The RDS instance will be in an ISOLATED subnet, whereas the EC2 instance will be in a PUBLIC subnet.

The code for this article is available on GitHub

Let's start by creating the VPC and the EC2 instance:

lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2';
import * as rds from '@aws-cdk/aws-rds';
import * as cdk from '@aws-cdk/core';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ๐Ÿ‘‡ create the VPC
    const vpc = new ec2.Vpc(this, 'my-cdk-vpc', {
      cidr: '10.0.0.0/16',
      natGateways: 0,
      maxAzs: 3,
      subnetConfiguration: [
        {
          name: 'public-subnet-1',
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 24,
        },
        {
          name: 'isolated-subnet-1',
          subnetType: ec2.SubnetType.ISOLATED,
          cidrMask: 28,
        },
      ],
    });

    // ๐Ÿ‘‡ create a security group for the EC2 instance
    const ec2InstanceSG = new ec2.SecurityGroup(this, 'ec2-instance-sg', {
      vpc,
    });

    ec2InstanceSG.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(22),
      'allow SSH connections from anywhere',
    );

    // ๐Ÿ‘‡ create the EC2 instance
    const ec2Instance = new ec2.Instance(this, 'ec2-instance', {
      vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      securityGroup: ec2InstanceSG,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO,
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),
      keyName: 'ec2-key-pair',
    });
  }
}

Let's go over the code snippet.

  1. we created a VPC with a PUBLIC and an ISOLATED subnet groups.

    An instance launched in a PUBLIC subnet has access to the internet and can be accessed from the internet via an internet gateway. Our EC2 instance will be launched in a PUBLIC subnet.

    Whereas an instance launched in an ISOLATED subnet has no access to the internet and can't be accessed from the internet. Isolated subnets are used mostly for intra-VPC communication.

    Our RDS instance will be launched in an ISOLATED subnet, because we will be connecting to it from our EC2 instance, which is in the same VPC.

  2. we created a security group for our EC2 instance, the security has a single inbound rule, which allows SSH connections from anywhere.

  3. we created a t2.micro EC2 instance with Amazon Linux 2 AMI and placed it in a PUBLIC subnet.

    Note that we've passed the keyName prop to the EC2 instance. We will SSH into the instance to interact with our RDS database. A key pair with the specified name has to exist in your default AWS region in order for the deployment to succeed.

Before we move onto creating the RDS instance, we have to make sure that a key pair with the specified name, in this case ec2-key-pair exists in your default AWS region.

Let's create a key pair in your default AWS region with the name of ec2-key-pair. Alternatively you could replace the value of the keyName prop with one that already exists in your account.

To create a key pair, open the EC2 Management console and click on Key Pairs > Create key Pair.

Depending on your operating system you can choose between pem (Mac, Linux) and ppk (Windows). I'm on Linux, so I've selected pem:

ec2 instance key pair creation

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

shell
chmod 400 ec2-key-pair.pem
The code for this article is available on GitHub

Let's add the RDS instance to our code, right below the EC2 instance definition:

lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2';
import * as rds from '@aws-cdk/aws-rds';
import * as cdk from '@aws-cdk/core';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ... rest

    // ๐Ÿ‘‡ create RDS instance
    const dbInstance = new rds.DatabaseInstance(this, 'db-instance', {
      vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.ISOLATED,
      },
      engine: rds.DatabaseInstanceEngine.postgres({
        version: rds.PostgresEngineVersion.VER_13_1,
      }),
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.BURSTABLE3,
        ec2.InstanceSize.MICRO,
      ),
      credentials: rds.Credentials.fromGeneratedSecret('postgres'),
      multiAz: false,
      allocatedStorage: 100,
      maxAllocatedStorage: 105,
      allowMajorVersionUpgrade: false,
      autoMinorVersionUpgrade: true,
      backupRetention: cdk.Duration.days(0),
      deleteAutomatedBackups: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      deletionProtection: false,
      databaseName: 'todosdb',
      publiclyAccessible: false,
    });

    dbInstance.connections.allowFrom(ec2Instance, ec2.Port.tcp(5432));

    new cdk.CfnOutput(this, 'dbEndpoint', {
      value: dbInstance.instanceEndpoint.hostname,
    });

    new cdk.CfnOutput(this, 'secretName', {
      // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
      value: dbInstance.secret?.secretName!,
    });
  }
}

Let's go over what we did in the code snippet.

  1. we created an RDS instance, by instantiating the DatabaseInstance class.
  2. the props we've passed to the constructor are:
nameDescription
vpcThe VPC in which the DB subnet group will be created
vpcSubnetsThe type of subnets the DB subnet group should consist of, in our case ISOLATED subnets.
engineThe engine for the database, in our case Postgres, version 13
instanceTypeThe class and size for the instance, in our case t3.micro
credentialsThe credentials for the admin user of the database. We've used the fromGeneratedSecret method and passed it a username of postres, the password will be auto generated and stored in secrets manager.
multiAzWhether the rds instance is a multi AZ deployment, in our case we've set it to false, which is also the default value. For production workloads you would most likely use a standby instance for high availability.
allocatedStorageThe allocated storage size of the database, in gigabytes. We've set the value to 100 gigabytes, which is also the default
maxAllocatedStorageThe upper limit for storage auto scaling, in our case we've set it to 105 gigabytes, by default there is no storage auto scaling
backupRetentionFor how many days automatic database snapshots should be kept. We've turned automated snapshots off, by setting the value to 0 days, the default value is 1 day.
deleteAutmtdBackupsSpecify whether automated backups should be deleted or retained when the rds instance is deleted. By default automated backups are retained on instance deletion.
removalPolicyThe policy that should be applied if the resource is deleted from the stack or replaced during an update. By default the instance is deleted, but a snapshot of the data is retained.
deletionProtectionSpecify whether the DB instance should have termination protection enabled. By default it's set to true if removalPolicy is RETAIN, otherwise - false
databaseNameSpecify the name of the database
publiclyAccessibleSpecify whether the rds instance should be publicly accessible. Set to true by default for instances launched in PUBLIC subnet groups, false otherwise.
  1. next, we allowed connections to our RDS instance, on port 5432, from the security group of the EC2 instance

  2. we created 2 outputs:

  • 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 our RDS Instance in AWS CDK #

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

shell
npx cdk deploy \
  --outputs-file ./cdk-outputs.json

We redirected the Outputs to a file named cdk-outputs.json located in the root directory.

After about 5 minutes the resources are created.

If we look at the security group of the RDS instance, we can see that it allows connections on port 5432 from the security group of our EC2 instance. This will allow us to connect to the database once we SSH into the EC2 instance:

rds instance inbound rules

Before we SSH into the EC2 instance, let's grab the value of the secret, storing the password for our db user.

The easiest way is to open Secrets Manager in the AWS management console and click on Retrieve Secret Value:

secrets manager retrieve secret value

Alternatively, replace YOUR_SECRET_NAME with the value of the secretName output from cdk-outputs.json, execute the following command, and copy the password:

shell
aws secretsmanager get-secret-value \
  --secret-id YOUR_SECRET_NAME --output yaml

Connecting to an RDS instance from EC2 #

Let's SSH into our EC2 instance and connect to the RDS instance.

Navigate to the directory where you stored the ec2-key-pair key and ssh into the instance:

shell
ssh -i "ec2-key-pair.pem" \
  ec2-user@YOUR_EC2_PUBLIC_IPV4_ADDRESS

Once we're in, we first need to install postgres:

shell
sudo amazon-linux-extras install epel -y

sudo yum install postgresql postgresql-server -y

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

rds endpoint value

shell
psql -p 5432 -h YOUR_DB_ENDPOINT -U postgres

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:

shell
\l

We can see that rds has created our database with name of todosdb:

rds list databases

Let's connect to it:

shell
# ๐Ÿ‘‡ print current database
SELECT current_database();

# ๐Ÿ‘‡ connect to todosdb
\c todosdb

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

shell
CREATE TABLE IF NOT EXISTS todos (todoid SERIAL PRIMARY KEY, text TEXT NOT NULL);

INSERT INTO todos (text) VALUES ('Walk the dog');

INSERT INTO todos (text) VALUES ('Buy groceries');

Finally, let's print the records from the todos table of our RDS instance:

shell
SELECT * FROM todos;

rds instance select

We were able to successfully connect and interact with our RDS instance from an EC2 instance.

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

Clean up #

To delete the resources we have provisioned, execute the destroy command:

shell
npx cdk destroy
Make sure to double check the Secrets Manager console to make sure the secret is deleted and the RDS management console to make sure all snapshots are deleted.

Further Reading #

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee