exanubes

Setup Aurora Serverless with CDK

This article looks into how to setup an RDS database cluster in a private VPC. Then we will connect to that database in an IDE using a Bastion Host as an SSH tunnel to gain access to the RDS instance.

CDK setup is not in the scope of this article. To learn about it you can read AWS CDK: Getting Started

Get finished code from github

VPC #

CDK makes it very easy to create a private vpc. Thanks to the pretty nifty defaults, this is all it takes. I explicitly set NAT Gateways to 1 for slightly lower cost of provisioning.

typescript
Double click to copy
1import { App, Stack } from "@aws-cdk/core"
2import { IVpc, Vpc } from "@aws-cdk/aws-ec2"
3
4export class NetworkStack extends Stack {
5 readonly vpc: IVpc
6 constructor(scope: App) {
7 super(scope, "network-stack")
8 this.vpc = new Vpc(this, "TheVPC", {
9 natGateways: 1,
10 })
11 }
12}

To demonstrate how absolutely bonkers the cdk is, in order to achieve the same result via the Console or even Cloud Formation template, we would have to define the VPC, public & private Subnets, Route Tables, Subnet and Route Table Associations, NAT and Internet Gateways and probably a few more that I can't remember.

Each of these has their own configs and depend on one another. The Resources section of the cdk-generated template is over 400 lines long. As I said. Bonkers.

Bastion Host #

Bastion Host is a little bit more involved but still very terse. This is a tiny preview of CDK's capabilities. When creating the BastionHostService class instance we will pass the vpc as props and then use it to provision the Bastion Host ec2 instance in the VPC that we created.

typescript
Double click to copy
1import { Construct, Stack } from "@aws-cdk/core"
2import { BastionHostLinux, IVpc } from "@aws-cdk/aws-ec2"
3
4interface Props {
5 vpc: IVpc
6}
7
8export class BastionHostService extends Stack {
9 bastion: BastionHostLinux
10 constructor(scope: Construct, props: Props) {
11 super(scope, "bastion-host-service")
12 this.bastion = new BastionHostLinux(this, "Bastion", {
13 vpc: props.vpc,
14 instanceName: "bastion-host",
15 })
16 }
17}

For people not familiar with the idea of a Bastion Host a.k.a Jump Box, it is a computer on a private network, e.g a private VPC on AWS. Its purpose is to allow access to the private subnet that does not have access to the internet. In this example, the RDS instance is in the private subnet, cutoff from the internet for security purposes. In order to connect to it, we will ssh into the Bastion Host and create a tunnel from the private subnet via the bastion host into our own computer, thus gaining access to the database.

RDS #

Even more things going on here, not only do we have a vpc but also a bastion host passed in through props. I used the aurora-postgres engine and use the default aurora-postgresql10 parameterGroup for config. Turned off the auto-pause and generated a secret for the username serverless.

Next, we define that the cluster should allow connections from the bastion host and pass in a tcp port range.

typescript
Double click to copy
1import { Stack, App, Duration } from "@aws-cdk/core"
2import { BastionHostLinux, IVpc, Port } from "@aws-cdk/aws-ec2"
3import {
4 Credentials,
5 DatabaseClusterEngine,
6 ParameterGroup,
7 ServerlessCluster,
8} from "@aws-cdk/aws-rds"
9
10interface Props {
11 vpc: IVpc
12 bastion: BastionHostLinux
13}
14
15export class DatabaseService extends Stack {
16 constructor(scope: App, props: Props) {
17 super(scope, "database-service")
18 const cluster = new ServerlessCluster(this, "serverless-db", {
19 engine: DatabaseClusterEngine.AURORA_POSTGRESQL,
20 parameterGroup: ParameterGroup.fromParameterGroupName(
21 this,
22 "ParameterGroup",
23 "default.aurora-postgresql10"
24 ),
25 defaultDatabaseName: "serverless",
26 vpc: props.vpc,
27 scaling: { autoPause: Duration.seconds(0) },
28 credentials: Credentials.fromGeneratedSecret("serverless"),
29 })
30
31 cluster.connections.allowFrom(
32 props.bastion.connections,
33 Port.tcp(cluster.clusterEndpoint.port),
34 "Bastion host connection"
35 )
36 }
37}

Putting it all together #

All we have to do now is create instances of those classes and pass in the relevant arguments

typescript
Double click to copy
1#!/usr/bin/env node
2import * as cdk from "@aws-cdk/core"
3import { NetworkStack } from "../lib/network.stack"
4import { DatabaseService } from "../lib/database.service"
5import { BastionHostService } from "../lib/bastion-host.service"
6
7const app = new cdk.App()
8const network = new NetworkStack(app)
9const host = new BastionHostService(app, network)
10new DatabaseService(app, {
11 vpc: network.vpc,
12 bastion: host.bastion,
13})

Now that it's ready we can start a session with aws-vault and run

bash
Double click to copy
npm run cdk:deploy

Provisioning will take several minutes. Keep in mind it will prompt you twice for input.

Don't forget to tear it down after you're done

bash
Double click to copy
npm run cdk:destroy

Summary #

To sum up, we created a full VPC network inside which we have provisioned a Bastion Host EC2 Instance and an RDS Cluster. Then using the cdk CLI, we have deployed it to the cloud via Cloudformation. We were able to achieve all that in under 100 lines of code!

Next up: Connecting to private RDS via Bastion Host

Thanks!

A verification email has been sent to

Keep in Touch

Join other developers in Exanubes Newsletter

© 2023