Cuando se trabaja con proyectos serverless y sobre todo lambdas en  AWS, existe la posibilidad de tener servicios en diferentes lenguajes de programación y utilizar diferentes estructuras de proyecto, hay dos enfoques principales mono-repo y multi-repo Ver Figura 1, los dos enfoques tienen diferentes ventajas, en este post nos centraremos en las ventajas del mono-repo, principalmente en como podemos aprovechar el código compartido

Figura 1: Mono-repo VS Multi-repo

Código compartido

Cuando se trabaja con lambdas y mono-repo, una de las ventajas es construir código en común y poder compartirlo entre las diferentes lambdas, hay dos formas de compartir el código en los mono-repos

  • Módulo global que se importan en el código y se utilizan: este caso es ideal para JavaScript por su forma de importar modulos, esto permite localizar el módulo fuera de la carpeta del proyecto y encontrar el código en una carpeta diferente, un ejemplo se podría ver así en la siguiente estructura de proyecto:
/
  package.json
  config.js
  serverless.common.yml
  libs/
  services/
    notes-api/
      package.json
      serverless.yml
      handler.js
    billing-api/
      package.json
      serverless.yml
      handler.js
    notify-job/
      serverless.yml
      handler.js

se podría crear dentro de libs un archivo llamado aws-sdk.js con el siguiente código

import aws from "aws-sdk";
import xray from "aws-xray-sdk";

// Do not enable tracing for 'invoke local'
const awsWrapped = process.env.IS_LOCAL ? aws : xray.captureAWS(aws);

export default awsWrapped;

y luego importarlo en cualquier handler de la siguiente forma

import AWS from '../../libs/aws-sdk';

esta forma de compartir código tiene algunas desventajas

  1. El código, cuando se despliega, se agrega dentro del código de cada proyecto, es decir, existirán en cada lambda el mismo módulo de js, lo que agrega más peso al despliegue del código
  2. No se puede aplicar en python, y esto porque python tiene un dolor de cabeza llamada el PYTHONPATH que es una variable de entorno global que utiliza el intérprete para buscar los módulos, no pueden existir más y es una ubicación en donde se instalan todas las librerías, por tanto, como cada proyecto tiene su entorno virtual, en desarrollo esta variable es modificada apuntando a ese entorno
  • Otra opción es utilizar layers, esto es un concepto propio de AWS que permite a cada lambda agregarle una capa de código y librerías que se pueden invocar dentro de la ejecución del código, una desventaja de este enfoque es que  la capa se inyecta cuando ya está construida la lambda y en desarrollo se pierde la posibilidad de tener acceso a ella en python, para ello veremos como solucionarlo

Layers en python

Utilizar layers en serverless ayuda entre muchas cosas a permitir reutilizar código,  reducir el tamaño del proyecto cuando se despliegue y  versionar las actualizaciones del código en el despliegue, este enfoque es muy útil para poder compartir código en python, como ya se vio en JS es mucho más fácil, lo primero que haré es generar un proyecto que contenga la siguiente estructura

Figura 2: Estructura proyecto de ejemplo

La carpeta user contendrá un proyecto de un código lambda y la carpeta shared/libs contendrá el layer, para ello en la carpeta libs agregaré los siguientes archivos

service: libs
frameworkVersion: '2'
provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
layers:
  sharedLibs:
    path: .
    compatibleRuntimes:
      - python3.8
    compatibleArchitectures:
      - x86_64
resources:
  Outputs:
    SharedLibsExport:
        Value:
          Ref: SharedLibsLambdaLayer
        Export:
          Name: SharedLibsLambdaLayer-${opt:stage, 'dev'}
serverless.yml
[tool.poetry]
name = "libs"
version = "0.1.0"
description = ""
authors = ["Jairo Castañeda"]
packages = [
    { include = "auxiliary" },
]

[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.dev-dependencies]


