diff --git a/lambda-elasticache-integrationpattern-cdk/.gitignore b/lambda-elasticache-integrationpattern-cdk/.gitignore new file mode 100644 index 000000000..ecbccaa05 --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/.gitignore @@ -0,0 +1,9 @@ +*.js +!jest.config.js +*.d.ts +node_modules +!lambda/*.js + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/lambda-elasticache-integrationpattern-cdk/.npmignore b/lambda-elasticache-integrationpattern-cdk/.npmignore new file mode 100644 index 000000000..d4656214c --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/.npmignore @@ -0,0 +1,5 @@ +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/lambda-elasticache-integrationpattern-cdk/README.md b/lambda-elasticache-integrationpattern-cdk/README.md new file mode 100644 index 000000000..ee7dca479 --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/README.md @@ -0,0 +1,50 @@ +# AWS Lamda to Amazon ElastiCache integration pattern with CDK + +Customers may want to connect to ElastiCache from their core lambda function(s) for use cases that involve fast read-write operations and improve latency of applications.In application like Leader board, Queue/wait-list , API rate limiting & Quota management, etc. that are built on Serverless platform there would a lambda that is computing would integrate with a either Redis or Memcache on Amazon ElastiCache. This documentation provides a quick start guide to launch Redis cluster in Amazon ElastiCache in a defined VPC and creates a lambda function that read writes from the Cache. Customer can further modify the code in lambda function as per their requirements. Customers can also configure Memcached by modifying the config and change their lambda appropriately. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/ + +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/?aws-products-pricing.sort-by=item.additionalFields.productNameLowercase&aws-products-pricing.sort-order=asc&awsf.Free%20Tier%20Type=*all&awsf.tech-category=*all) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +1. Create an [AWS account](https://portal.aws.amazon.com/billing/signup?redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start/email) 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. +2. [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed and configured +3. Git Installed +4. [Node and NPM](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed +5. [AWS Cloud Development Kit (AWS CDK)](https://docs.aws.amazon.com/cdk/v2/guide/cli.html) installed + +## Deployment Instructions +1. Open choice of terminal/command line. 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 lambda-elasticache-integrationpattern-cdk` +3. Run below command to install required dependancies: +`npm install` +4. Change the VPC details to desired VPC, Redis cluster would be created in your defined VPC. Line 22-25 on the file lib/lambda-elasticache-integrationpattern-cdk-stack.ts. If you are planing to deploy cache in private subnet or a particular list of subnets then modify the line 48 appropriately. +5. From the command line, run: `cdk deploy`. +6. CDK would display all the changes it will make to your AWS environment, accept the changes to deploy. +7. It will take 15 mins to deploy rsources and once it is complete the confirmation would be displayed on command line/terminal. +8. You can go to AWS Console/Search for Cloud Formation and look for stack name LambdaElasticacheIntegrationpatternCdkStack and monitor the events during deployment. + + +## Testing +1. Navigate to AWS Console and search for Cloud Formation +2. Click on Stacks on left and find the recently deployed pattern stack , name LambdaElasticacheIntegrationpatternCdkStack. +3. Click on the stack name and click on the resources tab +4. Find the lambda function , look for Type = AWS::lambda::Function and open the lambda function by clicking on the URL. The function name would be something like LambdaElasticacheIntegrat-ElasticacheRedisAccessXXXX +5. Follow the steps on lamdba console testing guide [here](https://docs.aws.amazon.com/lambda/latest/dg/testing-functions.html). For this lambda the test event cam be empty or you can leave to default hello-world. +`{"key1": "value1", "key2": "value2", "key3": "value3"}` + +## Cleanup +To delete the stack, run: +`cdk destroy --all` + +## Useful commands + +* `npm run build` compile typescript to js +* `npm run watch` watch for changes and compile +* `npm run test` perform the jest unit tests +* `cdk deploy` deploy this stack to your default AWS account/region +* `cdk diff` compare deployed stack with current state +* `cdk synth` emits the synthesized CloudFormation template diff --git a/lambda-elasticache-integrationpattern-cdk/bin/lambda-elasticache-integrationpattern-cdk.ts b/lambda-elasticache-integrationpattern-cdk/bin/lambda-elasticache-integrationpattern-cdk.ts new file mode 100644 index 000000000..9cb3d7ae7 --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/bin/lambda-elasticache-integrationpattern-cdk.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { LambdaElasticacheIntegrationpatternCdkStack } from '../lib/lambda-elasticache-integrationpattern-cdk-stack'; + +const app = new cdk.App(); +new LambdaElasticacheIntegrationpatternCdkStack(app, 'LambdaElasticacheIntegrationpatternCdkStack', { + /* 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. */ + + /* Currently we are using account and region from CLI configuration. Comment if + you are using other options*/ + 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 + +}); diff --git a/lambda-elasticache-integrationpattern-cdk/cdk.json b/lambda-elasticache-integrationpattern-cdk/cdk.json new file mode 100644 index 000000000..1b852a16f --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/cdk.json @@ -0,0 +1,50 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/lambda-elasticache-integrationpattern-cdk.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 + } +} diff --git a/lambda-elasticache-integrationpattern-cdk/example-pattern.json b/lambda-elasticache-integrationpattern-cdk/example-pattern.json new file mode 100644 index 000000000..cb3bac30b --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/example-pattern.json @@ -0,0 +1,67 @@ +{ + "title": "AWS Lamda to Amazon ElastiCache integration pattern", + "description": "Create a Redis Cluster in Amazon ElastiCache and read write with a Lambda function.", + "language": "TypeScript", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "Users may want to connect to ElastiCache from their core Lambda function for use cases that involve fast read-write operations and improve latency of applications.", + "In an application like a leader board, queue/wait-list , API rate limiting & quota management, etc. that are built on Serverless platform , the core computing Lambda function would integrate with a either Redis or Memcache on Amazon ElastiCache.", + "This pattern provides a quick start guide to launch a Redis cluster in Amazon ElastiCache in a defined VPC and creates a Lambda function that read writes from that cache.", + "Users can further modify the code in the Lambda function as per their requirements. You can configure Memcached by modifying the cdk config and change Lambda code appropriately." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-elasticache-integrationpattern-cdk", + "templateURL": "serverless-patterns/lambda-elasticache-integrationpattern-cdk", + "projectFolder": "lambda-elasticache-integrationpattern-cdk", + "templateFile": "lambda-elasticache-integrationpattern-cdk/lib/lambda-elasticache-integrationpattern-cdk-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "What is AWS Lambda", + "link": "https://aws.amazon.com/lambda/" + }, + { + "text": "What is Amazon ElastiCache", + "link": "https://aws.amazon.com/elasticache/" + }, + { + "text": "How to test Lambda with AWS Console", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/testing-functions.html" + }, + { + "text": "Further handson with ElastiCache", + "link": "https://aws.amazon.com/getting-started/hands-on/boosting-mysql-database-performance-with-amazon-elasticache-for-redis/" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the README.md file of GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk delete." + ] + }, + "authors": [ + { + "name": "Sushanth Kothapally", + "image": "https://avatars.githubusercontent.com/u/10820054?v=4", + "bio": "Sushanth is a Solutions Architect at AWS. He is passionate about solving technical problems to meet business objectives.", + "linkedin": "sushanthkothapally" + } + ] +} diff --git a/lambda-elasticache-integrationpattern-cdk/jest.config.js b/lambda-elasticache-integrationpattern-cdk/jest.config.js new file mode 100644 index 000000000..08263b895 --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/lambda-elasticache-integrationpattern-cdk/lambda/index.js b/lambda-elasticache-integrationpattern-cdk/lambda/index.js new file mode 100644 index 000000000..116800a65 --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/lambda/index.js @@ -0,0 +1,21 @@ +const redis = require('ioredis'); + +const client = new redis({ + port: process.env.REDIS_PORT, + host: process.env.REDIS_HOST, + maxRetriesPerRequest: 20, +}); + +client.on('connect', function () { + console.log('connected'); +}); + +exports.handler = async function (event) { + await client.set('foo', 'bar'); + let result = await client.get('foo'); + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: result, + }; +}; diff --git a/lambda-elasticache-integrationpattern-cdk/lambda/package.json b/lambda-elasticache-integrationpattern-cdk/lambda/package.json new file mode 100644 index 000000000..6fe568ec0 --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/lambda/package.json @@ -0,0 +1,14 @@ +{ + "name": "elasticache-redisaccess", + "version": "1.0.0", + "description": "pattern to integrate to elasticache-redis cluster", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "ioredis": "^5.3.1" + } +} diff --git a/lambda-elasticache-integrationpattern-cdk/lib/lambda-elasticache-integrationpattern-cdk-stack.ts b/lambda-elasticache-integrationpattern-cdk/lib/lambda-elasticache-integrationpattern-cdk-stack.ts new file mode 100644 index 000000000..6f1de95c6 --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/lib/lambda-elasticache-integrationpattern-cdk-stack.ts @@ -0,0 +1,88 @@ +import { Duration, Stack, StackProps } from 'aws-cdk-lib'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as redis from 'aws-cdk-lib/aws-elasticache'; +import { Construct } from 'constructs'; + +export class LambdaElasticacheIntegrationpatternCdkStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + // defines an AWS Lambda resource roles + const lambdarole = new iam.Role(this,'lambda-vpc-execution-role',{ + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + description: 'Lambda execution role for accessing VPC', + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AWSLambdaVPCAccessExecutionRole', + ), + ], + }); + + //get default or any private vpc + const defaultvpc = ec2.Vpc.fromLookup(this, 'ElastiCacheVPC', { + vpcName: "Default", // can be configured where ElastiCache is deployed + isDefault: true + }); + + //security group for lambda vpc access + const lambdasecuritygroup = new ec2.SecurityGroup(this, 'LambdaVPC-SG',{ + vpc:defaultvpc, + allowAllOutbound: true, + description: 'Security group for lambda to access Redis' + }); + + //get predefined securitygroup + const redissecuritygroup = new ec2.SecurityGroup(this, 'Redis-SG',{ + vpc:defaultvpc, + allowAllOutbound: true, + description: 'Security group for Redis' + }); + redissecuritygroup.addIngressRule( + ec2.Peer.securityGroupId(lambdasecuritygroup.securityGroupId), + ec2.Port.tcp(6379), + ); + + + // Get all public subnet ids, you can deploy it to privatesubnets as well + const Subnets = defaultvpc.publicSubnets.map((subnet) => { + return subnet.subnetId + }); + + // Create redis subnet group from subnet ids + const redisSubnetGroup = new redis.CfnSubnetGroup(this, 'RedisSubnetGroup', { + subnetIds: Subnets, + description: "Subnet group for redis" + }); + + // Create Redis Cluster + const redisCluster = new redis.CfnCacheCluster(this, 'RedisCluster', { + autoMinorVersionUpgrade: true, + cacheNodeType: 'cache.t2.small', + engine: 'redis', + numCacheNodes: 1, + cacheSubnetGroupName: redisSubnetGroup.ref, + clusterName: 'sample-redis' , + vpcSecurityGroupIds: [redissecuritygroup.securityGroupId] + }); + + // Define this redis cluster is depends on redis subnet group created first + redisCluster.node.addDependency(redisSubnetGroup); + + // Lambda creation + const redisaccess = new lambda.Function(this, 'Elasticache-RedisAccess', { + runtime: lambda.Runtime.NODEJS_18_X, // execution environment + code: lambda.Code.fromAsset('lambda'), // code loaded from "lambda" directory + handler: 'index.handler', // file is "index", function is "handler" + role: lambdarole, + vpc:defaultvpc, + allowPublicSubnet: true, + securityGroups: [lambdasecuritygroup], + timeout: Duration.minutes(5), + environment: { + REDIS_PORT: redisCluster.attrRedisEndpointPort, + REDIS_HOST: redisCluster.attrRedisEndpointAddress, + } + }); + } +} diff --git a/lambda-elasticache-integrationpattern-cdk/package.json b/lambda-elasticache-integrationpattern-cdk/package.json new file mode 100644 index 000000000..509c91f3d --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/package.json @@ -0,0 +1,27 @@ +{ + "name": "lambda-elasticache-integrationpattern-cdk", + "version": "0.1.0", + "bin": { + "lambda-elasticache-integrationpattern-cdk": "bin/lambda-elasticache-integrationpattern-cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk", + "postinstall": "cd lambda && npm install" + }, + "devDependencies": { + "@types/jest": "^29.4.0", + "@types/node": "18.14.6", + "jest": "^29.5.0", + "ts-jest": "^29.0.5", + "aws-cdk": "2.69.0", + "ts-node": "^10.9.1", + "typescript": "~4.9.5" + }, + "dependencies": { + "aws-cdk-lib": "2.69.0", + "constructs": "^10.0.0" + } +} diff --git a/lambda-elasticache-integrationpattern-cdk/test/lambda-elasticache-integrationpattern-cdk.test.ts b/lambda-elasticache-integrationpattern-cdk/test/lambda-elasticache-integrationpattern-cdk.test.ts new file mode 100644 index 000000000..cd9cdf24d --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/test/lambda-elasticache-integrationpattern-cdk.test.ts @@ -0,0 +1,16 @@ +import * as cdk from 'aws-cdk-lib'; +import { Template, Match } from 'aws-cdk-lib/assertions'; +import * as LambdaElasticacheIntegrationpatternCdk from '../lib/lambda-elasticache-integrationpattern-cdk-stack'; +import { Lambda } from 'aws-cdk-lib/aws-ses-actions'; + +test('Validate stack resources', () => { + const app = new cdk.App(); + const stack = new LambdaElasticacheIntegrationpatternCdk.LambdaElasticacheIntegrationpatternCdkStack(app, 'MyTestStack'); + const template = Template.fromStack(stack); + + // Assert it creates the function with the correct properties + template.hasResourceProperties("AWS::Lambda::Function", { + Handler: "handler", + Runtime: "nodejs18.x", + }); +}); diff --git a/lambda-elasticache-integrationpattern-cdk/tsconfig.json b/lambda-elasticache-integrationpattern-cdk/tsconfig.json new file mode 100644 index 000000000..fc44377a1 --- /dev/null +++ b/lambda-elasticache-integrationpattern-cdk/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020" + ], + "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" + ] +}