Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Use Valkey GLIDE with Spring Boot - ElastiCache Cluster Mode Enabled and IAM Auth

[Valkey Glide](https://github.com/valkey-io/valkey-glide) is the official open-source Valkey client library, proudly part of the Valkey organization.

[Spring Boot](https://docs.spring.io/spring-boot/index.html) helps you to create stand-alone, production-grade Spring-based applications that you can run. Spring Boot takes an opinionated view of the Spring platform and third-party libraries, so that you can get started with minimum fuss. It contains a [caching](https://docs.spring.io/spring-boot/reference/io/caching.html) component, making implementation of caching in your application quick and simple.

This demo will show you a simple way to implement the GLIDE client with Spring Boot caching. The target ElastiCache cluster will have cluster mode enabled and use IAM Authentication.

## Prerequesites

To build the application, you must have the following prerequisites.

Some compute with the following installed:
- *Java 17* - To install the Java Development Kit (JDK) 17, run `sudo yum install -y java-17-amazon-corretto-devel` on your EC2 instance
- *Maven* - To install Apache Maven, run `sudo yum install -y maven` on your EC2 instance

The compute must have access to Redis OSS or Valkey ElastiCache cluster with IAM Authentication configured.

## Running

To run the demo application, update `application.properties` to point to Redis or Valkey. Also update the `username`, `cachename` and `region` for your ElastiCache cluster.

`spring.data.valkey.host=cache1-XXXXX.serverless.euw2.cache.amazonaws.com`

Run `mvn spring-boot:run` in the root folder.

See the results!

## How it works

To use Valkey Glide, we add 3 classes to the application:

`SimpleValkeyGlideCache.java`. This class implements the Spring Framework Cache interface. It is simple in nature - all cache key objects must have the `toString()` method, and all cached objects must implement `java.lang.Serializable`. This class adapts Spring Framework to use the GLIDE client.

`ValkeyGlideCacheManager.java`. This class implements Spring Framework `org.springframework.cache.CacheManager` interface and instantiates a Bean Component. On startup, Spring Framework looks for a Bean implementing this interface and uses it to create and manage access to caches at runtime. This class uses `SimpleValkeyGlideCache` for caches.

`ElastiCachePasswordGenerator.java`. This class creates passwords for the ElastiCache cluster using IAM.

The Maven POM file contains three dependencies:

```
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>io.valkey</groupId>
<artifactId>valkey-glide</artifactId>
<classifier>${os.detected.classifier}</classifier>
<version>[1.0.0,2.0.0)</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>auth</artifactId>
<version>2.32.2</version>
</dependency>
```

`spring-boot-starter-cache` tells Spring Framework to implement caching.

`valkey-glide` includes the Valkey GLIDE client in your application. See the GLIDE [client documentation](https://valkey.io/clients/) for more details.

`software.amazon.awssdk.auth` provides utility classes for password creation using IAM.

## Want to try this with your existing Spring Boot application?

1. Remove any existing Caching providers from your dependencies (i.e. `spring-boot-starter-cache-redis`).
2. Add `SimpleValkeyGlideCache.java`, `ElastiCachePasswordGenerator.java` and `ValkeyGlideCacheManager.java` to your application.
3. Modify any entries in your `application.properties` that configure Redis, changing them to valkey (i.e. `spring.data.redis.host` becomes `spring.data.valkey.host`).
4. add `spring.data.valkey.username`, `spring.data.valkey.cacheName` and `spring.data.valkey.region` to `application.properties` with relevant values.

Give your application a try!
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>elasticache-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>elasticache-demo</name>
<description>Demo project for Spring Boot with Elasticache</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>io.valkey</groupId>
<artifactId>valkey-glide</artifactId>
<classifier>${os.detected.classifier}</classifier>
<version>[1.0.0,2.0.0)</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>auth</artifactId>
<version>2.32.2</version>
</dependency>
</dependencies>

<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.elasticache_demo;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements CommandLineRunner {

private static final long TEN_MINUTES = 60 * 10 * 1000;
private static final long ONE_HOUR_MILLIS = TEN_MINUTES * 6;
private static final long TWENTY_HOUR_MILLIS = ONE_HOUR_MILLIS * 20;

private CacheableComponent cacheableComponent;

public AppRunner(CacheableComponent cacheableComponent) {
this.cacheableComponent = cacheableComponent;
}

@Override
public void run(String... args) throws Exception {
long started = System.currentTimeMillis();
while ( System.currentTimeMillis() < started + TWENTY_HOUR_MILLIS ) {
String newValue = cacheableComponent.getCacheableValue("test-key" + System.currentTimeMillis());
System.out.println("READ " + newValue);
Thread.sleep(TEN_MINUTES);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.elasticache_demo;

import org.springframework.stereotype.Component;
import org.springframework.cache.annotation.Cacheable;

@Component
public class CacheableComponent {

@Cacheable("test")
public String getCacheableValue(String key) throws Exception {
return key + System.currentTimeMillis();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.elasticache_demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.beans.factory.annotation.Autowired;

@SpringBootApplication
@EnableCaching
public class ElasticacheDemoApplication {

public static void main(String[] args) {
SpringApplication.run(ElasticacheDemoApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.example.elasticache_demo;

import java.net.URI;
import java.time.Duration;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner.AuthLocation;
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
import software.amazon.awssdk.utils.StringUtils;

// example class from https://github.com/valkey-io/valkey-glide/wiki/Java-Wrapper#example---using-iam-authentication-with-glide-for-elasticache-and-memorydb
class ElasticachePasswordGenerator {
private final AwsV4HttpSigner awsV4HttpSigner;
private final AwsCredentialsProvider credentialsProvider;

private final String cacheName;
private final String cacheRegion;
private final String userId;
private final boolean isServerless;

private static final String FAKE_SCHEME = "https://";

ElasticachePasswordGenerator(
final String cacheName,
final String cacheRegion,
final String userId,
final AwsV4HttpSigner awsV4HttpSigner,
final AwsCredentialsProvider credentialsProvider,
final boolean isServerless) {

this.cacheName = cacheName;
this.cacheRegion = cacheRegion;
this.userId = userId;
this.awsV4HttpSigner = awsV4HttpSigner;
this.credentialsProvider = credentialsProvider;
this.isServerless = isServerless;
}

public static ElasticachePasswordGenerator create(
final String cacheName, final String cacheRegion, final String userId, final boolean isServerless) {
if (StringUtils.isEmpty(cacheName)) {
throw new IllegalArgumentException("cacheName must be provided");
}

if (StringUtils.isEmpty(cacheRegion)) {
throw new IllegalArgumentException("cacheRegion must be provided");
}

if (StringUtils.isEmpty(userId)) {
throw new IllegalArgumentException("userId must be provided");
}

return new ElasticachePasswordGenerator(
cacheName, cacheRegion, userId, AwsV4HttpSigner.create(), DefaultCredentialsProvider.create(), isServerless);
}

public String generatePassword() {
final var requestUri = URI.create(String.format("%s%s/", FAKE_SCHEME, cacheName));
final var requestBuilder = SdkHttpRequest.builder()
.method(SdkHttpMethod.GET)
.uri(requestUri)
.appendRawQueryParameter("Action", "connect")
.appendRawQueryParameter("User", userId);
if (this.isServerless) {
requestBuilder.appendRawQueryParameter("ResourceType", "ServerlessCache");
}

final var cacheRequest = requestBuilder.build();

final var signedRequest = awsV4HttpSigner.sign(signRequest -> signRequest
.request(cacheRequest)
.identity(credentialsProvider.resolveCredentials())
.putProperty(AwsV4HttpSigner.EXPIRATION_DURATION, Duration.ofMinutes(15))
.putProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, "elasticache")
.putProperty(AwsV4HttpSigner.REGION_NAME, cacheRegion)
.putProperty(AwsV4HttpSigner.AUTH_LOCATION, AuthLocation.QUERY_STRING));

String password = signedRequest.request().getUri().toString().replace(FAKE_SCHEME, "");
return password;
}
}
Loading