Connecting to RDS for Local Development
Overview
One common serverless application includes a Virtual Private Cloud (VPC) resource and a Bastion host (EC2 instance) inside of it that acts as a "jumpbox" for traffic destined to resources held in the VPC's private subnets. A cloud resource typically held in these private subnets is an RDS Database instance/cluster, with database actions driven by a Lambda function (also within the VPC).
To adhere to AWS best practices, Stackery automatically sets the following resources into appropriate subnets when they're placed inside of a Virtual Network resource.
- Bastion: inserted into the VPC's public subnets and accessible using the VPC's internet gateway.
- Function & Database: inserted into the VPC's private subnets and can only be accessed via the Bastion host, or the NAT Gateway that also sits in the VPC's public subnet.
With this setup, the Lambda function and RDS database residing in the private subnets are able to interact, but there is additional setup required in order for developers to iterate this function locally using stackery local invoke
.
This guide covers additional setup required to iterate on a function that interacts with an RDS database set inside a VPC's private subnets. By the end of this guide, you'll be able to utilize a SSH tunnel and stackery local invoke
to connect to your RDS DB cluster and develop locally.
Project Repository
- local-invoke-rds (Completed)
Installations
Make sure you have a Stackery account as well as the required installations described in the Local Development with Stackery walkthrough
Configuring Resources
Bastion Host
To grant your local machine access to the Bastion, you'll need to add your machine's public SSH key.
Copy your SSH public key from ~/.ssh/id_rsa.pub
on your machine and set it as a key-value pair under SSH Keys.
<Your Name>: <SSH Key>
Lambda Function
We're using the default settings for our Function (Nodejs10 runtime) so we don't need to update any resource settings.
RDS Database Cluster
The same goes for our DB cluster. We'll use the default Aurora Serverless DB Cluster as our type (MySQL engine) and for this example set our root db password to password
.
To provide our Function the IAM permissions it needs to interact with our RDS cluster, we'll connect the two resources using a service-discovery wire (dashed-line).
Deploy
With the resources configured, deploy the stack from the Stackery Dashboard or with the following Stackery CLI command:
$ stackery deploy --strategy local -n local-invoke-rds -e {ENV_NAME} --aws-profile {AWS_PROFILE}
Accessing the RDS DB inside the VPC
The following sections will show how to iterate on our Lambda function locally and connect to an RDS DB cluster that's inside of a VPC.
Creating a SSH Tunnel
To establish a SSH tunnel for traffic destined to resources in the VPC you'll need to retrieve the following:
- Navigate to the stack in the Stackery Dashboard, and select the View tab on the side panel
- From the latest deployment of local-invoke-rds, double-click the Bastion resource and copy the its IP address
- On the same page, double-click the Database resource and copy the DNS Name value
- Open a terminal window and construct then run the following command:
ssh -v -N -L 3306:{DNS_NAME}:3306 {BASTION_IP_ADDRESS}
- Leave this terminal window open while developing the function code
- You can confirm this tunnel has access to the RDS database by logging in with the following command and configured password:
mysql -h 127.0.0.1 -p -u root
Now we have our SSH tunnel that we can use with stackery local invoke
.
Writing our Lambda Function
- In the Stackery Dashboard, follow the repository link under the stack name and clone the project
cd
into the root directory of local-invoke-rds, then runnpm install mysql
since we'll be using the MySQL library to connect to our database- In a code editor, open up the local-invoke-rds stack, navigate to index.js, and paste the following code:
const mysql = require('mysql');
// if aws sam local is set to true, use host.docker.internal DNS name to access
// services running locally (localhost) otherwise, use the RDS DB address as the host
const dbHost = process.env.AWS_SAM_LOCAL === 'true' ? 'host.docker.internal' : process.env.DB_ADDRESS;
const connection = mysql.createConnection({
host: dbHost,
user: 'root',
password: 'password',
database: 'mysql'
});
exports.handler = (event, context, callback) => {
connection.query('show tables', function (error, results, fields) {
if (error) {
connection.destroy();
throw error;
} else {
callback(error, results);
connection.end(function (err) { callback(err, results); });
}
});
};
The dbHost
variable will be set to either host.docker.internal
or use the DB_ADDRESS environment variable that gets added to our Function via Stackery's service discovery wire (dashed line). This value depends on whether the aws sam local environment variable is set to true or not.
This environment variable is set to true
by AWS SAM when running sam local invoke
, which is used under the covers for the stackery local invoke
command.
Next we establish a database connection using the host, user, password, and database values we're aware of.
Finally, we use the connection and run a mysql query to display the tables within our MySQL database.
Run a stackery local invoke
command like the following to invoke your function code locally, and connecting to the RDS db cluster:
$ stackery local invoke -f {FUNCTION_NAME} -e {ENV_NAME} --aws-profile {AWS_PROFILE} --docker-network host
Behind the scenes of aws sam local
, is a Docker container running. The --docker-network host
flag on the command above allows this Docker container to connect to your host system's network, which has a SSH tunnel available.
You should receive a successful function response displaying the results of the MySQL query you decided to run, which means that our local function code can connect to, and query, our RDS DB cluster.