Back to home

Scheduling One-Off Cron Jobs With AWS EventBridge And AWS Lambda

Published: Nov 9, 2021

Last updated: Nov 9, 2021

This post will modify the example blog post "Tutorial: Schedule AWS Lambda functions using EventBridge" to demonstrate how we can schedule one-off AWS Lambda functions using AWS EventBridge and clean up as we go using v2 of the JavaScript AWS SDK.

It will not cover the basics of EventBridge, IAM or Lambda. It is expected that you having working knowledge of AWS products.

This tutorial will also not be using infrastructure-as-code nor obeying the principle of least privilege for the added permissions. I will point those concepts out when relevant during this post, but it is beyond the scope of this contrived example.

Prerequisites

  1. Read "Tutorial: Schedule AWS Lambda functions using EventBridge".
  2. An AWS account.
  3. Knowledge of TypeScript and how to run TypeScript files from the command line. I will not be covering ts-node or swc here and expect you will have the prerequisites to run (or convert my code to JavaScript).
  4. An npm project - I will not be initializing this with you on this post!

Creating our lambda function

Our first step in this post will be to create a new AWS Lambda function from the console. You can follow the steps outlined in "Tutorial: Schedule AWS Lambda functions using EventBridge".

The JavaScript code that will be required will be the following:

"use strict"; const AWS = require("aws-sdk"); const eb = new AWS.EventBridge(); exports.handler = async (event, context, callback) => { try { // Phase 1: Here you would run your lambda function actions... // await doAnotherCoolThing(); console.log("LogScheduledEvent"); console.log("Received event:", JSON.stringify(event, null, 2)); // Phase 2: ...then we can start to clean up the resources // We can access the resource from the given event values const [resource] = event.resources; const ruleNameArr = resource.split("/"); const ruleName = ruleNameArr[ruleNameArr.length - 1]; console.log("Attempting to delete rule:", ruleName); var params = { // We will stick with ID "1" for simplicity Ids: [/* required */ "1"], Rule: ruleName /* required */, Force: true, }; await eb.removeTargets(params).promise(); await eb .deleteRule({ Name: ruleName, }) .promise(); } catch (err) { console.error(err); } finally { callback(null, "Finished"); } };

This is an adjustment to the tutorial. We basically run through two phases:

  1. Phase 1: Here you run your lambda function actions.
  2. Phase 2: We clean up the EventBridge resources.

In your own work, you should stand-up the Lambda function using infrastructure-as-code. After my first spike, I personally opted to use the AWS CDK for my own stack to stand up the lambda function. Read my tutorial "Python Lambda Functions Deployed With The Typescript AWS CDK" to see an example of this.

Although the code I have in the following screenshot is not the same as the code I have in the tutorial, your work in the console should be similar:

Lambda console and ARN

Lambda console and ARN

Be sure to grab the Lambda function ARN as it will be required for our script.

Adding the correct permissions to our lambda function

At this point, we will need to adjust our execution role to have permissions to talk to AWS EventBridge.

For the sake of the demo, I have added AmazonEventBridgeFullAccess to the execution role.

Updated execution role permissions

Updated execution role permissions

Please note that this is not best practice. You should not grant this role full access in production and obey the principle of least privilege.

To be more granular, you can refer to the "EventBridge Permissions Reference". The relevant permissions we require are events:RemoveTargets and events:DeleteRule.

Writing a script to schedule a job

Inside of index.ts, we can add the following script to schedule our lambda function:

import { format, addMinutes } from "date-fns"; import { v4 as uuidv4 } from "uuid"; import * as AWS from "aws-sdk"; AWS.config.update({ region: process.env.AWS_REGION }); // Create the require EventBridge and Lambda instances const eb = new AWS.EventBridge({ accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, region: process.env.AWS_REGION, }); const lambda = new AWS.Lambda({ accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, region: process.env.AWS_REGION, }); // Helper function to convert to UTC function formatDate(date: Date) { return format( addMinutes(date, date.getTimezoneOffset()), "yyyy-MM-dd HH:mm:ss" ); } /** * Helper function to take a string output from `formatDate` and convert it to an AWS eligble cron expression. */ function formatCronExpression(dateStr: string) { const date = new Date(dateStr); const minutes = date.getMinutes(); const hours = date.getHours(); const day = date.getDate(); const month = date.getMonth() + 1; const year = date.getFullYear(); // Expected output format looks like "cron(15 10 9 11 ? 2021)" <- the date of the blog post format for 10:15am on November 9, 2021 return `cron(${minutes} ${hours} ${day} ${month} ? ${year})`; } const lambdaFnMeta = { name: "LogScheduledEvent", arn: "<your-lambda-arn>", // TODO: Replace with your Lambda ARN function. }; /** * Schedule an event for two minutes from now. */ async function main() { const result = addMinutes(new Date(), 2); const utcDate = formatDate(new Date(result)); const cronExpression = formatCronExpression(utcDate); // Create a random job id const jobId = uuidv4(); // Take the first element from the split array for ID to keep it short // e.g. abcd-efgh-ijkl-mnop -> abcd const [shortHash] = jobId.split("-"); // This is unnecessary assignment, but I had this in my code // so I've just pulled it across. const jobName = shortHash; const jobEvent = shortHash; const jobRule = shortHash; // Create job rules const putRuleParams = { Name: jobName /* required */, ScheduleExpression: cronExpression, Tags: [ { Key: "Product" /* required */, Value: "WOL" /* required */, }, { Key: "ID" /* required */, Value: shortHash /* required */, }, ], }; // Create a new rule. This emulates: // aws events put-rule \ // --name my-scheduled-rule \ // --schedule-expression 'rate(5 minutes)' const putRuleRes = await eb.putRule(putRuleParams).promise(); console.log(putRuleRes); if (!putRuleRes.RuleArn) { console.error(putRuleRes); throw new Error("Missing RuleArn"); } // Add Permission to Lambda. This emulates: // aws lambda add-permission \ // --function-name LogScheduledEvent \ // --statement-id my-scheduled-event \ // --action 'lambda:InvokeFunction' \ // --principal events.amazonaws.com \ // --source-arn arn:aws:events:us-east-1:123456789012:rule/my-scheduled-rule var addPermissionParams = { Action: "lambda:InvokeFunction" /* required */, FunctionName: lambdaFnMeta.name /* required */, // TODO Principal: "events.amazonaws.com" /* required */, StatementId: jobEvent /* required */, SourceArn: putRuleRes.RuleArn, }; await lambda.addPermission(addPermissionParams).promise(); var putTargetsParams = { Rule: jobRule /* required */, Targets: [ /* required */ { Arn: lambdaFnMeta.arn /* required */, // TODO Id: "1" /* required */, }, ], }; // Add our event targets to the rule. This emulates: // aws events put-targets --rule my-scheduled-rule --targets file://targets.json // Note: "--targets" is in params for tutorial and not a file const putTargetsRes = await eb.putTargets(putTargetsParams).promise(); console.log(putTargetsRes); } void main();

As you can see in the code, it is expected that you are provided the correct environment keys for AWS as well as some 3rd party packages (aws-sdk, uuid and date-fns) - you may replace this code with alternatives if you wish.

If we run the script at index.ts, a successful call will schedule our function to run at the specified time (in two minutes).

We can then check the EventBridge console to see if our scheduled event is there (if checked within the 2 minute timeframe - adjust if you require more time):

Rule added programmatically to EventBridge

Rule added programmatically to EventBridge

After the time passes, you can view the CloudWatch Logs for our function to the result when it is invoked.

Lambda function invocation

Lambda function invocation

Success!

Finally, if we check the EventBridge console again, we should see that our Lambda function also successfully removed the event that we scheduled from the command line.

Summary

Today's post demonstrated how to schedule a one-off invocation of a Lambda function using the AWS EventBridge API and how to clean that up as part of the Lambda function.

Some changes should be made for your production code, as this example is quite contrived to demonstrate the use of the EventBridge API.

Note that there are quotas and limits on how many rules you can have per Event Bus. Be sure to check the EventBridge quotas to find any limitations. If you are expecting to have a large number of scheduled events, you may want to consider using an alternative methods.

Also a reminder that in your own work, you should adjust the Lambda privileges to obey the principle of least privilege and that the lambda function itself should be stood up using infrastructure-as-code (although not required).

Resources and further reading

Photo credit: purzlbaum

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on Visibuild.

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.

Scheduling One-Off Cron Jobs With AWS EventBridge And AWS Lambda

Introduction

Share this post