サーバーサイドでセッション管理を行うようなWebアプリケーションをAzure App Service上で動作させる時、考えるのはセッション情報の保持をどこで行うかです。本エントリーでは、Spring BootのWebアプリケーション上でのSpring Securityでのセッション管理をAzureのRedis Cacheでレプリケーションする方法をまとめます。
そんなにこったアーキテクチャーでないしすぐ終わるだろう、と思ったら、Redisに対してSSL/TLS接続するところではまったので、このエントリーは無駄に長いです。
Azure Redis Cache
AzureによるRedisのフルマネージドサービスです。詳しくはこちら。
アプリケーションの実装
に従ってSpring Boot + Spring Secutiryのアプリを実装します。でき上がったものはこちらです。
pom.xmlに、以下の依存性を追加します。
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> <version>2.0.4.RELEASE</version> </dependency> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>5.0.4.RELEASE</version> </dependency>
Redis Cacheの作成
メニューにしたがってポチポチ作成します。アプリケーションの接続先に必要な設定は「アクセスキー」のメニューから取得します。
SSL接続対応
Azure Redis CacheとSpring Boot Initializerの組み合わせについては、以下のドキュメントがあるのですが、このドキュメントではRedisに対して非SSLなポート(6379)のポートを解放して接続を行っています。
SSL/TLSで対応するには、本来application.properties
にspring.redis.ssl=true
を記述すればよいのはずなのですが、以下にあげる二点の問題があります。
1.io.lettuce.core.RedisCommandExecutionException: ERR unknown command: CONFIG
スタックトレースがひたすら長いのですが、全部のせるとこんな感じ。
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2018-07-04 04:33:56.021 ERROR 20792 --- [ main] o.s.boot.SpringApplication : Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'enableRedisKeyspaceNotificationsInitializer' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Invocation of init method failed; nested exception is org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR unknown command: CONFIG at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1708) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:581) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:503) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:760) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:869) ~[spring-context-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) ~[spring-context-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) [spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:395) [spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:327) [spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1255) [spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1243) [spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE] at hello.Application.main(Application.java:11) [classes/:na] Caused by: org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR unknown command: CONFIG at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:54) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:52) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:257) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.lettuce.LettuceServerCommands.convertLettuceAccessException(LettuceServerCommands.java:571) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.lettuce.LettuceServerCommands.getConfig(LettuceServerCommands.java:307) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.DefaultedRedisConnection.getConfig(DefaultedRedisConnection.java:1119) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction.getNotifyOptions(ConfigureNotifyKeyspaceEventsAction.java:76) ~[spring-session-data-redis-2.0.4.RELEASE.jar:2.0.4.RELEASE] at org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction.configure(ConfigureNotifyKeyspaceEventsAction.java:57) ~[spring-session-data-redis-2.0.4.RELEASE.jar:2.0.4.RELEASE] at org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration$EnableRedisKeyspaceNotificationsInitializer.afterPropertiesSet(RedisHttpSessionConfiguration.java:286) ~[spring-session-data-redis-2.0.4.RELEASE.jar:2.0.4.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1767) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1704) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE] ... 16 common frames omitted Caused by: io.lettuce.core.RedisCommandExecutionException: ERR unknown command: CONFIG at io.lettuce.core.protocol.AsyncCommand.completeResult(AsyncCommand.java:118) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.protocol.AsyncCommand.complete(AsyncCommand.java:109) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:598) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:556) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:508) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:647) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:582) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:499) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:461) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884) ~[netty-common-4.1.25.Final.jar:4.1.25.Final] at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.25.Final.jar:4.1.25.Final] at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_162]
これはspring-sessionでRedisにTLS/SSL接続するときの既知の問題です。
これに対応するには、@Configuration
と@EnableRedisHttpSession
を記述したクラスで、
package hello; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.session.data.redis.config.ConfigureRedisAction; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import java.util.Properties; @Configuration @EnableRedisHttpSession public class Config { @Bean public static ConfigureRedisAction configureRedisAction() { return ConfigureRedisAction.NO_OP; } }
のように記述するのですが、NO_OP
を指定すると認証処理までスキップされるので、今度はこのようなエラーになります。
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: NOAUTH Authentication required. at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:54) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:52) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:257) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.connection.lettuce.LettuceConnection.pSubscribe(LettuceConnection.java:795) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.listener.RedisMessageListenerContainer$SubscriptionTask.eventuallyPerformSubscription(RedisMessageListenerContainer.java:785) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at org.springframework.data.redis.listener.RedisMessageListenerContainer$SubscriptionTask.run(RedisMessageListenerContainer.java:752) ~[spring-data-redis-2.0.8.RELEASE.jar:2.0.8.RELEASE] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_162] Caused by: io.lettuce.core.RedisCommandExecutionException: NOAUTH Authentication required. at io.lettuce.core.protocol.AsyncCommand.completeResult(AsyncCommand.java:118) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.protocol.AsyncCommand.complete(AsyncCommand.java:109) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:598) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.pubsub.PubSubCommandHandler.complete(PubSubCommandHandler.java:142) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:556) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.pubsub.PubSubCommandHandler.decode(PubSubCommandHandler.java:87) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:508) ~[lettuce-core-5.0.4.RELEASE.jar:na] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:647) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:582) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:499) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:461) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final] at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884) ~[netty-common-4.1.25.Final.jar:4.1.25.Final] at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.25.Final.jar:4.1.25.Final] ... 1 common frames omitted
これに対応するには、LettuceConnectionFactory
を返す@Bean
メソッド内で接続先情報を指定するようにします。これを指定したクラスは以下の通りとなります。
package hello; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.session.data.redis.config.ConfigureRedisAction; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import java.util.Properties; @Configuration @EnableRedisHttpSession public class Config { @Bean public LettuceConnectionFactory connectionFactory() { LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(); lettuceConnectionFactory.setHostName(System.getenv("spring_redis_host")); lettuceConnectionFactory.setPassword(System.getenv("spring_redis_password")); lettuceConnectionFactory.setPort(6380); lettuceConnectionFactory.setUseSsl(true); return lettuceConnectionFactory; } @Bean public static ConfigureRedisAction configureRedisAction() { return ConfigureRedisAction.NO_OP; } }
なお当初は、org.springframework.core.env.Environment
をInjectionしてapplication.propertiesにしていた記述を読み込もうとしたのですが、Azure App Serviceにデプロイするとapplication.propertiesに記述した
設定を読み込まずにデフォルト接続先(localhost:6379)に接続に行くという問題が解消されなかったので、 @kazuyukimiyake のアドバイスもあり、Azureポータル上でアプリケーション設定に記述した値を読み込むようにしています。ありがとうございました。
環境や状況がよくわかりませんが、通常接続先はApp Settingsに書くものかと。
— miyake@PaaSがかり (@kazuyukimiyake) July 5, 2018
アプリケーションの動作の確認
以下は、App Serviceにデプロイしたアプリを2インスタンスにスケールアウトして、Spring Securityによるログイン処理が行われていることを確認した様子です。