Amazon SAM - Probar dos lambdas simultaneamente en local

Amazon SAM - Probar dos lambdas simultaneamente en local

Comunicar dos lambdas en local con AWS Sam

Actualmente estoy involucrado en un proyecto que está muy relacionado con cloud computing en especial com AWS.
En este sentido he tenido que interactuar con las lambdas.
Si bien tenía un concepto teórico de lo que eran no había desarrollado ninguna.
Para ver en código como se configuraba me descargue el ejemplo HelloWorld que tiene Amazon (sam init)
Hice un mini proyecto sandbox para ver si podía realizar la comunicación de dos lambdas muy básicas en mi entorno local.

Prerrequisitos

  • Tener instalado aws SAM
  • Tener cuenta en github
La instalación y configuración se explica muy bien en la web de AWS.

Configuración

Las lambdas (en python) cuentan con una estructura similar a esta:

├── lambda-a │   ├── function │   │   ├── __init__.py │   │   ├── index.py │   │   └── requirements.txt │   ├── input.json │   └── template.yaml ... lambda-b tiene igual estructura

Explicación

Ejecución

El proceso de ejecución de una lambda tiene 2 fases.

  1. El "build"

    Consiste en empaquetar toda tu app en una carpeta. Esto se hace con el comando: sam build sam build MyFunctionA --template some-path/lambda-a/template.yaml --build-dir some-path/lambda-builds/prj-python/l-a --use-container --debug En la instrucción anterior le indico que el recurso a empaquetar es MyFunctionA y está defindo en el template template.yaml
    build-dir indica donde se guardará ese compilado. En definitiva es una aplicación aislada o un microservicio.
    use-container indica que emule el empaquetado como si estuviera en un contenedor de Docker.

    Una vez ejecutado el build asi se transforma la lambda (con los requirements antes explicados): l-a │   ├── MyFunctionA │   │   ├── __init__.py │   │   ├── boto3 │   │   ├── boto3-1.17.19.dist-info │   │   ├── botocore │   │   ├── botocore-1.20.27.dist-info │   │   ├── certifi │   │   ├── certifi-2020.12.5.dist-info ... │   │   ├── index.py ... │   │   ├── requests │   │   ├── requests-2.25.1.dist-info │   │   ├── requirements.txt │   │   ├── s3transfer │   │   ├── s3transfer-0.3.4.dist-info │   │   ├── six-1.15.0.dist-info │   │   ├── six.py ... │   └── template.yaml

  2. La llamada

    Como ya tenemos la función compilada podemos invocarla con sam local sam local invoke MyFunctionA --template some-path/l-a/template.yaml --event some-path/lambda-a/input.json --debug Con ese comando le decimos que ejecute el recurso: MyFunctionA que equivale a la función: function.index.fn_a pasandole como evento el json input.json

    Este es el contenido del archivo lambda-a/function/index.py donde se puede visualizar el handler

    def get_boto_client(): config_lambda = Config(retries={'total_max_attempts': 1}, read_timeout=1200) return boto3.client( 'lambda', config=config_lambda, # en mac endpoint_url='http://host.docker.internal:3050', # en windows # endpoint_url='http://localhost:3050', use_ssl=False, verify=False, ) # la función handler def fn_a(event, context): pprint("fn_a ini") now = datetime.utcnow().strftime("%Y-%m-%d-%H:%M:%S") eventjson = json.dumps(event) try: # llamda a la otra lambda Lambda B response = get_boto_client().invoke( # el alias de la otra lambda (lambda-b/template.yml) FunctionName="MyFunctionB", # que la comunicación sea por http InvocationType='RequestResponse', # no va en local. Vuelca las trazas de lambda-b en la respuesta #LogType='Tail', #datos para el parametro event de lambda-b Payload=json.dumps({"from": "This is some payload for Lambda B"}) ) except Exception as ex: ex = str(ex) return { "error": f"Error invoking Lambda B: {ex}" } if response['StatusCode'] == 200: payload = json.loads(response['Payload'].read()) pprint(payload) if "error" in payload: return { "error": f"Error response from Lambda B: {payload['error']}" } return { "event": f"input event:\n {eventjson}", "success": f"This is a response succes from Lambda A {now}", "from-lambda-b": f"{payload['success']}" } else: return { "error": f"Error in response from Lambda B status: {response['StatusCode']}" }

El punto interesante es que antes de invocar a lambda-a hay que tener en ejecución lambda-b de lo contrario nos mostraría el siguiente error:

REPORT RequestId: e1f1a776-15be-48cf-99e2-785ad8ccb1f9 Init Duration: 0.28 ms Duration: 2547.70 ms Billed Duration: 2600 ms Memory Size: 128 MB Max Memory Used: 128 MB { "error": "Error invoking Lambda B: Could not connect to the endpoint URL: http://host.docker.internal:3050/2015-03-31/functions/MyFunctionB/invocations" }

Es decir, estaría entrando por la primera excepción de fn_a.
Hay que hacer el build de lambda-b de forma similar a
a. Evidentemente en otra carpeta, por ejemplo l-b si no hacemos esto no podríamos levantar B ni invocarla.

Levantando Lambda B como servicio

sam local start-lambda -p 3050 --template some-path/lambda-builds/prj-python/l-b/template.yaml --debug 2021-03-13 23:32:33,288 | Telemetry endpoint configured to be https://aws-serverless-tools-telemetry.us-west-2.amazonaws.com/metrics 2021-03-13 23:32:33,449 | local start_lambda command is called 2021-03-13 23:32:33,451 | No Parameters detected in the template 2021-03-13 23:32:33,481 | 1 stacks found in the template 2021-03-13 23:32:33,481 | No Parameters detected in the template 2021-03-13 23:32:33,517 | 1 resources found in the stack 2021-03-13 23:32:33,517 | No Parameters detected in the template 2021-03-13 23:32:33,537 | No Parameters detected in the template 2021-03-13 23:32:33,558 | Found Serverless function with name='MyFunctionB' and CodeUri='MyFunctionB' 2021-03-13 23:32:33,574 | Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint. 2021-03-13 23:32:33,575 | Localhost server is starting up. Multi-threading = True 2021-03-13 23:32:33 * Running on http://127.0.0.1:3050/ (Press CTRL+C to quit)

Ahora que ya tenemos lambda-b como servicio podemos comunicarnos con ella desde otra lambda o incluso con postman y/o curl
Volvemos a ejecutar lambda-a y no deberíamos ver ningún error sino algo como esto:

Debug lambda-a (la que llama a lambda-b)
Invoking index.fn_a (python3.8) Skip pulling image and use local one: amazon/aws-sam-cli-emulation-image-python3.8:rapid-1.20.0. Mounting some-path/lambda-builds/prj-python/l-a/MyFunctionA as /var/task:ro, delegated inside runtime container 'fn_a ini' { 'event': 'input event:\n {"from": "This is some payload for Lambda B"}', 'success': 'This is a successfully response from Lambda B 2021-03-13-22:38:17' } END RequestId: 59126526-d3c5-4432-b127-7b58beac5a46 REPORT RequestId: 59126526-d3c5-4432-b127-7b58beac5a46 Init Duration: 0.55 ms Duration: 3519.18 ms Billed Duration: 3600 ms Memory Size: 128 MB Max Memory Used: 128 MB { "event": "input event:\n {\"input_for_a\": \"value for A\"}", "success": "This is a response succes from Lambda A 2021-03-13-22:38:15", "from-lambda-b": "This is a successfully response from Lambda B 2021-03-13-22:38:17" }
Debug lambda-b (la que es llamada por lambda-a)
'fn_b ini' END RequestId: c4acd1fd-8aae-4f2b-9e0f-6b78c7836133 REPORT RequestId: c4acd1fd-8aae-4f2b-9e0f-6b78c7836133 Init Duration: 0.33 ms Duration: 136.10 ms Billed Duration: 200 ms Memory Size: 128 MB Max Memory Used: 128 MB 2021-03-13 23:38:17 127.0.0.1 - - [13/Mar/2021 23:38:17] "POST /2015-03-31/functions/MyFunctionB/invocations HTTP/1.1" 200 -
El código fuente

Este ejemplo está subido a mi github. Concretamente en este enlace

Autor: Eduardo A. F.
Publicado: 14-03-2021 00:34