AWS EventBridge Replays

tastefulelk

Sebastian Bille

Posted on November 9, 2020

AWS EventBridge Replays

AWS recently announced native replay functionality for EventBridge! In my opinion, replayability and event archiving in event-driven architectures are often overlooked and a resilient, cost-effective solution is often needlessly complicated - so this is fantastic news! 🚀

I was eager to try it out so I decided to build a small demo - come join me!

Project setup

npm init -y, install Serverless framework, serverless-pseudo-parameters and the AWS SDK

npm install serverless serverless-pseudo-parameters --dev
Enter fullscreen mode Exit fullscreen mode

Let's start by creating a fake order producer that we later can pretend is some important service that lives somewhere else. The "createOrder" service will put an event on our message bus containing an order based on the orderId we pass.

// createOrder.js
const AWS = require('aws-sdk');

const eventBridge = new AWS.EventBridge({ region: 'eu-north-1' });

const handler = async (event) => {
  const order = {
    id: event.orderId,
  };

  return eventBridge.putEvents({
    Entries: [{
      EventBusName: 'orders',
      Source: 'elk.orders',
      DetailType: 'order',
      Detail: JSON.stringify(order)
    }]
  }).promise();
};

module.exports.handler = handler;
Enter fullscreen mode Exit fullscreen mode

next, let's create our "orderCreated" function which will listen for any order events on the bus and work its magic on them.

// orderCreated.js
const handler = async (event) => {
  console.log(`handled order ${event.detail.id}`);
};

module.exports.handler = handler;
Enter fullscreen mode Exit fullscreen mode

and finally, let's create our serverless.yml where we'll define all the infrastructure resources our service will need.

# serverless.yml
service: order-service

plugins:
  - serverless-pseudo-parameters

provider:
  name: aws
  region: eu-north-1
  runtime: nodejs12.x
  iamRoleStatements:
    - Effect: Allow
      Action: events:PutEvents
      Resource: ${self:custom.eventBusArn}

custom:
  eventBusArn: arn:aws:events:${self:provider.region}:#{AWS::AccountId}:event-bus/orders

functions:
  createOrder:
    handler: createOrder.handler
  orderCreated:
    handler: orderCreated.handler
    events:
      - eventBridge:
          eventBus: ${self:custom.eventBusArn}
          pattern:
            source:
              - elk.orders

resources:
  Resources:
    EventBus:
      Type: AWS::Events::EventBus
      Properties:
        Name: orders
    EventArchive:
      Type: AWS::Events::Archive
      Properties:
        ArchiveName: orders-archive
        SourceArn: ${self:custom.eventBusArn}
Enter fullscreen mode Exit fullscreen mode

There's quite a lot going on here, so let's break it down a bit, shall we? We shall.

We're specifying that the orderCreated function should be triggered by all events on the "orders" EventBus. The EventBus itself is defined as a CloudFormation resource.
However, at the time of writing this, there's a bug in Serverless framework that prevents us from referencing the ARN of the CF resource in the event definition so we reference the "calculated" ARN in a custom property, eventBusArn, instead.

In addition to the EventBus resource, there's an Archive resource as well. This is the resource that defines our archive, in which all events put on the bus will be archived (and thus available to be replayed). We also set up the Lambda EventBridge trigger and specified that we're interested in all events coming from the "elk.orders" source.

We've also added an IAM role statement that allows our service to put events on the bus since we're hosting our event producer in the same service.

Alright, let's try it out.

From your nearest terminal, deploy the service by running npx sls deploy. We can create our first event by invoking the producer with an order

npx sls invoke --function createOrder -d '{ "orderId": "1" }'
Enter fullscreen mode Exit fullscreen mode

if we now look at the logs of our orderCreated function, we should see that that it's received and "processed" the order.

npx sls logs --function orderCreated
[...]
START RequestId: d8069d8e-ed1a-4837-8e3f-b44f12e23773 Version: $LATEST
2020-11-09 00:18:41.268 (+01:00)    d8069d8e-ed1a-4837-8e3f-b44f12e23773    INFO    handled order 1
END RequestId: d8069d8e-ed1a-4837-8e3f-b44f12e23773
Enter fullscreen mode Exit fullscreen mode

Great, it works - let's destroy something! If our orderCreated Lambda were to fail, the events would automatically be retried with an automatic exponential backoff - but what happens if we don't actually "fail", we just fail to process the event correctly? Unfortunately, it would be lost.

Let's simulate something like that happening by changing the name of our order's id field from id to orderId in the event.

// createOrder.js
[...]
const handler = async (event) => {
  const order = {
    orderId: event.orderId,
  };
[...]
Enter fullscreen mode Exit fullscreen mode

deploy our service again, and then create another event, order 2

npx sls invoke --function createOrder -d '{ "orderId": "2" }'
Enter fullscreen mode Exit fullscreen mode

The logs from the orderCreated function will now say

INFO    handled order undefined
Enter fullscreen mode Exit fullscreen mode

Alright, I think we can figure out what the problem is, let's fix the bug

// orderCreated.js
const handler = async (event) => {
  console.log(`handled order ${event.detail.orderId}`);
};

module.exports.handler = handler;
Enter fullscreen mode Exit fullscreen mode

and deploy again.

Replays to the rescue

We botched the handling of that second order but luckily we created an archive for our events when we created the event bus.

@mhlabs/evb-cli is a neat little CLI tool to manage EventBridge, so let's use that to trigger our replay.

npm i @mhlabs/evb-cli -g
Enter fullscreen mode Exit fullscreen mode

Run the tool and specify the "orders" bus, evb replay -b orders, then adjust the time span as needed.

gif

It'll take a minute or so for EventBridge to fetch and re-send your events from the archive, but in a moment we should be able to see that the second order has now been processed by our orderCreated handler! 🎉

INFO    handled order 2
Enter fullscreen mode Exit fullscreen mode

Isn't that amazing? With just a handful of lines of code, we've set up an EventBus that durably stores all events for as long as we need them from which we can replay events sent within a given time span to any consumers that need to (re)process the events as well as a service that reads and writes to the bus.

Do keep in mind that when designing systems capable of replaying events, it's crucial to make sure your services are idempotent. If (when) the same order event is sent for the second time, your customer likely didn't buy two of the same Die Hard 2 DVD off your site at the same time.

The code for this demo can be found here.

PS.

Did you know EventBridge can automatically generate schemas and code bindings for Typescript, Python or Java? Using those would've prevented that whole id->orderId fiasco from before... Oh well.

If you enjoyed this guide and want to see more, follow me on Twitter at @TastefulElk where I frequently write about serverless tech, AWS and developer productivity!

💖 💪 🙅 🚩
tastefulelk
Sebastian Bille

Posted on November 9, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

AWS EventBridge Replays
eventbridge AWS EventBridge Replays

November 9, 2020