In November 2022, AWS unveiled something pretty exciting: the EventBridge Scheduler. This nifty tool is a game-changer for anyone needing to set up events that happen just once or on a regular schedule. It’s like having a super-smart assistant who never forgets to do your important tasks—whether that’s sending out reports every Friday or backing up your data.

Before the EventBridge Scheduler, we had to juggle a bunch of complex setups like cron jobs, delay step functions, and even using DynamoDB’s expiration feature. Too much to do for a simple thing

But now, it’s all about simplicity and clarity.

Want to see how easy it is? Let’s roll up our sleeves and dive into setting up both one-off and recurring tasks using the EventBridge Scheduler, all through the magic of AWS CDK and TypeScript. By the end of this post, you’ll see just how effortless and powerful your scheduling can be. Get ready to make your projects run smoother than ever!

So here is basically what we are going to do

  1. Create lambda functions, which will be invoke by our scheduler
  2. Now to invoke these lambda function we need to give our schedule permission a. so we will create policy which gives permission to invoke the lambda function b. assign that permission to a IAM role c. then assign that role to our scheduler, so it has the permission
  3. Then create our scheduler which will basically run recurring events from cdk
  4. Create a event scheduler creator, which we will be doing using sdk, (because most of you might want to create these schedulers on regular basis on user request, so you will need to know how to create it using SDK based on user need, ex: users asks for remainders, or scheduled email services you can pass the time and payload to the scheduler )

Setting the Stage with Lambda Functions

To kick things off, you’ll either need an existing CDK project or the zest to start a fresh one. Our goal here is to set up Lambda functions that will be triggered both as a one-off event and on a recurring schedule.

Here’s how you can define simple Lambda functions in your project:

  • One-off Function: This function logs “one off” when executed.

