Databases, Serverless, Connection Pooling all these terms are pretty cool in isolation but can be intimidating when tried to use together. And to add to that managing the orchestration of these via SAM or CDK makes it more.
AWS introduced RDS Proxy to handle some of the issues related to Connection Pooling. In this post, I will try to orchestrate my application to use RDS Proxy via SAM.
Table Of Contents
Secret
In order for accessing the database, you need to create a secret in AWS SecretsManager. The secret will be using the username and password for the user you want to use to connect the database. You will need to encrypt the secret. You can use it by using the default encryption key provided by AWS OR can create one yourself.
IAM Role 1
Now you have the secret, but your user will need to have permissions to access the secret. For that you will need to add the following policy to your existing role OR create a new one.
"Statement": [
{
"Sid": "GetSecretValue",
"Action": [
"secretsmanager:GetSecretValue"
],
"Effect": "Allow",
"Resource": [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:<secretId>"
]
},
{
"Sid": "DecryptSecretValue",
"Action": [
"kms:Decrypt"
],
"Effect": "Allow",
"Resource": [
"arn:aws:kms:us-east-1:123456789012:key/<keyId>"
],
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.<region>.amazonaws.com"
}
}
}
]
What this will do is get your role permissions to access the secret value using the encryption key.
RDS Proxy
Now we need to setup the rds proxy. You can do this via Console OR also SAM, if you want to have separate proxy for each of your application (There is a limitation of 20 proxies per AWS Account, so might want to consider that before going this route). Anyways, here is how you can do it via SAM,
Resources:
DBProxy:
Type: AWS::RDS::DBProxy
Properties:
DBProxyName: rds-proxy-test
EngineFamily: MYSQL
RoleArn: arn:aws:iam::123456789012:role/<role-with-permissions-to-access-secret>
RequireTLS: true
Auth:
- {AuthScheme: SECRETS, SecretArn: arn:aws:secretsmanager:us-east-1:123456789012:secret:secret-you-created-earlier, IAMAuth: ENABLED}
VpcSubnetIds: <Comma-separated list of vpc ids setup for your DB> # Out of scope of this post
ProxyTargetGroup:
Type: AWS::RDS::DBProxyTargetGroup
Properties:
DbProxyName: !Ref DBProxy
TargetGroupName: default
InstanceIdentifiers:
- Fn::ImportValue: DBInstanceName # Name of your RDS instance
DependsOn: DBProxy
Database Connection
Now that you have your database, user setup and proxy created, you would like to make use of it in your code. Here is what you need to do (I used typeorm and mysql2 for managing the connections and types),
const connectionOptions = {
type: 'mysql',
host: config.database.host, // This is the RDS Proxy host and NOT the db host
port: 3306,
username: config.database.username, // This needs to be the same user you have stored in secretsmanager
database: config.database.name,
entities: [], // List of your entities
};
const getDBConfig = () => {
// Since we have IAM enabled on our proxy, we will make use of the token to authenticate and connect to the db.
const signer = new RDS.Signer({
region: config.region,
username: connectionOptions.username,
hostname: connectionOptions.host,
port: connectionOptions.port,
});
const token = signer.getAuthToken({
username: connectionOptions.username,
});
return {
...connectionOptions,
password: token,
extra: {
authPlugins: {
mysql_clear_password: () => (): string => `${token}\0`,
},
},
// Drop minimum TLS version supported to 1.0.
ssl: {
...sslProfiles['Amazon RDS'],
minVersion: 'TLSv1',
},
};
};
export const getDBConnection = async (): Promise<Connection> => {
const CONNECTION_NAME = 'default';
const connectionManager = getConnectionManager();
let connection: Connection;
if (connectionManager.has(CONNECTION_NAME)) {
connection = connectionManager.get(CONNECTION_NAME);
if (!connection.isConnected) {
connection = await connection.connect();
}
} else {
connection = await createConnection(getDBConfig());
}
return connection;
};
Not too bad. Seems pretty straightforward from configurations aspect.
Lambda
Your configuration is in place, now you just need to make your lambda do some db operations. It can be done simply like this,
export default const handler = async () => {
try {
const connection = await getConnection();
// Perform db operations using connection
...
} finally {
await (await getDBConnection()).close();
}
};
Simple!!! Yes, it is. But you will ask why are we explicitly closing the connection, shouldn't lambda take care of that for us? Well, the answer is Yes and No both.
So, if you let AWS take care of closing the proxy connection, that's fine but AWS will keep the connection open for some unknown time and that connection, if reused will run into expired token problem, given the IAM token is very short-lived. This will probably happen in a frequently requested service.
But on the other hand, if you close the proxy connection after each lambda execution explicitly, each new lambda execution will create a new connection to proxy and you will get a new token to use.
IAM Role 2
Next up is your lambda execution role. This role can be the same as the one you used earlier OR a different one. Whichever you choose to go with, just make sure you have this policy attached to it,
{
"Statement": [
{
"Effect": "Allow",
"Action": [
"rds-db:connect"
],
"Resource": [
"arn:aws:rds-db:us-east-1:123456789012:dbuser:prx-123456789/db-user"
]
}
]
}
Thing to note here is that you will be using the proxy resource id and NOT the RDS database resource id. DB user should be the same as what you stored in the secretsmanager.
Alternative to IAM Role 2
If you don't want to update the execution role, you can also specify the policy directly on your lambda function. You can do this via SAM like this,
SomeLambdaFunction:
Type: AWS::Serverless::Function
Properties:
Description: Database Operations Function
FunctionName: db-function
Handler: db.handler
Role: !Sub ${SomeRole.Arn}
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- rds-db:connect
Resource:
- !Sub arn:aws:rds-db:us-east-1:123456789012:dbuser:prx-123456789/db-user
That's it. RDS Proxy can be a great tool to use in a system where you need to do db heavy operations like multiple reads in parallel OR you have a system with a state machine fanning out multiple requests. This takes the connection pooling overhead out of the equation and lets you focus on the business side of things.
Top comments (1)
I couldn't make it work this way. Is there a specific version of typeorm that u are using ?