TL;DR
In this series, I try to explain the basics of serverless on AWS, to enable you to build your own serverless applications. With last article, we discovered how to Lambda function destinations to avoid losing data when an asynchronous Lambda function fails. In this article, we will discover how to schedule tasks with EventBridge Scheduler.
What will we do today?
- Create a small memo application, where we can create a memo, and execute it at a specific date and time
- Create a Lambda function that creates a memo
- Create a Lambda function that is triggered at a specific date and time, and executes the memo
The architecture of our application will look like this:
⬇️ I post serverless content very regularly, if you want more ⬇️
Quick announcement: I also work on a library called 🛡 sls-mentor 🛡. It is a compilation of 40 serverless best-practices, that are automatically checked on your AWS serverless projects (no matter the framework). It is free and open source, feel free to check it out!
How to schedule tasks with AWS EventBridge Scheduler?
AWS EventBridge Scheduler is a service that allows you to schedule tasks in the future. It can be compared to AWS EventBridge Rules: while rules allow you to trigger tasks based on rates or cron expressions, scheduler allows you to trigger tasks at a specific date and time (they also support cron expressions and rates btw...).
Using the AWS EventBridge Scheduler programmatically to trigger a Lambda function has an easy and a not-so-easy step:
- 😁 The easy step is to specify a target, this can easily be done using the ARN of the Lambda function you want to trigger
-
🥵 The not-so-easy step is to give the permission to the scheduler to invoke your Lambda function. This is done by:
- Creating a role with a
scheduler.amazonaws.com
service principal (assumed by the scheduler) - Giving the permission to the scheduler to invoke your Lambda function
lambda:InvokeFunction
- Giving the permission to your input Lambda function (
addMemo
) to pass the role to the scheduleriam:PassRole
- Creating a role with a
This can be summarized in the following diagram:
Now, let's see how this works in practice, with a real code example!
Create a small memo application using AWS EventBridge Scheduler
To create our app, we will use the AWS CDK. If you are not familiar with it, I invite you to read the first article of this series where I explain properly how to setup a CDK project.
npx cdk init app --language typescript
npm i @aws-sdk/client-scheduler
npm i uuid # always useful to generate unique ids
npm i -D esbuild # Needed to bundle our Lambdas!
First, let's create the Infrastructure as Code (IAC) of the application. This is done by updating the CDK stack:
// stack.ts - Infrastructure as code
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import { join } from 'path';
export class Part17SchedulerStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Lambda function triggered by scheduler
const executeMemo = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'ExecuteMemo', {
entry: join(__dirname, 'executeMemo.ts'),
handler: 'handler',
runtime: cdk.aws_lambda.Runtime.NODEJS_18_X,
bundling: {
externalModules: ['@aws-sdk'],
},
});
// Create role for scheduler to invoke executeMemo
const invokeExecuteMemoRole = new cdk.aws_iam.Role(this, 'InvokeMemoRole', {
assumedBy: new cdk.aws_iam.ServicePrincipal('scheduler.amazonaws.com'),
});
invokeExecuteMemoRole.addToPolicy(
new cdk.aws_iam.PolicyStatement({
actions: ['lambda:InvokeFunction'],
resources: [executeMemo.functionArn],
}),
);
// Lambda function that schedules executeMemo
const addMemo = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'AddMemo', {
entry: join(__dirname, 'addMemo.ts'),
handler: 'handler',
runtime: cdk.aws_lambda.Runtime.NODEJS_18_X,
bundling: {
externalModules: ['@aws-sdk'],
},
environment: {
SCHEDULE_TARGET_ARN: executeMemo.functionArn,
SCHEDULE_ROLE_ARN: invokeExecuteMemoRole.roleArn,
},
});
// Allow addMemo to create a scheduler
addMemo.addToRolePolicy(
new cdk.aws_iam.PolicyStatement({
actions: ['scheduler:CreateSchedule'],
resources: ['*'],
}),
);
// Allow addMemo to pass the invokeExecuteMemoRole to the scheduler
addMemo.addToRolePolicy(
new cdk.aws_iam.PolicyStatement({
actions: ['iam:PassRole'],
resources: [invokeExecuteMemoRole.roleArn],
}),
);
// Trigger addMemo via API Gateway
const api = new cdk.aws_apigateway.RestApi(this, 'Api', {
restApiName: 'Part17Service',
});
api.root.addResource('addMemo').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(addMemo));
}
}
What is happening here?
- 1️⃣ We create the executeMemo Lambda function, that will be triggered by the scheduler
- 2️⃣ We create a role that will be assumed by the scheduler, and that will allow it to invoke the executeMemo Lambda function (see intro diagram)
- 3️⃣ We create the addMemo Lambda function, that will create a scheduler
- 4️⃣ We allow the addMemo Lambda function to create a scheduler by adding the
scheduler:CreateSchedule
permission and theiam:PassRole
(see intro diagram) permission to the addMemo role - 5️⃣ We create an API Gateway endpoint that will trigger the addMemo Lambda function, and a POST method to trigger it
Now, let's create the Lambda functions that will be triggered by the API Gateway endpoint. First, let's create the addMemo
Lambda function, that will create a schedule:
// addMemo.ts - Lambda function that creates a scheduler
import {
ActionAfterCompletion,
CreateScheduleCommand,
FlexibleTimeWindowMode,
SchedulerClient,
} from '@aws-sdk/client-scheduler';
import { v4 as uuidv4 } from 'uuid';
const client = new SchedulerClient({});
const scheduleTargetArn = process.env.SCHEDULE_TARGET_ARN as string;
const scheduleRoleArn = process.env.SCHEDULE_ROLE_ARN as string;
if (scheduleTargetArn === undefined || scheduleRoleArn === undefined) {
throw new Error('Missing environment variables');
}
export const handler = async ({
body,
}: {
body: string;
}): Promise<{
statusCode: number;
body: string;
}> => {
const {
memo,
date,
time,
timezone = 'Europe/Paris',
} = JSON.parse(body) as { memo?: string; date?: string; timezone?: string; time?: string };
if (memo === undefined || date === undefined) {
return {
statusCode: 400,
body: 'Bad Request',
};
}
await client.send(
new CreateScheduleCommand({
Name: uuidv4(),
Target: {
Arn: scheduleTargetArn,
RoleArn: scheduleRoleArn,
Input: JSON.stringify({ memo }),
},
ScheduleExpressionTimezone: timezone,
ScheduleExpression: `at(${date}T${time})`,
FlexibleTimeWindow: {
Mode: FlexibleTimeWindowMode.OFF,
},
ActionAfterCompletion: ActionAfterCompletion.DELETE,
}),
);
return {
statusCode: 200,
body: 'Memo scheduled',
};
};
What is happening here?
- 1️⃣ We setup a client and parse environment variables (see IAC where we set them)
- 2️⃣ We parse the body of the request, and extract the memo, date, time and timezone (default to Europe/Paris (where I live 😅))
- 3️⃣ We create a schedule using the AWS SDK for Javascript
- 🅰️ We set the target, using the
scheduleTargetArn
andscheduleRoleArn
environment variables - 🅱️ We set the schedule expression, using the date and time provided in the request, along with the timezone, and we set the schedule to be automatically deleted after execution
- 🅰️ We set the target, using the
Finally, let's create the executeMemo
Lambda function, that will be triggered by the scheduler. This function will simply log the memo to the console:
// executeMemo.ts - Lambda function that executes a memo
export const handler = async ({ memo }: { memo: string }): Promise<void> => {
console.log(memo);
return Promise.resolve();
};
Very easy! Notice that the lambda input is the same as the input
field specified in the CreateScheduleCommand
of the addMemo
Lambda function.
Test our app
We are done! Time to deploy and to test our API route /addMemo
npm run cdk bootstrap
npm run cdk deploy
I live in Paris 🇫🇷, so I used my default Europe/Paris
timezone, but you can specify the timezone of your choice. I specified a time of 22:37, and it is 22:36, if I head to Cloudwatch, I should see my executeMemo Lambda function being triggered in 1 minute.
It worked! I can also see the payload of the event, and it contains the memo I created.
Going further
What could be improved in this app?
- We could go further and add a
/getMemos
route, that would return all the memos that are not executed yet - We could also add a
/executeMemo
route, that would execute a memo immediately, and cancel the corresponding schedule - As always, implementing authentication would be a good idea 😅
Conclusion
This article was a basic introduction to AWS EventBridge Scheduler. We discovered how to create a scheduled task, and how to execute it. We also discovered how to use the AWS CDK to provision our infrastructure. I hope you enjoyed this article, and that you learned something new!
I plan to continue this series of articles on a bi-monthly basis. You can follow this progress on my repository! I will cover new topics in the future, if you have any suggestions, do not hesitate to contact me!
I would really appreciate if you could react and share this article with your friends and colleagues. It will help me a lot to grow my audience. Also, don't forget to subscribe to be updated when the next article comes out!
I you want to stay in touch here is my twitter account. I often post or re-post interesting stuff about AWS and serverless, feel free to follow me!
Top comments (1)
What happens if there is 2 memo's at the same time ? can you wrap schedulers ?