En mi artículo anterior me adentré un poco a hablar sobre layers en serverless en python, hoy continuaremos esa misma línea y trataremos de solucionar uno de los errores más frecuentes que ocurre al crear un layer propio e intentar usar layers de terceros, esto nos pondrá en una tarea de solucionar un error, pero generar 10 más, como se puede apreciar en la Figura 1

Figura 1: Parcheando errores

Layers en serverless

Las layers en AWS ofrecen una gran ventaja, esto porque permiten crear código compartido y separarlo del despliegue del código de la lambda, lo cual permite quitar peso al archivo que se construye a la hora de realizar un despliegue, si bien en serverless para nosotros es transparente, por debajo serverless en cada despliegue genera estos archivos .zip con todas las dependencias, otro beneficio de los layers es la capacidad de versionar librerías, haciendo actualización al código del layer sin dejar que el lambda que ya funcionaba tenga que actualizar de inmediato a la nueva versión, una estructuración de los layers se aprecia en la siguiente Figura

Figura 2: Layers en AWS Obtenida de https://aws.amazon.com/blogs/compute/working-with-aws-lambda-and-lambda-layers-in-aws-sam/

Como se puede ver, los layers se agrupan dentro del mismo entorno de ejecución, por lo tanto, comparte con la lambda recursos como variables de entorno, configuración, memoria, etc. Esto es bueno porque dentro de estos layers se podría obtener información cargada por la lambda y realizar diferentes operaciones, sin embargo, estos layers de terceros se ubican dentro de una carpeta llamada /opt/python, pero cuando es un layer propio se ubica solamente en la carpeta /opt, con esto tenemos un problema porque Python puede cargar los módulos solamente desde una ubicación, la cual busca en el PYTHONPATH

Layers compartidos y  PYTHONPATH

Entendida la teoría del porqué sucede, pasemos a lo importante, la solución, AWS la plantea muy sencillo, donde básicamente se plantea que existen unos PATH para cada runtime (python, javascript, java) donde el entorno de ejecución buscara los módulos, esta clasificación se aprecia en la Figura 3

Figura 3: Tabla de configuración de módulos en los layers

En la Figura se ve que:

  • Para Node.js buscará las librerías en el path nodejs/node{#version}/node_modules
  • Para Python buscará las librerías en el path
    python/lib/python#version/site-packages

con esto ya sabemos donde debemos guardar nuestro módulo compartido en nuestro layer, ahora como hacemos para guardarlo en ese folder en específico?, básicamente debemos empaquetar nuestro módulo en una carpeta llamada python y para Node.js en una carpeta llamada nodejs/node_modules, probemos esto mismo en código, para ello vamos a utilizar la misma estructura del post anterior con algunos cambios

Figura 5: Estructura del layer
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
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.py
[tool.poetry]
name = "libs"
version = "0.1.0"
description = ""
authors = ["Jairo Castañeda"]


[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

los cambios aquí son pocos, básicamente cambié la carpeta que contiene el código a src para que poetry lo agregue de forma automática, ahora voy a agregar la siguiente configuración adicional a la lambda que usara el layer

service: mylambdaprojectv2

frameworkVersion: '2'

provider:
  layers:
    - ${cf:libs-dev.SharedLibsExport}
    - arn:aws:lambda:us-east-1:770693421928:layer:Klayers-p38-pandas:1
    
  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
user/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"
user/pyproject.toml
import json
import os
from logging import basicConfig, getLogger, INFO
import auxiliary
import pandas as pd


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"})}
user/register.py

Ami lambda de user, agregue algunas configuraciones en el serverless.yml, agregue un layer público que agrega a pandas una librería en python, cambie el import porque ahora está dentro del folder src, agregue también al archivo pyproject.toml pandas (debe estar en dev-dependencies)  y la  importe en el archivo register.py, ahora despliego y pruebo como resultado se ve en la siguiente Figura

Figura 6: Error al ejecutar la lambda user/register

efectivamente, nos da error, este error les aseguro que puede tomar horas encontrarlo, porque hay muchos componentes que pueden estar generando la excepción, al final en este caso el problema es a nivel conceptual, pues se debe entender como funciona Python, cambiamos el nombre de la carpeta en nuestro módulo llamado src en shared/libs a python y en el serverless.yml de la lambda user quitamos la variable PYTHONPATH

Figura 7: Estructura aceptable por AWS del layer

nos quedaría el serverlees.yml del user de la siguiente forma, sin el PYTHONPATH

service: mylambdaprojectv2

frameworkVersion: '2'

provider:
  layers:
    - ${cf:libs-dev.SharedLibsExport}
    - arn:aws:lambda:us-east-1:770693421928:layer:Klayers-p38-pandas:1
    
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  region: us-east-1

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

probamos el servicio desplegado

Figura 8: Invocación desde correcta

y todo funciona perfecto, pero ahora para seguir trabajando en local, debo cambiar de nuevo el nombre de la carpeta python a src, esto lo vamos a solucionar con un script

Escribiendo un script

Ya hablé hace un tiempo de hooks, estos hooks  son código que ejecutan en cierto momento del ciclo de vida del despliegue de serverless, esto es lo que vamos a hacer antes del empaquetado, con el script vamos a cambiar el nombre de src a python y después del empaquetado lo cambiaremos o lo dejaremos con el nombre de src, crearé una carpeta scripts con dos archivos en python

import os

LOCATION_PATH = os.path.dirname(__file__)
path_python = f"{LOCATION_PATH}/../python"
path_src = f"{LOCATION_PATH}/../src"


def rename_folder_after():
    try:
        os.rename(path_python, path_src)
        print("Success")
    except Exception as error:
        print(f"The following error has occurred {error}")
        exit(1)


rename_folder_after()
rename_folder_after.py
import os
LOCATION_PATH = os.path.dirname(__file__)
path_python = f"{LOCATION_PATH}/../python"
path_src = f"{LOCATION_PATH}/../src"


def rename_folder_before():
    try:
        os.rename(path_src, path_python)
        print("Success")
    except Exception as error:
        print(f"The following error has occurred {error}")
        exit(1)

rename_folder_before()
rename_folder_before.py
service: libs
frameworkVersion: '2'
custom:
  scripts:
    hooks:
      'before:package:initialize': python3 scripts/rename_folder_before.py
      'before:package:finalize': python3 scripts/rename_folder_after.py
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'}

plugins:
  - serverless-plugin-scripts

para agregar el serverless plugin deben de instarlo con el siguiente comando

npm install --save serverless-plugin-scripts

Los archivos scripts lo que hacen es renombrar la carpeta src a python cada vez que se ejecute sls deploy, facilitando así el despliegue y el trabajo de estar renombrado la carpeta, con esto ya se puede tener los layers  propios junto con layers compartidos sin tener que cambiar la variable PYTHONPATH

Conclusiones

Los layers son muy importantes en serverless, estos errores son errores muy difíciles de encontrar, porque hay muchas herramientas de por medio y cualquiera puede ser la responsable, en estos casos de errores en módulos es de vital importancia entender como funcionan en Python, para descartar errores generados en alguna configuración, como sucedía en el caso de modificar la variable PYTHONPATH, que un principio fue funcional, pero después ocasiono inconvenientes. El ciclo de vida de serverless brinda un apoyo importante para estas transformaciones sencillas, y siempre son un buen aliado para  implementar el continuos deployment

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

Únete al equipo de Simetrik AQUI