Skip to content
Open
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.

## 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 <FunctionName> \
--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 });

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 });
}
}
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":"."}}