From 7a140463a5ca54e23f1df20b9938e8029e52734e Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Fri, 8 May 2026 04:40:44 +0000 Subject: [PATCH] feat(lambda-elasticache-valkey): Add ElastiCache Serverless Valkey pattern Deploy Lambda + ElastiCache Serverless with Valkey 8 engine for sub-millisecond key-value caching. First Valkey pattern in the repo. --- lambda-elasticache-valkey-cdk/README.md | 59 +++++++++++++++++++ lambda-elasticache-valkey-cdk/bin/app.ts | 6 ++ .../cdk.context.json | 52 ++++++++++++++++ lambda-elasticache-valkey-cdk/cdk.json | 1 + .../example-pattern.json | 1 + .../lib/lambda-elasticache-valkey-stack.ts | 42 +++++++++++++ lambda-elasticache-valkey-cdk/package.json | 14 +++++ lambda-elasticache-valkey-cdk/src/index.js | 41 +++++++++++++ lambda-elasticache-valkey-cdk/tsconfig.json | 1 + 9 files changed, 217 insertions(+) create mode 100644 lambda-elasticache-valkey-cdk/README.md create mode 100644 lambda-elasticache-valkey-cdk/bin/app.ts create mode 100644 lambda-elasticache-valkey-cdk/cdk.context.json create mode 100644 lambda-elasticache-valkey-cdk/cdk.json create mode 100644 lambda-elasticache-valkey-cdk/example-pattern.json create mode 100644 lambda-elasticache-valkey-cdk/lib/lambda-elasticache-valkey-stack.ts create mode 100644 lambda-elasticache-valkey-cdk/package.json create mode 100644 lambda-elasticache-valkey-cdk/src/index.js create mode 100644 lambda-elasticache-valkey-cdk/tsconfig.json diff --git a/lambda-elasticache-valkey-cdk/README.md b/lambda-elasticache-valkey-cdk/README.md new file mode 100644 index 000000000..72a454ccf --- /dev/null +++ b/lambda-elasticache-valkey-cdk/README.md @@ -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) │ +└──────────┘ └──────────────────┘ └─────────────────────────┘ +``` + +## 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 \ + --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 \ + --payload '{"body":"{\"action\":\"get\",\"key\":\"session:user1\"}"}' \ + --cli-binary-format raw-in-base64-out output.json +``` + +## Cleanup + +```bash +cdk destroy +``` diff --git a/lambda-elasticache-valkey-cdk/bin/app.ts b/lambda-elasticache-valkey-cdk/bin/app.ts new file mode 100644 index 000000000..f3f395070 --- /dev/null +++ b/lambda-elasticache-valkey-cdk/bin/app.ts @@ -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 } }); diff --git a/lambda-elasticache-valkey-cdk/cdk.context.json b/lambda-elasticache-valkey-cdk/cdk.context.json new file mode 100644 index 000000000..8c08843a3 --- /dev/null +++ b/lambda-elasticache-valkey-cdk/cdk.context.json @@ -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" + } + ] + } + ] + } +} diff --git a/lambda-elasticache-valkey-cdk/cdk.json b/lambda-elasticache-valkey-cdk/cdk.json new file mode 100644 index 000000000..bebf36838 --- /dev/null +++ b/lambda-elasticache-valkey-cdk/cdk.json @@ -0,0 +1 @@ +{"app":"npx ts-node --prefer-ts-exts bin/app.ts"} diff --git a/lambda-elasticache-valkey-cdk/example-pattern.json b/lambda-elasticache-valkey-cdk/example-pattern.json new file mode 100644 index 000000000..0ba3d7861 --- /dev/null +++ b/lambda-elasticache-valkey-cdk/example-pattern.json @@ -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"}]}} diff --git a/lambda-elasticache-valkey-cdk/lib/lambda-elasticache-valkey-stack.ts b/lambda-elasticache-valkey-cdk/lib/lambda-elasticache-valkey-stack.ts new file mode 100644 index 000000000..8d068af05 --- /dev/null +++ b/lambda-elasticache-valkey-cdk/lib/lambda-elasticache-valkey-stack.ts @@ -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 }); + + 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, + 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 }); + } +} diff --git a/lambda-elasticache-valkey-cdk/package.json b/lambda-elasticache-valkey-cdk/package.json new file mode 100644 index 000000000..3f473dce3 --- /dev/null +++ b/lambda-elasticache-valkey-cdk/package.json @@ -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" + } +} diff --git a/lambda-elasticache-valkey-cdk/src/index.js b/lambda-elasticache-valkey-cdk/src/index.js new file mode 100644 index 000000000..5c33216eb --- /dev/null +++ b/lambda-elasticache-valkey-cdk/src/index.js @@ -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 }) }; +}; diff --git a/lambda-elasticache-valkey-cdk/tsconfig.json b/lambda-elasticache-valkey-cdk/tsconfig.json new file mode 100644 index 000000000..5a686fa68 --- /dev/null +++ b/lambda-elasticache-valkey-cdk/tsconfig.json @@ -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":"."}}