[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
pyproject.toml
from typing import Union


def convert_data(data:Union[int, float]):
    if isinstance(data, int) or isinstance(data, float):
        return str(data)
    return data
auxiliary/auxiliary.py

Hemos definido la configuración del layer, el archivo importante es el serverless.yml en donde se define el layer, el path que empaqueta y se define una variable de CloudFormation, esto es muy importante porque los layers se versionan como se ve en la Figura, entonces cada vez que despliegue un cambio se actualizara la versión haciendo que cambie el arn  y los proyectos o lambdas seguirán apuntando a una versión antigua aun así cuando se vuelvan a  desplegar, tocaría actualizar esto manualmente, al utilizar esta variable se actualiza automáticamente cuando se despliegue esa lambda

Figura 3: Versionamiento de layers

bien, para desplegar se ejecuta el comando de siempre sls deploy y obtenemos el siguiente resultado

Figura 4: Despliegue exitoso del layer

ahora para referenciar ese layer en el folder user, debemos hacerlo de forma local y cuando se despliegue, para esto se necesitaran dos configuraciones, una en el archivo serverless y otra en el archivo pyproject.toml como se aprecia a continuación

service: mylambdaprojectv2

frameworkVersion: '2'

provider:
  layers:
    - ${cf:libs-dev.SharedLibsExport}
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  region: us-east-1
  environment:
    PYTHONPATH: /opt

functions:
  register:
    handler: service.register.register
    timeout: 10
    events:
      - http:
          path: register
          method: post
          cors: true


package:
  exclude:
    - node_modules/**
    - venv/**


plugins:
  - serverless-python-requirements

serverless.yml
[tool.poetry]
name = "mylambdaproject"
version = "0.1.0"
description = ""
authors = ["Jairo Castaneda"]

[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.dev-dependencies]
libs = {path = "../shared/libs" }
boto3 = "^1.20.23"
black = "^21.12b0"
pytest = "^6.2.5"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
pyproject.toml
import json
import os
from logging import basicConfig, getLogger, INFO
from auxiliary import auxiliary

logger = getLogger(__name__)

def register(event, context):

    try:

        logger.info("User created")
        user = {"name": "Jairo", "age":auxiliary.convert_data(25)}

        return {"statusCode": 200, "body": json.dumps(user)}

    except Exception as error:
        logger.exception(error)
        return {"statusCode":400, "body":json.dumps({"error":"error"})}

service/register.py

La configuración importante, de nuevo, radica en serverless.yml para el despliegue y para poder importar la librería en desarrollo se configura el pyproject.toml, en el archivo serverlees.yml se pone  a apuntar al layer desplegado anteriormente

 layers:
    - ${cf:libs-dev.SharedLibsExport}

cf referencia una variable de CloudFormation que exporte anteriormente, esto con el fin de lograr una actualización de versiones de este layer de forma automática, igualmente también podría haber puesto directamente con el arn, seguidamente agrego la variable PYTHONPATH

  environment:
    PYTHONPATH: /opt

esto debido a que el layer se agrega a una carpeta /opt no directamente a las libs de python, con este fix los layers que se creen automáticamente quedan en los módulos que se importan en el código, y aquí vemos como se importa en el módulo register.py

from auxiliary import auxiliary

esta importación en desarrollo vendrá del módulo local, en donde en el  pyprojet.toml se le indica en la siguiente línea

[tool.poetry.dev-dependencies]
libs = {path = "../shared/libs" }

donde el path es la ubicación del módulo a empaquetar localmente, y en despliegue del layer queda referenciado en la configuración del .yml, ahora despliego y veo lo siguiente

Figura 5: Arquitectura de componentes de la lambda desplegada

Aquí se puede  apreciar el despliegue y se aprecia el layer, ahora pruebo por medio del API

Figura 6: Prueba del API de la lambda

como se aprecia se realiza la conversión del campo age de int a str correctamente, y aquí ya se ha conectado el código de la lambda con el layer, ahora si desea utilizar junto a layers de terceros, te dará error porque PYTHONPATH no puede ser /opt para layers de terceros, esto lo solucionaremos en un siguiente artículo

Conclusiones

Como se pudo apreciar, compartir código en Python requiere de layers; sin embargo, también requiere en desarrollo el empaquetado y la instalación local de ese paquete, y poetry realmente lo hace muy fácil porque automáticamente gestiona este proceso, sin obligarme a agregar ningún archivo adicional, por eso poetry sigue ganando puntos, al final si se quieren utilizar layers propios mezclados con layers de terceros no funcionara por el python path, esto definitivamente tiene solución,  donde en el próximo artículo espero hablar de ello

¿Te gusto el post y quisieras poder aplicar lo aprendido?

Únete al equipo de Simetrik AQUI