-
Notifications
You must be signed in to change notification settings - Fork 1k
New pattern - lambda-elasticache-valkey-cdk #3105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # AWS Lambda with Amazon ElastiCache Serverless (Valkey) | ||
|
|
||
| This pattern deploys a Lambda function connected to Amazon ElastiCache Serverless running the Valkey engine for sub-millisecond key-value caching. | ||
|
|
||
| Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-elasticache-valkey-cdk | ||
|
|
||
| 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. | ||
|
|
||
| ## Requirements | ||
|
|
||
| * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured | ||
| * [Node.js 22+](https://nodejs.org/en/download/) installed | ||
| * [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed | ||
| * A VPC with private subnets (or NAT gateway for public subnets) | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| ┌──────────┐ ┌──────────────────┐ ┌─────────────────────────┐ | ||
| │ Client │────▶│ AWS Lambda │────▶│ ElastiCache Serverless │ | ||
| │ │ │ (VPC) │ │ (Valkey 8) │ | ||
| └──────────┘ └──────────────────┘ └─────────────────────────┘ | ||
| ``` | ||
|
Comment on lines
+18
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Architecture diagram herre understates the design. The ASCII diagram should show (or be replaced with a PNG that shows) the VPC, the security group, the private subnets, and that the connection is TLS-encrypted |
||
|
|
||
| ## How it works | ||
|
|
||
| 1. Lambda connects to ElastiCache Serverless (Valkey engine) via VPC networking. | ||
| 2. The function performs SET/GET/DEL operations using the RESP protocol. | ||
| 3. ElastiCache Serverless auto-scales based on demand — no capacity planning needed. | ||
| 4. Valkey 8 provides Redis-compatible commands with open-source licensing. | ||
|
|
||
| ## Deployment | ||
|
|
||
| ```bash | ||
| npm install | ||
| cdk deploy | ||
| ``` | ||
|
|
||
| Note: Lambda must be in subnets with connectivity to the ElastiCache endpoint (private subnets recommended). | ||
|
|
||
| ## Testing | ||
|
|
||
| ```bash | ||
| # Set a session key with 5-minute TTL | ||
| aws lambda invoke --function-name <FunctionName> \ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing snippet inconsistent with the stack. The CDK stack outputs FunctionUrl (Lambda Function URL with AWS_IAM auth), not a function name. Either CfnOutput the function name as well, or change the test to a SigV4-signed curl against the URL |
||
| --payload '{"body":"{\"action\":\"set\",\"key\":\"session:user1\",\"value\":\"active\",\"ttl\":300}"}' \ | ||
| --cli-binary-format raw-in-base64-out output.json | ||
|
|
||
| # Get the key | ||
| aws lambda invoke --function-name <FunctionName> \ | ||
| --payload '{"body":"{\"action\":\"get\",\"key\":\"session:user1\"}"}' \ | ||
| --cli-binary-format raw-in-base64-out output.json | ||
| ``` | ||
|
|
||
| ## Cleanup | ||
|
|
||
| ```bash | ||
| cdk destroy | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| #!/usr/bin/env node | ||
| import 'source-map-support/register'; | ||
| import * as cdk from 'aws-cdk-lib'; | ||
| import { LambdaElasticacheValkeyStack } from '../lib/lambda-elasticache-valkey-stack'; | ||
| const app = new cdk.App(); | ||
| new LambdaElasticacheValkeyStack(app, 'LambdaElasticacheValkeyStack', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION } }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| { | ||
| "vpc-provider:account=742460038667:filter.isDefault=true:region=us-east-1:returnAsymmetricSubnets=true": { | ||
| "vpcId": "vpc-0d2ccb9ba9da8174c", | ||
| "vpcCidrBlock": "172.31.0.0/16", | ||
| "ownerAccountId": "742460038667", | ||
| "availabilityZones": [], | ||
| "subnetGroups": [ | ||
| { | ||
| "name": "Public", | ||
| "type": "Public", | ||
| "subnets": [ | ||
| { | ||
| "subnetId": "subnet-09f81666a668b49d7", | ||
| "cidr": "172.31.16.0/20", | ||
| "availabilityZone": "us-east-1a", | ||
| "routeTableId": "rtb-0d6e0c8254189f150" | ||
| }, | ||
| { | ||
| "subnetId": "subnet-01e469e26a62f79cc", | ||
| "cidr": "172.31.32.0/20", | ||
| "availabilityZone": "us-east-1b", | ||
| "routeTableId": "rtb-0d6e0c8254189f150" | ||
| }, | ||
| { | ||
| "subnetId": "subnet-0edf680549fc43b35", | ||
| "cidr": "172.31.0.0/20", | ||
| "availabilityZone": "us-east-1c", | ||
| "routeTableId": "rtb-0d6e0c8254189f150" | ||
| }, | ||
| { | ||
| "subnetId": "subnet-0a4b73e1b77dfe7e3", | ||
| "cidr": "172.31.80.0/20", | ||
| "availabilityZone": "us-east-1d", | ||
| "routeTableId": "rtb-0d6e0c8254189f150" | ||
| }, | ||
| { | ||
| "subnetId": "subnet-007839dd58f1d60a0", | ||
| "cidr": "172.31.48.0/20", | ||
| "availabilityZone": "us-east-1e", | ||
| "routeTableId": "rtb-0d6e0c8254189f150" | ||
| }, | ||
| { | ||
| "subnetId": "subnet-03604e5a5796bce0a", | ||
| "cidr": "172.31.64.0/20", | ||
| "availabilityZone": "us-east-1f", | ||
| "routeTableId": "rtb-0d6e0c8254189f150" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"app":"npx ts-node --prefer-ts-exts bin/app.ts"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"title":"AWS Lambda with Amazon ElastiCache Serverless (Valkey)","description":"Deploy a Lambda function connected to ElastiCache Serverless with Valkey engine for sub-millisecond caching.","language":"TypeScript","level":"300","framework":"CDK","introBox":{"headline":"How it works","text":["Lambda connects to ElastiCache Serverless running the Valkey engine to perform key-value operations with sub-millisecond latency."]},"gitHub":{"template":{"repoURL":"https://github.com/aws-samples/serverless-patterns/tree/main/lambda-elasticache-valkey-cdk","templateURL":"serverless-patterns/lambda-elasticache-valkey-cdk","projectFolder":"lambda-elasticache-valkey-cdk"}},"resources":{"bullets":[{"text":"ElastiCache Serverless","link":"https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/serverless.html"}]},"deploy":{"text":["cdk deploy"],"commands":["npm install","cdk deploy"]},"testing":{"text":["Invoke the function URL with set/get/del operations"]},"cleanup":{"text":["cdk destroy"],"commands":["cdk destroy"]},"authors":[{"name":"Nithin Chandran R","bio":"Technical Account Manager at AWS","linkedin":"nithin-chandran-r"}],"services":{"from":[{"service":"lambda"}],"to":[{"service":"elasticache"}]}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import * as cdk from 'aws-cdk-lib'; | ||
| import * as ec2 from 'aws-cdk-lib/aws-ec2'; | ||
| import * as elasticache from 'aws-cdk-lib/aws-elasticache'; | ||
| import * as lambda from 'aws-cdk-lib/aws-lambda'; | ||
| import { Construct } from 'constructs'; | ||
|
|
||
| export class LambdaElasticacheValkeyStack extends cdk.Stack { | ||
| constructor(scope: Construct, id: string, props?: cdk.StackProps) { | ||
| super(scope, id, props); | ||
|
|
||
| const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: true }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The stack looks up the default VPC (isDefault: true), takes the first two public subnets, and uses them for both the cache and the Lambda (with allowPublicSubnet: true). Default VPCs have public-IP-by-default subnets and a route to the Internet Gateway. Even though the security group restricts ingress, putting a stateful cache in a public subnet of the default VPC is an anti-pattern that contradicts AWS guidance and the README's own "private subnets recommended" note |
||
|
|
||
| const sg = new ec2.SecurityGroup(this, 'ValkeySG', { vpc, allowAllOutbound: true }); | ||
| sg.addIngressRule(sg, ec2.Port.tcp(6379), 'Allow Valkey access from Lambda'); | ||
|
|
||
| // ElastiCache Serverless with Valkey engine (requires 2-3 subnets) | ||
| const subnets = vpc.publicSubnets.slice(0, 2); | ||
| const cache = new elasticache.CfnServerlessCache(this, 'ValkeyCache', { | ||
| serverlessCacheName: 'valkey-session-store', | ||
| engine: 'valkey', | ||
| majorEngineVersion: '8', | ||
| securityGroupIds: [sg.securityGroupId], | ||
| subnetIds: subnets.map(s => s.subnetId) | ||
| }); | ||
|
|
||
| const fn = new lambda.Function(this, 'CacheFn', { | ||
| runtime: lambda.Runtime.NODEJS_22_X, | ||
| handler: 'index.handler', | ||
| code: lambda.Code.fromAsset('src'), | ||
| vpc, | ||
| securityGroups: [sg], | ||
| allowPublicSubnet: true, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| environment: { CACHE_ENDPOINT: cache.attrEndpointAddress, CACHE_PORT: cache.attrEndpointPort }, | ||
| timeout: cdk.Duration.seconds(15) | ||
| }); | ||
|
|
||
| const fnUrl = fn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.AWS_IAM }); | ||
|
|
||
| new cdk.CfnOutput(this, 'FunctionUrl', { value: fnUrl.url }); | ||
| new cdk.CfnOutput(this, 'CacheEndpoint', { value: cache.attrEndpointAddress }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "name": "lambda-elasticache-valkey-cdk", | ||
| "version": "1.0.0", | ||
| "bin": { "app": "bin/app.js" }, | ||
| "scripts": { "build": "tsc", "cdk": "cdk" }, | ||
| "dependencies": { | ||
| "aws-cdk-lib": "^2.180.0", | ||
| "constructs": "^10.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "typescript": "~5.4.0", | ||
| "@types/node": "^20.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| const net = require('net'); | ||
|
|
||
| // Simple RESP protocol client (no external deps needed) | ||
| async function sendCommand(host, port, ...args) { | ||
| return new Promise((resolve, reject) => { | ||
| const socket = new net.Socket(); | ||
| socket.setTimeout(5000); | ||
| let data = ''; | ||
| const cmd = `*${args.length}\r\n${args.map(a => `$${Buffer.byteLength(a)}\r\n${a}`).join('\r\n')}\r\n`; | ||
| socket.connect(parseInt(port), host, () => socket.write(cmd)); | ||
| socket.on('data', chunk => { data += chunk; socket.end(); }); | ||
| socket.on('end', () => resolve(data.split('\r\n')[1] || data)); | ||
| socket.on('error', reject); | ||
| socket.on('timeout', () => { socket.destroy(); reject(new Error('timeout')); }); | ||
| }); | ||
| } | ||
|
|
||
| exports.handler = async (event) => { | ||
| const { CACHE_ENDPOINT, CACHE_PORT } = process.env; | ||
| const body = JSON.parse(event.body || '{}'); | ||
| const { action, key, value, ttl } = body; | ||
|
|
||
| if (!action || !key) { | ||
| return { statusCode: 400, body: JSON.stringify({ error: 'Missing action and key' }) }; | ||
| } | ||
|
|
||
| let result; | ||
| if (action === 'set') { | ||
| result = ttl | ||
| ? await sendCommand(CACHE_ENDPOINT, CACHE_PORT, 'SET', key, value || '', 'EX', String(ttl)) | ||
| : await sendCommand(CACHE_ENDPOINT, CACHE_PORT, 'SET', key, value || ''); | ||
| } else if (action === 'get') { | ||
| result = await sendCommand(CACHE_ENDPOINT, CACHE_PORT, 'GET', key); | ||
| } else if (action === 'del') { | ||
| result = await sendCommand(CACHE_ENDPOINT, CACHE_PORT, 'DEL', key); | ||
| } else { | ||
| return { statusCode: 400, body: JSON.stringify({ error: 'Invalid action. Use: set, get, del' }) }; | ||
| } | ||
|
|
||
| return { statusCode: 200, body: JSON.stringify({ action, key, result }) }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"compilerOptions":{"target":"ES2020","module":"commonjs","lib":["es2020"],"declaration":true,"strict":true,"noImplicitAny":true,"strictNullChecks":true,"noEmit":false,"resolveJsonModule":true,"esModuleInterop":true,"outDir":"./build","rootDir":"."}} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing required README sections. Per
_pattern-model/README.mdthe Requirements section needs an AWS account bullet and Git, Deployment needsgit clone+cdk bootstrap, and the file needs an Author section and the license footer.