Using a Distributed Memory Cache with .NET Lambda Functions

Want to learn more about AWS Lambda and .NET? Check out my A Cloud Guru course on ASP.NET Web API and Lambda.

Download full source code.

This post is a bit heretical. You need to assess for yourself if it is of benefit to you, or if it offends your serverless sensibilities too much. This is very much a case of “your mileage may vary”. You’ve been warned!

A few months ago I wrote about using an in-memory cache for Lambda functions. I had planned to follow it up with an example of a distributed memory cache but got distracted by other things. Thanks to Kyle for reminding me about it.

Introduction

For a few details on execution environments and cold starts, see the previous post.

In the previous post, I showed how to use a memory cache in a Lambda function. The in-memory cache was local to the execution environment and was not available to execution environments. In this post, I will show how to use a distributed memory cache, which is shared between execution environments.

I have chosen to use Memcached, but you could use Redis or any other distributed memory cache.

1. Create the Memcached cluster

If you are still on the AWS free tier, you should be able to use a few of the smallest Memcached nodes for free.

Check the documentation for your region to see what is available.

From the command line, run -

aws elasticache create-cache-cluster --cache-cluster-id my-memcached-cluster --engine memcached --cache-node-type cache.t4g.micro --num-cache-nodes 1

This will take a few minutes to create the cluster. You can check the status with -

aws elasticache describe-cache-clusters --cache-cluster-id my-memcached-cluster

When the status is “available”, you can get the endpoint address with -

aws elasticache describe-cache-clusters --cache-cluster-id my-memcached-cluster --query "CacheClusters[0].ConfigurationEndpoint.Address" --output text

Use that address in the next step.

2. Create the Lambda function

From the command line, run -

dotnet new lambda.EmptyFunction --name MemcachedLambdaFunctionURLs

3. Add the NuGet packages and update the function code

Add the following NuGet packages -

  • Amazon.Lambda.APIGatewayEvents
  • EnyimMemcachedCore
  • Microsoft.Extensions.Logging.Console

Update the code in the Function.cs file -

 1using System.Net;
 2using System.Text.Json;
 3using Amazon.Lambda.APIGatewayEvents;
 4using Amazon.Lambda.Core;
 5using Enyim.Caching;
 6using Enyim.Caching.Configuration;
 7using Enyim.Caching.Memcached;
 8using Microsoft.Extensions.Logging;
 9
10// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
11[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
12
13namespace MemcachedLambdaFunctionURLs;
14
15public class Function
16{
17    private MemcachedClient myCache;
18    private readonly string functionId;
19    private readonly string functionCreationTime; 
20    private readonly Random random;
21    
22    public Function()
23    {
24        using var loggerFactory = LoggerFactory.Create(builder =>
25        {
26            builder.AddConsole();
27        });
28        ILogger logger = loggerFactory.CreateLogger<Function>();
29
30        var config = new MemcachedClientConfiguration(loggerFactory, new MemcachedClientOptions());
31
32        config.AddServer("XXXXXXXXXXXXXXXXX.XXXXXXX.cfg.use1.cache.amazonaws.com", 11211); // change this address to your Memcached cluster endpoint
33        config.Protocol = MemcachedProtocol.Binary;
34        myCache = new MemcachedClient(loggerFactory, config);
35        
36        functionId = Guid.NewGuid().ToString().Substring(0,8); 
37        functionCreationTime = DateTime.Now.ToString("HH:mm:ss.fff");
38        random = new Random();
39    }
40
41    public APIGatewayHttpApiV2ProxyResponse FunctionHandler(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
42    {
43        string itemId = request.QueryStringParameters["itemId"];
44        Console.WriteLine($"Got itemId: {itemId}");
45
46        ResponseBody responseBody = new ResponseBody() {
47            FunctionId = functionId,
48            FunctionCreationTime = functionCreationTime,
49            ItemId = itemId
50        };  
51        
52        if (myCache.TryGet(itemId, out string quantity))
53        {
54            responseBody.Quantity = quantity;
55            responseBody.FromCache = true;
56        }
57        else
58        {   
59            quantity = random.Next(1000).ToString();
60            responseBody.Quantity = quantity;
61            responseBody.FromCache = false;
62            myCache.SetAsync(itemId, quantity, 10);
63        }
64        
65        var response = new APIGatewayHttpApiV2ProxyResponse
66        {
67            StatusCode = (int)HttpStatusCode.OK,
68            Body = JsonSerializer.Serialize(responseBody),
69            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
70        };
71
72        return response;
73    }
74}
75
76public class ResponseBody
77{
78    public string FunctionId { get; set; }
79    public string FunctionCreationTime { get; set; } 
80    public string ItemId { get; set; }  
81    public string Quantity { get; set; }
82    public bool FromCache { get; set; }   
83}

4. Deploy the function

Use the following to build the code and deploy the Lambda function -

dotnet lambda deploy-function MemcachedLambdaFunctionURLs 

You will be asked - “Select IAM Role that to provide AWS credentials to your code:”, select “*** Create new IAM Role ***”

You will then be asked - “Enter name of the new IAM Role:”, put in “MemcachedLambdaFunctionURLsRole”.

Then you will be asked to - “Select IAM Policy to attach to the new role and grant permissions”, select “AWSLambdaVPCAccessExecutionRole”, for me it is number 8 on the list.

Wait as the function and permissions are created.

Configuring the function for HTTP requests

From the command line, run -

aws lambda create-function-url-config --function-name MemcachedLambdaFunctionURLs --auth-type NONE

You will see -

{
    "FunctionUrl": "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/",
    "FunctionArn": "arn:aws:lambda:us-east-1:xxxxxxxxxxx:function:MemcachedLambdaFunctionURLs",
    "AuthType": "NONE",
    "CreationTime": "2022-07-13T00:43:45.699805Z"
}

At this point, a URL is attached to the Lambda function, requiring no authentication, but you’re not quite finished yet.

Note the FunctionUrl, you will use this to invoke the function.

Add a Resource-based policy to the Lambda function

One more thing to do - add a resource-based policy to allow the Lambda function to be called via the URL. See here for more.

aws lambda add-permission --function-name MemcachedLambdaFunctionURLs --statement-id AuthNone --action lambda:InvokeFunctionUrl --principal * --function-url-auth-type NONE

You will get a response like this -

{
    "Statement": "{"Sid":"AuthNone","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunctionUrl","Resource":"arn:aws:lambda:us-east-1:xxxxxxxxx:function:MemcachedLambdaFunctionURLs","Condition":{"StringEquals":{"lambda:FunctionUrlAuthType":"NONE"}}}"
}

Now the Lambda function can be accessed from the URL with no authentication needed.

5. Connect the Lambda function to the VPC

To access the Memcached cluster, the Lambda function needs to be connected to the same VPC as the Memcached cluster.

To connect the Lambda function to the VPC you need to decide what security group(s), and subnet(s) to use.

For simplicity, you should start with the default security group and a single subnet. In a realistic scenario, more subnets will give more resilience.

To get your default security group, and the VPC that is using it, run the following command -

aws ec2 describe-security-groups --query 'SecurityGroups[?GroupName==`default`].[GroupId,VpcId]' --output text

You will see output that looks like this -

sg-1111111     vpc-2222222

This will give you two ids, the first is the id of the default security group, and the second is the id of the VPC that is using it.

To find a subnet on that VPC run -

aws ec2 describe-subnets --query 'Subnets[?VpcId==`vpc-2222222`].SubnetId | [0]' --output text
subnet-3333333

Now you have all the info, you need to connect the Lambda function to the VPC.

aws lambda update-function-configuration --function-name MemcachedLambdaFunctionURLs --vpc-config SubnetIds=subnet-3333333,SecurityGroupIds=sg-1111111

A note on security groups

If your Lambda function and VPC endpoint are in different security groups, make sure that the security group the VPC endpoint is in gives inbound access to the security group the Lambda function is in.

6. Invoke the function

Open your browser and go to the URL you just created, adding ?itemId=1 to the end. N.B. itemId is case sensitive.

Say you open - https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/?itemId=1

The first response will be -

{
    "FunctionId": "78756f60",
    "FunctionCreationTime": "18:42:56.823",
    "ItemId": "1",
    "Quantity": "416",
    "FromCache": false
}

Now reload the page a few seconds later, you will get -

{
    "FunctionId": "78756f60",
    "FunctionCreationTime": "18:42:56.823",
    "ItemId": "1",
    "Quantity": "416",
    "FromCache": true
}

See how the FromCache went from false to true.

Also, keep an eye on the FunctionId and the FunctionCreationTime - they won’t change if you keep the function “warm” by invoking it regularly.

7. Deploying a second function

Follow the deployment step again, using the same code, but with a different function name.

dotnet lambda deploy-function MemcachedLambdaFunctionURLs2 

Do all the rest to connect give it a Function URL and connect it to the VPC.

Now you should have two functions, each with its own URL, accessing the same Memcached cluster.

8. Clean up

Delete the two Lambda functions -

dotnet lambda delete-function MemcachedLambdaFunctionURLs
dotnet lambda delete-function MemcachedLambdaFunctionURLs2
Note, deleting the functions does not the roles.

Delete the Memcached cluster -

aws elasticache delete-cache-cluster --cache-cluster-id my-memcached-cluster

Download full source code.

comments powered by Disqus

Related