Original article: How to Connect to AWS RDS from AWS Lambda

En este artículo, vamos a aprender cómo comunicarnos con AWS RDS desde AWS Lambda.

Vamos a usar AWS CDK (Cloud Development Kit), el cual es software, o marco de trabajo (del inglés framework) de código abierto, que te permite definir y crear infraestructura en la nube (cloud).

AWS CDK tiene soporte para muchos lenguajes de programación, incluyendo a TypeScript, Python, C#, Java y otros. Nosotros usaremos TypeScript en este tutorial.

Al desplegar usando el comando cdk deploy, tu código es transformado a plantillas CloudFormation y todos los recursos AWS correspondientes serán creados. Solo necesitas conocimientos básicos de CDK y TypeScript para seguir este tutorial. Claro que también necesitas una cuenta AWS para poder crear recursos AWS.

Puedes continuar aprendiendo más acerca de AWS CDK en la documentación oficial y también escribí una guía para principiantes en mi blog (en inglés).

Introducción a AWS Lambda y RDS

AWS Lambda es un servicio "serverless" de cómputo por eventos que te permite ejecutar código sin la necesidad de provisionar servidores.

AWS RDS es un servicio de base de datos relacional administrada que tiene soporte para varios sistemas RDMBS (Sistema de Bases de Datos Relacionales) como MySQL, Postgres, Oracle, SQL Server entre otros. AWS se encarga de los parches y el mantenimiento a estos servidores de bases de datos.

¿Porque usar RDS junto con Lambda?

AWS Lambda es un servicio de cómputo y no requiere o recomienda tecnologías específicas de almacenamiento de datos para su funcionamiento. De hecho, algunas de tus funciones lambda ni siquiera van a interactuar con almacenes de datos de algún tipo. Y si necesitaras utilizar una tecnología de almacenamiento de datos, podrías utilizar cualquier base de datos de acuerdo a tus necesidades.

Sin embargo, la mayoría de las arquitecturas serverless utilizan DynamoDB como almacén de datos sólo para reducir costos y eliminar la necesidad del mantenimiento de servidores de bases de datos.

DynamoDB es grandioso y tiene sus casos de uso aplicables. Pero usarla para todos los proyectos que involucren lambda no es posible por las siguientes razones:

Patrones de acceso dinámicos: Al usar DynamoDB tendrías que diseñar con anticipación tus propios patrones de consulta. Sin embargo, esto no siempre es posible dado que tu producto (y sus respectivos requerimientos) podría evolucionar en base a la retroalimentación de tu cliente.

Patrones de acceso limitado: DynamoDB no te brinda flexibilidad al escribir consultas. No puedes usar la funcionalidad group by en tus consultas como lo harías en un RDMBS. En cambio, necesitas exportar los datos y tener algún otro sistema que provea la funcionalidad deseada, en este caso la funcionalidad group by.

Base de datos existente: Si tienes una base de datos RDBMS no querrás migrar a DynamoDB a menos que haya una buena razón para hacerlo. Incluso si quieres usar DynamoDB necesitarías reescribir toda la capa de acceso de datos para poder usarla como remplazo de una base de datos relacional (RDBMS).

Ventajas de usar RDBMS:

Relaciones entre entidades: Las bases de datos relacionales (RDBMS) permiten las relaciones entre entidades. Puedes definir claves foráneas para restringir el almacenamiento de datos inválidos.

Patrones de acceso: Las bases de datos relacionales te permite el uso de patrones de acceso dinámico. Una nueva entidad puede ser traída casi sin la necesidad de realizar grandes cambios a los modelos existentes. Y tiene muchas funcionalidades cómo group_by para que no tengas necesidad de usar sistemas externos para facilitar estas funcionalidades.

Familiaridad con SQL: La mayoría de los desarrolladores están familiarizados con SQL para realizar consultas a las bases de datos y hay una gran variedad de dónde se puede seleccionar una, incluyendo Oracle, Postgres y MySQL.

Podrías elegir RDBMS si tienes los siguientes requerimientos:

  • Tienes una base de datos RDBMS existente y te gustaría adaptar computación serverless de AWS Lambda.
  • Ya cuentas con patrones dinámicos de acceso y no quieres cambiar mucho tus modelos existentes.

Habiendo cubierto los puntos anteriores, discutamos cómo vamos a conectarnos a RDS desde Lambda.

Arquitectura del Proyecto

En casi la totalidad de los casos, tu base de datos RDBMS será una sub-red (subnet) de la Nube Virtual Privada (VPC - Virtual Private Cloud), con la finalidad de que nadie del exterior pueda tener acceso. Dado que tu función Lambda contendrá lógica de negocios, también podría pertenecer a esta sub-red privada.

