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.