DEV Community

loading...

RDS Proxy via SAM

vikasgarghb profile image Vikas Garg ・5 min read

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.

Phoo, Phoo, Phoo
Phoo, Phoo, Phoo

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.

Can you keep the secret?
Can you keep the secret?

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

That's how you create it!!
That's how you create it!!

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.

Don't you say that...
Don't you say that...

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.

I am listening!!!
I am listening!!!

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.

BAM
BAM

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.

Bazingaaa
Bazingaaa

Discussion

pic
Editor guide