Usaremos Postgres cómo nuestra base de datos para este tutorial. Pero el proceso descrito aquí, es aplicable para cualquier base de datos relacional (MySQL, Oracle, MS SQL y otras) que podrías querer utilizar. La arquitectura será la misma.

Abajo puedes ver la arquitectura de este proyecto:

AWS-Lambda-RDS-Latest

Vamos a utilizar AWS CDK a modo de herramienta de tipo "Código cómo Infraestructura" para crear recursos AWS.

Cómo crear una nube virtual privada (VPC) para alojar nuestros Lambda y RDBMS

Vamos a crear 2 subnets, una pública y otra privada. En la privada vamos a alojar nuestra base de datos Postgres.

const vpc = new ec2.Vpc(this, 'VpcLambda', {
      maxAzs: 2,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'privatelambda',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
        {
          cidrMask: 24,
          name: 'public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
      ],
    });

Cuando creas una sub-red de tipo PRIVATE_WITH_EGRESS usando AWS CDK también creará un NAT Gateway (NAT: Network Access Translation, Gateway puerta de acceso o enlace) y la colocará en la sub-red pública.

El propósito de NAT Gateway es permitir sólo las conexiones de salida desde tu subnet privada hacia el internet. Nadie podrá iniciar conexiones con tu subnet privada desde el internet público.

¿Por qué usar NAT Gateway para la conectividad con internet?

Podrías estarte preguntando por qué es necesario una conexión a internet, si tenemos ambos servicios, Lambda y la base de datos RDS en la misma sub-red privada.

Secrets Manager es un servicio de AWS para el manejo y almacenamiento de datos secretos como contraseñas, certificados, etc. La contraseña para conectarse a la base de datos se encuentra almacenada en secrets manager y es accesible desde un endpoint (punto de acceso) público.  

Puedes usar ya sea NAT Gateway para tener acceso al endpoint público del servicio de secrets manager o puedes crear una endpoint a modo de interfaz, para conectar con el secrets manager usando la Red AWS sin salir al internet público.

Ambas formas generarían un costo, pero NAT Gateway puede ser reutilizada para realizar conexiones también desde Lambda (digamos si llamas a cualquier API externa), mientras que desde un endpoint-interfaz no sería posible hacerlo de esta manera.

Cómo crear una instancia de base de datos RDS para almacenar nuestros datos:

Para los propósitos de este tutorial vamos a usar una pequeña instancia. Pero para ambientes de producción lo más común es utilizar instancias de mayores tamaños.

Crearemos un nuevo grupo de seguridad, para de esta forma poder controlar quién tiene acceso a la instancia de la base de datos y a través de cual puerto.

const dbSecurityGroup = new ec2.SecurityGroup(this, 'DbSecurityGroup', {
      vpc,
    });

    const databaseName = 'cloudtechsimplified';

    const dbInstance = new rds.DatabaseInstance(this, 'Instance', {
      engine: rds.DatabaseInstanceEngine.postgres({
        version: rds.PostgresEngineVersion.VER_13,
      }),
      // optional, defaults to m5.large
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.BURSTABLE3,
        ec2.InstanceSize.SMALL
      ),
      vpc,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      }),
      databaseName,
      securityGroups: [dbSecurityGroup],
      credentials: rds.Credentials.fromGeneratedSecret('postgres'),
      maxAllocatedStorage: 200,
    });

El código CDK de arriba creará una instancia de la base de datos y la colocará en la subnet privada que ya hemos creado en la sección anterior.

El método fromGeneratedSecret creará el secreto en el servicio secrets manager pasándole el nombre de usuario como parámetro. Queremos que el nombre de usuario sea Postgres, por lo que será el valor que pasaremos.

Y finalmente, vamos a asignar un espacio de 200GB de almacenamiento para la base de datos.

Cómo configurar las propiedades de la función Lambda

Usaremos Node 16 para escribir y empaquetar nuestra función Lambda, y abajo te muestro las propiedades para la Lambda.

Queremos que el 'timeout' sea de 3 minutos en lugar de los 3 segundos que son por defecto, y queremos asignar un espacio de 256MB para la función Lambda.

Ya que el aws-sdk ya viene provisto por el ambiente de ejecución de lambda, vamos a excluir la liberaría aws-sdk al escribir nuestra lambda.

Hemos instalado el paquete npm llamado pg que nos es útil para comunicarnos con la base de datos Postgres y excluimos el paquete pg-native puesto que no lo necesitamos.

 const nodeJsFunctionProps: NodejsFunctionProps = {
      bundling: {
        externalModules: [
          'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
          'pg-native',
        ],
      },
      runtime: Runtime.NODEJS_16_X,
      timeout: Duration.minutes(3), // Default is 3 seconds
      memorySize: 256,
    };

