DZone
Cloud Zone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
  • Refcardz
  • Trend Reports
  • Webinars
  • Zones
  • |
    • Agile
    • AI
    • Big Data
    • Cloud
    • Database
    • DevOps
    • Integration
    • IoT
    • Java
    • Microservices
    • Open Source
    • Performance
    • Security
    • Web Dev
DZone > Cloud Zone > Serverless: Invalidating a DynamoDB Cache

Serverless: Invalidating a DynamoDB Cache

Want to know how to effectively invalidate a DynamoDB cache? Check out this article to find out.

Andreas Wittig user avatar by
Andreas Wittig
·
Jul. 11, 16 · Cloud Zone · Tutorial
Like (1)
Save
Tweet
5.60K Views

Join the DZone community and get the full member experience.

Join For Free

A cache in front of DynamoDB is boosting performance and saving costs. Especially true for read-intensive and spiky workloads. Why? Please have a look at one of my recent articles: Performance boost and cost savings for DynamoDB.

Caching itself is easy:

  1. Incoming request.
  2. Load data from cache.
  3. Query database if needed data is not available in cache.
  4. Insert data to the cache.

But as Phil Karlton said wisely:

There are only two hard things in Computer Science: cache invalidation and naming things.

Probably true. Especially the statement about naming things. But setting up cache invalidation is not that hard when using the following building blocks: ElastiCache, DynamoDB, DynamoDB Streams and Lambda as shown in the following figure. You'll learn how to implement a Lambda function doing cache invalidation in this article.

DynamoDB with ElastiCache

Combining DynamoDB Streams and Lambda

DynamoDB is publishing events for every change of an item to a DynamoDB Stream if needed. These events are perfectly suited for cache invalidation.

If you are into serverless, today is your lucky day!

  • DynamoDB Streams are integrated with AWS Lambda.
  • Lambda supports VPC which allows access to ElastiCache.

A typical event received by a Lambda function looks like this:

{
    "Records": [
        {
            "eventID": "fcd425364ee62afae661c7e69b5bbac0",
            "eventName": "INSERT",
            "eventVersion": "1.1",
            "eventSource": "aws:dynamodb",
            "awsRegion": "us-east-1",
            "dynamodb": {
                "ApproximateCreationDateTime": 1467991740,
                "Keys": {
                    "Id": {
                        "S": "3"
                    }
                },
                "NewImage": {
                    "Id": {
                        "S": "1"
                    },
                    "Name": {
                        "S": "John Doe"
                    }
                },
                "SequenceNumber": "800000000009116419690",
                "SizeBytes": 30,
                "StreamViewType": "NEW_IMAGE"
            },
            "eventSourceARN": "arn:aws:dynamodb:us-east-1:166876438428:table/serverless/stream/2016-07-08T15:00:10.731"
        }
    ]
}


The function receiving such an event needs to:

  1. Loop over all incoming events.
  2. Decide whether a cached item needs to be updated or deleted.
  3. Update or delete a cached item by sending a request to Redis (or Memcached).

Just a few lines of code are necessary to implement these steps with Node.js:

var redis = require("redis");
var async = require("async");

function handler(event, context, globalCallback) {
    var redisClient = redis.createClient({host: "<REDIS_HOST>"});
    async.each(event.Records, function(record, callback) { 
        var key = record.dynamodb.Keys.Id.S;
        if (record.eventName === "INSERT" || record.eventName === "MODIFY") {
            var value = JSON.stringify(record.dynamodb.NewImage);
            redisClient.set(key, value, function(err) {
                if(err) {
                    callback(err);
                } else {
                    redisClient.quit();
                    callback();
                }
            });
        } else if (record.eventName === "REMOVE") {
            redisClient.del(key, function(err) {
                if(err) {
                    callback(err);
                } else {
                    redisClient.quit();
                    callback();
                }
            });
        }
    }, function(err){
        if(err) {
            globalCallback(err);
        } else {
            globalCallback(null, "DONE");
        }
    });
};

