Serverless Scheduled Reporting Service with AWS EventBridge, Lambda, SNS, and CDK
Tags: aws, serverless, lambda, cdk
Published: 2023-11-09 17:39:56

Have you ever thought of building a simple scheduled reporting service without the need to manage a server and only pay per use (which in our case only runs 30 times a month)?

This scheduling service will send a daily total sales report to your email like this.
![sales notification](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6f5ffyzjwasdmk3tbbt5.png)

So in this article, we will learn how to build those services by utilizing several AWS services like Amazon EventBridge, AWS Lambda, Amazon SNS and deploy it with AWS CDK.

## Follow me for more
{% embed https://linktr.ee/fullstacksaiyan %}

## EventBridge (CloudWatch Events)

![amazon eventbridge scheduler](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2pxv1lzlyrql6n1nwy3m.png)
EventBridge (CloudWatch Events) is a serverless event bus that makes it easy to connect your applications and services to data sources and other AWS services. It enables you to build event-driven architectures that respond automatically to state changes in your AWS resources.

EventBridge provides a unified view of all your events, regardless of where they originate. You can create rules to match events and route them to targets such as AWS Lambda functions, Amazon Kinesis streams, and Amazon Simple Notification Service (SNS) topics. This allows you to automate your workflows and respond to events in real-time.

## AWS Lambda

![aws lambda](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hjlbchfupa0nc3fv5mhw.png)
AWS Lambda is a serverless computing service offered by Amazon Web Services (AWS). It allows developers to run code without provisioning or managing servers, as the underlying infrastructure is managed by AWS. With Lambda, developers can focus solely on writing and deploying their code, without worrying about scaling, capacity planning, or server management.

Lambda functions can be triggered by various events, such as changes to an S3 bucket, an HTTP request, or a message from an Amazon Kinesis stream. Once triggered, the function executes in a sandboxed environment, which runs on top of Amazon Elastic Container Service (ECS) containers. This environment provides isolation between functions and ensures that they do not interfere with each other.

## Amazon Simple Notification Service (SNS)

![amazon sns](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7xmr5fzhnihdl49b8vun.png)
Amazon Simple Notification Service (SNS) is a fully managed messaging service provided by Amazon Web Services (AWS). It enables developers to send individual messages or large-scale notifications to multiple subscribers or clients, such as mobile devices, email addresses, and HTTP endpoints.

With SNS, developers can fan out messages to a large number of recipients without having to worry about the underlying infrastructure, scaling, or delivery guarantees. The service takes care of distributing messages to the specified subscribers, handling duplication, throttling, and deliverability.

## AWS Cloud Development Kit (CDK)

![aws cdk](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jih6tngsomv7oiin1xoo.png)

AWS CDK (Cloud Development Kit) is an open-source software development framework created by Amazon Web Services (AWS). It allows developers to define cloud infrastructure and applications in code, rather than manually configuring them through the AWS Management Console or CLI.

CDK provides a programmatic way to define infrastructure using familiar programming languages like TypeScript, JavaScript, Python, and Java. It offers a set of libraries and constructs that enable developers to describe the desired state of their infrastructure, and then provisions and manages it automatically.

## Prerequisites

- [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
- [Configure AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)
- [Install AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install)

## Table of Contents

- [Create Project](#step1)
- [Create Lambda handler](#step2)
- [Create Stack](#step3)
- [Import modules](#step4)
- [Create Parameter](#step5)
- [Create SNS Topic](#step6)
- [Create Lambda Function](#step7)
- [Permission to publish a message to SNS Topic](#step8)
- [Create EventBridge Schedule Rule](#step9)
- [Trigger Lambda from EventBridge](#step10)
- [Complete Stack Code](#step11)
- [Deploy Stack](#step12)
- [Testing](#step13)
- [Conclusion](#step14)

## Create Project <a name="step1"></a>
- We will start by creating a folder.
```sh
mkdir cdk-scheduled-reporting && cd cdk-scheduled-reporting
```
- Then initialize our CDK project
```sh
cdk init app --language javascript
```

## Create Lambda handler <a name="step2"></a>
We will create our Lambda handler with the logic to collect the sales data and send it to Amazon SNS.
- Create `src` folder
- Create `index.js` inside `src` folder

```sh
touch src/index.js
```

- We will simulate our sales data by getting dummy cart data from `https://dummyjson.com/carts` REST API server.
- So, start typing the following code into `src/index.js`

```js
const AWS = require("aws-sdk");
const sns = new AWS.SNS({ apiVersion: "2010-03-31" });

exports.handler = async (event) => {
  try {
    const data = await sns
      .publish({
        Subject: "Daily Sales Report",
        Message: `Your today's total sales is $ ${await fetchTotalSales()}`,
        TopicArn: process.env.TOPIC_ARN,
      })
      .promise();
    console.log("data", data);
  } catch (err) {
    console.log("err", err);
  }
  return { message: "Successfully executed" };
};

async function fetchTotalSales() {
  const fetch = await import("node-fetch");
  return fetch
    .default("https://dummyjson.com/carts")
    .then((response) => response.json())
    .then((data) => {
      let total = 0;
      total = data.carts.reduce((acc, item) => {
        return acc + item.discountedTotal;
      }, 0);
      console.log(total);
      return total.toLocaleString();
    });
}
```

## Create Stack <a name="step3"></a>
- Go to `lib/cdk-scheduled-reporting-stack.js`
- Our `lib/cdk-scheduled-reporting-stack.js` will look like this for now.

```js
const { Stack, Duration } = require("aws-cdk-lib");

class CdkScheduledReportingStack extends Stack {
  constructor(scope, id, props) {
    super(scope, id, props);
  }
}

module.exports = { CdkScheduledReportingStack };
```

## Import modules <a name="step4"></a>

To create our stack, we will import several modules from `aws-cdk-lib` and `path`

```js
const { Stack, Duration, CfnParameter } = require("aws-cdk-lib");
const lambda = require("aws-cdk-lib/aws-lambda");
const event = require("aws-cdk-lib/aws-events");
const target = require("aws-cdk-lib/aws-events-targets");
const sns = require("aws-cdk-lib/aws-sns");
const subscription = require("aws-cdk-lib/aws-sns-subscriptions");

const path = require("path");
```

## Create Parameter <a name="step5"></a>

We will create an `EmailAddress` parameter of type string, which will be used to input an email address to which a sales report will be sent.

```js
const emailAddress = new CfnParameter(this, "EmailAddress", {
  type: "String",
  description: "Email address to send sales report",
});
```

## Create SNS Topic <a name="step6"></a>

Next, we will define Amazon SNS topic and subscribe to our `EmailAddress` so that when a message is published to the SNS topic, it will be sent to the email address specified in the `EmailAddress` parameter.

```js
const topic = new sns.Topic(this, "SalesReportTopic", {
  displayName: "ScheduledReportingTopic",
});

topic.addSubscription(
  new subscription.EmailSubscription(emailAddress.valueAsString)
);
```

## Create Lambda Function <a name="step7"></a>

Now, we will create our Lambda function and load our handler from `src` folder.

```js
const salesReportFunction = new lambda.Function(this, "SalesReportFunction", {
  runtime: lambda.Runtime.NODEJS_14_X,
  code: lambda.Code.fromAsset(path.join(__dirname, "../src")),
  handler: "index.handler",
  timeout: Duration.seconds(300),
  environment: {
    TOPIC_ARN: topic.topicArn,
  },
});
```

## Permission to publish message to SNS Topic <a name="step8"></a>

Next, we will give the `salesReportFunction` AWS Lambda function the necessary permissions to publish messages to the topic SNS topic.
This means that the Lambda function can send messages to the SNS topic, which will then distribute the messages to all of the topic's subscribers.

```js
topic.grantPublish(salesReportFunction);
```

## Create EventBridge Schedule Rule <a name="step9"></a>

Now, we will create an EventBrdige Schedule Rule that will trigger every day at 11:00 AM UTC or 06:00 PM GMT+7

```js
const rule = new event.Rule(this, "Rule", {
  schedule: event.Schedule.expression("cron(0 11 * * ? *)"),
});
```

## Trigger Lambda from EventBridge <a name="step10"></a>

Add the `salesReportFunction` Lambda function as a target to the rule EventBridge Schedule rule. This means that when the rule is triggered according to its schedule, it will invoke the salesReportFunction Lambda function.

```js
rule.addTarget(new target.LambdaFunction(salesReportFunction));
```

## Complete Stack Code <a name="step11"></a>

Finally, our complete stack code will look like this.

```js
const { Stack, Duration, CfnParameter } = require("aws-cdk-lib");
const lambda = require("aws-cdk-lib/aws-lambda");
const event = require("aws-cdk-lib/aws-events");
const target = require("aws-cdk-lib/aws-events-targets");
const sns = require("aws-cdk-lib/aws-sns");
const subscription = require("aws-cdk-lib/aws-sns-subscriptions");

const path = require("path");

class CdkScheduledReportingStack extends Stack {
  constructor(scope, id, props) {
    super(scope, id, props);

    const emailAddress = new CfnParameter(this, "EmailAddress", {
      type: "String",
      description: "Email address to send sales report",
    });

    const topic = new sns.Topic(this, "SalesReportTopic", {
      displayName: "ScheduledReportingTopic",
    });

    topic.addSubscription(
      new subscription.EmailSubscription(emailAddress.valueAsString)
    );

    const salesReportFunction = new lambda.Function(
      this,
      "SalesReportFunction",
      {
        runtime: lambda.Runtime.NODEJS_14_X,
        code: lambda.Code.fromAsset(path.join(__dirname, "../src")),
        handler: "index.handler",
        timeout: Duration.seconds(300),
        environment: {
          TOPIC_ARN: topic.topicArn,
        },
      }
    );

    topic.grantPublish(salesReportFunction);

    const rule = new event.Rule(this, "Rule", {
      schedule: event.Schedule.expression("cron(0 11 * * ? *)"),
    });

    rule.addTarget(new target.LambdaFunction(salesReportFunction));
  }
}

module.exports = { CdkScheduledReportingStack };
```

## Deploy Stack <a name="step12"></a>

To deploy our CDK Stack, we can run this command in our terminal.

```sh
cdk deploy --parameters EmailAddress=youremail@email.com
```
> You can define your own email address by changing `youremail@email.com` value.

CDK will start synthesizing our stack like this

```sh
✨  Synthesis time: 14.44s

CdkScheduledReportingStack:  start: Building f3a91fb0ff1605e49b4b00aa223098d7a9a5307e98027f77aecc36bc05a4c3e7:current_account-current_region
CdkScheduledReportingStack:  success: Built f3a91fb0ff1605e49b4b00aa223098d7a9a5307e98027f77aecc36bc05a4c3e7:current_account-current_region
CdkScheduledReportingStack:  start: Publishing f3a91fb0ff1605e49b4b00aa223098d7a9a5307e98027f77aecc36bc05a4c3e7:current_account-current_region
CdkScheduledReportingStack:  start: Building 517f907ce2318aa1a929b43bc1a0247ed509c92576c23a0a8aadfe1677f2ebd7:current_account-current_region
CdkScheduledReportingStack:  success: Built 517f907ce2318aa1a929b43bc1a0247ed509c92576c23a0a8aadfe1677f2ebd7:current_account-current_region
CdkScheduledReportingStack:  start: Publishing 517f907ce2318aa1a929b43bc1a0247ed509c92576c23a0a8aadfe1677f2ebd7:current_account-current_region
CdkScheduledReportingStack:  success: Published 517f907ce2318aa1a929b43bc1a0247ed509c92576c23a0a8aadfe1677f2ebd7:current_account-current_region
CdkScheduledReportingStack:  success: Published f3a91fb0ff1605e49b4b00aa223098d7a9a5307e98027f77aecc36bc05a4c3e7:current_account-current_region
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────────────────────┬────────┬───────────────────────┬────────────────────────────────────────┬───────────────────────────────────────────────┐
│   │ Resource                               │ Effect │ Action                │ Principal                              │ Condition                                     │
├───┼────────────────────────────────────────┼────────┼───────────────────────┼────────────────────────────────────────┼───────────────────────────────────────────────┤
│ + │ ${SalesReportFunction.Arn}             │ Allow  │ lambda:InvokeFunction │ Service:events.amazonaws.com           │ "ArnLike": {                                  │
│   │                                        │        │                       │                                        │   "AWS:SourceArn": "${Rule.Arn}"              │
│   │                                        │        │                       │                                        │ }                                             │
├───┼────────────────────────────────────────┼────────┼───────────────────────┼────────────────────────────────────────┼───────────────────────────────────────────────┤
│ + │ ${SalesReportFunction/ServiceRole.Arn} │ Allow  │ sts:AssumeRole        │ Service:lambda.amazonaws.com           │                                               │
├───┼────────────────────────────────────────┼────────┼───────────────────────┼────────────────────────────────────────┼───────────────────────────────────────────────┤
│ + │ ${SalesReportTopic}                    │ Allow  │ sns:Publish           │ AWS:${SalesReportFunction/ServiceRole} │                                               │
└───┴────────────────────────────────────────┴────────┴───────────────────────┴────────────────────────────────────────┴───────────────────────────────────────────────┘
IAM Policy Changes
┌───┬────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                           │ Managed Policy ARN                                                             │
├───┼────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${SalesReportFunction/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)?
```

After confirm the deployment, CDK will start deploying our service

```sh
CdkScheduledReportingStack: deploying... [1/1]
CdkScheduledReportingStack: creating CloudFormation changeset...
CdkScheduledReportingStack | 0/10 | 11:16:56 PM | CREATE_IN_PROGRESS   | AWS::SNS::Subscription  | SalesReportTopic/TokenSubscription:1 (SalesReportTopicTokenSubscription199EB8BBD)
CdkScheduledReportingStack | 0/10 | 11:16:56 PM | CREATE_IN_PROGRESS   | AWS::SNS::Subscription  | SalesReportTopic/TokenSubscription:2 (SalesReportTopicTokenSubscription246354DF1)
CdkScheduledReportingStack | 0/10 | 11:16:46 PM | REVIEW_IN_PROGRESS   | AWS::CloudFormation::Stack | CdkScheduledReportingStack User Initiated
CdkScheduledReportingStack | 0/10 | 11:16:51 PM | CREATE_IN_PROGRESS   | AWS::CloudFormation::Stack | CdkScheduledReportingStack User Initiated
CdkScheduledReportingStack | 0/10 | 11:16:55 PM | CREATE_IN_PROGRESS   | AWS::SNS::Topic         | SalesReportTopic (SalesReportTopic505419E0)
CdkScheduledReportingStack | 0/10 | 11:16:55 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata      | CDKMetadata/Default (CDKMetadata)
CdkScheduledReportingStack | 0/10 | 11:16:55 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role          | SalesReportFunction/ServiceRole (SalesReportFunctionServiceRole3A32E4C8)
CdkScheduledReportingStack | 0/10 | 11:16:56 PM | CREATE_IN_PROGRESS   | AWS::SNS::Topic         | SalesReportTopic (SalesReportTopic505419E0) Resource creation Initiated
CdkScheduledReportingStack | 0/10 | 11:16:56 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata      | CDKMetadata/Default (CDKMetadata) Resource creation Initiated
CdkScheduledReportingStack | 1/10 | 11:16:56 PM | CREATE_COMPLETE      | AWS::SNS::Topic         | SalesReportTopic (SalesReportTopic505419E0)
CdkScheduledReportingStack | 2/10 | 11:16:56 PM | CREATE_COMPLETE      | AWS::CDK::Metadata      | CDKMetadata/Default (CDKMetadata)
CdkScheduledReportingStack | 2/10 | 11:16:56 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role          | SalesReportFunction/ServiceRole (SalesReportFunctionServiceRole3A32E4C8) Resource creation Initiated
CdkScheduledReportingStack | 2/10 | 11:16:57 PM | CREATE_IN_PROGRESS   | AWS::SNS::Subscription  | SalesReportTopic/TokenSubscription:1 (SalesReportTopicTokenSubscription199EB8BBD) Resource creation Initiated
CdkScheduledReportingStack | 3/10 | 11:16:57 PM | CREATE_COMPLETE      | AWS::SNS::Subscription  | SalesReportTopic/TokenSubscription:1 (SalesReportTopicTokenSubscription199EB8BBD)
CdkScheduledReportingStack | 3/10 | 11:16:57 PM | CREATE_IN_PROGRESS   | AWS::SNS::Subscription  | SalesReportTopic/TokenSubscription:2 (SalesReportTopicTokenSubscription246354DF1) Resource creation Initiated
CdkScheduledReportingStack | 4/10 | 11:16:57 PM | CREATE_COMPLETE      | AWS::SNS::Subscription  | SalesReportTopic/TokenSubscription:2 (SalesReportTopicTokenSubscription246354DF1)
CdkScheduledReportingStack | 5/10 | 11:17:12 PM | CREATE_COMPLETE      | AWS::IAM::Role          | SalesReportFunction/ServiceRole (SalesReportFunctionServiceRole3A32E4C8)
CdkScheduledReportingStack | 5/10 | 11:17:13 PM | CREATE_IN_PROGRESS   | AWS::IAM::Policy        | SalesReportFunction/ServiceRole/DefaultPolicy (SalesReportFunctionServiceRoleDefaultPolicy1B643419)
CdkScheduledReportingStack | 5/10 | 11:17:15 PM | CREATE_IN_PROGRESS   | AWS::IAM::Policy        | SalesReportFunction/ServiceRole/DefaultPolicy (SalesReportFunctionServiceRoleDefaultPolicy1B643419) Resource creation Initiated
CdkScheduledReportingStack | 6/10 | 11:17:31 PM | CREATE_COMPLETE      | AWS::IAM::Policy        | SalesReportFunction/ServiceRole/DefaultPolicy (SalesReportFunctionServiceRoleDefaultPolicy1B643419)
CdkScheduledReportingStack | 6/10 | 11:17:33 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Function   | SalesReportFunction (SalesReportFunctionAFEC00DA)
CdkScheduledReportingStack | 6/10 | 11:17:35 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Function   | SalesReportFunction (SalesReportFunctionAFEC00DA) Resource creation Initiated
CdkScheduledReportingStack | 7/10 | 11:17:40 PM | CREATE_COMPLETE      | AWS::Lambda::Function   | SalesReportFunction (SalesReportFunctionAFEC00DA)
CdkScheduledReportingStack | 7/10 | 11:17:41 PM | CREATE_IN_PROGRESS   | AWS::Events::Rule       | Rule (Rule4C995B7F)
CdkScheduledReportingStack | 7/10 | 11:17:42 PM | CREATE_IN_PROGRESS   | AWS::Events::Rule       | Rule (Rule4C995B7F) Resource creation Initiated
7/10 Currently in progress: CdkScheduledReportingStack, Rule4C995B7F
CdkScheduledReportingStack | 8/10 | 11:18:43 PM | CREATE_COMPLETE      | AWS::Events::Rule       | Rule (Rule4C995B7F)
CdkScheduledReportingStack | 8/10 | 11:18:44 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Permission | Rule/AllowEventRuleCdkScheduledReportingStackSalesReportFunctionA41A35CE (RuleAllowEventRuleCdkScheduledReportingStackSalesReportFunctionA41A35CEB53F3E4B)
CdkScheduledReportingStack | 8/10 | 11:18:45 PM | CREATE_IN_PROGRESS   | AWS::Lambda::Permission | Rule/AllowEventRuleCdkScheduledReportingStackSalesReportFunctionA41A35CE (RuleAllowEventRuleCdkScheduledReportingStackSalesReportFunctionA41A35CEB53F3E4B) Resource creation Initiated
CdkScheduledReportingStack | 9/10 | 11:18:45 PM | CREATE_COMPLETE      | AWS::Lambda::Permission | Rule/AllowEventRuleCdkScheduledReportingStackSalesReportFunctionA41A35CE (RuleAllowEventRuleCdkScheduledReportingStackSalesReportFunctionA41A35CEB53F3E4B)
CdkScheduledReportingStack | 10/10 | 11:18:46 PM | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CdkScheduledReportingStack

 ✅  CdkScheduledReportingStack

✨  Deployment time: 125.83s
Outputs:
CdkScheduledResportingStack.LambdaFunction = CdkScheduledResportingSta-SalesReportFunctionAFEC0-ZnCpb5NTiaaq
Stack ARN:
arn:aws:cloudformation:ap-southeast-1:00000000000:stack/CdkScheduledReportingStack/60c2ae80-7f1b-11ee-9fa0-0abc96bce958

✨  Total time: 140.27s
```

> Pay attention to the outputs of our Lambda Function name `CdkScheduledResportingSta-SalesReportFunctionAFEC0-ZnCpb5NTiaaq`. We will use it later to test our Lambda function.

After the deployment is complete, you will receive a topic subscription confirmation like this that you need to confirm so that Amazon SNS can send messages to your email.
![Subscription Confirmation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e128bkgfkglsmj8vw6yw.png)

## Testing <a name="step13"></a>

To see whether our Lambda function is working or not, we have 3 options :

- Wait until the EventBridge schedule triggers our Lambda function at 11:00 AM UTC or 06:00 PM GMT+7.
- Trigger it manually through AWS Management Console.
- Trigger it manually through AWS CLI.

I will show you the last option, by triggering our Lambda function `CdkScheduledResportingSta-SalesReportFunctionAFEC0-ZnCpb5NTiaaq` manually through AWS CLI. You can do it by running the following command into your terminal.

```sh
aws lambda invoke --function-name CdkScheduledResportingSta-SalesReportFunctionAFEC0-ZnCpb5NTiaaq --invocation-type Event response.json
```

After running the above command, you will receive your sales notification in your email like this
![sales notification](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6f5ffyzjwasdmk3tbbt5.png)

By receiving the above email, that means our service is deployed and working perfectly.

![wohoo](https://media.giphy.com/media/xT5LMHxhOfscxPfIfm/giphy.gif)



## Conclusion <a name="step14"></a>

So in this article, we successfully learn that we can deploy our serverless scheduled reporting service with AWS EventBridge, Lambda, SNS, and CDK at ease without the need to manage a server and only pay per use.

## Check out my previous post

{% embed https://dev.to/aws-builders/unlocking-the-power-of-nosql-building-a-todo-api-with-hapijs-and-dynamodb-1f7n %}