EventBridge Rules to Invoke Lambda and StepFunction
Tags: aws, cloud, serverless, tutorial
Published: 2022-05-11 08:39:18

AWS EventBridge is an event bus system for loosely coupling different services and resources in a publish/subscribe pattern. A service can publish an event (i.e., a message) and multiple other services can subscribe to that event.

The subscription mechanism is done through individual rules that define the pattern of an event to be _catched on the event bus_ and define multiple targets to be _called upon that event_, e.g. a Lambda function or a StepFunction state machine.

## Motivation
This post is intended to serve as a blueprint for how to invoke these two services with rules and how to deal with typical authorization challenges along the way. The required resources are defined as a CloudFormation template in JSON, but can easily be converted to YAML.

I created a test Lambda function and a test StepFunction state machine to be called by their respective EventBridge rule. I refer to these resources in CloudFormation as `TestLambdaFunction` and `TestStateMachine`, but these are just their logical IDs and will be replaced with ARNs when deployed.

## Invoke StepFunction State Machine
This CloudFormation template defines an event rule with ID `InvokeStateMachineEventRule` and name `start-state-machine` that listens on the `default` event bus for events whose `detail-type` is set to `start-state-machine`. 

Targets are resources that are invoked when a rule is triggered. The intrinsic function `Fn::GetAtt` returns the ARN of `TestStateMachine` specified as the target resource. 
Additionally, each target must define an IAM role as `RoleArn` which is used to invoke the resource.
  
```json
{
  "InvokeStateMachineEventRule": {
    "Type": "AWS::Events::Rule",
    "Properties": {
      "EventBusName": "default",
      "EventPattern": {
        "detail-type": ["start-state-machine"],
      },
      "Name": "start-state-machine",
      "State": "ENABLED",
      "Targets": [
        {
          "Arn": {
            "Fn: :GetAtt": ["TestStateMachine","Arn"]
          },
          "Id": "TestStateMachine",
          "RoleArn": {
            "Fn: :GetAtt": ["InvokeStateMachineIamRole","Arn"]
          },
        },
      ],
    },
  },
}
```

The IAM role is defined as `InvokeStateMachineIamRole` and contains an inline policy that allows the `states:StartExecution` action for the `TestStateMachine` resource. Since the role is used by an EventBridge rule to invoke another service resource (i.e. StepFunction state machine), an additional `AssumeRolePolicyDocument` is provided to allow the role to be taken from EventBridge (i. e. principal `events.amazonaws.com`).

```json
{
  "InvokeStateMachineIamRole": {
    "Type": "AWS::IAM::Role",
    "Properties": {
      "Policies": [
        {
          "PolicyName": "InvokeStateMachineRolePolicy",
          "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Effect": "Allow",
                "Action": ["states:StartExecution"],
                "Resource": [
                  {
                    "Fn: :GetAtt": ["TestStateMachine", "Arn"]
                  }
                ],
              },
            ],
          },
        },
      ],
    },
  },
  "AssumeRolePolicyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": ["events.amazonaws.com"],
        },
        "Action": "sts:AssumeRole",
      },
    ],
  },
}

```

## Invoke Lambda Function
This event rule `InvokeLambdaFunctionEventRule` is similar to the previous one, except that it is named `start-lambda-function` and listens for events whose `detail-type` is set to `start-lambda-function`. Also, the target resource is `TestLambdaFunction` and `RoleArn` was intentionally omitted. 

```json
{
  "InvokeLambdaFunctionEventRule": {
    "Type": "AWS::Events::Rule",
    "Properties": {
      "EventBusName": "default",
      "EventPattern": {
        "detail-type": ["start-lambda-function"],
      },
      "Name": "start-lambda-function",
      "State": "ENABLED",
      "Targets": [
        {
          "Arn": {
            "Fn: :GetAtt": ["TestLambdaFunction","Arn"]
          },
          "Id": "TestLambdaFunction",
        },
      ],
    },
  },
}
```

If we provide a `RoleArn` for the `TestLambdaFunction` target, it will throw an error when deploying it:

> **UPDATE_FAILED**: InvokeLambdaFunctionEventRule _(AWS::Events::Rule)_
> RoleArn is not supported for target arn:aws:lambda:eu-central-1:xxxxxxxxxxxx:function:eventbridge-rule-example-dev-test.

Unlike other services, Lambda uses [resource-based permissions](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html) to allow other AWS services to invoke this function on your behalf. Therefore, we need to add an `InvokeLambdaFunctionPermission` definition that allows the `Lambda:InvokeFunction` action for the `TestLambdaFunction` resource (i.e. function name). Also this permission is used by an EventBridge rule to invoke this Lambda function. Therefore, we need to specify the principal `events.amazonaws.com` and the ARN of `InvokeLambdaFunctionEventRule` that actually calls the function.

```json
{
  "InvokeLambdaFunctionPermission": {
    "Type": "AWS::Lambda::Permission",
    "Properties": {
      "FunctionName": {
        "Fn: :GetAtt": ["TestLambdaFunction", "Arn"],
      },
      "Action": "lambda:InvokeFunction",
      "Principal": "events.amazonaws.com",
      "SourceArn": {
        "Fn: :GetAtt": ["InvokeLambdaFunctionEventRule", "Arn"]
      },
    },
  },
}
```

## GitHub Repository
I deployed all resources using the Serverless Framework, but only to quickly create the Lambda function and StepFunction state machine. All the important resources are defined as AWS CloudFormation templates and are valid when deployed in other ways, e.g. AWS SAM, AWS CLI, etc.

Feel free to check out the full template and deploy it on your own AWS account: [GitHub respository](https://github.com/zirkelc/eventbridge-rule-example)