Amazon ElastiCache for Redis in Lambda

Configure VPC

As there is no auth to connect to Redis server, all security policy are based on VPC‘s Security Group, so before setup Redis server, we have to configure the VPC.

  1. Sign in to VPC Console, make sure there is at least one VPC
  2. Open Security Group, create a new Security Group
    sec-group-define.png
    Note: The VPC should be used same one in the following configuration

Launch a Redis Cluster

Amazon ElastiCache for Redis clusters provide a variety of node configurations:

  1. Single Node
  2. Single Shard Multiple Nodes
    A single shard configuration groups up to 6 nodes in a single cluster, a read/write Primary node and up to 5 read-only Replica nodes.
  3. Multiple Shards Multiple Nodes
    A multiple shard configuration partitions your data across up to 20 shards in the cluster. Each shard has a read/write Primary node and up to 5 read-only replica nodes.

Now we setup a ‘Single Node’ Redis Cluster:

  1. Sign in to ElastiCache Console
  2. Choose Get Started Now.
    elasticache-start.png
  3. Choose the region
    redis-region.png
  4. Choose Redis as cluster engine
    redis-engine.png
  5. Complete ‘Redis settings’
    redis-setting.png
    Note: As we are configuring a Single Node Redis, so we set Number of Replicas as None.
  6. Complete ‘Advanced Redis settings’
    redis-setting-adv.png
    Note: Keep VPC ID same as the VPC configuration, and remember the Subnets
  7. The other items are kept as default
  8. Choose Create cluster to launch your cluster
    redis-launching.png
    The Create operation will take a while.
  9. View the details of cluster cluster we created
    cluster-details.png
    Primary Endpoint will be configured Redis client for connecting Redis server.
  10. Click on the cluster’s name, the node(s) in the cluster will be listed, and you can see the metrics of each node
    cluster-metrics.png
  11. The main step is checking the Security Group, make sure the its same as we created:
    redis-sec-group.png

Now the Redis Server on AWS is up.

Prepare Role to Run Lambda

  1. Sign in to IAM Console
  2. To create a new Role:
  3. Choose AWS Service as role type, and Lambda as service
    lmbda-role-1.png
  4. Selected AWSLambdaVPCAccessExecutionRole at step of Attach permissions policies
    This permission is enough for us to connect to Redis server in Lambda function.
    lmbda-role-2.png
  5. Named the role
    lmbda-role-3.png
  6. Role is created!
    lmbda-role-4.png

Lambda Function

Create Lambda Function

  1. Create a Lambda function
    User Node.js as dev env, and select the role we just created
    lmbda-1.png
  2. Created Lambda function
    lmbda-2.png
  3. Configure Lambda Role, min required memory or timeout configuration
    lmbda-3.png
  4. Configure Network
    lmbda-4.png
    lmbda-5.png
    Note:
    1. VPC must be same as above configuration
    2. Subnets must contain the same Subnets when we setup Redis
    3. Security Groups must be save as above configuration

Write Lambda Function to connect to Redis

We use node_redis as client to connect Redis server in our Lambda function.

  1. The project structure is as below:

    1
    2
    3
    4
    lambda_function
    |-- node_modules
    |-- package.json
    |-- index.js
  2. The dependencies in package.json is:

    1
    2
    3
    4
    5
    "main": "index.js",
    "dependencies": {
    "bluebird": "^3.5.1",
    "redis": "^2.8.0"
    }

    bluebird is for Promise implementation.

  3. The code implementation of Lambda function is in index.js:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    'use strict';

    var redis = require('redis');
    var bluebird = require('bluebird');
    bluebird.promisifyAll(redis.RedisClient.prototype);
    bluebird.promisifyAll(redis.Multi.prototype);

    const GLOBAL_KEY = 'lambda-test';
    const redisOptions = {
    host: "redistest.cbmmpl.0001.use1.cache.amazonaws.com",
    port: 6379
    }

    redis.debug_mode = true;

    exports.handler = (event, context, callback) => {
    console.info('Start to connect to Redis Server')
    var client = redis.createClient(redisOptions);
    console.info('Connected to Redis Server')

    console.info('event.pathParameters: ', event.pathParameters);
    console.info('event.httpMethod: ', event.httpMethod);
    let id = (event.pathParameters || {}).product || false;
    let data = event.data;

    switch (event.httpMethod) {

    case "GET":
    if (id) {
    console.info('get by id')
    client.hgetAsync(GLOBAL_KEY, id).then(res => {
    console.info('Redis responses for get single: ', res);
    callback(null, {body: "This is a READ operation on product ID " + id, ret: res});
    // callback(null, {body: "This is a READ operation on product ID " + id});
    }).catch(err => {
    console.error("Failed to get single: ", err)
    callback(null, {statusCode: 500, message: "Failed to get data"});
    }).finally(() => {
    console.info('Disconnect to Redis');
    client.quit();
    });

    return;
    } else {
    console.info('get all')
    client.hgetallAsync(GLOBAL_KEY).then(res => {
    console.info('Redis responses for get all: ', res)
    callback(null, {body: "This is a LIST operation, return all products", ret: res});
    // callback(null, {body: "This is a LIST operation, return all products"});
    }).catch(err => {
    console.error("Failed to post data: ", err)
    callback(null, {statusCode: 500, message: "Failed to get data"});
    }).finally(() => {
    console.info('Disconnect to Redis');
    client.quit();
    });
    }
    break;

    case "POST":
    if (data) {
    console.info('Posting data for [', id, '] with value: ', data);
    client.hmsetAsync(GLOBAL_KEY, id, data).then(res => {
    console.info('Redis responses for post: ', res)
    callback(null, {body: "This is a CREATE operation and it's successful", ret: res});
    // callback(null, {body: "This is a CREATE operation"});
    }).catch(err => {
    console.error("Failed to post data: ", err)
    callback(null, {statusCode: 500, message: "Failed to post data"});
    }).finally(() => {
    console.info('Disconnect to Redis');
    client.quit();
    });
    }
    else {
    callback(null, {statusCode: 500, message: 'no data is posted'})
    }
    break;

    case "PUT":
    callback(null, {body: "This is an UPDATE operation on product ID " + id});
    break;

    case "DELETE":
    console.info('delete a prod');
    client.delAsync(GLOBAL_KEY).then(res => {
    console.info('Redis responses for get single: ', res);
    callback(null, {body: "This is a DELETE operation on product ID " + id, ret: res});
    // callback(null, {body: "This is a DELETE operation on product ID " + id});
    }).catch(err => {
    console.error("Failed to delete single: ", err);
    callback(null, {statusCode: 500, message: "Failed to delete data"});
    }).finally(() => {
    console.info('Disconnect to Redis');
    client.quit();
    });

    break;

    default:
    // Send HTTP 501: Not Implemented
    console.log("Error: unsupported HTTP method (" + event.httpMethod + ")");
    callback(null, {statusCode: 501})
    }

    }

    The redisOptions is from the Redis cluster details.

  4. Package the function
    First install NPM packages:

    1
    npm install

    Then zip the files.

  5. Upload the package:
    lmbda-6.png
  6. Lambda function is deployed:
    lmbda-7.png

Test Lambda Function

To test the lambda, we need to create some Test Events:

Test writing value to Redis Server

  1. Create Test Event:

    1
    2
    3
    4
    5
    6
    7
    {
    "httpMethod": "POST",
    "pathParameters": {
    "product": "key-1"
    },
    "data": "I am a test string"
    }

    lmbda-test-1.png
    lmbda-test-2.png

  2. Then clicking Test to mock api call:
    lmbda-test-3.png
    lmbda-test-4.png
    lmbda-test-5.png
  3. We also can view the logs in AWS CloudWatch
    lmbda-test-6.png
    lmbda-test-7.png
    lmbda-test-8.png

Test reading value from Redis Server

  1. Create Test Event:

    1
    2
    3
    {
    "httpMethod": "GET"
    }

    lmbda-test-9.png

  2. Then clicking Test to mock api call:
    lmbda-test-10.png
    The value we set is populated from Redis server correctly.

Access ElastiCache from EC2

Excepted AWS Lambda Function, ElastiCache can only be accessed through an Amazon EC2 instance. Below is the step to setup a EC2 instance to access the Redis server:

  1. Sign in to EC2 Console
  2. Launch a new Instance
    ec2-1.png
    ec2-2.png
    ec2-3.png
    ec2-4.png
    ec2-7.png
    ec2-5.png
    When configuring the instance details, the Network must set the VPC we created, and Subnet must be same as the above Redis.
  3. If the Security Group doesn’t contain the same one as Redis server, you have to change the configuration:
    ec2-sec-group-1.png
    ec2-sec-group-2.png
  4. Connect the EC2 instance with ssh:
    ec2-6.png
  5. Access Redis server through EC2 instance:

    1
    2
    3
    4
    $ redis-cli -h redistest.yyyy.xxxx.use1.cache.amazonaws.com
    redistest.yyyy.xxxx.use1.cache.amazonaws.com:6379> ping
    PONG
    redistest.yyyy.xxxx.use1.cache.amazonaws.com:6379>

    When ping responses PONG, it means the EC2 instance has connected to ElastiCache service, now you can deploy/test your application which need Redis service.
    Note:For this step, the EC2 instance have to be installed redis-cli tool.

Scalability of Amazon ElastiCache for Redis

The above example of Redis cluster is a single node, we can add more readonly nodes for it.

Add Replication

  1. Goto Nodes tab, select the node, then click Add Replication
    redis-replica-1.png
  2. Complete the information, to change the current Single Node cluster to Single Shard and Single Node:
    redis-replica-2.png
  3. Now the changes is finished, you can see the Shards column is become to 1 from 0
    redis-replica-3.png
  4. Goto Nodes tab, we can add more readonly node for this shard
    redis-replica-4.png
  5. Named the new node
    redis-replica-5.png
  6. The readonly replica node has been created
    redis-replica-6.png

    Now the new node is up, we add more replica nodes if needed
    Note: As the current instance is setup without the Cluster Mode is disabled, so we can only have one shard. More info

Test Replication

Write Lambda Function

  1. Add new Lambda function to test the read write function:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    const redisReadOptions = {
    host: "firstreplicanode.cbmmpl.0001.use1.cache.amazonaws.com",
    port: 6379
    }
    const redisWriteOptions = {
    host: "redistest.cbmmpl.0001.use1.cache.amazonaws.com",
    port: 6379
    }

    ...

    function getReadClient(){
    console.info('Start to connect to Redis Server')
    var client = redis.createClient(redisReadOptions);
    console.info('Connected to Redis Server')
    return client
    }

    function getWriteClient(){
    console.info('Start to connect to Redis Server')
    var client = redis.createClient(redisWriteOptions);
    console.info('Connected to Redis Server')
    return client
    }

    ...

    case "GET":
    var client = getReadClient();
    console.info('get all')
    client.hgetallAsync(GLOBAL_KEY).then(res => {
    console.info('Redis responses for get all: ', res)
    callback(null, {body: "This is a LIST operation, return all products", ret: res});
    // callback(null, {body: "This is a LIST operation, return all products"});
    }).catch(err => {
    console.error("Failed to post data: ", err)
    callback(null, {statusCode: 500, message: "Failed to get data"});
    }).finally(() => {
    console.info('Disconnect to Redis');
    client.quit();
    });
    break;

    case "POST":
    var client = getWriteClient();
    if (data) {
    console.info('Posting data for [', id, '] with value: ', data);
    client.hmsetAsync(GLOBAL_KEY, id, data).then(res => {
    console.info('Redis responses for post: ', res)
    callback(null, {body: "This is a CREATE operation and it's successful", ret: res});
    // callback(null, {body: "This is a CREATE operation"});
    }).catch(err => {
    console.error("Failed to post data: ", err)
    callback(null, {statusCode: 500, message: "Failed to post data"});
    }).finally(() => {
    console.info('Disconnect to Redis');
    client.quit();
    });
    }
    else {
    callback(null, {statusCode: 500, message: 'no data is posted'})
    }
    break;

    ...
  2. Then deploy the code

Test Lambda Function

  1. Check the data in readonly node
    redis-replica-test-1.png
  2. Write data to the read/write Primary node
    redis-replica-test-2.png
  3. The re-check the data in readonly node
    redis-replica-test-3.png
    WORKING!!!

Summary

No mater EC2 instance or Lambda function to access ElastiCache service, they must:

  1. Be in one VPC
  2. Contain same Subnet
  3. Contain same Security Group