O blog da AWS

Implementação de funções idempotentes do AWS Lambda com Powertools for AWS Lambda (TypeScript)

Por Alexander Schüren, Sr Specialist SA, Powertools

Um dos princípios de design do AWS Lambda é “desenvolver para novas tentativas e falhas”. Se sua função falhar, o serviço Lambda tentará invocá-la novamente com a mesma carga de eventos. Portanto, quando sua função executa tarefas como processar pedidos ou fazer reservas, é necessário que sua função Lambda gerencie as solicitações de forma idempotente para evitar pagamentos duplicados ou processamento de pedidos, o que pode resultar em uma experiência ruim para o cliente.

Este artigo explica o que é idempotência e como tornar suas funções do Lambda idempotentes usando o utilitário de idempotência do AWS Lambda Powertools  (TypeScript). O utilitário de idempotência Powertools para TypeScript foi desenvolvido em conjunto com a Vanguard e agora está disponível ao público em geral.

Entendendo a idempotência

A idempotência é a propriedade de uma operação que pode ser aplicada várias vezes sem alterar o resultado além da execução inicial. Você pode executar com segurança uma operação idempotente várias vezes sem efeitos colaterais, como registros duplicados ou inconsistências de dados. Isso é especialmente relevante para pagamentos e processamento de pedidos ou integrações de API de terceiros.

Há conceitos importantes a serem considerados ao implementar a idempotência no AWS Lambda. Para cada invocação, você especifica qual subconjunto dos parâmetros (payload) do evento deseja usar para identificar uma solicitação idempotente. Isso é chamado de chave de idempotência. Essa chave pode ser um único campo, como transactionID, uma combinação de vários campos, como customerID e requestId, ou toda a carga útil (payload) do evento.

Como timestamps (data/hora), datas e outros valores gerados na carga afetam a chave de idempotência, recomendamos que você defina campos específicos em vez de usar toda a carga útil do evento.

Ao avaliar a chave de idempotência, você pode decidir se a função precisa ser executada novamente ou enviar uma resposta existente ao cliente. Para fazer isso, você precisa armazenar as seguintes informações para cada solicitação em uma camada de persistência (ou seja, Amazon DynamoDB):

  • Status: IN_PROGRESS, EXPIRED, COMPLETE
  • Dados de resposta: a resposta a ser enviada de volta ao cliente em vez de executar a função novamente
  • Data e hora de expiração: quando o registro de idempotência se torna inválido para reutilização

O diagrama a seguir mostra um fluxo de solicitações bem-sucedido para esse cenário de idempotência:

Idempotent request flow

Quando você invoca uma função do Lambda com um evento específico pela primeira vez, ela armazena um registro com uma chave de idempotência exclusiva vinculada a uma carga útil do evento na camada de persistência.

A função então executa seu código e atualiza o registro na camada de persistência com a resposta da função. Para invocações subsequentes com a mesma carga, você deve verificar se a chave de idempotência existe na camada de persistência. Se existir, a função retornará a mesma resposta para o cliente. Isso evita várias invocações da função, tornando-a idempotente.

Há mais casos extremos a serem considerados, como quando o registro de idempotência expirou ou o tratamento de falhas entre o cliente, a função Lambda e a camada de persistência. A documentação do Powertools for AWS Lambda (TypeScript) abrange detalhadamente todos os fluxos de solicitações.

Idempotência com Powertools for AWS Lambda (TypeScript)

O Powertools for AWS Lambda, disponível em Python, Java, .NET e TypeScript, fornece utilitários para funções do Lambda para facilitar a adoção das melhores práticas e reduzir a quantidade de código necessária para realizar tarefas recorrentes. Em particular, ele fornece um módulo para lidar com a idempotência.

Esta postagem mostra exemplos usando a versão TypeScript do Powertools. Para começar a usar o módulo de idempotência Powertools, você deve instalar a biblioteca e configurá-la em seu processo de compilação. Para obter mais detalhes, siga a documentação do Powertools for AWS Lambda.

Começando

O Powertools for AWS Lambda (TypeScript) é modular, o que significa que você pode instalar o utilitário de idempotência independentemente do Logger, Tracing, Metrics ou outros pacotes. Instale a biblioteca de utilitários de idempotência e o cliente AWS SDK v3 para DynamoDB em seu projeto usando npm:

npm i @aws-lambda-powertools/idempotency @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
Bash

Antes de começar, você precisa criar uma camada de armazenamento persistente na qual o utilitário de idempotência possa armazenar seu estado. O perfil (IAM Role) da função Lambda (AWS Identity and Access Management (IAM)) deve ter as permissões dynamodb:GetItem, dynamodb:PutItem, dynamodb:UpdateItem and dynamodb:DeleteItem permissions.

Atualmente, o DynamoDB é a única camada de armazenamento persistente compatível, então você precisará criar uma tabela primeiro. Use o AWS Cloud Development Kit (CDK), o AWS CloudFormation, o AWS Serverless Application Model (SAM) ou qualquer ferramenta de infraestrutura como código de sua escolha que ofereça suporte aos recursos do DynamoDB.

As seções a seguir ilustram como instrumentar seu código de função Lambda para torná-lo idempotente usando uma função wrapper ou usando middleware middy.

Usando o invólucro (wrapper) de funções

Supondo que você tenha criado uma tabela do DynamoDB com o nome IdemPotencyTable, crie uma camada de persistência no código da função do Lambda:

import { makeIdempotent } from "@aws-lambda-powertools/idempotency";
import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb";

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: "IdempotencyTable",
});
TypeScript

Agora, aplique o wrapper de função makeIDemPotent ao seu handler de funções do Lambda para torná-lo idempotente e usar o armazenamento de persistência configurado anteriormente.

import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import type { Context } from 'aws-lambda';
import type { Request, Response, SubscriptionResult } from './types';

export const handler = makeIdempotent(
  async (event: Request, _context: Context): Promise<Response> => {
    try {
      const payment =// create payment
	  
      return {
        paymentId: payment.id,
        message: 'success',
        statusCode: 200,
      };

    } catch (error) {
      throw new Error('Error creating payment');
    }
  },
  {
    persistenceStore,
  }
);
TypeScript

A função processa o evento recebido para criar um pagamento e devolver o PaymentID, a mensagem e o status ao cliente. Tornar o handler de funções do Lambda idempotente garante que os pagamentos sejam processados somente uma vez, apesar de várias invocações do Lambda com a mesma carga útil (payload) do evento. Você também pode aplicar o wrapper de função makeIDemPotent a qualquer outra função fora do seu handler.

Use as seguintes definições de tipo para este exemplo adicionando um arquivo types.ts à sua pasta de origem:

type Request = {
  user: string;
  productId: string;
};

type Response = {
  [key: string]: unknown;
};

type SubscriptionResult = {
  id: string;
  productId: string;
};
TypeScript

Usando middleware middy

Se você estiver usando o middleware middy, o Powertools fornece o middleware

import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import middy from '@middy/core';
import type { Context } from 'aws-lambda';
import type { Request, Response, SubscriptionResult } from './types';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'IdempotencyTable',
});

export const handler = middy(
  async (event: Request, _context: Context): Promise<Response> => {
    try {
      const payment =// create payment object
	  
      return {
        paymentId: payment.id,
        message: 'success',
        statusCode: 200,
      };
    } catch (error) {
      throw new Error('Error creating payment');
    }
  }
).use(
    makeHandlerIdempotent({
      persistenceStore,
  })
);
TypeScript

Opções de configuração

O utilitário de idempotência Powertools vem com várias opções de configuração para alterar o comportamento de idempotência que se adequará ao seu cenário de caso de uso. Esta seção destaca as configurações mais comuns. Você pode encontrar todas as opções de personalização disponíveis na documentação do AWS Powertools for Lambda (TypeScript).

Opções da camada de persistência

Quando você cria um objeto DynamoDBPersistenceLayer, somente o atributo tableName é necessário. O Powertools esperará a tabela com um ID de chave de partição e criará outros atributos com valores padrão.

Você pode alterar esses valores padrão, se necessário, passando o parâmetro options:

import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTableName',
  keyAttr: 'idempotencyKey', // default: id
  expiryAttr: 'expiresAt', // default: expiration
  inProgressExpiryAttr: 'inProgressExpiresAt', // default: in_progress_expiration
  statusAttr: 'currentStatus', // default: status
  dataAttr: 'resultData', // default: data
  validationKeyAttr: 'validationKey', .// default validation
});
TypeScript

Usando um subconjunto da carga útil (payload) do evento

Quando você configura a idempotência para seu handler de funções do Lambda, o Powertools usa toda a carga útil do evento para tratamento de idempotência por meio do hashing do objeto.

