This post gives an example how to read values and secrets from an alternative store instead of storing them in config files, which is never a good idea. The example uses the AWS parameter store, but can be easily adapted to the newer AWS Secrets Manager or any other store!

The goal is to avoid configuration files like these:

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://your-rds-endpoint:5432/database
    username: my_service_user
    password: my_super_secret

One way to fix this is to create the data source from code, so you can resolve the secrets manually:

@Configuration
class HikariAwsConfig {

    @Bean
    fun hikariDataSource(): HikariDataSource {
        val hikariConfig = HikariConfig()
        hikariConfig.jdbcUrl = "jdbc:postgresql://${getValueFromExternalStore("/service/db_endpoint")}:5432/database"
        hikariConfig.username = getValueFromExternalStore("/service/db_username")
        hikariConfig.password = getValueFromExternalStore("/service/db_password")
        hikariConfig.driverClassName = "org.postgresql.Driver"
        return HikariDataSource(hikariConfig)
    }

    fun getValueFromExternalStore(parameterName: String): String {
        // code that retrieves values from external store
    }
}

This is however adding a lot of code. It would be nice to be able to refer properties in an external store from within the configuration file. For example, this configuration refers to properties in AWS parameter store (does not work out of the box!):

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://${/ssm/service/db_endpoint}:5432/database
    username: ${/ssm/service/db_username}
    password: ${/ssm/service/db_password}

To make this work, we need to add a property source. The property source should only fetch values if the property name is prefixed with /ssm to avoid making calls to AWS for every property in the configuration file. When null is returned, another property source will be tried.

/**
 * Resolves properties from aws parameter store. Properties must be prefixed with /ssm
 * /ssm/property/name will be fetched from the parameter store as /property/name
 */
class AwsParameterStorePropertySource(name: String, awsSmmSource: AWSSimpleSystemsManagement) : PropertySource<AWSSimpleSystemsManagement>(name, awsSmmSource) {
    override fun getProperty(parameterName: String): String? {
        if (!parameterName.startsWith("/ssm")) {
            return null
        }
        val parameterNameWithoutPrefix = parameterName.substring(4)
        val request = GetParameterRequest().withName(parameterNameWithoutPrefix).withWithDecryption(true)
        return awsSmmSource.getParameter(request).parameter.value
    }
}

The last step is to configure Spring Boot to use this property source:

@Configuration
class AwsParameterStorePropertySourceConfig(val env: ConfigurableEnvironment) {

    @Value("\${aws.region:eu-west-1}")
    lateinit var awsRegion: String

    @PostConstruct
    fun init() {
        val clientBuilder = AWSSimpleSystemsManagementClientBuilder.standard().withRegion(awsRegion)
        val source = AwsParameterStorePropertySource("AwsSsmParameterStorePropertySource", clientBuilder.build())
        env.propertySources.addFirst(source)
        log.info("Configured AwsParameterStorePropertySource for resolving properties from SSM parameter store in region $awsRegion")
    }

    companion object {
        val log: Logger = LoggerFactory.getLogger(AwsParameterStorePropertySourceConfig::class.java)
    }
}

This is all you need to make it work. As you can see, this can be easily adapted to other systems as well.

This is one way to avoid putting secrets in source code.

shadow-left