Orquestrando Funções Lambda com AWS Step Functions

Lucas Lascasas
BRLink
Published in
9 min readFeb 24, 2021

Introdução

O Step Functions é um serviço da AWS para a orquestração de funções Lambda dentre outros serviços da AWS. Pelo console, você pode criar uma máquina de estados, onde um objeto é passado a cada transição, podendo ser modificado pelos resultados de execução de um estado. A máquina de estados segue a ordem de execução definida pelo programador, sendo que um estado só é executado se o seu anterior executou corretamente.

Para programar uma máquina de estados no Step Functions, basta criar um documento em Amazon States Language, que é uma linguagem estruturada baseada em JSON. Seguindo os padrões da linguagem, podemos definir estados e transições entre eles. Um estado é um nó de processamento de informação na máquina de estados, podendo ele disparar serviços, criar recursos, definir caminhos, executar processamento paralelo, dentre outros. Já uma transição é a passagem de informação do output de um estado para o input do seu sucessor.

Além de coordenar a execução de serviços, as máquinas de estado possuem um grande poder na tratativa de erros, permitindo a configuração de retries com exponential backoff. Além disso, podemos definir estados de erros quando, mesmo com os retries, um estado do fluxo apresentar erros de execução.

Com o uso do Step Functions, conseguimos desacoplar bem os nossos códigos e conseguimos orquestrá-los com facilidade, reduzindo a quantidade de serviços a gerenciar dentro do ambiente AWS.

Nesse post, iremos orientar a criação de máquinas de estado para orquestrar a execução de funções Lambda.

Meu Primeiro Orquestrador

O console da AWS oferece uma vasta variedade de templates para um ponto de partida ao trabalhar com máquinas de estados. Aqui, vamos explicar algumas propriedades básicas de qualquer máquina de estados. Assim, qualquer código no Step Functions deve conter dois campos obrigatórios, sendo eles StartAt e States. O conteúdo do StartAt é uma string com o nome do estado inicial da sua máquina de estados. Já o campo States contém um objeto JSON onde cada campo desse objeto possui o nome de um estado e seu conteúdo as propriedades desse estado.

No exemplo de código acima, temos um campo Comment opcional que é utilizado para fazer anotações em qualquer objeto dentro da Amazon States Language. O campo StartAt indica que o estado inicial é nomeado Hello e o campo States mapeia os estados. Essa máquina possui dois estados e uma transição. O estado Hello possui o campo Type com o valor Pass, indicando o tipo desse estado, que apenas realiza uma passagem de valor; o campo Result com o valor Hello, indicando que o resultado desse estado é a string “Hello”; e o campo Next, criando uma transição para o estado de nome World. Já o estado World possui campos similares ao seu antecessor, exceto pelo campo End que recebe um booleano indicando se esse estado é um estado final ou não.

No console AWS, sempre que inserimos um JSON válido pelas definições da Amazon States Language, uma representação visual é renderizada demonstrando a máquina de estados originada a partir do código. A imagem acima representa a máquina de estados do exemplo “Hello World”.

Caminho de Dados

Os dados enviados de um estado para o outro dentro de uma máquina de estados seguem o formato de um JSON. Quando um estado executa, ele recebe um objeto JSON como input, processa os seus dados e adiciona seus resultados ao objeto, finalmente o enviando para o estado sucessor.

Para acessar campos específicos desses objetos na Amazon State Language, utilizamos o Jayway JsonPath. Assim, o ‘$’ indica o elemento raiz do objeto e com o uso do operador ‘.’, conseguimos escolher campos específicos pelo nome deles.

Utilizando o objeto acima como exemplo, podemos acessar seus valores da seguinte forma:

Pela Amazon State Language, podemos definir quais dados desse objeto JSON será usado para input e output do estado. Para isso, utilizamos os campos InputPath, OutputPath e ResultPath. Para as subseções abaixo, considere esse código fonte como referência:

InputPath

