Mastering AWS RDS with CDK

A Deep Dive into Aurora PostgreSQL Setup using TypeScript

Mehran
ITNEXT

--

Generated using Lexica

In today’s post, we will talk about setting up an AWS RDS and zeroing in on AWS RDS Aurora PostgreSQL using AWS CDK and TypeScript. There are many ways to create resources in AWS, and each has its ups and downs. Let’s check out these methods, focusing on why AWS CDK is an excellent choice for our project. This practical walkthrough will give you the lowdown on managing AWS resources and why tools like AWS CDK and TypeScript are super handy.

  1. Using the AWS Console: This method is straightforward, involving the AWS web interface. However, it’s generally not recommended for production environments due to its manual nature, which can lead to inconsistencies and errors.
  2. CloudFormation involves defining your infrastructure as code using YAML or JSON formats. You can execute these templates using the AWS SDK. While it’s a robust option, it requires a good grasp of specific syntax and commands, which can be challenging for those unfamiliar with these languages.
  3. Terraform: This open-source tool allows you to define infrastructure as code. It’s highly effective but has a learning curve, particularly for beginners. Its complexity might be overwhelming initially, but it’s a powerful tool once mastered.
  4. AWS CDK (Cloud Development Kit): This approach is highly recommended for beginners and seasoned developers. It lets you use familiar programming languages like JavaScript/TypeScript, Python, and C#. The ability to write unit tests for your code makes your entire stack more maintainable, readable, and understandable.

This post will focus on option 4 — using AWS CDK. The reason for choosing AWS CDK, particularly for setting up RDS, is that it presents unique challenges that are often not well-documented, especially in the context of the CDK. Moreover, this exercise offers an excellent opportunity to deepen your understanding of AWS components like VPC and Security Groups.

Start the CDK Project

Starting a CDK project in TypeScript is a straightforward process, but it’s crucial to set it up correctly to ensure everything runs smoothly. Here’s how you can get your CDK project up and running:

  1. Install Node.js and npm: AWS CDK requires Node.js. If you haven’t already installed it, download and install Node.js, which includes npm (Node Package Manager) from nodejs.org.
  2. Install AWS CDK Toolkit: The AWS CDK Toolkit (cdk command line tool) is essential for working with CDK apps. Install it globally using npm by running the following command in your terminal:
npm install -g aws-cdk

3: Create a Directory for Your Project: Organize your work by creating a new directory for your CDK project. You can do this from the terminal:

mkdir my-cdk-project
cd my-cdk-project

4: Initialize a New CDK Project: Use the CDK command line to create a new TypeScript project:

cdk init app --language typescript

This command creates a new CDK app with an example stack.

5: Explore the Project Structure: The initialization process creates several files and folders in your project directory. Key among them is lib/my-cdk-project-stack.ts, where you can define your CDK resources.

6: Install Dependencies: Your new CDK app will have a package.json file defining project dependencies. Install them by running:

npm install

7: Bootstrap Your AWS Environment: Before deploying your CDK app for the first time, you must bootstrap your AWS environment. This sets up the resources needed by the CDK to deploy your app into an AWS account:

cdk bootstrap

8: Start Writing Your CDK Code: Now, you can start writing your CDK code in the lib/my-cdk-project-stack.ts file. This is where you'll define your AWS RDS Aurora PostgreSQL instance.

9: Compile TypeScript to JavaScript: Since CDK apps are executed as Node.js applications, ensure your TypeScript code is compiled to JavaScript:

npm run build

10: Deploy Your Stack: Once you’ve defined your stack and are ready to deploy, use the CDK deploy command:

cdk deploy

Remember, you’ll need to have your AWS credentials configured for the deployment to work. You can do this by setting up the AWS CLI and configuring it with your credentials.

Creating RDS

To create the RDS you have to put this code inside the my-cdk-project-stack.ts



import { aws_rds as rds, aws_ec2 as ec2, Stack, StackProps, CfnOutput } from 'aws-cdk-lib'
import { Construct } from 'constructs'

const allowedIpAddresses = ['56.644.232.323/32'] // Replace with actual IP addresses/ranges

export class SampleDataSourceStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props)

// Create a new VPC with public subnets and an internet gateway
const vpc = new ec2.Vpc(this, 'sample-rds', {
maxAzs: 3,
vpcName: 'sample-rds',
subnetConfiguration: [
{
name: 'PublicSubnet',
subnetType: ec2.SubnetType.PUBLIC,
},
// Add more subnet configurations if necessary
],
})