exports.handler = handler;


Almost done, next step is to setup the whole infrastructure.

Wiring All the Parts Together

It's time to create all the needed parts, as of the infrastructure:

  • DynamoDB table and stream.
  • Security Group allowing access from Lambda to ElastiCache.
  • ElastiCache cluster.
  • mapping between DynamoDB stream and Lambda.
  • IAM role for Lambda.
  • Lambda function.

The following snippet contains a CloudFormation template including all the needed parts.

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "DynamoDB with ElastiCache",
    "Parameters": {
        "LambdaCodeBucket": {
            "Description": "The name of the S3 bucket containing the source code for the Lambda function.",
            "Type": "String"
        },
        "LambdaCodeKey": {
            "Description": "The key of the S3 object containing the source code for the Lambda function.",
            "Type": "String"
        },
        "VPC": {
            "Description": "The VPC for Lambda function and ElastiCache.",
            "Type": "AWS::EC2::VPC::Id"
        },
        "Subnets": {
            "Description": "The Subnets for Lambda function and ElastiCache.",
            "Type": "List<AWS::EC2::Subnet::Id>"
        }
    },
    "Resources": {
        "DynamoDB": {
            "Type": "AWS::DynamoDB::Table",
            "Properties": {
                "AttributeDefinitions": [{"AttributeName": "Id","AttributeType": "S"}],
                "KeySchema": [{"AttributeName": "Id", "KeyType": "HASH"}],
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": "1",
                    "WriteCapacityUnits": "1"
                },
                "TableName": {"Ref": "AWS::StackName"},
                "StreamSpecification": {
                    "StreamViewType": "NEW_IMAGE"
                }
            }
        },
        "ElastiCacheSecurityGroup": {
            "Type": "AWS::EC2::SecurityGroup",
            "Properties": {
                "GroupDescription": "Elasticache Security Group",
                "VpcId": {"Ref": "VPC"},
                "SecurityGroupIngress": [{ 
                    "IpProtocol": "tcp", 
                    "FromPort": "6379", 
                    "ToPort": "6379",
                    "SourceSecurityGroupId": {"Ref": "LambdaSecurityGroup"}
                }]
            }
        },
        "ElastiCacheSubnetGroup" : {
            "Type" : "AWS::ElastiCache::SubnetGroup",
            "Properties" : {
                "Description" : "ElastiCache Subnet Group",
                "SubnetIds" : {"Ref": "Subnets"}
            }
        },
        "ElasticacheCluster": {
            "Type": "AWS::ElastiCache::CacheCluster",
            "Properties": {
                "AutoMinorVersionUpgrade": "true",
                "Engine": "redis",
                "CacheNodeType": "cache.t1.micro",
                "NumCacheNodes": "1",
                "VpcSecurityGroupIds": [{"Fn::GetAtt": ["ElastiCacheSecurityGroup", "GroupId"]}],
                "CacheSubnetGroupName": {"Ref": "ElastiCacheSubnetGroup"}
            }
        },
        "LambdaSecurityGroup": {
            "Type": "AWS::EC2::SecurityGroup",
            "Properties": {
                "GroupDescription": "Lambda Security Group",
                "VpcId": {"Ref": "VPC"}
            }
        },
        "EventSourceMapping": {
            "Type": "AWS::Lambda::EventSourceMapping",
            "Properties": {
                "EventSourceArn": {"Fn::GetAtt": ["DynamoDB", "StreamArn"]},
                "FunctionName": {"Fn::GetAtt": ["Lambda", "Arn"]},
                "StartingPosition": "TRIM_HORIZON"
            }
        },
        "LambdaRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [{
                        "Effect": "Allow",
                        "Principal": {"Service": "lambda.amazonaws.com"},
                        "Action": ["sts:AssumeRole"]
                    }]
                },
                "Path": "/",
                "Policies": [{
                    "PolicyName": "logs",
                    "PolicyDocument": {
                        "Statement": [{
                            "Effect": "Allow",
                            "Action": [
                                "logs:CreateLogGroup",
                                "logs:CreateLogStream",
                                "logs:PutLogEvents"
                            ],
                            "Resource": "arn:aws:logs:*:*:*"
                        }]
                    }
                },
                {
                    "PolicyName": "ec2",
                    "PolicyDocument": {
                        "Statement": [{
                            "Effect": "Allow",
                            "Action": [
                                "ec2:CreateNetworkInterface",
                                "ec2:DescribeNetworkInterfaces",
                                "ec2:DeleteNetworkInterface"
                            ],
                            "Resource": "*"
                        }]
                    }
                },
                {
                    "PolicyName": "dynamodb",
                    "PolicyDocument": {
                        "Statement": [{
                            "Action": [
                                "dynamodb:PutItem",
                                "dynamodb:UpdateItem",
                                "dynamodb:DescribeStream",
                                "dynamodb:GetRecords",
                                "dynamodb:GetShardIterator",
                                "dynamodb:ListStreams"
                            ],
                            "Effect": "Allow",
                            "Resource": [
                                {"Fn::Join": ["", ["arn:aws:dynamodb:*:", {"Ref": "AWS::AccountId"}, ":table/", {"Ref": "DynamoDB"}]]},
                                {"Fn::Join": ["", ["arn:aws:dynamodb:*:", {"Ref": "AWS::AccountId"}, ":table/", {"Ref": "DynamoDB"}, "/stream/*"]]}
                            ]
                        }]
                    }
                }]
            }
        },
        "Lambda": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Code": {
                    "S3Bucket" : {"Ref": "LambdaCodeBucket"},
                    "S3Key" : {"Ref": "LambdaCodeKey"}
                },
                "Handler": "index.handler",
                "MemorySize": 128,
                "Role": {"Fn::GetAtt": ["LambdaRole", "Arn"]},
                "Runtime": "nodejs4.3",
                "Timeout": 60,
                "VpcConfig": {
                    "SecurityGroupIds": [{"Ref": "LambdaSecurityGroup"}],
                    "SubnetIds": {"Ref": "Subnets"} 
                }
            }
        }
    }
}


