How to build a serverless cron job using AWS EventBridge with AWS CDK
Tags: aws, cdk, serverless, typescript
Published: 2023-07-02 16:36:03


Cron jobs are part and parcel of any platform. They are used to schedule and perform processes automatically - these are typically background processes that are asynchronous by nature, for example: batching processing of orders, triggering marketing emails.

Traditionally, these are scheduled on UNIX-based systems that support the crond daemon - but what happens if we don't have dedicated instances in our infrastructure? What if we have opted for a serverless-first approach to our platform? This is where AWS EventBridge and Lambda come into play.

Let's take a look and see how we can build one of them using AWS CDK as our infrastructure as code.

## 💡 What?

![PeteScript - AWS EventBridge rule with AWS Lambda function destination](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lagq6882sb8t7hsc3kw4.png)

AWS EventBridge is a serverless event router that allows us to build our decoupled and asynchronous components without having to run dedicated instances. As you can see from above, there are only a few components that we require to put this functionality together.

Combining AWS EventBridge with AWS Lambda, allows us to create a really flexible sandbox for creating custom scheduled processing logic. EventBridge can interface with a lot of other AWS services for various needs (both as event triggers and destinations), but with Lambda it provides us with the most flexibility depending on what we need to do.

## ⚡️ Lambda Function

First of all, we need to provision the lambda function and its associated role that is going to act as the destination target of our EventBridge rule.

The CDK for the lambda related infrastructure looks like the following:

```typescript
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";

const stack = cdk.Stack.of(this);

const lambdaRole = new iam.Role(
  this,
  `cdk-patterns-${stack.stackName}-lambda-execution-role`,
  {
    assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
    roleName: `cdk-patterns-${stack.stackName}-lambda-execution-role`,
    description: "Lambda function execution role that allows the function to write to CloudWatch",
    managedPolicies: [
      iam.ManagedPolicy.fromManagedPolicyArn(
        this,
        "cloudwatch-full-access",
        "arn:aws:iam::aws:policy/CloudWatchFullAccess"
      ),
    ],
  }
);

const lambdaHandler = new lambda.Function(
  this,
  `cdk-patterns-${stack.stackName}-lambda-function`,
  {
    runtime: lambda.Runtime.NODEJS_18_X,
    code: lambda.Code.fromAsset("src"),
    handler: "index.handler",
    functionName: `cdk-patterns-${stack.stackName}-lambda-function`,
    role: lambdaRole,
  }
);
```

From the above, you can see we're setting up a few things:
- IAM role that contains a AWS managed policy with full access to CloudWatch that is allowed to be assumed by Lambda
- Lambda function that runs Node.js v18, defines the main handler function and attaches the IAM role we've provisioned above.

You'll notice that I'm referencing the `/src` directory and `index.handler` as my source code references for the lambda function itself. This translates to the following in `/src/index.js`:

```javascript
/**
* Main handler method for the EventBridge trigger
*/
exports.handler = async (event) => {
  try {
    // TODO: Implement cron processing functionality
    console.log(event);

    // Simulate success for now
    return true;
  } catch (err) {
    console.error(err);
    throw err;
  }
};
```

If you now perform a `cdk deploy`, the function and role should be provisioned.

## 💥 EB Rule

Now it's time to layer on our cron-like scheduled trigger for the newly provisioned lambda function. This is where our EventBridge rule comes into play.

```typescript
import * as eventBridge from "aws-cdk-lib/aws-events";
import * as eventBridgeTargets from "aws-cdk-lib/aws-events-targets";

// UTC & 24hr syntax
const CRON_EXPRESSION = {
  minute: "0",
  hour: "9",
};

const eventBridgeRule = new eventBridge.Rule(
  this,
  `cdk-patterns-${stack.stackName}-eventbridge-rule`,
  {
    ruleName: `cdk-patterns-${stack.stackName}-eventbridge-rule`,
    schedule: eventBridge.Schedule.cron(CRON_EXPRESSION),
    targets: [
      new eventBridgeTargets.LambdaFunction(lambdaHandler, {
        retryAttempts: 2, // Max number of retries for Lambda invocation
      }),
    ],
  }
);

eventBridgeTargets.addLambdaPermission(eventBridgeRule, lambdaHandler);
```

With the above, are setting up the following:
- EventBridge rule with a cron expression schedule for 9am UTC that has the lambda function as the target registered with a max retry attempt of twice.
- We then add permissions for the Lambda function to be invoked from the EventBridge rule.

Finally, we can deploy this out using `cdk deploy` and we have our fully functioning serverless cron job!

## 🔥 Additional extra

Something super small to touch on, but useful, is that you can control the payload that is sent from the EventBridge rule trigger to the lambda function destination.

We can update our target on the rule to include input that can be specific to environments or anything you can to consistently replicate across each lambda invocation:

```typescript
const eventBridgeRule = new eventBridge.Rule(
  this,
  `cdk-patterns-${stack.stackName}-eventbridge-rule`,
  {
    ruleName: `cdk-patterns-${stack.stackName}-eventbridge-rule`,
    schedule: eventBridge.Schedule.cron(CRON_EXPRESSION),
    targets: [
      new eventBridgeTargets.LambdaFunction(lambdaHandler, {
        event: eventBridge.RuleTargetInput.fromObject({
          environment: "development",
        }),
        retryAttempts: 2, // Max number of retries for Lambda invocation
      }),
    ],
  }
);
```

## 💅 Conclusion

- EventBridge is super flexible, serverless event router that can interface with a lot of AWS services natively, but can interface with Lambda to provide a lot of flexibility depending on your requirements.
- Can configure EB to trigger the rules based on a cron-like schedule, or on an event that gets omitted from another AWS resource.
- CDK for the infrastructure required here is super lightweight and easy to maintain.
- EventBridge did release a new feature 'EventBridge scheduler' last November, however the support isn't quite there yet from a CDK perspective. So if you're configuring this through the console directly, you might see some references to this newer functionality. You can read about the RFC relating to the L2 CDK construct [here](https://github.com/aws/aws-cdk/issues/23394).