Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions lambda-elasticache-valkey-cdk/README.md
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.

Copy link
Copy Markdown
Contributor

@parikhudit parikhudit Jun 7, 2026

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.md the Requirements section needs an AWS account bullet and Git, Deployment needs git clone + cdk bootstrap, and the file needs an Author section and the license footer.

## 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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> \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
```
6 changes: 6 additions & 0 deletions lambda-elasticache-valkey-cdk/bin/app.ts
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 } });
52 changes: 52 additions & 0 deletions lambda-elasticache-valkey-cdk/cdk.context.json
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"
}
]
}
]
}
}
1 change: 1 addition & 0 deletions lambda-elasticache-valkey-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"app":"npx ts-node --prefer-ts-exts bin/app.ts"}
1 change: 1 addition & 0 deletions lambda-elasticache-valkey-cdk/example-pattern.json
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 });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowPublicSubnet: true should not be needed. Removing it forces the build to fail until the Lambda is moved to private subnets, which is what you would want.

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 });
}
}
14 changes: 14 additions & 0 deletions lambda-elasticache-valkey-cdk/package.json
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"
}
}
41 changes: 41 additions & 0 deletions lambda-elasticache-valkey-cdk/src/index.js
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 }) };
};
1 change: 1 addition & 0 deletions lambda-elasticache-valkey-cdk/tsconfig.json
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":"."}}