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.
import { App, Stack } from "@aws-cdk/core"
import { IVpc, Vpc } from "@aws-cdk/aws-ec2"
export class NetworkStack extends Stack {
readonly vpc: IVpc
constructor(scope: App) {
super(scope, "network-stack")
this.vpc = new Vpc(this, "TheVPC", {
natGateways: 1,
})
}
}
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.
import { Construct, Stack } from "@aws-cdk/core"
import { BastionHostLinux, IVpc } from "@aws-cdk/aws-ec2"
interface Props {
vpc: IVpc
}
export class BastionHostService extends Stack {
bastion: BastionHostLinux
constructor(scope: Construct, props: Props) {
super(scope, "bastion-host-service")
this.bastion = new BastionHostLinux(this, "Bastion", {
vpc: props.vpc,
instanceName: "bastion-host",
})
}
}
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.
import { Stack, App, Duration } from "@aws-cdk/core"
import { BastionHostLinux, IVpc, Port } from "@aws-cdk/aws-ec2"
import {
Credentials,
DatabaseClusterEngine,
ParameterGroup,
ServerlessCluster,
} from "@aws-cdk/aws-rds"
interface Props {
vpc: IVpc
bastion: BastionHostLinux
}
export class DatabaseService extends Stack {
constructor(scope: App, props: Props) {
super(scope, "database-service")
const cluster = new ServerlessCluster(this, "serverless-db", {
engine: DatabaseClusterEngine.AURORA_POSTGRESQL,
parameterGroup: ParameterGroup.fromParameterGroupName(
this,
"ParameterGroup",
"default.aurora-postgresql10"
),
defaultDatabaseName: "serverless",
vpc: props.vpc,
scaling: { autoPause: Duration.seconds(0) },
credentials: Credentials.fromGeneratedSecret("serverless"),
})
cluster.connections.allowFrom(
props.bastion.connections,
Port.tcp(cluster.clusterEndpoint.port),
"Bastion host connection"
)
}
}
Putting it all together
All we have to do now is create instances of those classes and pass in the relevant arguments
#!/usr/bin/env node
import * as cdk from "@aws-cdk/core"
import { NetworkStack } from "../lib/network.stack"
import { DatabaseService } from "../lib/database.service"
import { BastionHostService } from "../lib/bastion-host.service"
const app = new cdk.App()
const network = new NetworkStack(app)
const host = new BastionHostService(app, network)
new DatabaseService(app, {
vpc: network.vpc,
bastion: host.bastion,
})
Now that it’s ready we can start a session with aws-vault
and run
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
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!