Azure / AppConfiguration

Questions, feedback and samples for Azure App Configuration service
MIT License
227 stars 68 forks source link

Loading multiple keys not working in spring webflux application #939

Closed jyotijauhari closed 1 month ago

jyotijauhari commented 1 month ago

We are trying to load multiple keys (keys name: global, delegato) in 2 different configuration classes

our app configuration looks like this:

image

libraries:

    platform("io.opentelemetry:opentelemetry-bom:1.40.0")

    implementation(
        "com.microsoft.azure:applicationinsights-core:3.5.3"
    )
    implementation(
        "com.microsoft.azure:applicationinsights-runtime-attach:3.5.2"
    )
    implementation("com.azure.spring:spring-cloud-azure-appconfiguration-config:5.12.0")
    implementation("io.opentelemetry:opentelemetry-api")
    implementation("io.opentelemetry:opentelemetry-extension-kotlin")

    implementation("com.azure.spring:spring-cloud-azure-feature-management:5.14.0")

bootstrap.yaml:

spring:
  cloud:
    azure:
      appconfiguration:
        stores:
          - connection-string: our-app-config-connection-string
            selects:
              - key-filter: delegato
              - key-filter: global
                label-filter: dev
            feature-flags:
              enabled: true
              selects:
                - label-filter: dev

Configuration classes:


@Configuration
@ConfigurationProperties
class DelegatiConfiguration {
    var application: Application? = null
}

data class Application(val name: String)
@Configuration
@ConfigurationProperties
class GlobalConfiguration {
    var application: ApplicationInsight? = null
    var azurestorage: AzureStorage? = null
}

data class ApplicationInsight(val connectionString: String)
data class AzureStorage(val connectionString: String)

We are trying to autowire this configuration classes and trying to read the value, but we are not able to read value of delegationconfiguration, and seems like global key-filter overwriting it.

we tried adding prefix to configuration as well that is also not working.

Our controller:

@RestController
@RequestMapping("/app-config")
class AppConfigController {
    @Autowired private lateinit var globalConfiguration: GlobalConfiguration
    @Autowired private lateinit var delegatiConfiguration: DelegatiConfiguration