No entanto, eventos de serviços da AWS, como Amazon API Gateway ou Amazon Simple Queue Service (Amazon SQS), geralmente geram campos, como timestamp ou RequestID. Isso faz com que o Powertools trate cada carga útil do evento como única.

Para evitar isso, crie um IdempotencyConfig e configure qual parte da carga deve ser criptografada para a lógica de idempotência.

Crie o IDemPotencyConfig e defina eventKeyJMESPath como uma chave dentro da carga do evento:

import { IdempotencyConfig } from '@aws-lambda-powertools/idempotency';

// Extract the idempotency key from the request headers
const config = new IdempotencyConfig({
  eventKeyJmesPath: 'headers."X-Idempotency-Key"',
});
TypeScript

Use o cabeçalho X-Idempotency-Key para sua chave de
idempotência
. As invocações subsequentes com o mesmo valor de cabeçalho serão idempotentes.

Em seguida, você pode adicionar a configuração ao wrapper da função makeIDemPotent do exemplo anterior:

export const handler = makeIdempotent(
  async (event: Request, _context: Context): Promise<Response> => {
    try {
      const payment =// create payment
      
	  return {
        paymentId: payment.id,
        message: 'success',
        statusCode: 200,
      };
    } catch (error) {
      throw new Error('Error creating payment');
    }
  },
  {
    persistenceStore,
    config
  }
);
TypeScript

A carga útil do evento deve conter X-Idempotency-Key nos
cabeçalhos, para que o Powertools possa usar esse campo para lidar com a
idempotência
:

{
  "version": "2.0",
  "routeKey": "ANY /createpayment",
  "rawPath": "/createpayment",
  "rawQueryString": "",
  "headers": {
    "Header1": "value1",
    "X-Idempotency-Key": "abcdefg"
  },
  "requestContext": {
    "accountId": "123456789012",
    "apiId": "api-id",
    "domainName": "id.execute-api.us-east-1.amazonaws.com",
    "domainPrefix": "id",
    "http": {
      "method": "POST",
      "path": "/createpayment",
      "protocol": "HTTP/1.1",
      "sourceIp": "ip",
      "userAgent": "agent"
    },
    "requestId": "id",
    "routeKey": "ANY /createpayment",
    "stage": "$default",
    "time": "10/Feb/2021:13:40:43 +0000",
    "timeEpoch": 1612964443723
  },
  "body": "{\"user\":\"xyz\",\"productId\":\"123456789\"}",
  "isBase64Encoded": false
}
JSON

Há outras opções de configuração que você pode aplicar, como validação da carga útil, duração da expiração, armazenamento em cache local e outras. Consulte a documentação do Powertools for AWS Lambda (TypeScript) para obter mais informações.

Personalização da configuração do AWS SDK

O DynamoDBPersistenceLayer é integrado e permite que você armazene os dados de idempotência de todas as suas solicitações. Nos bastidores, a Powertools usa o AWS SDK para JavaScript v3. Altere a configuração do SDK passando um objeto ClientConfig.

O exemplo a seguir define a região como eu-west-1:

import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'IdempotencyTable',
  clientConfig: {
    region: 'eu-west-1',
  },
});
TypeScript

Se você estiver usando seu próprio cliente, poderá passá-lo para a camada de persistência:

import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

const ddbClient = new DynamoDBClient({ region: 'eu-west-1' });

const dynamoDBPersistenceLayer = new DynamoDBPersistenceLayer({
  tableName: 'IdempotencyTable',
  awsSdkV3Client: ddbClient,
});
TypeScript

Conclusão

Tornar suas funções do Lambda idempotentes pode ser um desafio e, se não for feito corretamente, pode levar à duplicação de dados, inconsistências e uma experiência ruim para o cliente. Esta postagem mostra como usar o Powertools for AWS Lambda (TypeScript) para processar suas transações críticas somente uma vez ao usar o AWS Lambda.

Para obter mais detalhes sobre o recurso de idempotência do Powertools e suas opções de configuração, consulte a documentação completa.

Para obter mais recursos de aprendizado serverless, visite Serverless Land.

 

Este artigo foi traduzido do Blog da AWS em Inglês.

 


Sobre o autor

Alexander Schüren, especialista sênior em AS – Powertools

 

 

 

 

Tradutor

Daniel Abib é Enterprise Solution Architect 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 segurança. Ele trabalha apoiando clientes corporativos, ajudando-os em sua jornada para a nuvem.

https://www.linkedin.com/in/danielabib/