แชร์ประสบการณ์ Config ตัว Amazon ElastiCache ให้ใช้งานกับ Spring Boot
Story
2 - 3 วันที่ผ่านมาพยายามต่อ Redis บน ElastiCache แล้ว Error ตลอดเวลาเรียกใช้ จะมี Log ขึ้นประมาณข้างล่างนี้ แล้วแก้จนมันใช้ได้แล้วเอาขึ้น Server ไปละ นี่คือเรื่องของมัน
org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to ███████-cache.███████.com:6379
Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to ███████-cache.███████.com:6379
e
Caused by: javax.net.ssl.SSLHandshakeException: General SSLEngine problem
Caused by: javax.net.ssl.SSLHandshakeException: General SSLEngine problem
Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching ███████-cache.███████.com found.
สันนิษฐานแรกคือน่าจะเกี่ยวกับ In-Transit Encryption feature ของ ElastiCache's Redis ซึ่งถูกเลือกไว้แล้ว แล้วหลังจาก Research ไปซักระยะเลยเจอว่า ElastiCache's Redis allow ให้ Connect ผ่าน VPC เท่านั้น
Amazon ElastiCache Nodes, deployed within a VPC, can never be accessed from the Internet or from EC2 Instances outside the VPC. Source
ระหว่างนั้นเลยไปทดลองสร้างเครื่อง EC2 ในวง VPC เดียวกันมา Connect แต่ก็เจอปัญหา Error: Connection reset by peer เวลาต่อจาก redis-cli ที่วิ่งอยู่บน Stunnel แล้ว ซึ่งแก้เท่าไรก็ยังไม่ได้ และไม่ได้เกี่ยวกับ IAM Role ด้วยเพราะลอง FullAccessElastiCache ก็ยังไม่ได้
สุดท้ายพี่กานต์ลองใช้ Python Redis client ยิงดูปรากฏว่าติด หน้าตาโค้ดประมาณนี้ เลยรู้สึกว่าไม่ใช่ละ
import redis
r = redis.Redis(host='███████-cache.███████.com', port=6379, db=0, ssl=True)
r.set('foo', 'bar')
print(r.get('foo'))
ซึ่งมีจุดที่ remind อย่างนึงคือ ssl=True
เลยพุ่งเป้ากลับมาที่เรื่อง SSL Certificate น่าจะมีปัญหาอะไรบางอย่าง หลังจากนั้นพยายามลองเซต spring.redis.ssl=true
ใน application.properties ก็ยังไม่ได้ขึ้น Exception เหมือนตอนแรก ก็เลยมานั่งมอง Exception ละเอียดๆ อีกทีจนไปสะดุดตาอันสุดท้าย
Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching ███████-cache.███████.com found.
Search ไปๆ มาๆ เลยมาเจอบล็อกนี้ [Solved] java.security.cert.CertificateException: No subject alternative names present ใจความหลักๆ เหมือนกันคือต่อ LDAP ไม่ได้ เพราะ Java 1.8.0_181 เปิด "Endpoint identification" เป็น default เพื่อให้ใช้กับ LDAPS connection ได้ ซึ่งก็มีวิธีปิดอยู่ง่ายๆ คือไปเพิ่ม property ข้างล่างก็ได้ละ
-Dcom.sun.jndi.ldap.object.disableEndpointIdentification=true
ซึ่งถึงแม้มันจะไม่ได้แก้ปัญหาของ ElastiCache ต่อไม่ได้ตรงๆ แต่ได้ keyword สำคัญมากคือ disableEndpointIdentification
ลิ้งค์ต่อๆ มาเลยยังพุ่งเป้าไปที่เรื่องนี้อยู่ แล้วมาเจอกับ issues นี้ใน github CertificateException while connecting to Azure redis cluster. #1296 ซึ่งเจอกับ Redis เต็มๆ แต่เป็น Azure แต่ที่สำคัญคือ อันนี้เป็นปัญหาบน Library: Redisson ซึ่งเป็น Redis Java Client เหมือนกัน และก็ยิ่งย้ำความเชื่อว่ามันเป็นเรื่อง EndpointIdentification นี่แหละ
ณ จุดนี้คือกลับมานั่งทำความเข้าใจอีกทีว่า spring-data-redis ใช้อะไรอยู่ข้างล่าง จาก Official Document เลยค้นพบว่าข้างล่างมันใช้ Lettuce เป็น Client อยู่ เลยมาเจอ issue Lettuce can't connect to Redis Cluster + SSL but can connect to same Redis server + SSL if treated as Standalone node. #1454 ซึ่งคำตอบแรกของ issue นี้ คือคำตอบเลย
There are a couple of things you can do about:
- If the Redis IP addresses can be mapped onto a hostname, then please configure a MappingSocketAddressResolver through ClientResources to map IP addresses into hostnames that are listed in the SSL certificate
- Reach out to the Azure support team so that they fix the SSL certificates and include all cluster IP addresses
- (least favorable option) Disable SSL certificate validation. That can be done with RedisURI.setVerifyPeer(false) when using Lettuce directly. For Spring, you should be able to register a LettuceClientConfigurationBuilder customizer to invoke LettuceSslClientConfigurationBuilder.disablePeerVerification().
ซึ่งทำให้เราได้คีย์เวิร์คสำคัญมาคือ disablePeerVerification
(มันต้องเหมือนข้างบนแน่ๆ) คำถามถัดมาคือ เราจะ disable มันยังไงใน Spring เลยต้องไปหาวิธีใช้ LettuceSslClientConfigurationBuilder อยู่ซักพักใหญ่ๆ เลย ซึ่งอ่าน Doc เฉยๆ ก็เขียนยังไม่เป็นด้วยประสบการณ์ Java แค่นี้
สุดท้ายเลยไปเริ่มจากตัวอย่างของ repository นี้ spring-data-redis-lettuce ซึ่งยกตัวอย่าง custom LettuceConnectionFactory ไว้ได้โอเคเลย แล้วทำให้เริ่มจากตรงนี้จนมาเป็น ลองผิดลองถูก อ่าน Document ของ LettuceClientConfigurationBuilder จนได้โค้ดของ configuration ข้างล่างนี้
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class RedisClientConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Bean
public StringRedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
final StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(lettuceConnectionFactory);
return template;
}
@Bean
LettuceConnectionFactory lettuceConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.useSsl()
.disablePeerVerification()
.build();
RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration(redisHost, redisPort);
return new LettuceConnectionFactory(serverConfig, clientConfig);
}
}
แล้วใน application.properties จะเหลือแค่
spring.cache.type=redis
spring.redis.host=███████-cache.███████.com
spring.redis.port=6379
spring.cache.redis.time-to-live=18000
Resources
- Official documentation ของ AWS ใช้ต่อ Redis ใน ElastiCache
- Connect to a Cluster's Node a.k.a. วิธี Build redis-cli
- ElastiCache for Redis In-Transit Encryption (TLS) a.k.a. วิธีเซต Stunnel
- How do I connect to an Amazon ElastiCache In-Transit encryption-enabled Redis node using redis-cli? a.k.a. วิธีเซต Stunnel
- สองบล็อกนี้แนะนำให้ไปใช้ stunnel วิธีเดียวกับ AWS Official ถ้าจะต่อเข้าไป Redis ใน ElastiCache
- Making a secure connection to ElastiCache (Redis)
- How to Fix Redis CLI Error Connection Reset by Peer
- Example how to config RedisConnectionFactory (link)
- Spring Official Document: Configuring the Lettuce Connector (link)
- Lettuce can't connect to Redis Cluster + SSL but can connect to same Redis server + SSL if treated as Standalone node. #1454 a.k.a. Issue ที่เป็นคำตอบ
- spring-data-redis-lettuce a.k.a. ตัวอย่าง Implement Custom LettuceConnection
- Lettuce Host/Peer Verification a.k.a. วิธีใช้ Builder กับ Peer verification
- Lettuce SSL มาจากตรงนี้ก่อน