Uno de los desafíos que encontré no resuelto es cómo respaldar las funciones, capas y configuraciones de las funciones lambda que iba desarrollando. Me puse en la situación de desplegar la misma función en otro ambiente, o tal vez migrar a contenedores o cambiar de proveedor de cloud. O simplemente prepararme para una pérdida total de dato.
¿Como podría obtener una copia de mis funciones de forma sencilla?
Investigando la sdk de aws para python descubrí boto3 que se encarga de ejecutar los comandos de api de cada uno de los servicios. Uno de estos comando puede traer una lista de las funciones y una lista de las capas creadas bajo la cuenta que realiza la petición.
Repositorio
La función es bien sencilla además se puede implementar como función lambda, si no quieres leer la explicación puedes clonar directamente el repositorio.
TL;DR:
ensamblador
/
backup_lambda
Respaldo de funciones Lambda AWS
Backup de Funciones Lambda
Instrucciones
- Definir s3bucket = "TU_BUCKET_PERSONAL" (el bucket debe estar creado)
- Definir base_path = './archivos/' (u otra subcarpeta)
- Llamar a backup_capas(folder) o backup_funciones(folder)
Pasos previos
Antes que todo, para poder implementar y ejecutar se requiere tener conocimiento en las funciones lambda y en el sistema de permisologías de aws llamado IAM (Identity and Access Management) que permitirá a la función ejecutar la llamada a lambda y la escritura en el bucket S3.
Rol IAM
El rol debe contar con permisos de lectura y escritura en S3, agregar estos permisos si es que no están.
S3BucketReadWriteBucket
Relación de confianza con Lambda
En el Rol IAM, aseguremos que la política permite a lambda asumir el rol, esto es necesario si lo queremos desplegar como una función lambda (no es necesario para ejecutar localmente)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Funcion principal (python)
Las librerías importantes son boto3 (para interactuar con el API de AWS) y requests para realizar la descarga del código .zip entregado por aws. Nota que si la función se está desplegando en aws lambda import requests
requiere una capa adicional de función. Esto lo explico al final, así que paciencia.
import boto3
import json
import shutil
import os
from requests import session
lam = boto3.client('lambda')
s3 = boto3.resource('s3')
s3bucket = "TU_BUCKET_PERSONAL"
s3bucket
representa el bucket donde se almacenarán los respaldos, debes crearlo previamente y actualizar la variable.
boto3.client('lambda') y boto3.resource('s3')
son los clientes para realizar llamadas a lambda y s3 respectivamente, todo a través de boto3 (el sdk de aws para python)
Obtener el listado de Funciones
def backup_funciones(base_path):
response = lam.list_functions()
funciones = response['Functions'] #Todas las funciones
print("Encontramos {} funciones".format(len (funciones)))
El listado de funciones se obtiene llamando a list_functions()
y guardamos el listado, indicando en el log la cantidad de funciones.
python3 lambda2S3.py
Encontramos 35 funciones
OK.
¿Y el código?
Para cada función iteramos el detalle, que nos permite obtener lo realmente importante: el código
for fn in funciones:
layers = 0
if hasattr(fn, 'Layers'):
layers = len(fn['Layers'])
print("Procesando [{}] : {}\nRuntime: {}\n Descripcion:{}\n Tamaño: {}\n Capas:{}\n".format(
fn['FunctionArn'],
fn['FunctionName'],
fn['Runtime'],
fn['Description'],
fn['CodeSize'],
layers))
full_fn = lam.get_function(
FunctionName=fn['FunctionName']
) #el detalle de la funcion incluyendo el código fuente
if hasattr(full_fn['Configuration'], 'Tags'):
full_fn['Configuration']['Tags'] = full_fn['Tags']
config_json = json.dumps(full_fn['Configuration']) # un json con con la configuración de la función.
full_fn = lam.get_function(FunctionName=fn['FunctionName']) obtiene el detalle completo de la función. Una vez recibida la respueta, full_fn['Code']['Location']
contiene la url prefirmada (y de que expira a los 15 minutos).
Respaldando
config_filename = fn['FunctionName']+".json"
code_filename = fn['FunctionName']+".zip"
with open(base_path+config_filename, "w") as config:
config.write(config_json) # Si es lambda se guardará en /tmp/ si no en /archivos/ (debe existir la subcarpeta)
#acá la magia para obtener la descarga desde la URL pre-firmada
#usamos la librería requests que realiza la http request con sesiones...
with session() as d:
peticion = d.get(full_fn['Code']['Location'], stream=True)
out_file = open(base_path+code_filename, 'wb')
out_file.write(peticion.content)
out_file.close()
print("Codigo Fuente:", code_filename)
Definamos el nombre del archivo configuración (runtime, capas, timeout, arn, etc) NombreDeLaFuncion.json y el código lo almacenamos en NombreDeLaFuncion.zip. Ambos almacenados en la carpeta base_path
En el ambiente del contenedor lambda, el único lugar con permisos de escritura es la carpeta /tmp/. Entonces, en ese caso de estar desplegando en lambda, nuestro debe ser base_path='/tmp'/
.
Como mencionamos, full_fn['Code']['Location']
almacena la url prefirmada. Esta url no es descargable con un get clásico. La url prefirmada es redireccionada a una verificación antes de entregar el contenido.
Visualicemos la url código del primer resultado:
full_fn = lam.get_function(FunctionName=fn['FunctionName'])
print (full_fn['Code']['Location'])
return
este archivo se puede descargar en un navegador o utilizando curl.
Respaldando el código
Esto entonces lo realizamos con session() de requests, quien establece una sesión para todos los siguientes peticiones (get o post), el contenido se almacena en el archivo de código.
with session() as d:
peticion = d.get(full_fn['Code']['Location'], stream=True)
out_file = open(base_path+code_filename, 'wb')
out_file.write(peticion.content)
out_file.close()
Y este archivo también se respalda en la carpeta base_path
Llevando los archivos a S3
En términos prácticos, si estamos respaldando de forma local podemos parar acá y cerrar el día. Cada vez que queramos respaldar ejecutamos
python3 lambda2S3.py
y en unos segundos tendríamos todas nuestras funciones en una carpetita de nuestro computador. Y si queremos guardarlo en S3:
#Utilizamos el Bucket de destino para guardar el código y el archivo de configuración.
#Elegí guardarlo en subcarpetas separados por runtime, pero es a elección de cada uno :)
s3.Bucket(s3bucket).upload_file(base_path+config_filename,
fn['Runtime']+'/' + config_filename)
with open(base_path+code_filename, 'rb') as data:
#upload_fileobj permite la subida de archivos binarios (ojo de debe abrirse como 'rb': read and binary)
s3.Bucket(s3bucket).upload_fileobj(
data, fn['Runtime']+'/' + code_filename)
print("\nArchivo de Configuracion:", config_filename)
print("Subido a:", s3bucket+'/' +
fn['Runtime']+'/' + config_filename, "\n")
print("Archivo de Codigo Fuente:", code_filename)
print("Subido a:", s3bucket+'/' +
fn['Runtime']+'/' + code_filename, "\n")
Ambos archivos son subidos utilizando upload_file y upload_fileobj
este último para archivos binarios (para eso el archivo debe ser leído binario: rb)
Cada función almacernará en la subcarpeta asociada al runtime (Java, Python, Node etc), por ejemplo en este caso mis funciones quedan en las siguientes carpetas.
Opcional Respaldando las Capas
Las capas de las funciones son las librerías que hace uso y que no están en el ambiente del tiempo de ejecución. Por ejemplo librerías propias o especializadas. De hecho esta misma función requiere hacer uso de requests que no está en lambda y se debe agregar como capa.
Las capas de las funciones no se editan normalmente y son reconstruíbles. Sin embargo si se requiere respaldar las capas, dentro del mismo repositorio está creada la función backup_capas
.
Esta función utiliza la misma estructura que backup_funciones
pero realiza la llamada a list_layers y get_layer_version_by_arn
Obtiene la lista de capas:
response = lam.list_layers()
layers = response['Layers']
obtiene el detalle de cada capa:
for layer in response['Layers']:
print("\nProcesando [", layer['LayerName'], ":", layer['LayerArn'], "]\n", 100*"*", "\n")
print("Version:", layer['LatestMatchingVersion']['Version'])
print("Runtimes:", repr(layer['LatestMatchingVersion']['CompatibleRuntimes']))
full_layer = lam.get_layer_version_by_arn(
Arn=layer['LatestMatchingVersion']['LayerVersionArn']
)
El url para descargar el paquete de la capa viene en full_layer['Content']['Location']
y se descarga de la misma forma.
Opcional Implementando la función como lambda
Como sabemos en el ambiente lambda la única carpeta con permisos de escritura es /tmp/ entonces es importante que base_path = '/tmp'
. Ahora bien, para no preocuparse nunca más con ese tema podemos aplicar un pequeño truco.
Lidiando con /tmp/
Lo ideal es que la carpeta sea '/tmp/' sólo si estamos en lambda, en caso contrario podría ser subcarpeta './archivos/' (u otra a elección).
Si estamos en lambda, hay ciertas variables de entorno que están definidas, por ejemplo:
print (os.environ.get('AWS_EXECUTION_ENV'))
AWS_Lambda_python3.6
Podemos usar esto para detectar si estamos ejecutando la función en ambiente lambda, si es así la carpeta de trabajo debe ser '/tmp/' y luego llamamos a backup_funciones.
def lambda_handler(event, context):
#una técnica para detectar el ambiente de ejecución: si está seteado AWS_EXECUTION_ENV siginfica que estoy en lambda.
if os.environ.get('AWS_EXECUTION_ENV') is not None:
isLambda = 1
else:
isLambda = 0
base_path = './archivos/'
#si estoy en lambda cambio el base_path al /tmp/ (el único lugar con permisos de escritura del contenedor)
#el límite de /tmp/ son 500 MB ojo!
if isLambda == 1:
print("Runtime API:", os.environ['AWS_EXECUTION_ENV'])
base_path = '/tmp/'
backup_funciones(base_path)
Crear la función
Podemos crear la funcion en la consola o a través de la consola (sitio web)
AWS Consola
AWS cli
rm function.zip
zip function.zip *.py
aws lambda create-function --function-name respalda-funciones \
--zip-file fileb://function.zip --handler lambda2S3.lambda_handler --runtime python3.6 \
--role TU_ROL_DE_EJECUCION \
--description "Respalda todas las funciones en un bucket S3" \
--publish --timeout 900 \
--layers "arn:aws:lambda:us-west-2:823794707078:layer:python36-requests-bs4-lxml:2"
Nota: La funcion requiere para ejecutarse el módulo requests
que originalmente no está presente en el ambiente python de lambda. Este módulo (entre otros) lo provee la capa arn:aws:lambda:us-west-2:823794707078:layer:python36-requests-bs4-lxml:2
. Esta capa tiene permisos para ser utilizada en cualquier función.
Si estás interesado en cómo generar tus propias capas pre-compiladas para el ambiente python de lambda, te recomiendo esta guía (en inglés) que utilicé para crear la capa.

