Blog de Amazon Web Services (AWS)
Aprovechando las nuevas extensiones de AWS Lambda
Por Servio Reyes, Arquitecto de Soluciones AWS México y
Iván González, Arquitecto de Soluciones AWS México
AWS lanzó su primer servicio de cómputo serverless (sin servidor) en el 2014 conocido como AWS Lambda, un servicio que permite la ejecución de código sin la necesidad de aprovisionar ni administrar servidores, altamente enfocado en la creación de micro-servicios. Y desde su inicio muchos más servicios se han incorporado a AWS como lo son AWS Fargate o Amazon Aurora Serverless con las ventajas del enfoque serverless. Siempre buscando crear soluciones basadas en las necesidades y retro-alimentación de los clientes que usan AWS. De ahí que no únicamente se han lanzado nuevos servicios, sino los existentes como AWS Lambda han mejorado e incorporado nuevas funcionalidades. Solo en lo que se lleva del año se han sumado nuevas funcionalidades como: integración con Amazon EFS, creación de funciones definidas por el usuario compatibles con Redshift y también la incorporación de las extensiones de AWS Lambda, funcionalidad que se describe a continuación.
Descripción de las extensiones de AWS Lambda
Las extensiones de AWS Lambda surgieron como respuesta a la solicitud de los desarrolladores con la finalidad de incorporar herramientas de monitoreo, seguridad y gobierno de una forma más directa dentro de las invocaciones de las funciones con lo cual pudieran centralizar el monitoreo en herramientas que ya poseían. Antes de las extensiones de AWS Lambda había dos principales formas de monitorear la función:
- Forma síncrona: Enviar los logs durante la ejecución de los elementos de la función, lo cual puede aumentar el tiempo de ejecución de laAWS Lambda. Derivando en una latencia mayor al usuario y también aumentando el costo de la invocación.
- Forma asíncrona: Después de la ejecución se envían los logs, principalmente usando AWS CloudWatch. Lo cual, si bien mejora la eficiencia de laAWS Lambda, requiere un post-procesamiento para filtrar los logs más relevantes (si así se desea) y enviarlos a la herramienta de monitoreo. Generando retrasos en la actualización de la información, considerando que el envío de logs a AWS CloudWatch puede aumentar el costo promedio de ejecución de la función. Por último, también está la posibilidad de perder los logs si el ambiente se cierra inmediatamente antes del envío de la información.
Las extensiones le permiten al desarrollador poder generar procesos separados con la finalidad que pueda enviar información de monitoreo de forma síncrona y también que pueda personalizar o preparar el ambiente de ejecución de la AWS Lambda antes de su invocación. Dividiéndose en dos tipos:
- Extensiones internas: Este tipo se puede identificar como wrappers del código, lo que significa que compartirán el mismo proceso y por tal motivo deberán ser del mismo lenguaje de programación que el de la función principal. Su propósito es modificar el inicio de la función, permitiendo agregar o modificar argumentos de ejecución, variables de ambiente, obtener y proporcionar datos secretos o datos necesarios para la adecuada ejecución de la función.
- Extensiones externas: Permiten un hilo de ejecución separado, pero manteniendo el mismo ambiente de ejecución de la función AWS Lambda. Esta separación permite que puedan ser ejecutadas en un lenguaje de programación diferente al establecido en la función. Además, que se pueden invocar antes y seguir ejecutándose después del tiempo de ejecución de la función.
El ambiente de ejecución (Execution Environment) principalmente se compone de los API Endpoints y los procesos (Processes). Donde los procesos son creados por la ejecución del código de la extensión, y el Runtime junto con la función. El servicio de AWS Lambda se comunica por medio de HTTP con el Runtime usando el Runtime API permitiendo la salida y entrada de información a otros elementos de la función o al servicio de AWS Lambda durante la existencia del proceso.
Por otro lado, las extensiones pueden comunicarse con los demás componentes de dos formas:
- Extensions API: Por medio de este HTTP API es que las extensiones reciben las señales del Runtime de la función, permitiéndoles ejecutar diferentes tipos de lógica dependiendo el estado general de la función.
- Logs API: Este HTTP API permite a la extensión enviar registros directamente a AWS CloudWatch o suscribirse para su recepción.
El nuevo ciclo de vida de AWS Lambda’s se compone de tres fases distintas:
- init: Dividido en tres etapas internas, esta separación permite ejecutar los diferentes componentes de manera independiente y en diferentes tiempos. Las etapas son:
- Inicialización de las extensiones.
- Inicialización del Runtime.
- Inicialización de la función.
- invoke: AWS Lambda ejecuta la función y el código de extensión en respuesta a los disparadores. En esta fase las extensiones pueden estar escuchando los eventos de vida de la función para obtener o enviar información, con la posibilidad incluso de influenciar el congelado y descongelado de la invocación. Esta etapa es la limitada por el tiempo de espera establecido en la configuración. Si se pone un tiempo máximo de espera de 30 segundos, tanto la función como las extensiones deberán acabar en ese tiempo.
- shutdown: Después de que se haya completado la ejecución de la función, en esta parte la extensión puede seguir ejecutándose para limpiar, enviar información y finalizar su proceso. Previo a la terminación de las extensiones el tiempo de ejecución envía una señal de apagado a las extensiones que puede servir para determinar el punto de término de toda la AWS Lambda. Esta fase está limitada a un máximo de 2 segundos.
Casos de uso
Dado el enfoque de seguridad con el que fueron creadas las extensiones sus principales casos de uso donde se recomienda implementarlas están relacionados con:
- Monitoreo: Generación de logs con información relacionada a recursos utilizados (CPU, memoria, red) y envío de información a otros servicios o peticiones web.
- Seguridad: Limitación de las acciones o comunicaciones que vaya a realizar el código en su ejecución.
- Configuración: Estandarizar el ambiente de ejecución de las funciones.
Adicionalmente, un beneficio que se puede derivar de su uso, es una optimización de costos. Dado que las extensiones en varios casos estarán corriendo como segundo plano de la función de invocación. De esta forma se reduce el tiempo de ejecución, comparado con el envío síncrono de información de monitoreo de las AWS Lambda’s.
Mejores prácticas
El modelo de precios de las extensiones es el mismo que el de las AWS Lambda’s, donde se cobra por la petición, memoria utilizada y tiempo de ejecución. En este caso el tiempo en paralelo de la extensión no se cobrará siempre y cuando sea menor o igual a la invocación de la AWS Lambda. Cuando el tiempo de proceso sea mayor se facturará sobre la duración completa del tiempo de ejecución. Siempre redondeando al milisegundo más cercano.
Al momento de la publicación de este blog las extensiones de AWS Lambda se encontraban en una fase preliminar donde solo se cobrará por el tiempo de ejecución en la fase de invocación (invoke). Aunque posteriormente la facturación de las extensiones irá sobre todas las fases de la función en donde se ejecuten (init, invoke y shutdown). Para conocer la política más actualizada de la facturación revise la sección de preguntas frecuentes de AWS Lambda.
Para aprovechar lo más posible los tiempos de ejecución se recomienda que se busque hacer los cambios de ambiente más relevantes a la ejecución. Para las extensiones externas usar paquetería compilada en paquetes binario auto-contenidos lo cual es más eficiente para la ejecución. Además de identificar los elementos a los que se les hará el monitoreo evitando enviar información posiblemente poco relevante para el seguimiento de la función. En conjunto con el hecho que las extensiones pueden ejecutarse después de completada la función es importante antes de desplegar en producción una función con extensiones verificar que estas finalicen adecuadamente y evitar costos extras de ejecución.
Características técnicas
Para hacer uso de las extensiones se tienen que tomar en cuenta las siguientes características:
- Las extensiones y la función comparten recursos como el CPU, memoria, almacenamiento y variables del entorno.
- Los permisos asignados por IAM son compartidos tanto por la función como las extensiones.
- Se pueden configurar un máximo de 10 extensiones.
- El máximo de capas ejecutando concurrentemente es de 5.
- Múltiples extensiones se pueden configurar en una misma capa respetando los límites anteriores.
- El uso de extensiones en conjunto con el código de la función descomprimido no puede exceder los 250 MB.
Extensiones de AWS Lambda y AWS EFS
En este ejemplo se usará un archivo de configuración tipo YAML que se encuentra en un Amazon EFS para cargar la ubicación y el nombre del archivo al que se enviarán los logs con información de la ejecución de la AWS Lambda. Siendo los logs manejados por una extensión externa de AWS Lambda.
Pre-Requisitos
- Para el correcto funcionamiento del ejemplo todos los servicios deberán estar en la misma región. En este ejemplo, todo se realizará en la región de N. Virginia (us-east-1).
- Se necesitará una instancia de AWS Cloud9con Amazon Linux 2 donde montar AWS EFS.
- Las instancias de AWS EFS,AWS Lambda y AWS Cloud9, que se creen, deberán de compartir el mismo grupo de seguridad para que se comuniquen adecuadamente.
- Instalar el manejador de NFS para Linux con sudo yum -y install nfs-utils
- Instalar la librería boto3 con sudo python3 -m pip install boto3
Arquitectura final del ejemplo
Pasos de desarrollo
- Creamos y editamos el siguiente archivo con el editor de su preferencia, en este caso usaremos el editor de Linux nano:
nano ~/environment/efs_creation.py
- Le agregamos el siguiente código. El cual creará un sistema de archivos en AWS EFS, sus destinos de montaje y el punto de acceso.
#!/usr/bin/python3 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import boto3, json, random, time, subprocess, time REGION_NAME = "us-east-1" def get_default_vpc(): """Get the default VPC id from the region and account""" client = boto3.client('ec2') vpcs = client.describe_vpcs(Filters=[{'Name' : 'isDefault', 'Values' : ['true',]}]) vpcs_str = json.dumps(vpcs) resp = json.loads(vpcs_str) data = json.dumps(resp['Vpcs']) vpcs = json.loads(data) return(vpcs[0]["VpcId"]) def get_subnets_ids(): """Get the subnet ids from the default VPC""" default_vpc = get_default_vpc() session = boto3.Session(region_name=REGION_NAME) ec2_resource = session.resource("ec2") ec2_client = session.client("ec2") subnet_ids = [] for vpc in ec2_resource.vpcs.all(): if vpc.id in default_vpc: for subnet in vpc.subnets.all(): subnet_ids.append(subnet.id) subnets_ids = [] for subnet in ec2_client.describe_subnets(SubnetIds=subnet_ids)["Subnets"]: subnets_ids.append(subnet.get("SubnetId")) return subnet_ids def create_efs(): """Create the EFS""" print("Creating EFS") client = boto3.client('efs', region_name=REGION_NAME) response = client.create_file_system( CreationToken=f'first-efs{random.random()}', PerformanceMode='generalPurpose', Encrypted=True, ThroughputMode='bursting', Tags=[{'Key': 'Name','Value': 'lambda-efs-test'}] ) time.sleep(5) return response.get("FileSystemId") def add_mounting_targets_and_access_point(efs_id): """Adds the mounting targets using each default subnet, ending with the creation of the access point""" print("Adding mounting targets") client = boto3.client('efs', region_name=REGION_NAME) subnets = get_subnets_ids() for subnet in subnets: client.create_mount_target(FileSystemId=efs_id, SubnetId=subnet) print("Adding access point") client.create_access_point( Tags=[{'Key': 'Name', 'Value': 'lambda-efs'}], FileSystemId=efs_id ) while True: print("Waiting for mounting points creation...") mounting_targets = client.describe_mount_targets(FileSystemId=efs_id,)["MountTargets"] if all(target["LifeCycleState"].lower() in "available" for target in mounting_targets): break time.sleep(10) def link_EFS(efs_id): bash_command = "mkdir efs" process = subprocess.Popen(bash_command.split(), stdout=subprocess.PIPE) process.communicate() def main(): efs_id = create_efs() add_mounting_targets_and_access_point(efs_id) print(f"Creation Completed.") link_EFS(efs_id) print(f"EFS linked. EFS link command: \nsudo mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport {efs_id}.efs.us-east-1.amazonaws.com:/ efs") if __name__ == "__main__": main()
- Ejecutamos el código con
sudo python3 ~/environment/efs_creation.py
- Ligamos el AWS EFS con nuestro ambiente de AWS Cloud9 con el comando que nos regresa el script. El cual es similar al siguiente:
sudo mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport fs-XXXXXXXX.efs.us-east-1.amazonaws.com:/ efs
- Ejecutamos el siguiente comando que dará permiso de escritura a nuestra AWS Lambda
sudo chmod 777 efs
- Creamos una carpeta en nuestra instancia de AWS Cloud9 llamada Dentro de la carpeta creamos otras dos carpetas: extensions y python-example-extension. Comandos:
a. mkdir ~/environment/Extensions_dir
b. mkdir ~/environment/Extensions_dir/extensions
c. mkdir ~/environment/Extensions_dir/python-example-extension
- Creamos y editamos el archivo con
nano ~/environment/Extensions_dir/extensions/python-example-extension
- El contenido será el siguiente script, el cual relaciona e invoca la extensión de AWS Lambda:
#!/bin/bash
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
set -euo pipefail
OWN_FILENAME="$(basename $0)"
LAMBDA_EXTENSION_NAME="$OWN_FILENAME" # (external) extension name has to match the filename
echo "${LAMBDA_EXTENSION_NAME} launching extension"
exec "/opt/${LAMBDA_EXTENSION_NAME}/extension.py"
- Creamos y editamos un nuevo archivo con
nano ~/environment/Extensions_dir/python-example-extension/extension.py
- El archivo py, tendrá la implementación de la extensión manejando los eventos recibidos del Runtime API. Su código:
#!/usr/bin/env python3 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import datetime import os import requests import signal import sys import datetime import fcntl from pathlib import Path # GLOBAL VARIABLES # extension name has to match the file's parent directory name LAMBDA_EXTENSION_NAME = Path(__file__).parent.name # Duration text string DURATION = "DURATION" def handle_signal(signal, frame): # if needed pass this signal down to child processes print(f"[{LAMBDA_EXTENSION_NAME}] Received signal={signal}. Exiting.", flush=True) sys.exit(0) def register_extension(): print(f"[{LAMBDA_EXTENSION_NAME}] Registering...", flush=True) headers = { 'Lambda-Extension-Name': LAMBDA_EXTENSION_NAME, } payload = { 'events': [ 'INVOKE', 'SHUTDOWN' ], } response = requests.post( url=f"http://{os.environ['AWS_LAMBDA_RUNTIME_API']}/2020-01-01/extension/register", json=payload, headers=headers ) ext_id = response.headers['Lambda-Extension-Identifier'] print(f"[{LAMBDA_EXTENSION_NAME}] Registered with ID: {ext_id}", flush=True) return ext_id def write_log(log_path, data_type, data): with open(log_path, 'a') as f: f.write(f"Lambda API: {os.environ['AWS_LAMBDA_RUNTIME_API']}, {data_type}: {data}\n") if data_type in DURATION: sys.exit(0) def process_events(ext_id): headers = { 'Lambda-Extension-Identifier': ext_id } time = datetime.datetime.now() log_path = "/mnt/efs/lambda_logs.txt" print("Downloaded data path for logs in EFS, path: ", log_path) while True: print(f"[{LAMBDA_EXTENSION_NAME}] Waiting for event...", flush=True) response = requests.get( url=f"http://{os.environ['AWS_LAMBDA_RUNTIME_API']}/2020-01-01/extension/event/next", headers=headers, timeout=None ) event = json.loads(response.text) if event['eventType'] == 'INVOKE': print(f"[{LAMBDA_EXTENSION_NAME}] Received INVOKE event. Exiting.", flush=True) write_log(log_path, "INVOCATION ON DATE", datetime.datetime.now()) elif event['eventType'] == 'SHUTDOWN': print(f"[{LAMBDA_EXTENSION_NAME}] Received SHUTDOWN event. Exiting.", flush=True) write_log(log_path, DURATION, datetime.datetime.now() - time) def main(): # handle signals signal.signal(signal.SIGINT, handle_signal) signal.signal(signal.SIGTERM, handle_signal) # execute extensions logic extension_id = register_extension() process_events(extension_id) if __name__ == "__main__": main()
- Cambiamos de directorio con
cd ~/environment/Extensions_dir/python-example-extension/
- Ejecutamos la descarga de las dependencias con
pip3 install "requests==2.24.0" -t .
- Cambiamos los permisos de ejecución de lo carpeta
sudo chmod -R 777 ~/environment/Extensions_dir/
- Cambiamos al directorio de Extensions_dir con
cd ~/environment/Extensions_dir/
- Empaquetamos el contenido con
zip -r extension.zip .
- Publicamos esta layer con la extensión usando el siguiente comando. El comando regresará un ARN de la extensión creada, la copiamos en un editor de texto para utilizarla posteriormente
aws lambda publish-layer-version \ --layer-name "python-example-extension" \ --region us-east-1 \ --zip-file "fileb://extension.zip" \ --query "LayerVersionArn" \ --output text \ --compatible-runtime python3.8
- Sin cerrar la pestaña con nuestro ambiente de AWS Cloud9. Abrimos la consola de IAM en una nueva pestaña. En el panel lateral seleccionamos Roles y posteriormente le damos clic en Crear un Rol.
- Elegimos el caso de uso para Lambda y clic en Siguiente: Permisos.
- En la barra de búsqueda ponemos AWSLambdaBasicExecutionRoley seleccionamos la política. Repetimos el paso anterior, pero con AmazonEC2FullAccess, seleccionamos la política y clic en Siguiente: Etiquetas.
- Saltamos el agregar etiquetas con clic en Siguiente: Revisar
- Ponemos en nombre de rol: AWSLambdaEFSExtension y clic en Crear un rol.
- Abrimos la consola de AWS Lambday creamos una nueva función.
- Seleccionamos Crear función desde cero, le damos un nombre, en este caso lambda-extension. Runtime Python 3.8, el rol de ejecución: Uso de un rol existente, y seleccionamos AWSLambdaEFSExtension.
- En la parte de Configuración avanzada seleccionar la misma AWS VPC que la de nuestro AWS EFS. Seleccionamos las subredes en las cuales las AWS Lambda’s podrán crearse y el grupo de seguridad. NOTA: El AWS EFS, la AWS Lambda y AWS Cloud9 deben de compartir el mismo grupo de seguridad para que se comuniquen adecuadamente.
- En la parte inferior clic en Crear una función.
- Una vez terminado el proceso de creación (puede tardar hasta 5 minutos). Asignamos la extensión a la AWS Lambda. Para esto en la pantalla principal de la AWS Lambda le damos clic a Layers:
- Abajo saldrá un cuadro como el siguiente y clic en Añadir una capa.
- En la siguiente ventana seleccionamos la opción de Especificar un ARN y en el campo ponemos el ARN qué copiamos en el paso 14 para crear la extensión. Le damos clic en Agregar.
- Ahora vincularemos AWS EFS a nuestra AWS Lambda, primero en la pantalla principal de la AWS Lambda en la parte inferior buscamos la opción de Sistema de archivos y clic en Agregar sistema de archivos. En la nueva pantalla de sistema de archivos seleccionamos el sistema de archivos que creamos, el punto de acceso y la ruta de montaje (esta será la forma en que AWS Lambda accede a los archivos) /mnt/efs. Clic en Guardar.
- De regreso en la pantalla principal, buscamos la sección de Configuración básica y cambiamos el tiempo de ejecución máximo (Tiempo de espera) a 30 segundos y clic en el Guardar:
- Seleccionamos Seleccionar un evento de prueba, en la parte superior de la configuración y luego Configurar eventos de prueba.
- Seleccionamos la plantilla de hello-world, le asignamos el nombre del evento prueba y lo creamos.
- Probamos la extensión dando clic en Probar.
- De ser exitosa la configuración debemos ver algo similar a lo siguiente:
- Regresando a nuestra instancia de AWS Cloud9 cambiamos al directorio
cd ~/environment/efs
, y de ser exitosa la ejecución ahí deberá existir un archivollamado txt
(abrir con nanolambda_logs.txt
) con un contenido similar al siguiente.
- ¡Felicidades has configurada tu AWS Lambda con extensiones de escritura usando Amazon EFS!
Extensiones disponibles para su implementación
Este es un ejemplo de los múltiples enfoques que se le podrían dar a las extensiones, pero muchos más casos prácticos se pueden derivar del propósito inicial de ellas. Una fuente de referencia de implementaciones es el repositorio de ejemplos en GitHub.
Diversos Partners de AWS han desarrollado extensiones dentro de sus soluciones para permitir la integración del monitoreo de las AWS Lambda’s con las plataformas ya existentes de muchas empresas y desarrolladores. Algunos son:
- AppDynamics
- Datadog
- Dynatrace
- Epsagon
- HashiCorp Vault
- Lumigo
- Check Point CloudGuard
- New Relic
- Thundra
- Splunk
Conclusión
El desarrollar usando AWS Lambda tiene los beneficios del enfoque serverless dándole a su negocio una gran agilidad en el desarrollo de sus ideas, una mayor elasticidad y escalabilidad para adaptarse a las demandas de sus clientes, así como una mejor utilización de los recursos. Además, que se obtienen las ventajas de un monitoreo, de sus ejecuciones, centralizado usando sus extensiones. Donde los beneficios de las extensiones incluyen reducción de costos al no necesitarse un daemon corriendo prolongadamente para la obtención de los logs, o menor uso de herramientas intermediarias para su proceso como lo podría ser AWS CloudWatch.
Las extensiones de AWS Lambda son herramientas que buscan facilitar la integración de herramientas relacionadas al monitoreo, seguridad y gobierno, manteniendo el enfoque serverless que tanto distingue a las AWS Lambda’s. Ya sea que uno desarrolle sus extensiones para configurar su ambiente de ejecución y/o su monitoreo, o que use herramientas de terceros. Características que irán cambiando y seguirán adaptándose de acuerdo a las nuevas necesidades que los desarrolladores vayan teniendo.
Autores
Servio Reyes es Arquitecto de Soluciones en AWS México.
Iván González es Arquitecto de Soluciones en AWS México.
Revisores técnicos
Arturo Velasco es Arquitecto de Soluciones Especialista en Media y Entretenimiento.
Rodrigo Cabrera es Arquitecto de Soluciones en AWS México.