Lettuce、Jedisを使い、Managed IdentityでAzure Managed Redisに接続しようとして例外が発生した

このエントリは2025/08/25現在の情報に基づいています。将来の機能追加や変更に伴い、記載事項からの乖離が発生する可能性があります。

問い合わせ

Azure Cache for Redis (以後ACR) からAzure Managed Redis (以後AMR) に乗り換えようとしている人から、以下のような問いが届いた。

現在Managed Identitiesを使い、Lettuce、Jedisを使ってAMRへの接続を試しているが、以下のような事象が発生しており、検証が手詰まりになっている。解決の手がかりを教えてほしい。
Q1) DefaultAzureCredentialを明示的に宣言してAccess Tokenを取得するパターンだと、User assigned managed identityではアクセスできるのだが、System assignedだとアクセスできない。
Q2) TokenBasedRedisCredentialsProviderを使ってアクセスしようとすると、User/System assignedのいずれにおいても例外が発生する。

TokenBasedRedisCredentialsProviderは、redis-authx-entraidというパッケージに含まれるもので、Entra IDへの認証や認証トークンの更新などをよろしくやってくれるもの。

Token-based Authentication Providers for Redis Java clients
https://github.com/redis/jvm-redis-authx-entraid

利用しているリソースは以下の通り。

Java version21.0.8
Jedis6.1
Lettuce6.8.RELEASE
redis-authx-entraid0.1.1-beta2
Azure Identity SDK1.17.0
Azure Managed Redis SKUBalanced B0 / Japan East
RuntimeAzure App Service (Linux)

設定内容を確認したところ、以下のよう。

  • Azure Managed Redisでは、Managed Identities (System assigned、User assigned) を認証で通すように設定済み
  • 接続に使うManaged identitiesのPrincipal ID、Object IDとも取得済み
  • スコープは https://redis.azure.com/.default を指定

Q1の例外は以下の通り。

// Jedisの場合
redis.clients.jedis.exceptions.JedisAccessControlException: WRONGPASS invalid username-password pair
        at redis.clients.jedis.Protocol.processError(Protocol.java:108)
        at redis.clients.jedis.Protocol.process(Protocol.java:158)
        at redis.clients.jedis.Protocol.read(Protocol.java:221)
        at redis.clients.jedis.Connection.protocolRead(Connection.java:384)
        at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:397)
        at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:306)
        at redis.clients.jedis.Connection.authenticate(Connection.java:576)
        at redis.clients.jedis.Connection.helloAndAuth(Connection.java:546)
        at redis.clients.jedis.Connection.initializeFromClientConfig(Connection.java:470)
        at redis.clients.jedis.Connection.<init>(Connection.java:77)
        at redis.clients.jedis.Connection.<init>(Connection.java:65)
        at redis.clients.jedis.Jedis.<init>(Jedis.java:76)
        at redis.clients.jedis.Jedis.<init>(Jedis.java:72)
        ...

// Lettuceの場合
java.lang.IllegalArgumentException: User name must not be null
        at io.lettuce.core.internal.LettuceAssert.notNull(LettuceAssert.java:56)
        at io.lettuce.core.RedisURI$Builder.withAuthentication(RedisURI.java:1749)
        ...

Q2のJedisで発生した例外は以下の通り。

redis.clients.jedis.authentication.JedisAuthenticationException: AuthXManager failed to start!
        at redis.clients.jedis.authentication.AuthXManager.start(AuthXManager.java:52)
        at redis.clients.jedis.ConnectionFactory.<init>(ConnectionFactory.java:67)
        at redis.clients.jedis.ConnectionFactory.<init>(ConnectionFactory.java:44)
        at redis.clients.jedis.ConnectionFactory.<init>(ConnectionFactory.java:38)
        at redis.clients.jedis.ConnectionPool.<init>(ConnectionPool.java:17)
        at redis.clients.jedis.providers.PooledConnectionProvider.<init>(PooledConnectionProvider.java:29)
        at redis.clients.jedis.UnifiedJedis.<init>(UnifiedJedis.java:104)
        at ...