`

// ./resources/one-off.ts 
export const handler = (payload:string) => {
	console.log(JSON.parse(payload))
	// do something just once
}
  • Recurring Function: This function logs “recurring” each time it’s invoked.
// ./resources/recurring.ts 
export const handler = (payload:string) => {
	console.log(JSON.parse(payload))
	// do something multiple times in regular intervals
}

In the CDK stack file located in your lib directory, instantiate these functions like so:

 
export class EventbridgeSchedulerStack extends Stack {   
	constructor(scope: Construct, id: string, props?: StackProps) {
	super(scope, id, props);      
	
	const recurringLambda = new NodejsFunction(this, 'RecurringLambda', {
		entry: './resources/recurring.ts',       
		handler: 'handler',       
		runtime: Runtime.NODEJS_16_X,       
		timeout: Duration.seconds(10),     
	});      
	
	const oneOffLambda = new NodejsFunction(this, 'OneOffLambda', { 
		entry: './resources/one-off.ts',       
		handler: 'handler',
		runtime:   Runtime.NODEJS_16_X,
		timeout: Duration.seconds(10),     	
	});   
 
}}
	

The Art of IAM: Roles and Policies

Hey, so before we dive into building our scheduler, we’ve gotta set up a new role. It’s like giving it a backstage pass so it can call up our Lambda functions when needed. Just use the snippet below to create a role that hooks up with the scheduler.amazonaws.com service principal. Super straightforward, right? Here’s how you can do it:

 
const schedulerRole = new Role(this, 'schedulerRole', {   
	assumedBy: new ServicePrincipal('scheduler.amazonaws.com'), 
	
});
 

Next up, we need to whip up a new policy for that role we just created. This is basically giving it the green light to kick off our Lambda functions. You’ll just need to include the ARNs of the functions in the policy.

 
const invokePolicy = new Policy(this, 'invokeLambdaPolicy', {
	document: new PolicyDocument({     
	statements: [new PolicyStatement({         
	actions: ['lambda:InvokeFunction'],         
	resources: [recurringLambda.functionArn, oneOffLambda.functionArn],         effect: Effect.ALLOW,       
	}),],   
}), 
});  
 

just assign it to the scheduler role

schedulerRole.attachInlinePolicy(invokeLambdaPolicy);

EventBridge Scheduler CDK Construct

Heads up, things might change down the line, but as of now (April 2023), there isn’t a fancy L2 construct for the EventBridge Scheduler in the AWS CDK. There’s some buzz about it on GitHub, but for the moment, we’re sticking with the basic L1 construct (CfnResource) to get the job done through the CDK.

If you haven’t bumped into the CfnResource construct before, think of it like a straightforward way to sling some CloudFormation code within the CDK to roll out resources on AWS. Sure, using L2 constructs would be smoother, but just because we don’t have one for EventBridge Scheduler doesn’t mean we should pass on using the CDK to build out our setup.

// 3. Defining our one-off schedule
new CfnResource(this, 'oneOffSchedule', {
  type: 'AWS::Scheduler::Schedule',
  properties: {
    Name: 'oneOffSchedule',
    Description: 'Runs a schedule at a fixed time',
    FlexibleTimeWindow: { Mode: 'OFF' }, 
	ActionAfterCompletion: "DELETE",
    ScheduleExpression: 'at(2023-04-21T07:20:00)',
    Target: {
      Arn: oneOffLambda.functionArn,
      RoleArn: schedulerRole.roleArn,
    },
  },
});
 
// 4. Defining our recurring schedule
new CfnResource(this, 'recurringSchedule', {
  type: 'AWS::Scheduler::Schedule',
  properties: {
    Name: 'recurringSchedule',
    Description: 'Runs a schedule for every 5 minutes',
    FlexibleTimeWindow: { Mode: 'OFF' },
    ScheduleExpression: 'cron(*/5 * * * ? *)',
    Target: {
      Arn: recurringLambda.functionArn,
      RoleArn: schedulerRole.roleArn,
    },
  },
});

Explanation of each property

  1. FlexibleTimeWindow -> Here you want give a time window, in which the scheduler can work, basically if you set it 15 mins, then after your timer is hit, it can invoke lambda anytime within next 15 mins, probably you want it to be immediately after the settime is hit in most cases, I set it to ‘OFF’
  2. ScheduleExpression: The expression that defines when the schedule runs. The following formats are supported. [
  • at expression - at(yyyy-mm-ddThh:mm:ss)
  • rate expression - rate(value unit)
  • cron expression - cron(fields) You can use at expressions to create one-time schedules that invoke a target once, at the time and in the time zone, that you specify. You can use rate and cron expressions to create recurring schedules. Rate-based schedules are useful when you want to invoke a target at regular intervals, such as every 15 minutes or every five days. Cron-based schedules are useful when you want to invoke a target periodically at a specific time, such as at 8:00 am (UTC+0) every 1st day of the month.

A cron expression consists of six fields separated by white spaces: (minutes hours day_of_month month day_of_week year).

A rate expression consists of a value as a positive integer, and a unit with the following options: minute | minutes | hour | hours | day | days

] from aws-sdk docs

EventBridge Scheduler CDK Construct from Sdk

okay to do this we will need arns of the role, and target lamda, here our target lamda is one-off function

lets just log the arn values after it is deployed, here is how you can do it

 
new cdk.CfnOutput(this, "SchedulerLambdaArn", {
value: oneOffLambda.functionArn,
exportName: "SchedulerLambdaArn",
});
 
new cdk.CfnOutput(this, "SchedulerRoleArn", {
value: schedulerRole.roleArn,
exportName: "SchedulerRoleArn",
 
});

just copy them to your env file and install

pnpm install @aws-sdk/client-scheduler

and then create a client

Small Disclaimer: I am using t3-env library so you will see env. instead of process.env.

 
export const scheduler = new Scheduler({
	region: env.AWS_REGION,
	credentials: {
		accessKeyId: env.AWS_ACCESS_KEY_ID,
		secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
	},
});
 

then give the input and invoke it

 
const schedularInput = {
	FlexibleTimeWindow: {
		Mode: "OFF",
	},
	ActionAfterCompletion: "DELETE",
	Target: {
		Arn: env.LAMBDA_HIT_ROUTE_ARN,
		RoleArn: env.SCHEDULAR_IAM_ROLE_ARN,
		Input: JSON.stringify(eventDetail), // pass the a json
	},
	ScheduleExpression: `at(${formatUTCDate(time)})`,
	ScheduleExpressionTimezone: "UTC",
	Name: `scheduleforpost${postid}`,
} as CreateScheduleCommandInput;
 
try {
	const res = await scheduler.createSchedule(schedularInput);
	return res;
} catch (error) {
	console.error("Failed to schedule event:", error);
	throw error;
}
 

here is a function that come in handy

function formatUTCDate(date: Date): string {
 
	const pad = (num: number) => num.toString().padStart(2, "0");
	const year = date.getUTCFullYear();
	const month = pad(date.getUTCMonth() + 1); // JavaScript months are zero-indexed
	const day = pad(date.getUTCDate());
	const hours = pad(date.getUTCHours());
	const minutes = pad(date.getUTCMinutes());
	const seconds = pad(date.getUTCSeconds());
	
	return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
}

Thank you!!!

references: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/scheduler/command/CreateScheduleCommand/