O campo InputPath define qual campo do objeto JSON recebido pelo estado será de fato utilizado como input durante o processamento. No código de exemplo, o campo InputPath do estado HelloWord indica que esse estado receberá um objeto com diversos campos, mas deve considerar apenas o campo de nome lambda.

Assim, se o objeto de entrada for o seguinte, apenas o objeto com o campo value e o seu valor correspondente serão passados para o estado.

Por padrão, todos os estados que não possuírem o campo InputPath utilizam o objeto inteiro como input.

ResultPath

Com o campo ResultPath, podemos definir em qual local do objeto o resultado de execução do nó será salvo. No exemplo de referência, os dados serão salvos dentro do campo data com um JSON de referência de nome lambdaresult. Assim, o objeto após a execução do estado seria o seguinte:

Por padrão, todos os estados que não possuírem o campo ResultPath sobrescrevem o objeto inteiro com os seus resultados.

OutputPath

Com o campo OutputPath, podemos definir quais dados do objeto serão encaminhados para o estado seguinte, ou seja, o output do estado atual. No exemplo de referência, apenas o objeto do campo data será encaminhado para o próximo estado da máquina de estados. Como o estado possui um campo “End”: true, esse estado será o último do fluxo de execução e seu output será a saída da máquina de estados em si.

Por default, todos os estados que não possuírem um campo OutputPath enviam todo o objeto como saída do estado.

Tipos de Estados

Existem diversos tipos de estados na Amazon States Language. Cada tipo de estado possui uma funcionalidade diferente e possui propriedades diferentes. Nessa seção, entraremos em alguns detalhes dos principais estados para a construção de um orquestrador de funções lambda.

Task

Um estado do tipo Task é utilizado para disparar funções lambda, dentre outras funcionalidades. Para isso, esse estado possui um campo Resource que indica o tipo de recursos que ele está manipulando (utilizamos arn:aws:states:::lambda:invoke para invocar funções lambda). Além disso, esse estado utiliza o campo Parameters para indicar os parâmetros de execução do recurso indicado. Ao lidar com funções lambda, indicamos nesse campo o arn da função em FunctionName e o input dela no campo Payload.

Vale mencionar que podemos passar o arn da função diretamente no campo Resource e o input pelo InputPath, como feito em um exemplo em seções anteriores. Utilizando o campo Payload, temos mais controle dos dados que serão enviados para a função lambda. Note que no exemplo, o campo myName do objeto é mapeado para o campo name do Payload da função, para isso, utilizamos a sintaxe “.$” no final do nome do campo.

Choice

Um estado do tipo Choice é usado para definir o caminho da execução do fluxo com base na análise de uma ou mais variáveis. Assim, esse estado possui um campo Choices com uma lista de possíveis escolhas a serem feitas, além de um campo opcional Default para direcionar o fluxo caso nenhuma condição seja satisfeita.

Cada item do array Choices deve conter ao menos uma comparação com um estado a ser executado na sequência. Essas comparações podem agregar regras booleanas, como Not, And e Or. As comparações em si podem ser do tipo numéricas ou strings, variando com uma boa gama de opções dentro da Amazon States Language.

No Exemplo acima, temos duas variáveis sendo analisadas nas Choices do estado. A comparação das condições é feita em ordem, logo, primeiro avalia-se a variável type e só se avalia a variável value se o type não for “Private”.

Essa é a máquina de estados gerada pelo código de exemplo. Note que o estado Choose Next Step possui quatro transições de saída, mas elas não podem ser disparadas simultaneamente. Um estado Choice realiza apenas uma transição por vez, respeitando a ordem das comparações feitas.

Wait

O estado de tipo Wait é utilizado em fluxos de controle para definir um tempo de espera antes de transitar para o próximo estado. Esse estado pode ser utilizado para dar tempo para um processing job concluir sua execução por exemplo. Assim, esse estado possui um campo Seconds com o tempo em segundos da espera e o campo Next indicando o próximo estado a ser executado. O campo Seconds também pode ser dinâmico ao ser substituído pelo SecondsPath, permitindo que o tempo seja indicado no objeto JSON de input do estado. Além disso, podemos utilizar os campos Timestamp e TimestampPath para trabalhar com formatos do tipo timestamp.

