loading...
Cover image for Respaldando tus funciones Lambda

Respaldando tus funciones Lambda

ensamblador profile image ensamblador ・8 min read

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:

GitHub logo ensamblador / backup_lambda

Respaldo de funciones Lambda AWS

Backup de Funciones Lambda

Instrucciones

  1. Definir s3bucket = "TU_BUCKET_PERSONAL" (el bucket debe estar creado)
  2. Definir base_path = './archivos/' (u otra subcarpeta)
  3. Llamar a backup_capas(folder) o backup_funciones(folder)

La guia de uso está acá




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)

Alt Text

{ 
 "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)

Documentacion boto3: Lambda

Documentacion boto3: S3

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

Resultado:
Alt Text

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.

Alt Text

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

crear la funcion en lambda

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.

Alt Text

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.

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
Alt Text

Alt Text

Y seleccionamos nueva programación utilizando una expresion cron (sí, cron)

Alt Text

De esta forma queda configurada la ejecución todos los días domingo (UTC)

Alt Text

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 😎

Posted on by:

ensamblador profile

ensamblador

@ensamblador

Haciendo cosas divertidas con Python y Javascript

Discussion

markdown guide
 

¿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:

  • Desplegar la misma infraestructura (Lambdas, EC2 etc) muchas veces, con CloudFormation
  • Desplegar la misma web app, API, etc muchas veces, con CodeBuild + CodePipeline

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.