Sunday, April 30, 2017

Storing User Sessions in Redis Using Amazon ElastiCache


In this series of posts, I am writing about various AWS services. In my last post, I have shown how to use S3 direct uploads for uploading the files to S3 from the browser.
The application I have developed for that post used HTTP sessions for session management. In this post, I will show how to use Redis to store user sessions when using many EC2 instances.
The Problem
By default, Spring Boot applications use HTTP sessions that are valid only in the JVM they are created. If we use only one EC2 instance for our application, the application works as expected. But if we use multiple EC2 instances behind an Elastic Load Balancer, one HTTP session created in an EC2 instance will not be valid if the subsequent request is handled by another EC2 instance.
The Solution
In a load balanced multi EC2 instance scenario, we should store the session information outside of the EC2 instances. There are different solutions like a database or an in-memory store.
Nowadays the best practice for storing session information is using an in-memory cache store like Memcached or Redis.
Amazon provides a managed in-memory data store called Amazon ElastiCache that can be used as a cache that is compatible with both Memcache and Redis.

For this post, I will use an ElastiCache Redis cluster with 1 node. The picture below shows the structure of the session management solution that I will use.




I will use the application in my previous post as a starting point. The code can be found here.

The steps for adding Redis as a session store are below.

1. Create the ElastiCache Redis cluster

2. Add dependencies

3. Configure Redis session store

4. Deploy the application

Let's start.


1. Create the ElastiCache Redis cluster

We can use the AWS ElastiCache CLI command below to create a Redis cluster with 1 node.

aws elasticache create-cache-cluster --cache-cluster-id CardStoreRedis --cache-node-type cache.t2.micro --engine redis --engine-version 3.2.4 --num-cache-nodes 1

By default, the cluster will use the default security group in the default VPC in the AWS region. To allow the EC2 instances to access Redis, we can enable the inbound traffic to default security group on 6379 port from the security group of EC2 instances by specifiying the security group of the EC2 instances as source with the command below.

aws ec2 authorize-security-group-ingress --group-name default --protocol tcp --port 6379 --source-group CardStoreSG

It will take some time to create Redis cluster. After it is created we can get the public address of the cache node with the command below.
aws elasticache describe-cache-clusters --cache-cluster-id CardStoreRedis --show-cache-node-info|grep Address

It should be like the address below.
cardstoreredis.XXXX.001.euc1.cache.amazonaws.com  


We will use this address to access Redis cache node from the application.

2. Add dependencies

Add Maven dependencies as below for Spring Session and Redis.

        <dependency>
                <groupId>org.springframework.session</groupId>
                <artifactId>spring-session</artifactId>
        </dependency>

        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

3. Configure Redis session store

To enable Redis as session store we use  @EnableRedisHttpSession annotation. By default Spring Boot Redis library will try to configure Redis Key Space Notifications for session deletion and expiration events but configuration requests from Redis clients are disabled in ElastiCache Redis clusters. We can configure the application in a way that it will not try to configure key space notifications by creating a ConfigureRedisAction.NO_OP instance as a bean. For more information, see here.

If you want to allow your Spring Boot application to use these session lifecycle events, you can set  notify-keyspace-events parameter by using custom Parameter Groups while creating your ElastiCache Redis Cluster. For more information, see here.

@EnableRedisHttpSession
public class RedisSessionConfig {
      
       @Bean
       public static ConfigureRedisAction configureRedisAction() {
           return ConfigureRedisAction.NO_OP;
       }
      
}

4. Deploy the application

After preparation, we can deploy the application to AWS.  We can create an ELB and an auto scaling group to use multiple EC2 instances.
We package the application and send the WAR to S3 with the commands below.
mvn package
aws s3 cp target/cardstore-0.0.1-SNAPSHOT.war s3://cardstoredeploy/

The Spring Boot application will read Redis cache node address from spring.redis.host variable. We can specify this value as a JVM system property when launching the JVM in EC2 init script like below.

#!/bin/bash
yum update -y
yum install java-1.8.0 -y
yum remove java-1.7.0-openjdk -y

mkdir /app

aws s3 cp --region eu-central-1 s3://cardstoredeploy/cardstore-0.0.1-SNAPSHOT.war /app/

java -Dspring.redis.host=cardstoreredis.XXX.0001.euc1.cache.amazonaws.com -Duser.activation.queue.name=XXX -Dmail.from.address=XXX -Duser.card.upload.s3.bucket.name=XXX -Duser.card.upload.s3.bucket.region=XXX -Duser.card.upload.s3.bucket.awsId=XXX -Duser.card.upload.s3.bucket.awsSecret=XXX -jar /app/cardstore-0.0.1-SNAPSHOT.war

We can create and configure ELB with the commands below.

aws elb create-load-balancer --load-balancer-name CardStoreLB --listeners "Protocol=HTTP,LoadBalancerPort=8080,InstanceProtocol=HTTP,InstancePort=8080" --security-groups sg-8567a2ee --availability-zones eu-central-1a
aws elb configure-health-check --load-balancer-name CardStoreLB --health-check Target=TCP:8080,Interval=5,UnhealthyThreshold=2,HealthyThreshold=2,Timeout=2

Then, we can create the launch configuration and the auto scaling group using the commands below.

aws autoscaling create-launch-configuration --launch-configuration-name CardStoreLC --key-name CardStoreKP --image-id ami-af0fc0c0 --instance-type t2.micro --user-data file://cardstore_ec2_init_script.txt --security-groups sg-8567a2ee  --iam-instance-profile CardStoreRole

aws autoscaling create-auto-scaling-group --auto-scaling-group-name CardStoreASG --launch-configuration-name CardStoreLC --load-balancer-names CardStoreLB --min-size 2 --max-size 2 --termination-policies "OldestInstance"  --availability-zones eu-central-1a


With this commands we have created 2 EC2 instances that are load balanced. After you logged in to the application at the load balancer address and 8080 port, you can refresh the dashboard page to make sure that the requests are distributed to the both instances. If you are not directed to the login page and still see the dashboard page,  that means both instances can access the session information from Redis cache node. You can tail the cloud-init-output.log files of the instances with the command below to be sure that both instances are receiving requests.
tail -200f /var/log/cloud-init-output.log


Summary

When using multiple EC2 instances, we should store the session information outside of the EC2 instances. In this post, I have shown how to use Amazon ElastiCache to create and use a Redis cache cluster for storing user session information. The code can be found here.


2 comments: