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.
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).
An npm project - I will not be initializing this with you on this post!
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:
Phase 1: Here you run your lambda function actions.
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
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
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
After the time passes, you can view the CloudWatch Logs for our function to the result when it is invoked.
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).