// Create a security group for the serverless application
const serverlessSecurityGroup = new ec2.SecurityGroup(this, 'sample-rds-serverless-security-group', {
vpc: vpc,
securityGroupName: 'sample-rds-serverless-security-group',
description: 'Allow the serverless application to connect to the database'
})

const rdsSecurityGroup = new ec2.SecurityGroup(this, 'rds-security-group', {
vpc: vpc,
securityGroupName: 'rds-security-group',
description: 'Security group for RDS',
})

// Allow connections from specific IP addresses
allowedIpAddresses.forEach(ip => {
rdsSecurityGroup.addIngressRule(ec2.Peer.ipv4(ip), ec2.Port.tcp(5432), 'Allow PostgreSQL access from specific IP addresses')
})

// Allow Lambda security group to access the RDS
rdsSecurityGroup.addIngressRule(serverlessSecurityGroup, ec2.Port.tcp(5432), 'Allow PostgreSQL access from Lambda security group')
// Specify the engine version
const engineVersion = rds.AuroraPostgresEngineVersion.VER_15_4

// Create the writer and reader instances for the Aurora Cluster
const writerInstance = rds.ClusterInstance.provisioned('writer-instance', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE4_GRAVITON, ec2.InstanceSize.MEDIUM),
instanceIdentifier: 'writer-instance',
})

const readerInstance = rds.ClusterInstance.provisioned('reader-instance', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE4_GRAVITON, ec2.InstanceSize.MEDIUM),
instanceIdentifier: 'reader-instance',
})

// Create an Aurora PostgreSQL-Compatible Cluster
const cluster = new rds.DatabaseCluster(this, 'AuroraPostgresCluster', {
engine: rds.DatabaseClusterEngine.auroraPostgres({ version: engineVersion }),
vpc: vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
clusterIdentifier: 'sample',
writer: writerInstance,
readers: [readerInstance],
defaultDatabaseName: 'sample',
credentials: rds.Credentials.fromGeneratedSecret('sampleadmin', { secretName: 'sample-database-config' }),
securityGroups: [rdsSecurityGroup],
})

// Create a VPC endpoint for Secrets Manager
const secretsManagerEndpoint = new ec2.InterfaceVpcEndpoint(this, 'SecretsManagerEndpoint', {
vpc: vpc,
service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
subnets: { subnetType: ec2.SubnetType.PUBLIC },
})

// Update the serverless application security group to allow outbound traffic to the VPC Endpoint
secretsManagerEndpoint.connections.securityGroups.forEach(sg => {
serverlessSecurityGroup.addEgressRule(
ec2.Peer.securityGroupId(sg.securityGroupId),
ec2.Port.tcp(443), // Secrets Manager uses HTTPS
'Allow outbound HTTPS to Secrets Manager VPC Endpoint'
)
})
// Configure the VPC Endpoint's Security Groups to allow inbound traffic from the serverless application
secretsManagerEndpoint.connections.securityGroups.forEach(sg => {
sg.addIngressRule(
serverlessSecurityGroup,
ec2.Port.tcp(443), // Allow inbound HTTPS from the serverless application security group
'Allow inbound HTTPS from serverless application'
)
})

// Allow the serverless application to connect to the database
cluster.connections.allowFrom(serverlessSecurityGroup, ec2.Port.tcp(cluster.clusterEndpoint.port))

// Outputs
new CfnOutput(this, 'ClusterARN', { value: cluster.clusterEndpoint.hostname })
new CfnOutput(this, 'ClusterSecretARN', { value: cluster.secret!.secretArn })
}
}

Let’s break down the code to understand each component and its role in the setup.

  1. Import Statements:
