O blog da AWS

Simplificando a experiência do desenvolvedor com variáveis e JSONata no AWS Step Functions

Este post foi escrito por Uma Ramadoss, Especialista Principal SA em Serverless e Dhiraj Mahapatro, Especialista Principal SA em Amazon Bedrock

O AWS Step Functions está introduzindo variáveis e transformações de dados JSONata. As variáveis permitem que os desenvolvedores atribuam dados em um estado e os referenciem em qualquer etapa subsequente, simplificando o gerenciamento da carga útil do estado sem a necessidade de passar dados por vários estados intermediários. Com o JSONata, uma linguagem de consulta e transformação de código aberto, agora você executa manipulação e transformação avançadas de dados, como formatação de data e hora e operações matemáticas.

Este blog explora os poderosos recursos dessa nova funcionaldiade, aprofundando a simplificação do compartilhamento de dados entre estados usando variáveis e reduzindo a complexidade da manipulação de dados por meio de expressões JSONata avançadas

Visão geral

Os clientes escolhem o Step Functions para criar fluxos de trabalho complexos que envolvem vários serviços, como AWS Lambda, AWS Fargate, Amazon Bedrock e integrações de API HTTP. Dentro desses fluxos de trabalho, você cria estados para interagir com esses vários serviços, passando dados de entrada e recebendo respostas como saída. Embora você possa usar as funções do Lambda para manipulações de data, hora e números além dos recursos intrínsecos do Step Functions, esses métodos enfrentam dificuldades crescentes de complexidade, levando a restrições de carga útil, sobrecargas de conversão de dados e mais mudanças de estado. Isso afeta o custo geral da solução. Você usa variáveis e JSonata para resolver isso.

Para ilustrar esses novos recursos, considere o mesmo caso de uso comercial do blog JSONPath, um processo de integração de clientes no setor de seguros. Um cliente em potencial fornece informações básicas, incluindo nomes, endereços e interesses de seguro, ao se inscrever. Esse processo Know-Your-Customer (KYC) inicia um fluxo de trabalho do Step Functions com uma carga contendo esses detalhes. O fluxo de trabalho decide a aprovação ou recusa do cliente, seguido pelo envio de uma notificação.

{
  "data": {
    "firstname": "Jane",
    "lastname": "Doe",
    "identity": {
      "email": "jdoe@example.com",
      "ssn": "123-45-6789"
    },
    "address": {
      "street": "123 Main St",
      "city": "Columbus",
      "state": "OH",
      "zip": "43219"
    },
    "interests": [
      {"category": "home", "type": "own", "yearBuilt": 2004, "estimatedValue": 800000},
      {"category": "auto", "type": "car", "yearBuilt": 2012, "estimatedValue": 8000},
      {"category": "boat", "type": "snowmobile", "yearBuilt": 2020, "estimatedValue": 15000},
      {"category": "auto", "type": "motorcycle", "yearBuilt": 2018, "estimatedValue": 25000},
      {"category": "auto", "type": "RV", "yearBuilt": 2015, "estimatedValue": 102000},
      {"category": "home", "type": "business", "yearBuilt": 2009, "estimatedValue": 500000}
    ]
  }
}
JSON

O diagrama de fluxo de trabalho original ilustra o fluxo sem novos recursos, enquanto o novo diagrama mostra o fluxo de trabalho criado aplicando variáveis e JSONata. Acesse os fluxos de trabalho no repositório do GitHub a partir das ramificações (branch) principal (fluxo de trabalho original) jsonata-variables (novo fluxo de trabalho).

Fluxo de trabalho original

Figura 1: Fluxo de trabalho original

Novo fluxo de trabalho

Figura 2: Novo fluxo de trabalho

Configuração

Siga as etapas no README para criar essa máquina de estado e limpar depois que o teste for concluído.

Simplificando o compartilhamento de dados com variáveis

As variáveis permitem que você instancie ou atribua resultados de estado a uma variável referenciada em estados futuros. Em um único estado, você atribui várias variáveis com valores diferentes, incluindo dados estáticos, resultados de um estado, expressões JSONPath ou JSONata e funções intrínsecas. O diagrama a seguir ilustra como as variáveis são atribuídas e usadas dentro de uma máquina de estado:

Atribuição e escopo de variáveis

Figura 3: Atribuição e escopo de variáveis

Escopo variável

Em Step Functions, as variáveis têm um escopo semelhante às linguagens de programação. Você define variáveis em níveis diferentes, com escopo interno e externo. As variáveis de escopo interno são definidas dentro de fluxos de trabalho mapeados, paralelos ou aninhados, e essas variáveis só podem ser acessadas dentro de seu escopo específico. Como alternativa, você define variáveis de escopo externo no nível superior. Uma vez atribuídas, essas variáveis podem ser acessadas de qualquer estado posterior, independentemente de sua ordem de execução no futuro. No entanto, a partir do lançamento deste blog, o estado do mapa distribuído não pode referenciar variáveis em escopos externos. O guia do usuário sobre escopo variável detalha esses casos extremos.