Caused by: redis.clients.authentication.core.TokenRequestException: Token request/renewal failed! Identity provider request failed!Failed to acquire token!
        at redis.clients.authentication.core.TokenManager.prepareToPropogate(TokenManager.java:96)
        at redis.clients.authentication.core.TokenManager.renewToken(TokenManager.java:80)
        at redis.clients.authentication.core.RenewalScheduler.lambda$scheduleNext$0(RenewalScheduler.java:36)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
        at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: redis.clients.authentication.entraid.RedisEntraIDException: Failed to acquire token!
        at redis.clients.authentication.entraid.EntraIDIdentityProvider.requestWithManagedIdentity(EntraIDIdentityProvider.java:120)
        at redis.clients.authentication.entraid.EntraIDIdentityProvider.lambda$createManagedIdentityApp$3(EntraIDIdentityProvider.java:76)
        at redis.clients.authentication.entraid.EntraIDIdentityProvider.requestToken(EntraIDIdentityProvider.java:101)
        at redis.clients.authentication.core.Dispatcher.requestToken(Dispatcher.java:56)
        at redis.clients.authentication.core.Dispatcher.lambda$requestTokenAsync$0(Dispatcher.java:37)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
        ... 3 more
Caused by: java.util.concurrent.ExecutionException: com.microsoft.aad.msal4j.MsalClientException: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.microsoft.aad.msal4j.ManagedIdentityErrorResponse$ErrorField` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('invalid_resource')
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 10] (through reference chain: com.microsoft.aad.msal4j.ManagedIdentityErrorResponse["error"])
        at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396)
        at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2073)
        at redis.clients.authentication.entraid.EntraIDIdentityProvider.requestWithManagedIdentity(EntraIDIdentityProvider.java:118)
        ... 8 more
Caused by: com.microsoft.aad.msal4j.MsalClientException: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.microsoft.aad.msal4j.ManagedIdentityErrorResponse$ErrorField` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('invalid_resource')
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 10] (through reference chain: com.microsoft.aad.msal4j.ManagedIdentityErrorResponse["error"])
        at com.microsoft.aad.msal4j.JsonHelper.convertJsonToObject(JsonHelper.java:35)
        at com.microsoft.aad.msal4j.AbstractManagedIdentitySource.getMessageFromErrorResponse(AbstractManagedIdentitySource.java:81)
        at com.microsoft.aad.msal4j.IMDSManagedIdentitySource.handleResponse(IMDSManagedIdentitySource.java:107)
        at com.microsoft.aad.msal4j.AbstractManagedIdentitySource.getManagedIdentityResponse(AbstractManagedIdentitySource.java:46)
        at com.microsoft.aad.msal4j.ManagedIdentityClient.getManagedIdentityResponse(ManagedIdentityClient.java:48)
        at com.microsoft.aad.msal4j.AcquireTokenByManagedIdentitySupplier.fetchNewAccessTokenAndSaveToCache(AcquireTokenByManagedIdentitySupplier.java:90)
        at com.microsoft.aad.msal4j.AcquireTokenByManagedIdentitySupplier.execute(AcquireTokenByManagedIdentitySupplier.java:79)
        at com.microsoft.aad.msal4j.AuthenticationResultSupplier.get(AuthenticationResultSupplier.java:69)
        at com.microsoft.aad.msal4j.AuthenticationResultSupplier.get(AuthenticationResultSupplier.java:18)
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768)
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1760)
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1310)
        at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1841)
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1806)
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.microsoft.aad.msal4j.ManagedIdentityErrorResponse$ErrorField` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('invalid_resource')
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 10] (through reference chain: com.microsoft.aad.msal4j.ManagedIdentityErrorResponse["error"])
        at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
        at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1754)
        at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1379)
        at com.fasterxml.jackson.databind.deser.std.StdDeserializer._deserializeFromString(StdDeserializer.java:311)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1592)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:197)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
        at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:310)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
        at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4931)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3868)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3836)
        at com.microsoft.aad.msal4j.JsonHelper.convertJsonToObject(JsonHelper.java:33)
        ... 15 more

Q2のLettuceで発生した例外は以下の通り。

io.lettuce.core.RedisConnectionException: Unable to connect to <Redis_Instance>.japaneast.redis.azure.net/<unresolved>:10000
        at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:63)
        at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:41)
        at io.lettuce.core.AbstractRedisClient.getConnection(AbstractRedisClient.java:352)
        at io.lettuce.core.RedisClient.connect(RedisClient.java:220)
        at ...
Caused by: io.lettuce.core.RedisCommandTimeoutException: Connection initialization timed out after 1 minute(s)
        at io.lettuce.core.protocol.RedisHandshakeHandler.lambda$channelRegistered$0(RedisHandshakeHandler.java:52)
        at io.netty.util.concurrent.PromiseTask.runTask(PromiseTask.java:98)
        at io.netty.util.concurrent.PromiseTask.run(PromiseTask.java:106)
        at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:173)
        at io.netty.util.concurrent.DefaultEventExecutor.run(DefaultEventExecutor.java:66)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:1583)

勘のいい人はわかったかもしれない。

原因

Q1、Q2では原因が異なるが、特にQ2については、ドキュメント上の表現に少々問題がある点が原因になっているよう。AMRのドキュメントを見ると、指定すべきScopeやUser、Passwordに関して以下のような記載がある。

Microsoft Entra クライアント ワークフロー / Microsoft Entra client workflow
https://learn.microsoft.com/azure/redis/entra-for-authentication#microsoft-entra-client-workflow

  1. Configure your client application to acquire a Microsoft Entra token for scope, https://redis.azure.com/.default, or acca5fbb-b7e4-4009-81f1-37e38fd66d78/.default, by using the Microsoft Authentication Library (MSAL).
  2. Update your Redis connection logic to use the following User and Password:
    • User = Object ID of your managed identity or service principal
    • Password = Microsoft Entra token that you acquired by using MSAL

1) Q1

System assigned managed identityで例外が発生している(と言っている)のは、スタックトレースに記載の通り、WRONGPASS invalid username-password pairであることが原因。

通常、DefaultAzureCredentialを使ってAccessTokenを取得し、Jedisでアクセスする場合、以下のようなコードを使う(System assigned managed identityの場合、2行目でClient IDを指定しない)。

DefaultAzureCredential credential = new DefaultAzureCredentialBuilder()
        .managedIdentityClientId(CLIENT_ID) // System assignedではClient IDを指定しない
        .build();
String token = credential.getToken(new TokenRequestContext().addScopes(SCOPE))
        .block()
        .getToken();

Jedis jedis = new Jedis(HOST, PORT, DefaultJedisClientConfig.builder()
        .ssl(true)
        .user(OBJECT_ID) // System assignedの場合にエラーが発生(?)
        .password(token)
        .build());

本来はUser assigned、System assignedのいずれであっても、10行目でObject IDを指定しないと、ユーザー名とトークンが一致しない、ということで例外が発生する。そもそもどのManaged Identityに対してアクセスを許可しているかを確認しないといけない。Previewではあるものの、Azure CLIには割り当て済みのManaged Identity/Service PrincipalのObject IDを確認するためのコマンドがある。

az redisenterprise database access-policy-assignment
https://learn.microsoft.com/cli/azure/redisenterprise/database/access-policy-assignment?view=azure-cli-latest

az redisenterprise database access-policy-assignment list --cluster-name
                                                          --database-name
                                                          --resource-group
                                                          [--max-items]
                                                          [--next-token]

これで確認したManaged IdentitiesのObject IDを使ってアクセスすればよい。今回のケースでは、Object IDとして利用していた文字列が間違っていた、という初歩的なことが原因だった。Azure Portalでも確認できるのだが、どうやらチェックが足りなかったようす(いわゆるケアレスミス、ってやつですな)。

なお、System assigned managed identityの場合、Object IDでなくても、名前(例えばSystem assigned managed identityの名前(つまりAzureリソースの名前)がlogicojp-webappだとしたら、logicojp-webapp)を指定することでも接続できる。

DefaultAzureCredential credential = new DefaultAzureCredentialBuilder()
        .build();
String token = credential.getToken(new TokenRequestContext().addScopes(SCOPE))
        .block()
        .getToken();

Jedis jedis = new Jedis(HOST, PORT, DefaultJedisClientConfig.builder()
        .ssl(true)
        .user("logicojp-webapp") // もちろんObject IDでも可
        .password(token)
        .build());

2) Q2

こちらは指定するScopeの問題である。確かに本来指定すべきScopeは https://redis.azure.com/.default であり、Redis、Lettuce、Jedisのドキュメントにもその旨の記述がある。

Connect to Azure Managed Redis (Jedis)
https://redis.io/docs/latest/develop/clients/jedis/amr/
Using With Microsoft EntraID
https://redis.github.io/jedis/advanced-usage/#using-with-microsoft-entraid
Connect to Azure Managed Redis (Lettuce)
https://redis.io/docs/latest/develop/clients/lettuce/amr/
Microsoft Entra ID Authentication
https://redis.github.io/lettuce/user-guide/connecting-redis/#microsoft-entra-id-authentication

ただ、現時点では /.default が存在すると例外が発生してしまうので、Scopeとして、https://redis.azure.com を指定する必要がある。

これらの原因と解決策を伝えたところ、無事に接続テストが完了し、トークンの更新も成功したようである。

今回の動作確認目的で、サンプルコードを作成したので、必要であればどうぞ。

amr-quickstart : Access AMR (Azure Managed Redis) with Managed Identity authentication.
https://github.com/anishi1222/amr-quickstart

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください