Buenas!! :D
Hago el post hoy porque ayer fue feriado.
En este capítulo vamos a estar viendo los siguientes temas.
- Step Functions actualizada.
- Lambdas dentro de la step.
- Lambda fuera de la step.
- Eventos HTTP.
- Roles.
- Nuevas validaciones.
- Pruebas.
En el primer capítulo, hablamos de las task manuales. Las cuales, son las tasks que dependen de una confirmación externa para poder continuar su funcionamiento.
Ejemplo gráfico
En este capítulo vamos a agregar este tipo de task a nuestra máquina de estado actual.
Step Functions actualizada.
En esta imagen tenemos el ejemplo de la step function que vamos a querer crear.
Para ello vamos a empezar orquestando las lambdas extras que vamos a utilizar.
En el archivo .asl
de la máquina de estados, dentro de States
y arriba de la task Medios de pago
, vamos a agregar el siguiente código.
"States": {
"Obtener Planes": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": {
"Fn::GetAtt": [
"obtenerPlanes",
"Arn"
]
},
"Payload": {
"Input.$": "$"
}
},
"Next": "Elegir planes",
"Catch": [
{
"ErrorEquals": [
"Error"
],
"Next": "Lambda Error"
}
]
},
"Elegir planes": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
"TimeoutSeconds": 300,
"Parameters": {
"FunctionName": {
"Fn::GetAtt": [
"elegirPlanes",
"Arn"
]
},
"Payload": {
"Input.$": "$",
"token.$": "$$.Task.Token"
}
},
"Next": "Medios de Pago",
"Catch": [
{
"ErrorEquals": [
"Error"
],
"Next": "Lambda Error"
}
]
},
"Medios de Pago": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.medioDePago",
"StringEquals": "Debito",
"Next": "Pago Debito"
}
],
"Default": "Pago Credito"
},
Estas van a ser dos states de tipo Task que van a tener como recursos lambdas (aún no creadas).
- Obtener Planes: Es un state de tipo Task que va a obtener los planes de wifi que ofrece la empresa.
- Elegir planes : Es un state de tipo Task, qué diferencia de obtener planes, su
resource
va a agregar en el invoke la siguiente prop.waitForTaskToken
. Y este tipo de lambda va a necesitar un task token que se va a agregar en el apartado dePayload
.
"Payload": {
"Input.$": "$",
"token.$": "$$.Task.Token"
}
Elegir planes va a ser la encargada de enviar las opciones al cliente.
Mientras espera la respuesta va a quedar en un estado de Pending, hasta que reciba la data necesaria para continuar.
Por eso lleva la propiedad de TimeoutSeconds
, para regular el consumo de memoria de nuestra máquina de estado y que no quede en pending eternamente.
"TimeoutSeconds": 300,
Es importante cambiar el valor de la propiedad StartAt
por Obtener Planes porque ahora nuestra máquina de estado comienza con un nuevo state.
Todavía no podemos hacer el deploy porque las lambdas que pasamos por parámetro no existen realmente.
Lambdas dentro de la step.
Lambda ObtenerPlanes
Vamos a comenzar creando la lambda obtenerPlanes.js
. Lo que quiero que devuelva es un json con los distintos planes a los que puede acceder el cliente. Los voy a importar desde un json porque después quiero usarlos.
const planes = require("./../resources/planes")
const obtenerPlanes = () => {
return planes
};
exports.handler = (event, context, callback) => {
callback(null, obtenerPlanes());
};
En el const planes
tenemos el require
.
Yo guarde el json en resources => planes.js
planes.js
exports.planes = [
{
"plan": "1MB",
"precio": 1000
},
{
"plan": "10MB",
"precio": 2000
},
{
"plan": "50MB",
"precio": 5000
},
{
"plan": "100MB",
"precio": 8000
}
]
Lambda elegirPlanes
Vamos a crear la lambda elegirPlanes.js
. En esta vamos a tener distintos pasos. Primero debemos instalar el paquete de aws-sdk
Es importante instalarlo en el devDependecies
para que no sobrecargue a la lambda.
npm install –save-dev aws-sdk
Una vez instalado, ya lo podemos hacer importar y comenzar a trabajar en nuestra lambda.
const AWS = require('aws-sdk');
const getParameters = (event) => {
const urlQueue = process.env.URL_SQS || '';
console.log(urlQueue);
if (urlQueue === '') {
throw new Error('La URL no existe')
}
const params = {
MessageBody: JSON.stringify({
planes: event.Input.Payload,
taskToken: event.token
}),
QueueUrl: urlQueue,
};
return params;
};
exports.handler = async (event) => {
try {
const sqs = new AWS.SQS();
console.log('event: ', JSON.stringify(event, null, 2));
const params = getParameters(event);
console.log(params);
await sqs.sendMessage(params).promise();
return event;
} catch (e) {
throw new Error(e);
}
};
Lo que deseamos hacer en esta lambda, es enviar los planes al cliente mediante la cola de sqs que creamos antes.
Vamos a instanciar el servicio de sqs con aws en el handler.
const sqs = new AWS.SQS();
Luego, para enviar un mensaje a la cola de sqs debemos correr el siguiente código.
await sqs.sendMessage(params).promise();
De dónde sale esa información?
De la documentación de aws-sdk para sqs.
Sabemos que necesitamos los parámetros para que el mensaje sea enviado. Para eso vamos a trabajar sobre la función de getParameters()
que nos tiene que devolver estos parámetros.
const params = {
MessageBody: JSON.stringify({
planes: event.Input.Payload,
taskToken: event.token
}),
QueueUrl: urlQueue,
};
Los parámetros a devolver son
- el mensaje que queremos enviar.
- el token que vamos a necesitar para referirnos a la instancia de la stepFunction.
- la url de la cola de sqs.
La url de la cola de sqs, la podíamos importar desde el stack de cloudFormation (tal cual lo hacemos en el archivo asl en el state final donde mandamos el mensaje). Pero a esa variable la vamos a importar a nivel serverless (lo vamos a ver en unos párrafos más adelante).
En la lambda lo importamos de la siguiente manera
const urlQueue = process.env.URL_SQS || '';
Lambdas en serverless
Vamos a agregar las dos funciones junto las que ya estaban creadas. (En el apartado functions)
obtenerPlanes:
handler: ./src/lambdas/obtenerPlanes.handler
elegirPlanes:
handler: ./src/lambdas/elegirPlanes.handler
La parte importante está en elegirPlanes
porque es donde debemos agregar la url de la sqs.
La agregamos en el serverless.yml
y sobre la lambda en donde queremos importar.
¿Por qué? Porque es la forma más segura de crear variables de entorno seguras, ya que evitamos que el resto de las lambdas acceda a información que no necesitan.
environment:
URL_SQS: ${cf:contratarwifiplan-${opt:stage, 'dev'}.SendQueueURL}
Como ven, lo importamos desde el stack de cloudFormation como en el capítulo pasado.
Y la lambda de elegir planes debería quedar así.
elegirPlanes:
handler: ./src/lambdas/elegirPlanes.handler
environment:
URL_SQS: ${cf:contratarwifiplan-${opt:stage, 'dev'}.SendQueueURL}
Lambdas fuera de la step.
Cuando enviemos la data al cliente con la lambda elegirPlanes.js
, esta Task va a quedar pendiente esperando una respuesta.
Para retomar el flujo de la stepFunction necesitamos de una lambda que, usando el token de único uso que le enviamos al cliente, “reviva” la stepFunction para que continúe su flujo.
Esto lo creamos de la siguiente manera:
Creamos la lambda llamada recibirRespuesta.js
, la cual va a recibir la respuesta del cliente, y enviar la señal a la step function para continuar.
Esta lambda no forma parte del flujo que escribimos en el asl.
El código es el siguiente:
const AWS = require('aws-sdk');
const recibirRespuesta = (event) => {
const eventParse = JSON.parse(event.body);
console.log(eventParse)
return {
output: JSON.stringify(eventParse),
taskToken: eventParse.taskToken,
};
};
exports.handler = async (event) => {
const params = recibirRespuesta(event);
try {
const stepfunctions = new AWS.StepFunctions();
console.log(
`Llamando a la stepFunction con estos parametros ${JSON.stringify(
params
)}`
);
await stepfunctions.sendTaskSuccess(params).promise();
return {
statusCode: 200,
body: JSON.stringify(params),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify(error),
};
}
};
Parecido a la lambda elegirPlanes.js
, necesitamos instanciar el servicio de stepFunction de aws, importando el aws-sdk
.
La función que se utiliza en estos casos es la de sendTaskSucces()
que comunica el mensaje de éxito para que la step Function continúe.
Les dejo la documentación del aws-sdk.
Ahora sabemos cuales son los parámetros necesarios para esta función.
return {
output: JSON.stringify(eventParse),
taskToken: eventParse.taskToken,
};
En la prop output
va a ir la data que necesitamos que la task elegiPlanes devuelva como output, y el tasktoken
nos sirve para hacer referencia a cuál instancia de StepFunction nos estamos refiriendo.
La razón de estos returns
return {
statusCode: 200,
body: JSON.stringify(params),
};
lo vamos a estar explicando más adelante cuando hablemos de los eventos HTTP que despiertan ciertas lambdas.
Lambda en serverless
Vamos a declarar la lambda en el serverless.yml
recibirRespuesta:
handler: ./src/lambdas/recibirRespuesta.handler
Eventos HTTP.
En el capítulo anterior habíamos visto como una cola de sqs podía despertar una lambda.
En este, vamos a ver que las lambdas también pueden ser despertadas por eventos http
, trabajando con el servicio de ApiGateway
.
¿Cómo lo configuramos?
Vamos a querer que nuestra lambda recibirRespuesta
sea despertada por un POST
con los datos del servicio y método de pago que eligió el cliente.
Debajo del handler
de la función, vamos a agregar el siguiente código.
events:
- http:
path: /contratar-wifi/recibirRespuesta
method: post
El método que queremos utilizar, es un post, y en el path va la ruta del endpoint.
En el servicio de ApiGateway
vamos a poder acceder al endpoint. Y haciendo click en pruebas se puede acceder al cuerpo del endpoint.
En el cuerpo de la solicitud. Va a ir el JSON que queremos enviarle a la lambda.
Si lo notan, es el json que utilizábamos en el capítulo pasado pero con el agregado del task token.
{
"servicio": {
"plan": "15MB",
"precio": 1000
},
"medioDePago": "Debito",
"tarjeta": {
"cantCuotas": "06",
"nroTarjeta": "1234567898745896"
},
"taskToken": "AAAAKgAAAAIAAAAAAAAAAQ9OfVcpRULG9PyaPvbJhBV2NFiha4ILZcflTahDJbdQ/gFRlyzjh7UVvijwZyvXMRz64qH1kF3aUkTX18Dh0EfJWZzMJ0zEhPemHjct6KmkWqSb0+BpFmq3x0HlpOlam9W3tXD1Flp7nnaSPs+hfN6877ele8f0721HaQujSasqrQpsNjTVYpiRxrDOL1sgIpv2UX9oflVkETfsYERnce+ijtxdEQVf/nXyizc7F+AZTzIp0AG4FBmS5yNXgSWLWD0cvNHmz2ngtx1Fv3MfhSyAY/f0hpCY1h53fyYqnuodJH3AQiwii6cDHU1Bdd3oGlMioWU5OYXXv/jrZwAuy7oH1CheD91c+b/xerKEfKmn3KM8w6yebO8wWUosq8mbfGbPvaElj8WHkg7YdEmnixFccevbyX5RrVZOuNAGKJp2zBouEa6RcaowISvMv1NMbbiXKPp1MMzx3bfo5+0S+sOjagmneER6O5Y0cZXpeiji/4vGFIcDrd1bEcHID1FNll1OXhWXO8MUb7PHWH07JxnNyV0nrrTNHE4YZZlg6rR48+gD7IaGko5Kc/pzR84CExw1UbWtLMNaYhlP1GVfMkAbJ3/LX0Zocq5kDfZhu2V50l1tHoMqhNTRGo2o824Q+g=="
}
Y por ultima, la razón de los returns en la lambda .
return {
statusCode: 200,
body: JSON.stringify(params),
};
y
return {
statusCode: 500,
body: JSON.stringify(error),
};
Esto sucede porque el evento http necesita que la respuesta cuente con una prop statusCode
con un number, y con un body
que contenga la data en formato string.
Si hacemos el return diferente, ¿va a funcionar?
La respuesta es sí, porque el sendTaskSucces()
se envía antes que el return
, entonces, la stepFunction va a continuar con su ejecución PERO la respuesta que vamos a obtener por http sera de error, por no tener el formato de respuesta correcto.
Roles
No tenemos que olvidar que nuestros servicios a veces necesitan permisos para funcionar y estos se otorgan mediante roles.
En este caso, vamos a necesitar dos roles (que vamos a escribir en resources => LambdaRole.yml
) para dos de nuestras lambdas.
1 - El primer role, va a ser para la lambda que contiene el WaitForTaskToken
.
Necesitamos permisos :
- Para loguear la data de la lambda.
- Para poder enviar mensajes a la cola de SQS.
ElegirPlanesLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: ElegirPlanesLambdaRole
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service:
- 'lambda.amazonaws.com'
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: statePolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- sqs:SendMessage
Resource:
- Fn::GetAtt: [SendQueue, Arn]
- Effect: Allow
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
- 'logs:DescribeLogStreams'
Resource:
- 'arn:aws:logs:*:*:*'
2 - El segundo role, va a ser para conceder permisos a la lambda que va a recibir la data
desde el endpoint y continuar la ejecución de la stepFunctions.
Esos permisos son:
- El de loguear la información de la lambda
- El de poder enviar el éxito de la ejecución.
RecibirRespuestasLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: RecibirRespuestasLambdaRole
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service:
- 'lambda.amazonaws.com'
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: statePolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- states:SendTaskSuccess
- states:SendTaskFailure
Resource: "*"
- Effect: Allow
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
- 'logs:DescribeLogStreams'
Resource:
- 'arn:aws:logs:*:*:*'
Y por último vamos a importar, los roles en serverless.yml
y después vamos a asignarlos en las lambdas correspondientes.
Nuestras importaciones en resources deberían quedar de la siguiente manera, con los dos nuevos roles agregados.
resources:
Resources:
SendQueue: ${file(./src/resources/SQS.yml):SendQueue}
SendQueueDLQ: ${file(./src/resources/SQS.yml):SendQueueDLQ}
ContratarServicioWifiMachineRole: ${file(./src/resources/StepFunctionsRole.yml):ContratarServicioWifiMachineRole}
ElegirPlanesLambdaRole: ${file(./src/resources/LambdaRole.yml):ElegirPlanesLambdaRole}
RecibirRespuestasLambdaRole: ${file(./src/resources/LambdaRole.yml):RecibirRespuestasLambdaRole}
Y las lambdas deberían quedar de la siguiente manera.
elegirPlanes:
handler: ./src/lambdas/elegirPlanes.handler
environment:
URL_SQS: ${cf:contratarwifiplan-${opt:stage, 'dev'}.SendQueueURL}
role:
Fn::GetAtt: ['ElegirPlanesLambdaRole', 'Arn']
y
recibirRespuesta:
handler: ./src/lambdas/recibirRespuesta.handler
events:
- http:
path: /contratar-wifi/recibirRespuesta
method: post
role:
Fn::GetAtt: ['RecibirRespuestasLambdaRole', 'Arn']
Actualizar roles de las step
Como agregamos nuevas lambdas a nuestra step function debemos ir a el archivo StepFunctionsRole.yml
y agregarlas tambien en el role.
- !Join ['-', [ !Join [':', ['arn:aws:lambda',!Ref 'AWS::Region', !Ref 'AWS::AccountId' ,'function', !Ref 'AWS::StackName']], 'obtenerPlanes' ]]
- !Join ['-', [ !Join [':', ['arn:aws:lambda',!Ref 'AWS::Region', !Ref 'AWS::AccountId' ,'function', !Ref 'AWS::StackName']], 'elegirPlanes' ]]
Agregar nuevas validaciones
Antes de terminar la máquina de estado, y poder hacer nuestras pruebas, necesitamos agregar unas nuevas validaciones en las lambdas de pagos.
Queremos asegurarnos que los planes que elige el cliente, pertenecen a la oferta de la empresa.
En ambas lambdas debemos importar los planes ofrecidos.
const planes = require("./../resources/planes")
Y luego la función que va a validar la existencia.
const validarPlan = (data) => {
const { plan } = data.servicio;
console.log(plan);
console.log(planes.planes.length);
let fueValidado = false;
let arrayPlanes = planes.planes
for(let i = 0; i < arrayPlanes.length; i++) {
console.log('entro');
console.log( arrayPlanes[i].plan + " " + plan);
if (arrayPlanes[i].plan == plan) {
fueValidado = true;
return
}
}
console.log(fueValidado);
if (!fueValidado) throw new Error('El plan no existe')
}
En mi caso agregue esta validación dentro de la función pagoConDebito
y pagoConDebito
. Debajo de validarPago
, agrego:
validarPlan(inputData)
Ahora sí, ya tenemos nuestra StepFunction completa.
Y podemos correr el
sls deploy
Pruebas
Una vez que tengamos nuestra stepFunction en la nube vamos a comenzar con las pruebas.
A la hora de iniciar la ejecución, el json con el que lo iniciamos no es de gran importancia.
Una vez iniciada podemos ver como la lambda que tiene el resource waitForTaskToken
queda pendiente después de enviar la data.
Si vamos al historial de ejecuciones, vamos a poder sacar el token que necesitamos para hacer referencia a la misma instancia.
Con esta data, vamos a ir al servicio de apiGateway así continuamos con la ejecución.
CASO DE ERROR
En el cuerpo del endpoint debemos usar el siguiente json.
{
"servicio": {
"plan": "15MB",
"precio": 1000
},
"medioDePago": "Debito",
"tarjeta": {
"cantCuotas": "06",
"nroTarjeta": "1234567898745896"
},
"taskToken": "AAAAKgAAAAIAAAAAAAAAAYWwkS4HEc5xR92k3T7sftkXFTOXMIE06rDrmlQ5Fr7rFSgqK+lIC6T2xB5mOydgGAdRNhjJk6zHuMhriHC1YeYmTdRVwx1m6i8t0ZpGgeD+2xDhw7oCE7uomervRzTQshROjUIgyXFuK4zP7EkqDg952/V1vFO/rw4k7eCufoKfnjkrFEwnyWj31V5cIUWSfZyjF5xe4KPrvzACqR2TZFdKu5SPpU5vikBPpmdIVyFMnSudPR1asv7j3hEvjF/ZKrYSPDok27wLjH9shaYysPncEiDbe1AysIq10bbI+YyeeUWm7kWC4xeVJcNqv5aupX2xGifWmolvvXlHFCXAjpoUTkPNpYO1jrgE2/p2QBGURzDaEWgs4ffJLxMGwdVDYeRZPK+y1EmESnbk5zys38MNy3iQVd++vvFD90EzOKAHpGGQ9iXBvp12prXbywUg/CUSxPBS/wKQCSsdYjImfLC+NXgXCDXmi8Bsc980vyXnZfVEc6Aq8h7NKE6rJTBkCb1BD34rox1Rqs4zkp31Gf57E33tC5oJSIStbNx2ltSJPMOKqOeQvaKmzI30lsfudpM56mEWnV8vEykyLfGTwxZymHj1U3RUaLhbIoKI7GzMggFDuwy9uZhDVXzak0A7rQ=="
}
Recuerden que debemos modificar el token con el valor que obtengan de su ejecución.
Si el endpoint está construido de manera correcta y el json está correcto, el resultado debería ser el siguiente.
Al volver a la ejecución, debemos notar que la stepFunction terminó con error debido a que el plan no existe en la oferta.
CASO DE ÉXITO
En el caso de éxito el json debería ser el siguiente.
{
"servicio": {
"plan": "1MB",
"precio": 1000
},
"medioDePago": "Debito",
"tarjeta": {
"cantCuotas": "06",
"nroTarjeta": "1234567898745896"
},
"taskToken": "AAAAKgAAAAIAAAAAAAAAAYWwkS4HEc5xR92k3T7sftkXFTOXMIE06rDrmlQ5Fr7rFSgqK+lIC6T2xB5mOydgGAdRNhjJk6zHuMhriHC1YeYmTdRVwx1m6i8t0ZpGgeD+2xDhw7oCE7uomervRzTQshROjUIgyXFuK4zP7EkqDg952/V1vFO/rw4k7eCufoKfnjkrFEwnyWj31V5cIUWSfZyjF5xe4KPrvzACqR2TZFdKu5SPpU5vikBPpmdIVyFMnSudPR1asv7j3hEvjF/ZKrYSPDok27wLjH9shaYysPncEiDbe1AysIq10bbI+YyeeUWm7kWC4xeVJcNqv5aupX2xGifWmolvvXlHFCXAjpoUTkPNpYO1jrgE2/p2QBGURzDaEWgs4ffJLxMGwdVDYeRZPK+y1EmESnbk5zys38MNy3iQVd++vvFD90EzOKAHpGGQ9iXBvp12prXbywUg/CUSxPBS/wKQCSsdYjImfLC+NXgXCDXmi8Bsc980vyXnZfVEc6Aq8h7NKE6rJTBkCb1BD34rox1Rqs4zkp31Gf57E33tC5oJSIStbNx2ltSJPMOKqOeQvaKmzI30lsfudpM56mEWnV8vEykyLfGTwxZymHj1U3RUaLhbIoKI7GzMggFDuwy9uZhDVXzak0A7rQ=="
}
Al igual que en el caso de error la respuesta por http debe dar 200, pero la step function debe continuar su ejecución sin error.
Final
Bueno, llegamos al final de este capítulo.
Y ya tenemos una stepFunction que tiene una intervención manual por parte del cliente. :D
Espero que les haya sido de ayuda.
Recuerden que si quieren, pueden invitarme un cafecito.
Y que si tienen alguna duda pueden dejarla en los comentarios.