Escribir todo el código de lógica del dominio empresarial en la función de entrada del controlador Lambda puede resultar muy tentador. He escuchado muchas excusas durante la revisión del código: "Son solo unas pocas docenas de lÃneas de código" o "Aún es muy legible".
Pero seamos realistas: cuando desarrollaste servicios regulares que no eran Serverless, no escribiste todo el código de servicio en un solo archivo o función.
Modeló su código en clases y módulos y asignó a cada uno una única responsabilidad basada en los principios SOLID .
La escritura de funciones Lambda no deberÃa ser diferente. Su función de controlador no deberÃa contener la lógica del dominio empresarial real ni deberÃa acceder a su tabla DynamoDB. Y existen buenas razones para ello, que abordaremos en esta publicación.
En esta publicación del blog, aprenderá a escribir código de función de AWS Lambda que contenga tres capas arquitectónicas: el controlador, la lógica y la capa de acceso a datos. Estas capas dan como resultado un código bien organizado que es más fácil de mantener y de probar y, como resultado, genera menos errores en el futuro. Además, analizaremos el manejo de errores.
Puede encontrar un proyecto de servicio Python Serverless complementario que implementa estos conceptos aquà .
Tabla de contenido
Capas de arquitectura de la función AWS Lambda
Las capas de arquitectura de AWS Lambda son los componentes básicos de las aplicaciones sin servidor que permiten a los desarrolladores escribir código reutilizable, organizado y fácil de mantener con una única responsabilidad.
Estos principios combinan bien con los principios SOLID, donde la "S" representa responsabilidad única.
Encapsular un único código de responsabilidad en una única capa reduce la SÃndrome del código espagueti . Por ejemplo, imagine un caso de uso en el que las llamadas de API a su base de datos están dispersas en archivos y funciones no relacionados. Ahora imagine un escenario mejor en el que todas las llamadas de API a su base de datos residen en un solo módulo o archivo, una sola capa. La única responsabilidad de esta capa es manejar las conexiones y las llamadas de API a la base de datos. Esta encapsulación y responsabilidad única la hacen:
Es fácil compartir el código entre múltiples funciones Lambda, es decir, llamarlo desde diferentes funciones: cero duplicación de código.
Probar el código de responsabilidad única, es decir, probar todas las API relacionadas con la base de datos y los casos extremos en un solo módulo.
Es más fácil mantener el código. Cuando desea cambiar una llamada de API o agregar un caché, realiza los cambios en un módulo o archivo en lugar de hacerlo en varios archivos dispersos con posible duplicación de código.
Creo que hay tres capas en las funciones de AWS Lambda:
La capa del controlador
La capa lógica
La capa de acceso a datos (DAL)
En Python, una capa es un módulo reutilizable, es decir, un conjunto de archivos.
Manejo de errores entre capas
Hay dos métodos: generar una excepción o devolver un valor de tipo Ninguno o Verdadero/Falso para marcar un éxito o un fracaso. Probé el método de valor de retorno en uno de mis servicios y se complica rápidamente. Hay una gran discusión en Stack Overflow y te sugiero que la consultes. En Python, las excepciones son la forma Pythonica de marcar que algo salió mal, pero debes asegurarte de capturar todas las excepciones. Las excepciones dejan de procesarse rápidamente en las distintas capas.
Sin embargo, generar excepciones especÃficas de una capa a otra puede romper su concepto de responsabilidad única. ¿Por qué las capas lógicas o de controlador deberÃan estar familiarizadas con las excepciones de DynamoDB? No deberÃan. Por eso deberÃa usar excepciones personalizadas que marquen su tipo: error interno del servidor, excepción de solicitud incorrecta, etc.
¿Cómo funciona en la práctica? Cada capa es responsable de capturar sus excepciones especÃficas, registrarlas y volver a generarlas como una de las excepciones personalizadas relevantes con el seguimiento de la pila.
Pero, ¿quién detecta estas excepciones personalizadas? La capa del controlador las detectará, ya que es la capa que sabe cómo convertir estas excepciones en una salida relevante para el autor de la llamada. Lo analizaremos con más detalle en la sección sobre la capa del controlador.
Ejemplo de servicio sin servidor
Repasemos nuestro servicio 'Orden' de muestra y analicemos de qué es responsable cada capa.
El proyecto de plantilla que utilizaremos es un servicio de 'pedidos' simple.
Tiene una API GW que activa una función AWS Lambda en la ruta POST /api/orders.
Almacena todos los pedidos en una tabla de Amazon DynamoDB.
También implementa y almacena configuraciones dinámicas y marcas de caracterÃsticas en AWS AppConfig.
Lea más sobre esto aquà .
El código completo se puede encontrar aquà .
La capa del controlador
La capa del controlador es la función de entrada que se llama cuando se invoca la función.
Tiene varias responsabilidades:
Cargar y verificar la configuración desde las variables del entorno
Inicializar utilidades globales como registrador, trazador, manejador de métricas, etc.
Manejar la validación de entrada
Pasar la entrada a la capa lógica para continuar manejando la solicitud
Devolver la salida de la capa lógica al llamador
Como puede ver, existe una segregación completa de responsabilidades entre el controlador y otras capas. El controlador carga la función, se asegura de que esté configurada correctamente y delega la entrada a la capa lógica que sabe qué hacer con la información.
Luego, cuando recibe el resultado de la capa lógica, lo devuelve al autor de la llamada de una manera que solo ella conoce. Ninguna otra capa creará la respuesta del autor de la llamada. En nuestro ejemplo, la función Lambda devuelve una respuesta JSON al autor de la llamada. Por lo tanto, solo el controlador creará el JSON y asignará el código de respuesta HTTP correcto. Esa es su responsabilidad.
Manejo de errores
La capa de control detectará cualquier excepción generada debido a configuraciones incorrectas, errores de utilidad global y entradas no válidas, y devolverá la respuesta correcta al autor de la llamada. Si la capa de control se activa mediante un SQS, la capa de control puede enviar la entrada no válida a una cola de mensajes no entregados o generar una excepción para devolver la carga útil a la cola.
La capa del controlador también es necesaria para capturar cualquier excepción global generada por la capa de acceso a datos o lógica (consulte la explicación en la sección de manejo de errores).
Veamos el controlador "crear pedido" del servicio "Orden". La función Lambda "crear pedido" crea nuevos pedidos para los clientes y los guarda en la tabla DynamoDB.
En las lÃneas 22 a 26, inicializamos nuestras utilidades globales: variables de entorno validadoras, trazadoras y utilidades de métricas para la invocación actual y establecemos el identificador de correlación del registrador.
En las lÃneas 31 a 36, obtenemos nuestra configuración dinámica de AWS AppConfig y analizamos la respuesta para asegurarnos de que trabajamos con la configuración correcta. Cualquier error provocará que se envÃe una respuesta HTTP de error interno del servidor al autor de la llamada.
En las lÃneas 38 a 44, analizamos y validamos la entrada de acuerdo con nuestro esquema de entrada. Cualquier error provocará que se envÃe un error de solicitud HTTP BAD al autor de la llamada (como deberÃa).
En las lÃneas 48 a 55, enviamos la entrada del cliente a la capa lógica. El controlador no sabe cómo se maneja la entrada; simplemente la delega a la siguiente capa. La capa lógica devuelve un objeto 'CreateOrderOutput'.
En la lÃnea 58, el controlador devuelve 'CreateOrderOutput' al llamador como una respuesta HTTP OK 200 con un cuerpo JSON creado a partir del objeto 'CreateOrderOutput'.
La capa lógica
Como su nombre lo indica, la capa lógica contiene todo el código lógico, las funciones y los módulos del dominio empresarial necesarios para procesar la solicitud. Es donde ocurre la magia. La capa lógica puede y debe ser compartida por varios controladores Lambda.
En el contexto de nuestro servicio de "pedidos", la capa lógica puede tener funciones de Python como "crear pedido", "obtener pedido", "actualizar pedido" y "eliminar pedido". Cada función representa un caso de uso empresarial encapsulado y contiene las validaciones necesarias, las comprobaciones lógicas, los indicadores de funciones y, en última instancia, el código que lo implementa.
Las capas lógicas obtienen una entrada de sus parámetros requeridos de la capa del controlador que la llama, principalmente una mezcla de la configuración requerida (nombre de la tabla de las variables del entorno) y el parámetro de entrada del cliente (en nuestro caso de uso: la cantidad de productos a comprar y el nombre del cliente).
La capa lógica maneja la solicitud, llama a la capa de acceso a datos (si es necesario) y devuelve un objeto de salida a la capa de controlador. La capa lógica NO accede directamente a ninguna base de datos; siempre delega la acción requerida a través de interfaces definidas a la capa de acceso a datos.
Relación con la capa de acceso a datos
La capa lógica es la única capa que llama a la capa de acceso a datos. Como tal, está familiarizada con la implementación de su interfaz concreta; en nuestro caso, inicializará un controlador de capa de acceso a datos de DynamoDB y proporcionará a su constructor el nombre de la tabla de DynamoDB llamando a una función de obtención que devuelve un objeto que implementa la interfaz DAL.
La capa lógica es la única capa familiarizada con el esquema de salida del controlador y el esquema de entrada de la base de datos DAL. En nuestro caso, creamos un nuevo pedido y devolvemos el ID del pedido, el nombre del cliente y la cantidad de productos comprados. El ID del pedido se genera en la capa de acceso a datos y representa una clave de tabla principal de DynamoDB. La capa lógica llamará a la función de interfaz 'create_order_in_db' en la capa DAL, obtendrá su objeto de salida y lo convertirá en el objeto de esquema de salida requerido.
Importante : no utilice el mismo esquema para la entrada de la base de datos y la salida, ya que de ese modo se producirÃa un acoplamiento entre ellas. Las capas DAL y del controlador deben permanecer desacopladas, por lo que no necesitará cambiar su API cuando agregue un campo al esquema de entrada de la base de datos. Además, normalmente, la entrada de la base de datos contiene metadatos que pertenecen a la capa DAL pero que no deben devolverse al llamador de la API REST. La función de conversión entre la entrada DAL y el esquema de salida filtrará los campos no deseados.
Repasemos algunos ejemplos de código:
En la lÃnea 11, creamos un nuevo controlador de capa DAL llamando a una función de obtención de controlador DAL desde el módulo de controlador DAL de DynamoDB. La función se define en la capa DAL en la implementación concreta de DynamoDB. La clase implementa las funciones de interfaz DAL: 'crear pedido', 'obtener pedido', etc.
En la lÃnea 12, llamamos a la función de interfaz DAL "create_order_in_db" y guardamos el nuevo pedido. La capa lógica trabaja con un objeto que implementa la interfaz y no está familiarizada con ninguna implementación interna que no sea la función de inicialización (obtención).
El ID del pedido se genera en la capa DAL. En este ejemplo, no existe ninguna lógica particular en torno a la creación del pedido, salvo guardarlo en la tabla DynamoDB.
En la lÃnea 14, convertimos la entrada DAL al esquema 'CreateOrderOutput'. Como puede ver, dado que se trata de un ejemplo simple, son idénticos; sin embargo, como se mencionó anteriormente, en casos de uso más avanzados, solo se devuelve un subconjunto del esquema DAL al llamador.
Las definiciones de ambos esquemas se muestran a continuación:
'OrderEntry' se define en la carpeta de esquemas de la capa DAL, ya que representa una entrada de base de datos. Contiene el ID del pedido, el nombre del cliente y la cantidad de productos pedidos.
'CreateOrderOutput' se define en la carpeta de esquema de los controladores.
Capa de acceso a datos (DAL)
La capa de acceso a datos es la única capa que accede a la infraestructura de la base de datos real, crea las conexiones, está familiarizada con los detalles de implementación de la base de datos y llama a sus API.
La capa DAL presenta una interfaz para todas las acciones de base de datos requeridas por la capa lógica y un controlador de base de datos concreto que la implementa.
El tipo de base de datos y la implementación se abstraen mediante el controlador de base de datos concreto que hereda esta interfaz. El uso de la interfaz representa otro principio de SOLID, la inversión de dependencia .
Nuestro servicio 'order' tiene una interfaz de acciones de base de datos que contiene una función 'create_order_in_db' y en otro archivo en la capa, una clase controladora DAL de DynamoDB que implementa la interfaz.
¿Por qué deberÃa utilizar una interfaz y un controlador de base de datos que la implemente?
Esta interfaz simplifica la sustitución de la base de datos en el futuro. Todo lo que necesita hacer es crear un nuevo controlador que herede de la interfaz. Por supuesto, deberá manejar la parte de IaC (infraestructura como código), crear los recursos y establecer el rol de la función Lambda con los nuevos permisos, pero en cuanto al código de la aplicación, todo está encapsulado en una nueva clase de controlador de base de datos. Una vez que el nuevo controlador esté listo, simplemente configure la capa lógica para crear una nueva instancia del nuevo controlador de base de datos y úselo. Incluso puede usar ambos como una fase de prueba hasta que tenga la suficiente confianza de que está funcionando correctamente.
Echemos un vistazo al ejemplo de código de interfaz:
En la lÃnea 9, definimos el pedido creado en la función de base de datos. Cada implementación de base de datos guardará el nombre del cliente y la cantidad de artÃculos del pedido, pero con diferentes llamadas a la API. Las funciones de interfaz futuras pueden incluir obtener un pedido por una identificación, actualizar un pedido y eliminar un pedido.
Aquà está el ejemplo de código completo para una clase concreta que implementa la interfaz para un controlador DAL de DynamoDB:
En la lÃnea 16, heredamos la clase abstracta, implementando asà la interfaz en Python.
En la lÃnea 28, implementamos la función de la interfaz 'create_order_in_db'.
En la lÃnea 29, generamos un ID de pedido que sirve como clave principal de DynamoDB.
En la lÃnea 32, creamos un objeto de entrada para insertar en la tabla.
En la lÃnea 34, creamos un objeto de tabla DynamoDB boto3. Almacenamos en caché el recurso (en las lÃneas 22 a 25) durante hasta 5 minutos para mantener una conexión activa con la tabla entre invocaciones como una optimización del rendimiento.
En la lÃnea 35, insertamos el nuevo orden en la tabla.
En las lÃneas 37 a 39, manejamos las excepciones. Como se mencionó anteriormente , registramos la excepción y la volvemos a generar como una excepción global: una excepción de error interno del servidor que la capa del controlador detectará y manejará.
En la lÃnea 42, devolvemos la entrada creada a la capa lógica.
En las lÃneas 45 a 47, creamos un objeto controlador de DAL de DynamoDB concreto y usamos el decorador 'lru_cache' para convertirlo en una instancia de clase singleton de modo que pueda reutilizarse en múltiples invocaciones. Este es el controlador que usa la capa lógica. Es la única implementación de DAL concreta con la que está familiarizada.