A continuación, crearemos un grupo de seguridad para la función lambda. Nuestra lambda debe contener información acerca del "endpoint", el nombre de usuario y la contraseña para que la lambda pueda conectarse a la base de datos.

Vamos a pasar estos valores en forma de variables de entorno a la función lambda.

 const lambdaSG = new ec2.SecurityGroup(this, 'LambdaSG', {
      vpc,
    });

    const rdsLambdaFn = new NodejsFunction(this, 'rdsLambdaFn', {
      entry: path.join(__dirname, '../src/lambdas', 'rds-lambda.ts'),
      ...nodeJsFunctionProps,
      functionName: 'rdsLambdaFn',
      environment: {
        DB_ENDPOINT_ADDRESS: dbInstance.dbInstanceEndpointAddress,
        DB_NAME: databaseName,
        DB_SECRET_ARN: dbInstance.secret?.secretFullArn || '',
      },
      vpc,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      }),
      securityGroups: [lambdaSG],
    });

Nota Importante: No vamos a pasar la contraseña de la base de datos cómo variable de entorno, sino que pasaremos el ARN (Nombre de Recurso de Amazon) en su lugar y traeremos la mera contraseña de forma dinámica, (en tiempo de ejecución) desde el secrets manager al interior de Lambda para más y mejor seguridad.

Permisos de acceso a la contraseña de la base de datos para lambda

Aunque le hemos pasado el arn secreto (secret arn) en forma de variable de entorno, lambda debería tener los permisos necesarios para poder leer el secreto del secrets manager, en este caso la contraseña de la base de datos.

La línea de código de abajo provee de estos permisos:

dbInstance.secret?.grantRead(rdsLambdaFn);

La línea de código CDK anterior creará un rol para la lambda con 2 permisos (DescribeSecret y GetSecretValue) para el secrets manager, para que de esta forma nuestra lambda tenga permitido obtener el valor secreto (la contraseña de la base de datos) antes de intentar comunicarse con ella.

Es posible ver lo mismo desde la consola AWS en el servicio Lambda.

AWS Lambda Permissions for Secrets Manager
Permisos de AWS Lambda para Secrets Manager

Grupo de seguridad para la instancia de la base de datos RDS

No queremos permitir que la conexión a la base de datos esté disponible para todos, pero sí queremos que las conexiones desde lambda sean posibles.

El siguiente código CDK agrega una regla de ingreso que permite la conectividad a la instancia RDS desde nuestra función lambda a través del puerto 5432 (el puerto para la base de datos Postgres):

  dbSecurityGroup.addIngressRule(
      lambdaSG,
      ec2.Port.tcp(5432),
      'Lambda to Postgres database'
    );

Código de la función lambda para la comunicación con la base de datos

El código específico de la función lambda que le permite la comunicación con la base de datos es muy simple. Ya que estamos usando una base de datos Postgres, usaremos el paquete pg para comunicarnos con Postgres desde el ambiente nodejs.

Antes de iniciar la conexión a la base de datos, traeremos la cadena de texto desde el servicio secrets manager. Este texto secreto es una cadena de texto JSON que contiene tanto el nombre de usuario como la contraseña. Por lo que sólo necesitaremos usar JSON.parse() y tomar sólo la contraseña.

import * as AWS from 'aws-sdk';
import { Client } from 'pg';

export const handler = async (event: any, context: any): Promise<any> => {
  try {
    const host = process.env.DB_ENDPOINT_ADDRESS || '';
    console.log(`host:${host}`);
    const database = process.env.DB_NAME || '';
    const dbSecretArn = process.env.DB_SECRET_ARN || '';
    const secretManager = new AWS.SecretsManager({
      region: 'us-east-1',
    });
    const secretParams: AWS.SecretsManager.GetSecretValueRequest = {
      SecretId: dbSecretArn,
    };
    const dbSecret = await secretManager.getSecretValue(secretParams).promise();
    const secretString = dbSecret.SecretString || '';

    if (!secretString) {
      throw new Error('secret string is empty');
    }

    const { password } = JSON.parse(secretString);

    const client = new Client({
      user: 'postgres',
      host,
      database,
      password,
      port: 5432,
    });
    await client.connect();
    const res = await client.query('SELECT $1::text as message', [
      'Hello world!',
    ]);
    console.log(res.rows[0].message); // Hello world!
    await client.end();
  } catch (err) {
    console.log('error while trying to connect to db');
  }
};

Y finalmente, vamos a ejecutar una simple consulta select en nuestra base de datos.

Cómo probar y comprobar el proyecto

Ahora ya podrás ingresar a tu consola AWS para la realización de pruebas. Selecciona el servicio Lambda y luego selecciona tu función lambda, en nuestro caso sería rdsLambdaFn.

