Did I just invent CDK Level 4 Constructs?!?
Tags: aws, cdk, eventdriven, serverless
Published: 2022-04-22 14:48:45

At work we use a [Multi-bus, multi-account](https://github.com/aws-samples/amazon-eventbridge-resource-policy-samples/tree/main/patterns/multi-bus-multi-account-pattern) Event Driven Architecture pattern.  It works great, but a pain point is making event interfaces match across multiple git repos.

CDK Constructs are great even though they don't span multiple stacks... but *what if they did?* 🧐 ***Did I just invent CDK Level 4 Constructs?!?*** 💡

Let's do a quick review of the existing constructs...

- 👶 L1: (Single Stack) Cfn* Constructs.  I remember this like "Level 1 is 1:1 with cloudformation"
- 🧒 L2: *(Single Stack)* Opinionated / improved L1 Constructs
- 🧑 L3: ***(Single Stack!)*** Combinations of Constructs
- 🦸 ***L4: (Multi-Stack) Multi-Stack L3 Constructs***

⚡️ This "Level 4 Construct" could also apply to stacks in different accounts or regions!

Code: [GitHub: CDK L4 Construct Example](https://github.com/martzcodes/cdk-L4-construct)

## What are we going to do?
This is a very basic example.  We're going to create a lambda that puts an event on its bus, the bus forwards the event to another bus where a different lambda gets invoked.  Like this:

![L4 Construct Example.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650633660724/6bcy_EuxB.png)

We're going to create two stacks that are initially identical... they just have an event bus and that bus is exposed publicly on the class.

[The Stack](https://github.com/martzcodes/cdk-L4-construct/blob/main/lib/blog-l4-construct-stack.ts#L8-L18)
```typescript
export class BlogL4ConstructStack extends Stack {
  bus: EventBus;
  constructor(scope: Construct, id: string, props: BlogL4ConstructStackProps) {
    super(scope, id, props);

    // could also be a lookup
    this.bus = new EventBus(this, 'Bus', {
      eventBusName: `${props.namespace}-bus`
    });
  }
}
```

🚫This is where the stack would normally differentiate and you'd have two custom stacks.

- TransmitStack has a TransmitLambda
- TransmitStack has a rule on the Bus to forward to the Receive Stack Bus
- ReceiveStack has the Receive Lambda
- ReceiveStack has a rule that invokes the Receive Lambda from the event
- Grant all appropriate permissions

Things to keep in mind:

- TransmitLambda needs putEvent access to its own bus
- TransmitBus needs putEvent access to ReceiveBus (ReceiveBus needs to know TransmitStack's account)
- The Transmit rule needs to know the target bus
- Event pattern matches between Transmit and Receive Stacks

There's some chicken 🐓 and egg 🥚 stuff going on here... The ReceiveStack needs to grant putEvent access to the TransmitStack's account and the TransmitStack's rule needs to know the address of the bus on the ReceiveStack.

## How are we going to do it?
We'll create a L4 Construct (which is really just a function, for now...) that takes two stacks as the input and the "source" for the event pattern.

[L4 Construct](https://github.com/martzcodes/cdk-L4-construct/blob/main/lib/l4-construct.ts)
```typescript
export interface L4ConstructProps {
  receiveEntry?: string;
  receiveStack: BlogL4ConstructStack;
  source: string;
  transmitEntry?: string;
  transmitStack: BlogL4ConstructStack;
}
```

Then we'll create the transmit lambda and rule.  Normally this would be in a TransmitStack class, possibly in a separate git repo.
```typescript
  const transmitFn = new NodejsFunction(transmitStack, `transmit-${source}-fn`, {
    functionName: `transmit-${source}-fn`,
    entry: transmitEntry,
    runtime: Runtime.NODEJS_14_X,
    environment: {
      EVENT_SOURCE: source,
      EVENT_BUS: transmitStack.bus.eventBusName,
    },
  });

  // allow transmitFn to put events to transmit bus
  transmitStack.bus.grantPutEventsTo(transmitFn);

  const transmitRule = new Rule(transmitStack, `transmit-${source}-rule`, {
    ruleName: `transmit-${source}-rule`,
    eventPattern: {
      source: [source],
    },
    eventBus: transmitStack.bus,
  });
  transmitRule.addTarget(new EventBusTarget(receiveStack.bus));
```

And then we'll create the Receive stuff (again, this would probably be in a separate git repo).
```typescript
  // allow transmit bus to send events to receive bus
  // this part is a little inefficient
  new CfnEventBusPolicy(receiveStack, `receive-${source}-policy`, {
    eventBusName: receiveStack.bus.eventBusName,
    principal: transmitStack.account,
    action: 'events:PutEvents',
    statementId: `allow_put_events_for_${source}`,
  });

  const receiveFn = new NodejsFunction(receiveStack, `receive-${source}-fn`, {
    functionName: `receive-${source}-fn`,
    entry: receiveEntry,
    runtime: Runtime.NODEJS_14_X,
  });
  // invoke receiveFn from same source as transmit
  const rule = new Rule(receiveStack, `receive-${source}-rule`, {
    description: "this is some rule for the eventbus...",
    eventPattern: {
      source: [source],
    },
    eventBus: receiveStack.bus,
  });
  rule.addTarget(new LambdaFunction(receiveFn));
```

To call it all we need to do is call the method [within the bin file](https://github.com/martzcodes/cdk-L4-construct/blob/main/bin/blog-l4-construct.ts#L24-L25):
```typescript
L4Construct({ receiveStack, transmitStack, source: 'someSource' });
```

... which we can now do multiple times...

```typescript
L4Construct({ receiveStack, transmitStack, source: 'someSource' });
L4Construct({ receiveStack, transmitStack, source: 'anotherSource' });
```

💪 Which would result in two lambdas in transmitStack and two lambdas in receiveStack with their own coordinated rules!

## Why do this?
A big benefit of this is for simple architectures with a reusable pattern (like this example) the entire end-to-end of this pub/sub setup is self-contained in one file, so it's easy to understand.  I can see everything related to this one workflow in a single file as opposed to jumping around between files/repos.

You can also `cdk deploy --all` to the multiple stacks (including across accounts if you have the perms for it) from one project.  In this examples the lambdas were trivially basic but you could share typescript interfaces for the putEvent and the handler event.  If you make this property driven (like all *good* CDK Constructs you could swap out the lambda handlers [similar to what I did here](https://github.com/martzcodes/cdk-L4-construct/blob/main/lib/l4-construct.ts#L20-L21)

I think it's a fairly elegant solution for setting up repeatable patterns like this.

- Where else could you use this?
- How would you expand this?
- Why *wouldn't* you use this?
- Is this a great idea?  A horrible idea?

I'd love to hear your thoughts.  Hit me up on [Twitter @martzcodes](https://twitter.com/martzcodes)

Unfortunately I came up with this idea *just after* the [CDK Day 2022](https://www.cdkday.com) deadline... but I highly recommend attending for the other great talks 🚀