import { aws_rds as rds, aws_ec2 as ec2, Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';

The code starts by importing necessary modules from the AWS CDK library. aws_rds and aws_ec2 are aliased as rds and ec2, respectively, for RDS and EC2 services. Stack, StackProps, and CfnOutput are also imported for creating and managing the CDK stack and its outputs.

2. IP Address Whitelisting:

const allowedIpAddresses = ['56.644.232.323/32']; // Replace with actual IP addresses/ranges

This line defines an array of IP addresses that will be granted access to the RDS instance. It’s a placeholder and should be replaced with the actual IP addresses you wish to whitelist.

3. Class Definition:

export class SampleDataSourceStack extends Stack {
  • This line begins the definition of a new class SampleDataSourceStack, extending the Stack class from AWS CDK. This class will represent the CDK stack for our AWS resources.

4. Constructor and VPC Creation:

constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, 'sample-rds', { ... });
  • The constructor of SampleDataSourceStack is defined with parameters scope, id, and props.
  • A new VPC is created within this constructor, indicating that the database and other resources will be within this VPC.

5. Serverless Application Security Group:

const serverlessSecurityGroup = new ec2.SecurityGroup(this, 'sample-rds-serverless-security-group', { ... });
  • Here, a security group for a serverless application (like AWS Lambda) is defined, dictating the rules for inbound and outbound network traffic for the application.

6. RDS Security Group:

const rdsSecurityGroup = new ec2.SecurityGroup(this, 'rds-security-group', { ... });
  • This segment creates a separate security group specifically for the RDS instance, which will be used to control access to the database.

7. IP Whitelisting in RDS Security Group:

allowedIpAddresses.forEach(ip => {
rdsSecurityGroup.addIngressRule(ec2.Peer.ipv4(ip), ec2.Port.tcp(5432), 'Allow PostgreSQL access from specific IP addresses');
});
  • This loop adds ingress rules to the RDS security group to allow connections on the PostgreSQL port (5432) from the whitelisted IP addresses.

8. Lambda Access to RDS:

rdsSecurityGroup.addIngressRule(serverlessSecurityGroup, ec2.Port.tcp(5432), 'Allow PostgreSQL access from Lambda security group');
  • This line allows the serverless application (Lambda) to access the RDS instance by adding an ingress rule to the RDS security group.

9. RDS Cluster Creation:

const cluster = new rds.DatabaseCluster(this, 'AuroraPostgresCluster', { ... });
  • This part of the code sets up an Aurora PostgreSQL-compatible database cluster, configuring details like the engine version, instance types, credentials, and attaching the previously defined RDS security group.

10. Secrets Manager VPC Endpoint:

const secretsManagerEndpoint = new ec2.InterfaceVpcEndpoint(this, 'SecretsManagerEndpoint', { ... });
  • A VPC endpoint for AWS Secrets Manager is created, facilitating secure access to database credentials from within the VPC.

11. Configuring Security Group Rules for VPC Endpoint:

  • The following code updates the serverless application’s security group to allow communication with the Secrets Manager VPC Endpoint, ensuring that the serverless application can securely access the database credentials.

12. Database Connectivity and Outputs:

cluster.connections.allowFrom(serverlessSecurityGroup, ec2.Port.tcp(cluster.clusterEndpoint.port));
new CfnOutput(this, 'ClusterARN', { value: cluster.clusterEndpoint.hostname });
new CfnOutput(this, 'ClusterSecretARN', { value: cluster.secret!.secretArn });

The first line allows the serverless application to connect to the database. The subsequent lines output the cluster’s ARN and the ARN of the secret containing the database.

More on VPC and Security groups

This setup of RDS is quite unique. In this setup, we created a new VPC with a public subnet where the RDS is located. To limit access to the database, we decided to create a security group that only allows traffic from certain IP addresses and specific security group. This arrangement enables us to connect to the database from particular IP addresses or Lambda function for performing SQL queries.

In this setup, we created a security group for a Lambda function (though the creation of the Lambda function is not part of this stack). If you create a Lambda function and assign this security group to it, the Lambda function will have access to the database. However, it’s important to remember that Lambda functions are not created inside the VPC by default. If you want your Lambda function to access the database, besides assigning the security group created in this stack, you must also ensure that the Lambda function is created within the VPC.

Another key point in this stack is the management of credentials and all database configurations, such as host, port, and credentials. They are automatically stored in AWS Secret Manager. However, when a Lambda function is created inside the public VPC, it cannot access the internet and therefore cannot fetch the database configuration from Secret Manager. To resolve this issue, we create a VPC Endpoint for the Secret Manager. Then, we allow the Lambda security group to access this endpoint, ensuring seamless connectivity and security.

--

--

Writer for

Tech Team Lead | Cloud, Video & Microservices Expert | Insights on streaming innovations & programming. #ContinuousLearning