Para este tutorial no necesitas preocuparte por la propiedad lambda event ya que no la vamos a utilizar en el código de nuestra función. Haz click en el botón de prueba "Test" y podrás ver los logs.                

Test Lambda function
Comprobando (Test) la función Lambda

El problema de rendimiento

Lambda es bien conocido por funciones cuya ejecución es de corta duración. De hecho, el límite máximo de "timeout" es 15 minutos.

Cómo puedes ver en el código de la función, estamos iniciando la conexión a la base de datos cada vez que la función lambda es invocada.

Dependiendo en el evento fuente de la lambda (digamos la fila SQS), esto podría crear conexiones a un ritmo mayor y se desconectaría al finalizar la función lambda.

Esto incrementa significativamente la carga al servidor de la base de datos RDS, lo que a su vez reduce el rendimiento. ¿Cómo arreglar este problema entonces?

Cómo usar RDS Proxy

Como alternativa a la creación directa de conexiones desde lambda hacia la base de datos, podemos tener una Proxy a RDS como intermediario entre lambda y la base de datos RDS.

El propósito del proxy RDS es mantener un conjunto de conexiones para que cualquier consumidor pueda conectarse al proxy y en su momento, a la base de datos. Nótese que aquí, no estaríamos creando una conexión, sólo obteniendo una conexión ya creada.

AWS-RDS-Proxy-Logical-1
Usando la Proxy RDS con Lambda

Hay 2 ventajas al usar este método:

  1. Reduce la carga al servidor de la base de datos: Dado que no necesitamos crear una conexión para cada invocación lambda en el servidor, la carga se reduce significativamente.
  2. Rendimiento lambda mejorado: Desde lambda, sólo estaremos recibiendo una conexión al proxy RDS y no estaremos creando una nueva conexión a la base de datos, lo que mejora el rendimiento de la función lambda.

Cambios requeridos para usar el RDS Proxy

No necesitamos realizar grandes cambios a nuestra arquitectura ni a nuestro código. Sólo hay que hacer un par de cosas:

  • Crear la proxy RDS y asociarle el grupo de seguridad de la base de datos que hemos creado anteriormente.
  • Actualizar la variable de entorno del endpoint lambda para que la función pueda conectarse al proxy RDS y no así a la base de datos RDS directamente.

No es necesario cambiar el código de la función lambda.

Arquitectura actualizada

Abajo puedes ver el diagrama actualizado de la arquitectura.

RDS Proxy with Lambda - Architecture
RDS Proxy con Lambda - Arquitectura

Cómo crear el proxy RDS

Necesitamos crear el proxy RDS y agregar la instancia de la base de datos cómo el objetivo del proxy.

const dbProxy = new rds.DatabaseProxy(this, 'Proxy', {
      proxyTarget: rds.ProxyTarget.fromInstance(dbInstance),
      secrets: [dbInstance.secret!],
      securityGroups: [dbSecurityGroup],
      vpc,
      requireTLS: false,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      }),
    });

Nótese que también estamos pasando el secreto de la base de datos al proxy pues este será responsable de mantener las conexiones. Estamos usando el mismo grupo de seguridad de la base de datos, como hicimos para poder abrir el puerto 5432.

Cómo actualizar el endpoint para lambda

No es necesario que modifiquemos el código de la función lambda. Sólo necesitamos actualizar el endpoint que lo hemos pasado como variable de entorno.

 environment: {
        DB_ENDPOINT_ADDRESS: dbProxy.endpoint,
        DB_NAME: databaseName,
        DB_SECRET_ARN: dbInstance.secret?.secretFullArn || '',
      },

No haremos ningún otro cambio.

Mejoras de rendimiento

Cuando realizas pruebas a tu función lambda, puedes ver que se conecta al proxy, en vez de a la instancia de la base de datos (ya que estamos imprimiendo la información del endpoint como host).

También debes notar que el rendimiento mejora significativamente. Antes tomaba cerca de 500ms (milisegundos), ahora sólo le toma 50ms..

Performance of lambda with RDS Proxy
Rendimiento de lambda con RDS Proxy

Nótese que podría tomar tiempo adicional cuando se obtiene la conexión inicial del proxy RDS, pero obtener las subsecuentes conexiones, será rápido como se muestra arriba.

Conclusión

Espero que este tutorial te haya ayudado a aprender cómo realizar una conexión a RDS desde Lambda.

Gracias por leer hasta este punto, yo escribo acerca de AWS Lambda, Fargate, CI/CD Pipeline y Tecnologías Serverless en  https://www.cloudtechsimplified.com. Si te interesa puedes suscribirte aquí.