    @GetMapping
    suspend fun startOnboarding(): Boolean {
        logger.info("test app configuration new")
        logger.info(globalConfiguration.toString())
        logger.info(delegatiConfiguration.toString())
        return true
    }

we even tried using key-filter: "" (i.e empty string as mentioned in https://microsoft.github.io/spring-cloud-azure/docs/azure-app-configuration/2.9.0/reference/html/index.html#loading-configuration, but thats also not working to load all keys)

Problem is we want to load both keys together in application, but seems like we cant load multiple keys.

mrm9084 commented 1 month ago

@jyotijauhari, I think your issue is that by default the app configuration provider library trims all key filters off configurations. i.e. delegato.application.name became just application.name.

To stop this you need to set the spring.cloud.azure/appconfiguration.stores[0].trim-key-prefix value to be ''.

This library defaults to the key format of /<application-name>/configuration-properties-name.config to match how spring config files work.

Also, I can't see everything you have, but your @ConfigurationProperties value might need to have a prefix set, @ConfigurationProperties(prefix = "global")

jyotijauhari commented 1 month ago

Hi @mrm9084 thanks for your comment, my goal is to fetch both delegato and global keys from this personal app config.

  1. Do you mean its convention to store my config in format //configuration-properties-name.config
  2. I am trying to get 2 keys which are not in format /application-name/configuration-properties but one is global.somekey1.somekey2 and delegato.somekey3.somekey4. I want to load both global and delegato keys (they are not having "/" as prefix or suffix), something like this: image

I can get one key using key-filter: global and it loads that key directly in configurationClass. But I want to load both in 2 different configuration classes.

As shared by you I tried spring.cloud.azure/appconfiguration.stores[0].trim-key-prefix: ' '

my updated bootstrap.yaml

image

my global configuration class

image

my delegato configuration class

image

But value is not injected now in those classes

mrm9084 commented 1 month ago

If that is the format you want this should work:

spring:
  cloud:
    azure:
      appconfiguration:
        stores:
            selects:
              - 
                 key-filter: "global"
                 label-filter: "dev"
              - 
                 key-filter: "delegato"
                 label-filter: "dev"
           trim-key-prefix: 
                 - ""

The issue is that we require a prefix to load, and the default one is /application/, this is no way to not have a key filter, if one isn't provided it goes to the default. You may want to check this out, https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/spring-cloud-azure-starter-appconfiguration-config#supported-properties, it provides all of our

jyotijauhari commented 1 month ago

Thanks @mrm9084 , It's working now. Loading multiple keys was not much clear from documentation link .

jyotijauhari commented 1 month ago

On side note I have another question, we are using managed identity for pod, the value of connectionString is coming as null

image

I have following libraries:

springboot version : 2.7.7 azure spring cloud version: 4.14.0

        <dependency>
            <groupId>com.azure.spring</groupId>
            <artifactId>spring-cloud-azure-appconfiguration-config</artifactId>
        </dependency>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.azure.spring</groupId>
                <artifactId>spring-cloud-azure-dependencies</artifactId>
                <version>4.14.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

cloud version : ( as using springboot 2, reffering to this article: https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-java-spring-app?tabs=spring-boot-2#connect-to-an-app-configuration-store)

I have this configuration file where i am trying to inject:

image

My bootstrap.yaml

spring:
  cloud:
    azure:
      appconfiguration:
        stores:
          - endpoint: <endpoint>
            selects:
              - key-filter: global
                label-filter: dev
              - key-filter: globaltest
                label-filter: dev
            trim-key-prefix:
              - ""
            feature-flags:
              enabled: true
              selects:
                - label-filter: dev
        credential:
          managed-identity-enabled: true
        enabled: true

Its not loading configurations and its coming as null. note: 1.I confirm that manged identity is working as I am able to fetch feature flag from feature manager using managed identity.

mrm9084 commented 1 month ago

I'm unsure why that would be happening the easiest way to see what is going on would be to add the Environment somewhere in your code, and check out which configs we loaded. You will find them under: Environment.propertySources which is a list of all the property sources. One will be of type BootstrapPropertySource with the name something like bootstrapProperties-<key-filter><store-name><label-filter>. Then in that object under delegate you will is a properties object with a map of all the loaded key values.

All of the key values in the map are the names after the trim-key-prefix operation. so they would be as you are looking for them. i.e. global test.applicationinsight.connectionstring.

Side Note, it isn't suggest to store secrets like connection strings in app configuration. We support key vault references to load key vault secrets as configuration. See: https://learn.microsoft.com/en-us/azure/azure-app-configuration/faq#how-is-app-configuration-different-from-azure-key-vault

jyotijauhari commented 1 month ago

Hi @mrm9084 , we tried to debug the same way you mentioned i.e to load BootstrapProperty. In properties we found its having FM_configurl/label-filter. (managed identity approach)

In connection string approach we analysed same object there as well same property we found i.e FM_configurl/label-filter. (looks like configs are not there in bootstrap properties in both)

The interesting thing is with connectionString approach the configs are loading and having value inglobaltest.applicationinsight.connectionstring while same key is null when using managed identity.

I am not able to figure out why. Do you know any other way to debug? the approach you mention bootstrap properties its just having feature management in properties.

Also, thanks for mentioning the point of not storing secrets directly. We are using key-vault itself. Initially while testing loading config with fetch value from key-vault was also returning null, so we tried with normal value.

mrm9084 commented 1 month ago

@jyotijauhari, there should be a non FM_* object. The FM stands for feature management and would be any loaded feature flags. The same thing without that prefix should exist.

jyotijauhari commented 1 month ago

Hi @mrm9084 , I have tried to log the properties, I was not able to log the whole properties object as its protected method, but I was able to log the keys in properties and I can see the keys are getting fetched and environment is dev (correct one). The problem is values are null when I am trying to access/log it.

I tried to log values from env as you mentioned and its having values, but injection of those values by @Configuration @ConfigurationProperties(prefix="global") is not working.

Same configuration in my other project which uses springboot 3 and latest app config and connection string works (able to inject value).

mrm9084 commented 1 month ago

@jyotijauhari I think I figured it out. I missed that your configuration are not initialized. Spring doesn't auto create the classes for you, it just fills out the properties. So, you need to initialize your 3 classes.

jyotijauhari commented 1 month ago

I am not able to understand about initislisation part.

This is my one of configuration class

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration

@Configuration
@ConfigurationProperties(prefix = "globaltest")
open class GlobalConfigurationTest {
    var applicationInsight: ApplicationInsight? = null
}

I am enabling configuration properties in main class

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties

@SpringBootApplication
@EnableConfigurationProperties(GlobalConfiguration::class)
open class TestAppConfigApplication

fun main(args: Array<String>) {
    ApplicationInsights.attach()
    SpringApplication.run(TestAppConfigApplication::class.java, *args)
}

controller which I am using to test

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cloud.bootstrap.config.BootstrapPropertySource
import org.springframework.core.env.ConfigurableEnvironment
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/test/v1")
class AppConfigController {

    @Autowired private lateinit var environment: ConfigurableEnvironment
    @Autowired private lateinit var globalConfiguration: GlobalConfiguration
    @Autowired private lateinit var globalConfigurationTest: GlobalConfigurationTest

 @GetMapping("/configs-test")
    fun getConfigsTest() {
        val configurableEnvironment = environment

        println("FINALLLYYYY global-> ${globalConfiguration.applicationInsight?.connectionString}")
        println("FINALLLYYYY globaltest-> ${globalConfigurationTest.applicationInsight?.connectionString}")
    }

I am not getting what I am missing here for initialisation? I thought @Autowired private lateinit var globalConfigurationTest: GlobalConfigurationTest initialise the object

mrm9084 commented 1 month ago

I'm not familiar with how Kotlin works but, I don't think var applicationInsight in your GlobalConfiguration file is being set to a non null value, which it needs to be before the property can be set into it. Spring will not auto create it.

jyotijauhari commented 1 month ago

Just to mention if it helps, I have same code in my other application which uses springboot 3 and latest config dependency (but it has connection string approach to fetch config) and it works. Its having non null value in applicationInsight. I am not able to figure out on how can I debug it.

jyotijauhari commented 1 month ago

Able to get it working. Its bit strange that with var in connection string approach is working, but not with managed identity. We made it val and it worked. Thanks @mrm9084 !

mrm9084 commented 1 month ago

@jyotijauhari, I think this is good to be closed then. If you have another issue another issue can be opened.