Creating New AWS Lambda Layer For Python Pandas Library | by Quy Tang | Medium
Quy Tang ・ ・ 5 min read
Medium
Opcional Programación Automática
Una de las gracias de contar con la función en ambiente lambda es que puedes programar su ejecución cada cierto tiempo.
Lo primero es generar un trigger basado en cloudwatch events
Y seleccionamos nueva programación utilizando una expresion cron (sí, cron)
De esta forma queda configurada la ejecución todos los días domingo (UTC)
Conclusion
Finalmente realizar el respaldo es bien sencillo y no toma mucho tiempo y recursos. Si la función se quisiera mejorar podría pensar en :
- Ejecutar la descarga del archivo .zip y subida a S3 en un sólo movimiento, así no nos preocupamos del límite de 500 MB de espacio en /tmp/
- Utilizar el cURL del ambiente en vez de requests, aunque si hacemo eso perdemos el control de la función dejando esta parte en manos del entorno.
- La traducción al inglés se viene pronto 😎
Top comments (4)
Hola!, Estoy haciendo un bot en telegram el cual uso una libreria de fotos. para un servidor local funciona bien ya que todas las carpetas estan juntas. pero en Lambda y S3 la historia cambia, como podria crear una ruta desde mi lambda hasta S3 para que mi bot envie la imagen al usuario?
¿no sería una solución mas ordenada el utilizar CloudFormation?
Estuve pensando en como usar CF para respaldar las lambda pero no se me ocurre...
Como sería?
Yo descompondría el problema en varios tracks:
Y si no queremos amarrarnos a un vendor utilizar algunas tools agnósticas como las que ofrece Hashicorp: Terraform, Packer, Vault, etc
De igual manera trataré de buscar más y poner un ejemplo claro de como resolver el escenario que planteas en este post mediante alguna tool.