Use this template to create your own environment by following these steps:

  1. git clone https://github.com/widdix/lambda-dynamodb-elasticache.git.
  2. cd lambda-dynamodb-elasticache.
  3. Bundle files into ZIP file and upload to S3.
  4. Click Next to proceed with the next step of the wizard.
  5. Specify the parameters for the stack.
  6. Click Next to proceed with the next step of the wizard.
  7. Click Next to skip the Options step of the wizard.
  8. Click Create to start the creation of the stack.
  9. Wait until the stack reaches the state CREATE_COMPLETE.
  10. Update index.js with the host of ElastiCache cluster created by CloudFormation.
  11. Bundle files into ZIP file and upload to S3.
  12. Update stack, keep template, update LambdaCodeKey.
  13. Wait until the stack reaches the state UPDATE_COMPLETE.

Everything ready for testing! Insert, update and delete items from the DynamoDB table named cacheinvalidation. Watch CloudWatch Logs of Lambda to gain insight into cache invalidation.

Read on

  • Learn more about AWS with our book Amazon Web Services in Action.
  • The Life of a Serverless Microservice on AWS.
  • Integrate SQS and Lambda: serverless architecture for asynchronous workloads.
  • Serverless image resizing at any scale.

Feedback

Is anything missing? Looking forward to your feedback! @andreaswittig or andreas@widdix.de.

Cache (computing) Amazon Web Services

Published at DZone with permission of Andreas Wittig. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Deployment of Low-Latency Solutions in the Cloud
  • Types of UI Design Patterns Depending on Your Idea
  • Debugging Java Collections Framework Issues in Production
  • Maven Tutorial: Nice and Easy [Video]

Comments

Cloud Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • MVB Program
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends:

DZone.com is powered by 

AnswerHub logo