No exemplo acima, temos um estado que realiza a espera por 5 segundos antes de realizar a transição para o Final State.

Parallel

Um estado do tipo Parallel é capaz de disparar múltiplos estados simultaneamente, em paralelo. Esse tipo de estado é ideal para realizar um scatter-gather, onde um dado é enviado para múltiplas funções lambda distintas e em seguida os resultados delas são agregados em um único. Assim, um estado do tipo Parallel possui um campo Branches com uma lista de máquinas de estado próprias. Essas máquinas possuem o campo StartAt e o campo States, podendo ter a complexidade de uma máquina de estados comum. Além disso, esse estado possui um campo Next para indicar uma transição para outro estado depois que todos os estados disparados em paralelo finalizarem suas execuções.

No exemplo acima, temos um estado Parallel com três sub-máquinas de estados, onde duas possuem apenas um estado e uma possui dois. Vale ressaltar que o Final State só é executado ao final da execução dos três caminhos do estado Parallel.

Essa é a representação gráfica de um estado Parallel no Step Functions.

Map

Por fim, temos os estados do tipo Map. Esse tipo de estados permite a paralelização de uma sub-máquina de estados. Dessa forma, podemos disparar um conjunto de estados idênticos para cada elemento de uma lista. Assim, temos o campo ItemsPath com o caminho no objeto de input do estado onde a lista de itens é encontrada para encaminhar cada elemento para uma instância da Map. Além disso, temos o campo MaxConcurrency com o valor máximo de execuções paralelas da sub-máquina. Por fim, temos o campo Iterator com a sub-máquina de estados.

Nesse cenário, temos uma máquina de estados que recebe uma lista de nomes e instancia uma função lambda para cada nome da lista, respeitando o limite de concorrência máximo de 5. Caso a lista tenha mais de 5 entradas, a função irá disparar o máximo possível e, assim que uma função finalizar, um novo item da lista será disparado até que a lista acabe.

Esse seria um exemplo de input do estado Map para a máquina de estados descrita.

Essa é a representação gráfica de uma máquina de estados com Map.

Tratamento de Erros

A Amazon States Language permite o tratamento de erros de duas formas distintas, que se complementam. A primeira é através de retries, onde o campo Retry possui uma lista de tratamentos de erros. Cada item dessa lista possui um campo ErrorEquals com uma lista de possíveis erros a serem capturados, um campo IntervalSeconds com um intervalo em segundos entre retries, um campo BackoffRate com a taxa do exponential backoff e o campo MaxAttempts com a quantidade de tentativas máximas a serem feitas. Ao manipular esses retries como itens de uma lista, podemos definir regras distintas de retry para cada tipo de erro.

A outra forma de tratar erros é com o campo Catch, que funciona de maneira similar ao Retry. O campo Catch possui uma lista de items, onde cada item possui um campo ErrorEquals e um campo Next. Sendo assim, se um dos erros forem capturados por esse campo, o estado indicado em Next será executado. A Amazon States Languagem possui alguns erros padrão que podem ser utilizados nesses campos, como o States.ALL, que indica qualquer erro. Assim como no estado Choice, os erros são avaliados em ordem e apenas um dos itens da lista será acionado.

No exemplo acima, temos uma função que realiza 2 retries com um intervalo de 1 segundo e um exponential backoff de fator 2. Caso todas as tentativas de execução falhem, o campo Catch captura o erro e encaminha a máquina para o estado de erro. Vale apontar os tipos Fail e Succeed dos estados finais dessa máquina. Um estado Fail encerra a execução da máquina de estados como falha, enquanto o Succeed encerra com sucesso. Esses tipos de estados deve ser finais obrigatoriamente.

--

--

Lucas Lascasas
BRLink
Writer for

Master in Computer Science | Manager at BRLink/INGRAM | 4x AWS Certified | AI/ML Black Belt