Atribuição e uso de variáveis

Para definir o valor de uma variável, use o campo especial Assign. A parte JSONata desta postagem do blog mais abaixo explica o propósito de {%%}.

"Assign": {
  "inputPayload": "{% $states.context.Execution.Input %}",
  "isCustomerValid": "{% $states.result.isIdentityValid and $states.result.isAddressValid %}"
} 
JSON

Use uma variável escrevendo um cifrão ($) antes do nome.

{
  "TableName": "AccountTable",
  "Item": {
    "email": {
      "S": "{% $inputPayload.data.email %}"
    },
    "firstname": {
      "S": "{% $inputPayload.data.firstname %}"
    },....
} 
JSON

Simplificando as manipulações de dados com o JSONata

JSONata é uma linguagem leve de consulta e transformação para dados Json. O JSONata oferece mais recursos em comparação com o JSONPath no Step Functions.

Ao definir queryLanguage como “JSonata” e usar tags {%%} para expressões JSONata permite que você aproveite o JSONata em uma máquina de estado. Aplique essa configuração no nível superior da máquina de estado (workflow) ou em cada nível de tarefa. O JSONata no nível da tarefa oferece um controle refinado da escolha entre JSONata e JSONPath. Essa abordagem é valiosa para fluxos de trabalho complexos nos quais você deseja simplificar um subconjunto de estados com o JSONata e continuar usando o JSONPath para o resto. O JSonata fornece mais funções e operadores do que JSONPath e funções intrínsecas em Step Functions. Ativar o atributo queryLanguage como JSONata no nível da máquina de estado desativa o JSONPath e, portanto, restringe o uso de inputPath, Parameters, resultPath, resultSelector e outputPath. Em vez desses parâmetros JSONPath, o JSONata usa Argumentos e Output

Otimizando estados simples

Uma das primeiras coisas a notar na nova máquina de estado é que o processo de verificação não usa mais as funções do Lambda, conforme visto na comparação a seguir:

Funções do Lambda substituídas por estados de passage

Figura 4: Funções do Lambda substituídas por estados de passagem

Na abordagem anterior, uma função Lambda é usada para validar e-mail e SSN usando expressões regulares:

const ssnRegex = /^\d{3}-?\d{2}-?\d{4}$/;
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;

exports.lambdaHandler = async event => {
  const { ssn, email } = event;
  const approved = ssnRegex.test(ssn) && emailRegex.test(email);

  return {
    statusCode: 200,
    body: JSON.stringify({ 
      approved,
      message: `identity validation ${approved ? 'passed' : 'failed'}`
    })
  }
};
JavaScript

Com o JSonata, você define expressões regulares diretamente na Amazon States Language (ASL) da máquina de estado. Você usa um estado Pass e $match () do JSONata para validar o e-mail e o SSN.

{
  "StartAt": "Check Identity",
   "States": {
    "Check Identity": {
      "Type": "Pass",
      "QueryLanguage": "JSONata",
      "End": true,
      "Output": {
        "isIdentityValid": "{% $match($states.input.data.identity.email, /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/) and $match($states.input.data.identity.ssn, /^(\\d{3}-?\\d{2}-?\\d{4}|XXX-XX-XXXX)$/) %}"
      }
    }
   }
}
JSON

O mesmo se aplica à validação do endereço dentro de um estado Pass usando funções sofisticadas de string JSONata, como $length, $trim, $each e $not da JSONata:

{
  "StartAt": "Check Address",
  "States": {
    "Check Address": {
      "Type": "Pass",
      "QueryLanguage": "JSONata",
      "End": true,
      "Output": {
        "isAddressValid": "{% $not(null in $each($states.input.data.address, function($v) { $length($trim($v)) > 0 ? $v : null })) %}"
      }
    }
  }
}
JSON

Ao usar JSONata, $states se torna uma variável reservada.

Agregação de resultados

Anteriormente, com o JSONPath, o uso de uma expressão fora do estado Choice não estava disponível. Esse não é mais o caso da JSONata. O estado paralelo, no exemplo, reúne os resultados da verificação de identidade e endereço de cada subetapa. Você mescla os resultados em uma variável booleana isCustomerValid.

"Verification": {
  "Type": "Parallel",
  "QueryLanguage": "JSONata",
  ...
  "Assign": {
    "inputPayload": "{% $states.context.Execution.Input %}",
    "isCustomerValid": "{% $states.result.isIdentityValid and $states.result.isAddressValid %}"
  },
  "Next": "Approve or Deny?"
}
JSON

A parte crucial a ser observada aqui é o acesso aos resultados por meio de $states.result e o uso do operador booleano AND dentro de {%%}. Em última análise, isso torna o estado Choice downstream, que usa essa variável, mais simples. Os operadores no JSONata oferecem flexibilidade para escrever expressões como essas sempre que possível, o que reduz a necessidade de uma camada de computação para processar transformações de dados simples.

Além disso, o estado Choice se torna mais simples de usar com operadores e expressões flexíveis do JSONata, desde que as expressões dentro de {%%} resultem em um valor verdadeiro ou falso.

"Approve or Deny?": {
  "Type": "Choice",
  "QueryLanguage": "JSONata",
  "Choices": [
    {
      "Next": "Add Account",
      "Condition": "{% $isCustomerValid %}"
    }
  ],
  "Default": "Deny Message"
}
JSON

Funções intrínsecas como funções JSONata

O Step Functions fornece funções JSonata integradas para permitir a paridade com as funções intrínsecas do Step Functions. A etapa PutItem do DynamoDB mostra como você usa $uuid () que tem a mesma funcionalidade da função intrínseca States.uuid (). Você também obtém funções específicas do JSonata em data e hora. O estado a seguir mostra o uso de $now () para obter o timestamp atual como ISO-8601 como uma string antes de inserir esse item na tabela do DynamoDB.

"Add Account": {
  "Type": "Task",
  "QueryLanguage": "JSONata",
  "Resource": "arn:aws:states:::dynamodb:putItem",
  "Arguments": {
    "TableName": "AccountTable",
    "Item": {
      "PK": {
        "S": "{% $uuid() %}"
      },
      "email": {
        "S": "{% $inputPayload.data.identity.email %}"
      },
      "name": {
        "S": "{% $inputPayload.data.firstname & ' ' & $inputPayload.data.lastname  %}"
      },
      "address": {
        "S": "{% $join($each($inputPayload.data.address, function($v) { $v }), ', ') %}"
      },
      "timestamp": {
        "S": "{% $now() %}"
      }
    }
  },
  "Next": "Interests"
}
JSON

Observe que você não mais aplica a notação .$ em S.$, pois as expressões JSONata reduzem a dor do desenvolvedor ao criar a ASL da máquina de estado. Explore as funções adicionais do JSonata acessíveis no Step Functions.

JSONata avançado

A flexibilidade da JSonata vem de suas funções pré-construídas, suporte a funções de ordem superior e de programação funcional. Com o JSONPath, você usou as expressões avançadas “inputPath”: “$.. interest [? (@.category==home)]” para filtrar os interesses relacionados ao seguro residencial da matriz de juros. O JSONata faz muito mais do que filtrar. Por exemplo, você procura juros de seguro residencial, totalAssetValue do tipo de categoria como casa e se refere a campos existentes, como nome e e-mail, como variáveis JSONata:

(
    $e := data.identity.email;
    $n := data.firstname & ' ' & data.lastname;
    
    data.interests[category = 'home']{
      'customer': $n,
      'email': $e,
      'totalAssetValue': $sum(estimatedValue),
      category: {type: yearBuilt}
    }
)
JSON

O resultado JSON será:

{
  "customer": "Jane Doe",
  "email": "jdoe@example.com",
  "totalAssetValue": 1400000,
  "home": {
    "own": 2004,
    "business": 2009
  }
}
JSON

Ao seguir essas etapas, você sobe um nível coletando todos os juros do seguro e seus resultados agregados. Observe que o filtro de categoria não está mais presente.

(
    $e := data.identity.email;
    $n := data.firstname & ' ' & data.lastname;
    
    data.interests{
      'customer': $n,
      'email': $e,
      'totalAssetValue': $sum(estimatedValue),
      category: {type: yearBuilt}
    }
)
JSON

o que resulta em:

{
  "customer": "Jane Doe",
  "email": "jdoe@example.com",
  "totalAssetValue": 1549000,
  "home": {
    "own": 2004,
    "business": 2009
  },
  "auto": {
    "car": 2012,
    "motorcycle": 2018,
    "RV": 2015
  },
  "boat": {
    "snowmobile": 2020
  }
}
JSON

Descobrindo expressões complexas

Use o playground do JSONata com seus exemplos para descobrir expressões detalhadas e complexas que atendam às suas necessidades. Veja a seguir um exemplo de uso do playground JSONata:

Image of JSONata playground.

Figura 5: JSonata playground

Considerações

Tamanho variável

O tamanho máximo de uma única variável é 256 KiB. Esse limite ajuda você a ignorar a restrição de tamanho da carga útil do Step Functions, permitindo que você armazene saídas de estado em variáveis separadas. Embora cada variável individual possa ter até 256 KiB de tamanho, o tamanho total de todas as variáveis em um único campo Assign não pode exceder 256 KiB. Use estados de PASS para contornar essa limitação, no entanto, o tamanho total de todas as variáveis armazenadas não pode exceder 10 MiB por execução.

Visibilidade variável

As variáveis são um mecanismo poderoso para simplificar o compartilhamento de dados entre estados. Prefira-os aos campos de saída resultPath, outputPath ou JSonata por causa de sua facilidade de uso e flexibilidade. Há duas situações em que você ainda pode usar Output. Primeiro, você não pode acessar variáveis com escopo interno no escopo externo. Nesses casos, os campos na Saída podem ajudar a compartilhar dados entre diferentes níveis de fluxo de trabalho. Segundo, ao enviar uma resposta do estado final do fluxo de trabalho, talvez seja necessário usar campos nos campos Output. O diagrama de transição a seguir do JSONPath para o JSONata fornece detalhes adicionais:

Image of Transition from JSONPath to JSONata.

Figura 6: Transição do JSONPath para o JSONata

Além disso, as variáveis atribuídas a um estado específico não podem ser acessadas nesse mesmo estado:

"Assign Variables": {
  "Type": "Pass",
  "Next": "Reassign Variables",
  "Assign": {
    "x": 1,
    "y": 2
  }
},
"Reassign Variables": {
  "Type": "Pass",
  "Assign": {
    "x": 5,
    "y": 10,
      ## The assignment will fail unless you define x and y in a prior state.
      ## otherwise, the value of z will be 3 instead of 15.
    "z": "{% $x+$y %}"
  },
  "Next": "Pass"
}
JSON

Melhores práticas

A API de validação do Step Functions fornece verificações semânticas para fluxos de trabalho, permitindo a identificação precoce de problemas. Para garantir atualizações seguras do fluxo de trabalho, é melhor combinar a API de validação com controle de versão e aliases para implantação incremental.

Expressões de várias linhas em JSonata não são JSON válidas. Portanto, use uma única linha como string delimitada por um ponto e vírgula “;”, onde a última linha retorna a expressão.

Mutuamente exclusivo

O uso do tipo QueryLanguage é mutuamente exclusivo. Não misture JSONPath/funções intrínsecas e JSONata durante atribuições de variáveis. Por exemplo, a tarefa abaixo falha porque a variável b usa JSonata, enquanto c usa uma função intrínseca.

"Store Inputs": {
  "Type": "Pass",
  "QueryLanguage": "JSONata"
  "Assign": {
    "inputs": {
      "a": 123,
      "b": "{% $states.input.randomInput %}",
      "c.$": "States.MathRandom($.start, $.end)"
    }
  },
  "Next": "Average"
}
JSON

Para usar variáveis com JSONPath, defina o QueryLanguage como JSONPath ou remova esse atributo da definição da tarefa.

Conclusão

Com variáveis e JSONata, o AWS Step Functions agora eleva a experiência do desenvolvedor para escrever fluxos de trabalho elegantes com código mais simples na Amazon States Language (ASL) que corresponda ao paradigma de programação normal. Agora, os desenvolvedores podem criar com mais rapidez e escrever códigos mais limpos eliminando etapas extras de transformação de dados. Esses recursos podem ser usados em fluxos de trabalho novos e existentes, oferecendo a flexibilidade de atualizar do JSONPath para o JSONata e variáveis.

Variáveis e JSonata estão disponíveis sem custo adicional para clientes em todas as regiões da AWS onde o AWS Step Functions está disponível. Para obter mais informações, consulte o guia do usuário para JSONATA e variáveis, bem como o aplicativo de amostra na ramificação jsonata-variables.

Para expandir seu conhecimento sem servidor, visite Serverless Land.

Este blog é uma tradução do conteúdo original em inglês (link aqui).

Biografia do autor

Uma Ramadoss é Arquiteta de soluções Principal na Amazon Web Services (AWS).

Biografia do tradutor

Daniel Abib Daniel Abib é Arquiteto de Soluções Sênior e Especialista em Amazon Bedrock na AWS, com mais de 25 anos trabalhando com gerenciamento de projetos, arquiteturas de soluções escaláveis, desenvolvimento de sistemas e CI/CD, microsserviços, arquitetura Serverless & Containers e especialização em Machine Learning. Ele trabalha apoiando Startups, ajudando-os em sua jornada para a nuvem.

Biografia do Revisor

Rodrigo Peres é Arquiteto de Soluções na AWS, com mais de 20 anos de experiência trabalhando com arquitetura de soluções, desenvolvimento de sistemas e modernização de sistemas legados