Step Functions para no morir. Parte 4: Creación de Step.
Tags: stepfunctions, node, aws, tutorial
Published: 2022-02-22 13:28:59

Holis! 
HOY... Hoy es el día en que vamos a crear nuestra stepFunction.

![Elmo on fire](https://media.giphy.com/media/P7JmDW7IkB7TW/giphy.gif)


En el capítulo de hoy vamos a estar trabajando en nuestra máquina de estado. Y lo vamos a hacer desde el código, usando Serverless Framework. 

Va a estar dividido en partes.

- Iniciar proyecto 
- Orquestación
- Lambdas
- SQS
- Eventos
- Roles 
- Voilà


## Iniciar proyecto

{% youtube I6MY4PHgJhw %}

Vamos a comenzar iniciando nuestro proyecto Serverless.
Para eso vamos a pararnos en la carpeta en la cual queremos guardar nuestro repo (supongamos la carpeta `contratarWifiPlan`) y abrimos la consola, ahí vamos a correr el siguiente comando. 

```
sls create --template aws-nodejs

```
Esto va a crear un template de Serverless para nuestro  proyecto utilizando `obviamente` node.


![Comando por consola](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wluzo5jxywrk7vxeqf5r.png)

Una vez creado el template vamos a abrir esa carpeta con nuestro IDE y vamos a contar con un boilerplate que cuenta con 3 archivos creados. 


![Boilerplate serverless](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tks7qje6t9yqqb6pzkas.png)

`serverless.yml` => Es donde va a estar toda nuestra configuración’
`handler.js` => El ejemplo de una lambda
`.npmignore` => En este archivo vas los documentos que queremos que se ignoren cuando corremos el npm.

Para nuestro proyecto vamos a hacer algunos cambios.
1 - Creamos una carpeta llamada src. 
2 - Dentro de esta vamos a crear otras 3 carpetas llamadas `lambdas`, `resources` y `stepFunctions`. 


![Carpetas del src](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ml5nliko30z32rbb5asr.png)

En el archivo `serverless.yml`, vamos a hacer los siguientes cambios: 

- En `service` va a ir el nombre de nuestro stack. 


```
service: contratarwifiplan

```

- Y en `provider` debemos especificar el perfil con el que vamos a estar trabajando. 
 

![Serverless campo provider](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/73xdcc9yyyfx14hq1emk.png)

## Orquestación

{% youtube ED_72x0fimE %}

Queremos crear esta Step Function...

![StepFunction modelo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wsrr5l8ticdrvrgbafkb.png)

Una vez iniciado, vamos a comenzar con lo que se conoce como orquestación,y vamos a trabajar en el archivo asl. 
Entonces, en la carpeta de stepFunctions vamos a crear un archivo llamado `contratarServicioWifi.asl.json`

En este archivo vamos a orquestar la máquina de estado. 

```
{
    "Comment": "State Machine para contratar servicio de Wifi",
    "StartAt": "Medios de Pago",
    "States": {
        "Medios de Pago": {
            "Type": "Choice",
            "Choices": [
                {
                    "Variable": "$.medioDePago",
                    "StringEquals": "Debito",
                    "Next": "Pago Debito"
                }
            ],
            "Default": "Pago Credito"
        },
        "Pago Debito": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "Parameters": {
                "FunctionName": {
                    "Fn::GetAtt": [
                        "pagoDebito",
                        "Arn"
                    ]
                },
                "Payload": {
                    "Input.$": "$"
                }
            },
            "Next": "Respuesta SQS",
            "Catch": [
                {
                    "ErrorEquals": [
                        "Error"
                    ],
                    "Next": "Lambda Error"
                }
            ]
        },
        "Pago Credito": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "Parameters": {
                "FunctionName": {
                    "Fn::GetAtt": [
                        "pagoCredito",
                        "Arn"
                    ]
                },
                "Payload": {
                    "Input.$": "$"
                }
            },
            "Next": "Respuesta SQS",
            "Catch": [
                {
                    "ErrorEquals": [
                        "Error"
                    ],
                    "Next": "Lambda Error"
                }
            ]
        },
        "Lambda Error": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "Parameters": {
                "FunctionName": {
                    "Fn::GetAtt": [
                        "formatError",
                        "Arn"
                    ]
                },
                "Payload": {
                    "Input.$": "$"
                }
            },
            "Next": "Respuesta SQS"
        },
        "Respuesta SQS": {
            "Type": "Task",
            "Resource": "arn:aws:states:::aws-sdk:sqs:sendMessage",
            "Parameters": {
                "MessageBody.$": "$.Payload",
                "QueueUrl": "no hay"
            },
            "End": true
        }
    }
}

```

Vamos a explicar algunas partes...

```
   "Comment": "State Machine para contratar servicio de wifi",
   "StartAt": "Medios de Pago",
```

+ Comment, va a ir una pequeña descripción de lo que hace nuestra máquina de estado. 
+ StartAt : Hace referencia a con qué task empieza nuestra máquina de estado. 
+ States: Acá van a estar todos los steps de nuestra máquina:
  

#### Medios de Pago


```
        "Medios de Pago": {
            "Type": "Choice",
            "Choices": [
                {
                    "Variable": "$.medioDePago",
                    "StringEquals": "Debito",
                    "Next": "Pago Debito"
                }
            ],
            "Default": "Pago Credito"
        },

```

   El primer state es de `tipo Choice`, porque dependiendo del medio de pago, elige un flujo o el otro. 
Tenemos el caso en el que, si el medio de pago dice Débito, se seguirá por el state `Pago Debito`, caso contrario elige `Pago Crédito`. 

**_Disclaimer_**: Este state (y toda la máquina) fue creado con la suposición de que el json que va a recibir sigue este esqueleto 

```
{
   "servicio":{
      "plan":"String",
      "precio":"Number"
   },
   "medioDePago":"String",
   "tarjeta":{
      "cantCuotas":"String",
      "nroTarjeta":"String"
   }
}

```
 

#### Pago Debito

```
"Pago Debito": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "Parameters": {
                "FunctionName": {
                    "Fn::GetAtt": [
                        "pagoDebito",
                        "Arn"
                    ]
                },
                "Payload": {
                    "Input.$": "$"
                }
            },
            "Next": "Respuesta SQS",
            "Catch": [
                {
                    "ErrorEquals": [
                        "Error"
                    ],
                    "Next": "Lambda Error"
                }
            ]
        },

```

Tenemos un step de `tipo Task`, que va a tener como recurso una lambda que va a hacer el trabajo de procesar el pago con débito. 

```
FunctionName": {
    "Fn::GetAtt": [
        "pagoDebito",
        "Arn"
    ]
},
```

La lambda aún no está creada, pero en ciertos casos es cómodo tener referenciado el nombre de la función que vamos a crear. 

También tenemos un `Catch` que va a manejar los errores que recibimos y los va a redirigir a la lambda que procese los errores. 

#### Pago Crédito

```
 "Pago Credito": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "Parameters": {
                "FunctionName": {
                    "Fn::GetAtt": [
                        "pagoCredito",
                        "Arn"
                    ]
                },
                "Payload": {
                    "Input.$": "$"
                }
            },
            "Next": "Respuesta SQS",
            "Catch": [
                {
                    "ErrorEquals": [
                        "Error"
                    ],
                    "Next": "Lambda Error"
                }
            ]
        },
```

El orden de los States Pago Débito o Pago Crédito puede estar invertido y no cambiaría la ejecución. 
Al igual que el anterior, escribí el nombre que voy a querer que tenga la función de la lambda. Y también tiene un manejo de error que se maneja con el `Catch`. 

Cualquiera sea el método de pago, si funciona, el siguiente state es `Respuesta SQS`.

#### Lambda Error 

```
"Lambda Error": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "Parameters": {
                "FunctionName": {
                    "Fn::GetAtt": [
                        "formatError",
                        "Arn"
                    ]
                },
                "Payload": {
                    "Input.$": "$"
                }
            },
            "Next": "Respuesta SQS"
        },
```

Este state también es de `tipo Task`. Igual que las anteriores, inventé el nombre de la función de la lambda. 
Cómo state siguiente tiene `Respuesta SQS` porque una vez que ya fue manejado el error queremos devolverlo al cliente.  

####  Respuesta SQS

```
 "Respuesta SQS": {
            "Type": "Task",
            "Resource": "arn:aws:states:::aws-sdk:sqs:sendMessage",
            "Parameters": {
                "MessageBody.$": "$.Payload",
                "QueueUrl": "no esiste"
            },
            "End": true
        }

```
State de `tipo Task`, la diferencia es que en el resource no tenemos la invocación de una lambda, sino que el envío de un mensaje a una cola de mensajes, usando el servicio de SQS.  
 
Al final tenemos el `End:  true` porque es donde finaliza la máquina de estado que habíamos creado.

---

Una vez orquestada nuestra stepFunction tenemos que registrarla en el archivo `Serverless.yml`

Para ellos debemos: 

+ Instalar el siguiente plugin.

```
npm install --save-dev serverless-step-functions

```

+ Registrarlo en los plugins del archivo serverless.

```
plugins:
 - serverless-step-functions

```

+ Debajo de provider tenemos que escribir las siguientes 4 líneas. 

```
stepFunctions: 
  stateMachines: 
    contratarServicioWifi: 
      name: ${self:service}-stateMachine
      definition: ${file(./src/stepFunctions/contratarServicioWifi.asl.json)}

```

El nombre de la step Function va a ser 
`contratarServicioWifi-stateMachine` la variable `${self:service}` hace referencia al nombre de nuestro stack, nombrado previamente en el serverless.yml  


## Lambdas

{% youtube QhAj0rVifSM %}

Vamos a comenzar entonces a crear las lambdas que vamos a necesitar. 
En principio, vamos a tener 3 lambdas que pertenezcan a nuestra máquina de estado. 

Dentro de la carpeta lambdas vamos a crear tres archivos llamados `formatError.js`, `pagoCredito.js` y `pagoDebito.js`


![tres lambdas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hn6hrmarr0rwcdpefhgd.png)

#### PAGO DEBITO

Esta lambda va contener el siguiente código. 

```
const pagoDebito = (event)  => {
    console.log('event: ', JSON.stringify(event, null, 2)); 

    const inputData = event.Input;

    validarPago(inputData);

    return {
        status: 200,
        servicio: {
            plan: inputData.servicio.plan,
            precio: inputData.servicio.precio
        },
        estado: 'Pagado',
        cantCuotas: inputData.tarjeta.cantCuotas
    }

}

const validarPago = (data) => {
    const {medioDePago} = data;
    const {nroTarjeta} = data.tarjeta;
    if(nroTarjeta.length > 17 || nroTarjeta.length < 16) throw new Error('Numero de tarjeta invalido');
    if(medioDePago !== 'Debito') throw new Error('Metodo de pago invalido');
}
 
exports.handler = (event, context, callback) => {
    callback(null, pagoDebito(event));
}
```

Por convención las lambdas exportan una función llamada `handler`,  es con la cual se referencia la función en el serverless.yml

El `handler` debe funcionar como una función asincrónica, en el caso de no serlo, se va a trabajar con un callback. 

En este caso, en handler llamamos a la función `pagoDébito`, es la función que hace las validaciones y “procesa” el pago. 

En este caso las validaciones fueron: 
 - Validar que el medio de pago sea correcto,
 - El número de tarjeta integrado debe tener la longitud correcta. 


**Data importante mencionada en otro capítulo**, las lambdas trabajan con eventos, te recomiendo realizar el console log como en el ejemplo para poder entender que está recibiendo nuestra lambda. 

#### PAGO CRÉDITO

La lambda que procesa el código para pago con crédito es la siguiente. 

```
const pagoCredito = (event)  => {
    console.log('event: ', JSON.stringify(event, null, 2)); 

    const inputData = event.Input;

    validarPago(inputData);

    return {
        status: 200,
        servicio: {
            plan: inputData.servicio.plan,
            precio: inputData.servicio.precio
        },
        estado: 'Pagado',
        cantCuotas: inputData.tarjeta.cantCuotas
    }

}

const validarPago = (data) => {
    const {medioDePago} = data;
    const {nroTarjeta, cantCuotas} = data.tarjeta;
    if(nroTarjeta.length > 17 || nroTarjeta.length < 16) throw new Error('Numero de tarjeta invalido');
    if(medioDePago !== 'Debito') throw new Error('Metodo de pago invalido');
    if(!cantCuotas) throw new Error('Se necesita espicificar cantidad de cuotas')
}
 
exports.handler = (event, context, callback) => {
    callback(null, pagoCredito(event));
}

```

Al igual que la anterior exportamos la función `handler`, y a la hora de procesar el pago tenemos un par de validaciones. 

#### FORMAT ERROR

Esta va a ser la lambda que maneje los errores que se reciban del “procesamiento de los pagos” 

La información que le va a llegar es distinta  a las de las otras lambdas, porque al ser redirigido por el catch solo se envía el error. 

Una vez aclarado eso: 

```
const formatError = (event) => {
    console.log('event: ', JSON.stringify(event, null, 2)); 

    return {
        status: 500,
        estado: "Cancelado",
        cause: JSON.parse(event.Input.Cause).errorMessage
    }
}

exports.handler = (event, context, callback) => {
    callback(null, formatError(event));
}

```

Para cuando falle decidí enviar un `status : 500`, la `descripción del error` y la aclaración de que el `estado` del pago fue `cancelado`. 

Ahora que ya tenemos las lambdas debemos agregarlas al serverless.yml para que cuando hagamos el deploy se creen en la nube. 

```
functions:
  pagoCredito:
    handler: ./src/lambdas/pagoCredito.handler
  pagoDebito:
    handler: ./src/lambdas/pagoDebito.handler
  formatError:
    handler: ./src/lambdas/formatError.handler
```

Vamos a declarar el apartado `functions` y debajo vamos a llamar las lambdas con los nombres de las funciones que declaramos en el archivo asl. 
Y luego para que serverless sepa qué función debe exportar, en el campo `handler` declaramos la función que se exporta de nuestro archivos. 
Como dije en un principio, por convención, es el handler. 


## SQS

{% youtube Dz1enl1n3uU %}


Vamos a crear nuestra SQS (cola de mensajes) para poder manejar los mensajes recibidos por las lambdas de proceso de pago o el de format error. 


Cuando creamos nuestra `SQS` se recomienda crear también una dead letter queue (`DLQ`). Esta es la cola a la que van a ir los mensajes que no pudieron ser procesados por la cola de mensajes principal. 

Para crear estos servicios vamos a crear un archivo en la carpeta `resources`, llamado `SQS.yml`

En ese yml vamos a crear la `SQS` y su `DLQ`.

```
SendQueue: 
  Type: AWS::SQS::Queue
  Properties: 
    RedrivePolicy: 
      deadLetterTargetArn: !GetAtt SendQueueDLQ.Arn
      maxReceiveCount: 3
  DependsOn: SendQueueDLQ

SendQueueDLQ: 
  Type: AWS::SQS::Queue

```

Una vez creada debemos llevarla al archivo `serverless.yml` para que podamos usarla para trabajar. 

Creamos el campo `resources`. 

Este puede contar con dos campos extras, el campo `Resources` (con mayúscula) en donde se declaran los recursos que queremos llevar a la nube, y el campo `Outputs` que nos permite publicar servicios en nuestro stack que luego van a necesitar ser consultados incluso por otros stacks. Es una forma de dejar la data de algunos servicios de forma “pública”.

```
resources: 
  Resources: 
    SendQueue: ${file(./src/resources/SQS.yml):SendQueue}
    SendQueueDLQ: ${file(./src/resources/SQS.yml):SendQueueDLQ}
```

Para completar la máquina de estado vamos a necesitar la url de la Queue, es por eso que la vamos a exportar por medio de un output.

```
  Outputs: 
    SendQueueURL:
      Value: 
        Ref: SendQueue
      Export:
        Name: SendQueue

```

Con estos datos ya completos vamos a hacer el primer deploy. 

```
sls deploy
```

Debido a que para consumir la url necesitamos que el stack esté en `cloudFormation`. 
 
_CloudFormation es otro servicio de AWS que vamos a estar utilizando para guardar los stacks y los outputs. Tiene una capa gratuita de 1000 operaciones del controlador por mes por cuenta._ 

Una vez que ya esté el deploy hecho, vamos a ir a nuestra orquestación de la máquina y vamos a reemplazar el string anterior por una variable de cloudformation de la siguiente manera: 

```
        "Respuesta SQS": {
            "Type": "Task",
            "Resource": "arn:aws:states:::aws-sdk:sqs:sendMessage",
            "Parameters": {
                "MessageBody.$": "$.Payload",
                "QueueUrl": "${cf:contratarwifiplan-${opt:stage, 'dev'}.SendQueueURL}"
            },
            "End": true
        }

```

Ahora nuestra última task va a mandar todos los mensajes del proceso de pago a una cola de mensajes. 

## Eventos

{% youtube ChCFAzG61GA %}

Bueno, en este caso vamos a tener un extra para el proceso de datos de la máquina de estado. 

Esa cola de sqs recibe los mensajes, pero _¿Qué hacemos con esa cola de mensajes?_

Bueno, para eso vamos a crear una lambda por fuera de la stepFunction que procese los mensajes recibidos. 

_¿Como?_

Como dijimos en el capítulo pasado, las lambdas pueden ser invocadas por distintos eventos. Esta lambda que vamos a crear va a ser invocada por cada mensaje que reciba la cola sqs. 

En la carpeta lambdas, vamos a crear una lambda que se llame `enviarData.js` con el siguiente código.

```
const enviarData = (event) => {
    console.log('event: ', JSON.stringify(event, null, 2)); 

    console.log(JSON.parse(event.Records[0].body))

    return JSON.parse(event.Records[0].body)
}

exports.handler = (event, context, callback) => {
    callback(null, enviarData(event));
}
 
```

El código lo que hace es retornar la data del mensaje. Las colas de sqs trabajan con eventos que contienen `Records`. 

Una vez creada la lambda, vamos a registrarla al archivo `serverless.js`

Debajo de las anteriores agregamos la nueva lambda. Para declarar el evento que la va a despertar agregamos el campo events, y aclaramos el servicio sqs. 
El arn que referenciamos es el de nuestra Queue creada anteriormente.

```
  enviarData: 
    handler: ./src/lambdas/enviarData.handler
    events: 
      - sqs: 
          batchSize: 1
          arn: 
            Fn::GetAtt: [SendQueue, Arn]
```

## Roles

{% youtube y1q8E2MQGDA %}

En algunos casos nuestros servicios necesitan permisos específicos para utilizar nuestros servicios. 

En el caso de nuestra stepFunction necesitamos dos permisos
1. Para invocar las lambdas que funcionan dentro de la máquina de estado (`lambda:InvokeFunction`)
2. Para que nuestra stepFunction pueda mandar mensajes a la sqs. (`sqs:SendMessage`)


Para otorgar estos permisos vamos a ir a la carpeta resources y crear el archivo `StepFunctionRole.yml`

El código es el siguiente: 

```
ContratarServicioWifiMachineRole:
 Type: AWS::IAM::Role
 Properties:
   RoleName: ContratarServicioWifiMachineRole
   AssumeRolePolicyDocument:
     Statement:
     - Effect: Allow
       Principal:
         Service:
           - 'states.amazonaws.com'
       Action:
         - 'sts:AssumeRole'
   Policies:
     - PolicyName: statePolicy
       PolicyDocument:
         Statement:
          - Effect: Allow
            Action: 
              - sqs:SendMessage
            Resource: 
              - Fn::GetAtt: [SendQueue, Arn]
          - Effect: Allow
            Action: 
              - 'lambda:InvokeFunction'
            Resource: 
              - !Join ['-', [ !Join [':', ['arn:aws:lambda',!Ref 'AWS::Region', !Ref 'AWS::AccountId' ,'function', !Ref 'AWS::StackName']], 'pagoCredito' ]]
              - !Join ['-', [ !Join [':', ['arn:aws:lambda',!Ref 'AWS::Region', !Ref 'AWS::AccountId' ,'function', !Ref 'AWS::StackName']], 'pagoDebito' ]]
              - !Join ['-', [ !Join [':', ['arn:aws:lambda',!Ref 'AWS::Region', !Ref 'AWS::AccountId' ,'function', !Ref 'AWS::StackName']], 'formatError' ]]


```

El formato de los permisos es 
+ Effect : Hace referencia a la acción de autorización que queremos. 
+ Action: Cual es la acción que va a ser afectada por el efecto anterior. 
+ Resources: Recomiendo siempre poner el recurso específico que queremos afectar, aunque existe la posibilidad de usar el `“*”`, que hace referencia a todos. 

Cuando tenemos los permisos creados vamos a importarlos al archivo `serverless.yml`

En `Resources`, debajo de la declaración de las SQS vamos a escribir…

```
 ContratarServicioWifiMachineRole: ${file(./src/resources/StepFunctionsRole.yml):ContratarServicioWifiMachineRole}
```
Y ya referenciado, le asignamos el rol a nuestra máquina de estado. 

```
stepFunctions: 
  stateMachines: 
    contratarServicioWifi: 
      name: ${self:service}-stateMachine
      definition: ${file(./src/stepFunctions/contratarServicioWifi.asl.json)}
      role: 
        Fn::GetAtt: [ContratarServicioWifiMachineRole, Arn]

```
Una vez que esta el role asignado. 

Corremos 

```
sls deploy  
```

y… 

## Voilà

{% youtube 9sQeCYWeivY %}


Tenemos nuestra máquina de estado en la nube. 

Les dejo el link al [repo](https://github.com/GiulianaOlmos/contratarWifiPlan)

Para probar la máquina de estado debemos hacer click en `Start Execution`.


![Start Execution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7smreyoe4clr7a890v7h.png)

Tenemos dos json de ejemplos, uno de éxito y el otro de fracaso. 

#### Éxito

```
{
    "servicio": {
        "plan": "1MB",
        "precio": 1000
    },
    "medioDePago": "Debito",
    "tarjeta": {
        "cantCuotas": "06",
        "nroTarjeta": "1234567898745896"
    }
}

```


![Execution Success](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmejjkjz679ztncfwgm4.png)

Las máquinas de estados generan logs, que son un detalle de los inputs y outputs de cada task .
Las lambdas también general logs, que se guardan en el servicio de `CloudWatch`, ahí podemos revisar los resultados de nuestros `console.logs()`

#### Fracaso

```
{
    "servicio": {
        "plan": "1MB",
        "precio": 1000
    },
    "medioDePago": "Debito",
    "tarjeta": {
        "cantCuotas": "06",
        "nroTarjeta": "123545646544567898745896"
    }
}

```


![Execution Failed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vbd85acigy36nmob91zp.png)

Este es el caso en el que falla y el error es atrapado en el lambda error. 

Tenemos el ejemplo del input que recibe la respuesta de SQS con el error que formatea el Lambda Error. 


![Input Format Error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifthgq1hkf5wwu7ce2ff.png)


## EL FIN


Por fin tenemos nuestra stepFunction lista y funcionando :D 
Espero que se hayan divertido y hayan renegado. 
En los próximos capítulos vamos a tener agregados extras para aumentar la complejidad de nuestra StepFunction, y algunas nuevas explicaciones y funcionalidades. 

Recuerden que si este post les fue útil, pueden invitarme un cafecito. 

[![Invitame un café en cafecito.app](https://cdn.cafecito.app/imgs/buttons/button_2.svg)](https://cafecito.app/giulianaeolmos)

No duden en escribirme si tienen alguna duda. 

Nos vemos la semana que viene.