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
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
-
lambda-a
Es la carpeta raiz y la puedes llamar como te interese. En mi caso tengo dos lambda-a (la que llama) y lambda-b (la que será llamada)
-
function
Es una subcarpeta de tipo módulo de python (un paquete). El nombre es arbitrario.
-
index.py
Nombre arbitario. El módulo que tiene la función handler. La función handler es la función principal (la lambda en cuestión).
Em mi caso se define así:def fn_a(event, context)
el estándard dice que se debería llamar handler(event, context)
pero por fines didácticos uso fn_a para tenerla localizada en el debug
event
Es el parámetro de entrada, el input que recibe la función al ser invocada. Se verá con más detalle posteriormente. context
Es información adicional sobre la función en cuestión. Por ejemplo su ARN, tamaño en memoria, etc.
Este objeto lo inyecta AWS. -
requirements.txt
Son las dependencias. En mi caso, con fines de prueba solo necesito:
requests
boto3==1.17.19
-
input.json
El evento (datos input) en formato json. En mi caso es
{"input_for_a": "value for A"}
-
template.yaml
Es el fichero de configuración que le indica a AWS o SAM que función es el handler y que alias va a tener para ser invocada desde afuera.
En mi caso tiene alias: MyFunctionA y apunta a index.fn_a (handler) que está dentro de function/ (codeUri)
Ejecución
El proceso de ejecución de una lambda tiene 2 fases.
-
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
-
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.jsonEste 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