diff --git a/cdk-lambda-scheduler-ses/.gitignore b/cdk-lambda-scheduler-ses/.gitignore new file mode 100644 index 000000000..f60797b6a --- /dev/null +++ b/cdk-lambda-scheduler-ses/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/cdk-lambda-scheduler-ses/.npmignore b/cdk-lambda-scheduler-ses/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/cdk-lambda-scheduler-ses/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/cdk-lambda-scheduler-ses/README.md b/cdk-lambda-scheduler-ses/README.md new file mode 100644 index 000000000..d3fb5faf9 --- /dev/null +++ b/cdk-lambda-scheduler-ses/README.md @@ -0,0 +1,94 @@ +# Using EventBridge Scheduler to send scheduled reminder emails. + +This pattern demonstrates how to create an EventBridge scheduler that would send scheduled reminder email and would then be deleted. The pattern uses a Lambda function to create EventBridge scheduler. + +Learn more about this pattern at Serverless Land Patterns: << Add the live URL here >> + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/v2/guide/cli.html) + + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +2. Change directory to the pattern directory: + ``` + cd cdk-lambda-scheduler-ses + ``` +3. Install dependencies + ``` + npm install + ``` +4. From the command line, configure AWS CDK: + ``` + cdk bootstrap aws://ACCOUNT-NUMBER/REGION + + eg: cdk bootstrap aws://123456789012/us-east-1 + ``` + +5. Synthesize CloudFormation template from the AWS CDK app: + ``` + cdk synth + ``` +6. To deploy your stack, run the following the command line : + ``` + cdk deploy --all --parameters SenderEmail={source-email-address} + ``` + Here, + + * Replace {source-email-address} with the email address that should be sending reminder email address. The email address should be verified identity from Amazon SES. + +## Architecture + +![Architecture](images/scheduler-reminder-architecture.png) + +## How it works + +Let us now dive deeper into the architecture : + +1. Scheduler Lambda Invocation: + + When the Scheduler Lambda is invoked. It extracts the following details from event: + * Message to be sent. + * Date and time when the message should be sent. + * Email ID to whom the message should be sent. + + The lambda function then creates the EventBridge schedule and passes the details extracted as a payload to the scheduler. + +2. EventBridge Scheduler Activation: + + The EventBridge Scheduler sends email using SES. After the scheduler has sent email, it is deleted automatically. + +## Testing + +Once the whole setup is deployed using CDK, you can carry out testing by creating a [Test event](https://docs.aws.amazon.com/lambda/latest/dg/testing-functions.html#creating-private-events) for the Lambda function created and invoking the lambda function with the Test event. Expected input for the event: + +``` + { + "message": "Hello, this is a simple test", + "datetime": "2023-12-22T10:02:00Z", + "email": "{email-address-to-whom-email-should-be-sent}" + } +``` +These three fields are required and are case-sensitive. Also note that "{email-address-to-whom-email-should-be-sent}" should be a verified identity from Amazon SES + +## Cleanup + +1. Delete the stack + + ``` + cdk destroy --all + ``` + +---- +Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 \ No newline at end of file diff --git a/cdk-lambda-scheduler-ses/bin/cdk-lambda-scheduler-ses.ts b/cdk-lambda-scheduler-ses/bin/cdk-lambda-scheduler-ses.ts new file mode 100644 index 000000000..b3886a107 --- /dev/null +++ b/cdk-lambda-scheduler-ses/bin/cdk-lambda-scheduler-ses.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { CdkLambdaSchedulerSesStack } from '../lib/lambda-scheduler-ses-stack'; + +const app = new cdk.App(); +new CdkLambdaSchedulerSesStack(app, 'CdkLambdaSchedulerSesStack', { + /* If you don't specify 'env', this stack will be environment-agnostic. + * Account/Region-dependent features and context lookups will not work, + * but a single synthesized template can be deployed anywhere. */ + + /* Uncomment the next line to specialize this stack for the AWS Account + * and Region that are implied by the current CLI configuration. */ + // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + + /* Uncomment the next line if you know exactly what Account and Region you + * want to deploy the stack to. */ + // env: { account: '123456789012', region: 'us-east-1' }, + + /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ +}); \ No newline at end of file diff --git a/cdk-lambda-scheduler-ses/cdk-lambda-schedular-ses.json b/cdk-lambda-scheduler-ses/cdk-lambda-schedular-ses.json new file mode 100644 index 000000000..74287a625 --- /dev/null +++ b/cdk-lambda-scheduler-ses/cdk-lambda-schedular-ses.json @@ -0,0 +1,88 @@ +{ + "title": "Amazon EventBridge Scheduler with Amazon SES", + "description": "A serverless event-driven pattern that creates Amazon EventBridge schedules dynamically to send email reminders using Amazon SES.", + "language": "Python", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to create an EventBridge scheduler that would send scheduled reminder email and would then be deleted. The pattern uses a Lambda function to create EventBridge scheduler." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/cdk-lambda-scheduler-ses", + "templateURL": "serverless-patterns/cdk-lambda-scheduler-ses", + "projectFolder": "cdk-lambda-scheduler-ses", + "templateFile": "lib/lambda-scheduler-ses-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Getting started with the AWS CDK", + "link": "https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html" + }, + { + "text": "Amazon EventBridge Scheduler", + "link": "https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html" + }, + { + "text": "Verify an email address using Amazon SES", + "link": "https://docs.aws.amazon.com/ses/latest/dg/creating-identities.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Anirudh Sharma", + "image": "https://avatars.githubusercontent.com/u/144559992", + "bio": "Cloud Support Engineer 2 @AWS", + "linkedin": "anirudh-sharma-279248142" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "eventbridge-scheduler", + "label": "Amazon EventBridge Scheduler" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "ses", + "label": "Amazon SES" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "line2": { + "from": "icon2", + "to": "icon3" + } + } +} diff --git a/cdk-lambda-scheduler-ses/cdk.json b/cdk-lambda-scheduler-ses/cdk.json new file mode 100644 index 000000000..f88921f70 --- /dev/null +++ b/cdk-lambda-scheduler-ses/cdk.json @@ -0,0 +1,64 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/cdk-lambda-scheduler-ses.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true + } +} diff --git a/cdk-lambda-scheduler-ses/example-pattern.json b/cdk-lambda-scheduler-ses/example-pattern.json new file mode 100644 index 000000000..b9aadb4d8 --- /dev/null +++ b/cdk-lambda-scheduler-ses/example-pattern.json @@ -0,0 +1,60 @@ +{ + "title": "Amazon EventBridge Scheduler with Amazon SES", + "description": "A serverless event-driven pattern that creates Amazon EventBridge schedules dynamically to send email reminders using Amazon SES.", + "language": "Python", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to create an EventBridge scheduler that would send scheduled reminder email and would then be deleted. The pattern uses a Lambda function to create EventBridge scheduler." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/cdk-lambda-scheduler-ses", + "templateURL": "serverless-patterns/cdk-lambda-scheduler-ses", + "projectFolder": "cdk-lambda-scheduler-ses", + "templateFile": "lib/lambda-scheduler-ses-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Getting started with the AWS CDK", + "link": "https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html" + }, + { + "text": "Amazon EventBridge Scheduler", + "link": "https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html" + }, + { + "text": "Verify an email address using Amazon SES", + "link": "https://docs.aws.amazon.com/ses/latest/dg/creating-identities.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Anirudh Sharma", + "image": "https://avatars.githubusercontent.com/u/144559992", + "bio": "Cloud Support Engineer 2 @AWS", + "linkedin": "anirudh-sharma-279248142" + } + ] + } diff --git a/cdk-lambda-scheduler-ses/images/scheduler-reminder-architecture.png b/cdk-lambda-scheduler-ses/images/scheduler-reminder-architecture.png new file mode 100644 index 000000000..b86cfc37a Binary files /dev/null and b/cdk-lambda-scheduler-ses/images/scheduler-reminder-architecture.png differ diff --git a/cdk-lambda-scheduler-ses/jest.config.js b/cdk-lambda-scheduler-ses/jest.config.js new file mode 100644 index 000000000..08263b895 --- /dev/null +++ b/cdk-lambda-scheduler-ses/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/cdk-lambda-scheduler-ses/lib/lambda-scheduler-ses-stack.ts b/cdk-lambda-scheduler-ses/lib/lambda-scheduler-ses-stack.ts new file mode 100644 index 000000000..77f4cce60 --- /dev/null +++ b/cdk-lambda-scheduler-ses/lib/lambda-scheduler-ses-stack.ts @@ -0,0 +1,65 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import path = require('path'); +import * as iam from 'aws-cdk-lib/aws-iam'; + +export class CdkLambdaSchedulerSesStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const senderEmail = new cdk.CfnParameter(this, 'SenderEmail', { + type: 'String', + description: 'Email ID of the sender who would send reminder emails using SES', + }); + + const schedulerRole = new iam.Role(this, 'SchedulerRole', { + assumedBy: new iam.ServicePrincipal('scheduler.amazonaws.com') + }); + + const schedulerPolicy = new iam.Policy(this, 'SchedulerPolicy', { + statements: [ + new iam.PolicyStatement({ + actions: ['ses:SendEmail'], + resources: ['*'] + }) + ] + }); + schedulerPolicy.attachToRole(schedulerRole); + const role = new iam.Role(this, 'LambdaFunctionRole', { + + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com') + }); + role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEventBridgeSchedulerFullAccess')); + role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess')); + + const fn = new lambda.Function(this, 'SchedulerFunction', { + runtime: lambda.Runtime.PYTHON_3_11, + handler: 'eventbridge_scheduler.lambda_handler', + code: lambda.Code.fromAsset(path.join(__dirname, '../scheduler_function'), { + bundling: { + image: lambda.Runtime.PYTHON_3_11.bundlingImage, + command: [ + 'bash', '-c', + 'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output' + ], + }, + }), + environment: { + 'SCHEDULER_ROLE_ARN': schedulerRole.roleArn, + 'SES_SENDER_IDENTITY': senderEmail.valueAsString + }, + role: role + }); + + } +} + + + + + + + + + diff --git a/cdk-lambda-scheduler-ses/package.json b/cdk-lambda-scheduler-ses/package.json new file mode 100644 index 000000000..551afe393 --- /dev/null +++ b/cdk-lambda-scheduler-ses/package.json @@ -0,0 +1,27 @@ +{ + "name": "cdk-lambda-scheduler-ses", + "version": "0.1.0", + "bin": { + "cdk-lambda-scheduler-ses": "bin/cdk-lambda-scheduler-ses.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "@types/node": "20.10.4", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "aws-cdk": "2.117.0", + "ts-node": "^10.9.2", + "typescript": "~5.3.3" + }, + "dependencies": { + "aws-cdk-lib": "2.117.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} diff --git a/cdk-lambda-scheduler-ses/scheduler_function/eventbridge_scheduler.py b/cdk-lambda-scheduler-ses/scheduler_function/eventbridge_scheduler.py new file mode 100644 index 000000000..0e29e863e --- /dev/null +++ b/cdk-lambda-scheduler-ses/scheduler_function/eventbridge_scheduler.py @@ -0,0 +1,33 @@ + +import json +import boto3 +import os +from datetime import datetime + +scheduler = boto3.client('scheduler') + +def create_scheduler(formatted_expirationDate, lambda_target_template): + res=scheduler.create_schedule( + Name='reminder_scheduler', + Description='Pipeline Approval Reminder', + ScheduleExpression='at({0})'.format(formatted_expirationDate.strftime("%Y-%m-%dT%H:%M:%S")), + Target=lambda_target_template, + FlexibleTimeWindow ={'Mode': 'OFF'}, + ActionAfterCompletion = 'DELETE' + ) + return res + +def lambda_handler(event, context): + + sendTime=event['datetime'] + message=event['message'] + destination_email=event['email'] + formatted_expirationDate= datetime.strptime(sendTime,"%Y-%m-%dT%H:%M:%SZ") + + lambda_target_template= { + 'Arn': 'arn:aws:scheduler:::aws-sdk:ses:sendEmail', + 'RoleArn': os.environ['SCHEDULER_ROLE_ARN'], + 'Input': "{\"Destination\": {\"ToAddresses\": [\"" + destination_email + "\"] }, \"Message\": {\"Body\": {\"Text\": {\"Data\": \"" + message + "\"}}, \"Subject\": {\"Data\": \"Reminder Email\"}}, \"Source\": \"" + os.environ['SES_SENDER_IDENTITY'] + "\"}" + } + create_scheduler(formatted_expirationDate, lambda_target_template) + diff --git a/cdk-lambda-scheduler-ses/scheduler_function/requirements.txt b/cdk-lambda-scheduler-ses/scheduler_function/requirements.txt new file mode 100644 index 000000000..de93b7f9b --- /dev/null +++ b/cdk-lambda-scheduler-ses/scheduler_function/requirements.txt @@ -0,0 +1 @@ +boto3==1.28.83 \ No newline at end of file diff --git a/cdk-lambda-scheduler-ses/test/cdk-lambda-scheduler-ses.test.ts b/cdk-lambda-scheduler-ses/test/cdk-lambda-scheduler-ses.test.ts new file mode 100644 index 000000000..89885a916 --- /dev/null +++ b/cdk-lambda-scheduler-ses/test/cdk-lambda-scheduler-ses.test.ts @@ -0,0 +1,17 @@ +// import * as cdk from 'aws-cdk-lib'; +// import { Template } from 'aws-cdk-lib/assertions'; +// import * as CdkLambdaSchedulerSes from '../lib/cdk-lambda-scheduler-ses-stack'; + +// example test. To run these tests, uncomment this file along with the +// example resource in lib/cdk-lambda-scheduler-ses-stack.ts +test('SQS Queue Created', () => { +// const app = new cdk.App(); +// // WHEN +// const stack = new CdkLambdaSchedulerSes.CdkLambdaSchedulerSesStack(app, 'MyTestStack'); +// // THEN +// const template = Template.fromStack(stack); + +// template.hasResourceProperties('AWS::SQS::Queue', { +// VisibilityTimeout: 300 +// }); +}); diff --git a/cdk-lambda-scheduler-ses/tsconfig.json b/cdk-lambda-scheduler-ses/tsconfig.json new file mode 100644 index 000000000..aaa7dc510 --- /dev/null +++ b/cdk-lambda-scheduler-ses/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +}