Thanks!
A verification email has been sent to
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
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.
Double click to copy1import { App, Stack } from "@aws-cdk/core"2import { IVpc, Vpc } from "@aws-cdk/aws-ec2"34export class NetworkStack extends Stack {5 readonly vpc: IVpc6 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 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.
Double click to copy1import { Construct, Stack } from "@aws-cdk/core"2import { BastionHostLinux, IVpc } from "@aws-cdk/aws-ec2"34interface Props {5 vpc: IVpc6}78export class BastionHostService extends Stack {9 bastion: BastionHostLinux10 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.
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.
Double click to copy1import { 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"910interface Props {11 vpc: IVpc12 bastion: BastionHostLinux13}1415export 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 })3031 cluster.connections.allowFrom(32 props.bastion.connections,33 Port.tcp(cluster.clusterEndpoint.port),34 "Bastion host connection"35 )36 }37}
All we have to do now is create instances of those classes and pass in the relevant arguments
Double click to copy1#!/usr/bin/env node2import * 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"67const 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
Double click to copynpm 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
Double click to copynpm run cdk:destroy
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
A verification email has been sent to