Skip to content

Instantly share code, notes, and snippets.

@jcyuyi
Last active November 28, 2018 01:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcyuyi/bfd19029d5cd238a42dd780b54947036 to your computer and use it in GitHub Desktop.
Save jcyuyi/bfd19029d5cd238a42dd780b54947036 to your computer and use it in GitHub Desktop.
Use AWS Secrets Manager to config secret properties in Spring using EnvironmentPostProcessor
package io.nayu.spring.secretsdemo
import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder
import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest
import org.springframework.boot.SpringApplication
import org.springframework.boot.context.event.ApplicationFailedEvent
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.context.event.SpringApplicationEvent
import org.springframework.core.env.ConfigurableEnvironment
import org.springframework.boot.env.EnvironmentPostProcessor
import org.springframework.boot.json.JsonParserFactory
import org.springframework.boot.logging.DeferredLog
import org.springframework.context.ApplicationListener
import org.springframework.core.env.MapPropertySource
/**
* Use [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) to config secret properties.
*
* Configurable property:
* - `aws.secretsmanager.enabled` : Enable AWS Secrets Manager or not
* - `aws.secretsmanager.secrets` : Secret id list
* - `aws.secretsmanager.region` : AWS region
*
* EnvironmentPostProcessor implementations have to be registered in `META-INF/spring.factories`,
* using the fully qualified name of this class as the key.
**/
class AWSSecretsProcessor : EnvironmentPostProcessor, ApplicationListener<SpringApplicationEvent> {
// Use deferred log since application is not ready yet
private val logger = DeferredLog()
private val parser = JsonParserFactory.getJsonParser()
private val clientBuilder: AWSSecretsManagerClientBuilder
constructor() {
clientBuilder = AWSSecretsManagerClientBuilder.standard()
}
constructor(awsSecretsManagerClientBuilder: AWSSecretsManagerClientBuilder) {
clientBuilder = awsSecretsManagerClientBuilder
}
override fun postProcessEnvironment(env: ConfigurableEnvironment, application: SpringApplication) {
// add SpringApplication listener to replay logs
application.addListeners(this)
// Use AWS Secrets Manager only when running on AWS
if (!env.getProperty(AWS_SECRETSMANAGER_ENABLED_PROPERTY, Boolean::class.java, false)) {
logger.info("AWS Secrets Manager is not enabled")
return
}
val secretIds = env.getProperty(AWS_SECRETSMANAGER_SECRETS_PROPERTY, Array<String>::class.java)
?: throw IllegalStateException("Failed to load property: $AWS_SECRETSMANAGER_SECRETS_PROPERTY")
val region = env.getProperty(AWS_SECRETSMANAGER_REGION_PROPERTY)
?: throw IllegalStateException("Failed to load property: $AWS_SECRETSMANAGER_REGION_PROPERTY")
val client = clientBuilder
.withRegion(region)
.build()
secretIds.forEach { secretId ->
logger.info("fetch secretId: $secretId")
val req = GetSecretValueRequest().withSecretId(secretId)
val secretString = client.getSecretValue(req).secretString
val map = parser.parseMap(secretString)
if (!map.isEmpty()) {
val propertySourceName = "$AWS_SECRETSMANAGER_SECRETS_PROPERTY/$secretId"
env.propertySources.addFirst(MapPropertySource(propertySourceName, map))
logger.info("${map.size} secrets added to propertySource: $propertySourceName, keys: " + map.keys)
}
}
}
override fun onApplicationEvent(event: SpringApplicationEvent) {
if (event is ApplicationReadyEvent || event is ApplicationFailedEvent)
this.logger.switchTo(javaClass)
}
companion object {
/**
* Name of the *aws.secretsmanager.enabled* property.
*/
const val AWS_SECRETSMANAGER_ENABLED_PROPERTY = "aws.secretsmanager.enabled"
/**
* Name of the *aws.secretsmanager.secrets* property.
*/
const val AWS_SECRETSMANAGER_SECRETS_PROPERTY = "aws.secretsmanager.secrets"
/**
* Name of the *aws.secretsmanager.region* property.
*/
const val AWS_SECRETSMANAGER_REGION_PROPERTY = "aws.secretsmanager.region"
}
}
@jcyuyi
Copy link
Author

jcyuyi commented Nov 28, 2018

AWSSecretsProcessorTest

see Mock the unmockable: opt-in mocking of final classes/metho

package io.nayu.spring.secretsdemo

import com.amazonaws.services.secretsmanager.AWSSecretsManager
import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder
import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest
import com.amazonaws.services.secretsmanager.model.GetSecretValueResult
import com.nhaarman.mockitokotlin2.*
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.*
import org.junit.Test

import org.junit.Before
import org.springframework.boot.SpringApplication
import org.springframework.core.env.ConfigurableEnvironment
import org.springframework.core.env.MapPropertySource
import org.springframework.core.env.MutablePropertySources
import java.lang.IllegalStateException

class AWSSecretsProcessorTest {

    private val propertySources: MutablePropertySources = spy()
    private val env: ConfigurableEnvironment = mock {
        on { propertySources }.doReturn(propertySources)
    }
    private val application: SpringApplication = mock()
    private val client: AWSSecretsManager = mock()
    private val clientBuilder: AWSSecretsManagerClientBuilder = mock()

    private lateinit var awsSecretsProcessor: AWSSecretsProcessor

    @Before
    fun before() {
        awsSecretsProcessor = AWSSecretsProcessor(clientBuilder)
    }

    @Test
    fun postProcessEnvironment_notEnabled() {
        whenever(env.getProperty("aws.secretsmanager.enabled", Boolean::class.java, false))
            .thenReturn(false)
        // execute
        awsSecretsProcessor.postProcessEnvironment(env, application)
        // assert
        verify(application).addListeners(awsSecretsProcessor)
        verify(clientBuilder, never()).build()
    }

    @Test(expected = IllegalStateException::class)
    fun postProcessEnvironment_noSecrets() {
        whenever(env.getProperty("aws.secretsmanager.enabled", Boolean::class.java, false))
            .thenReturn(true)
        whenever(env.getProperty("aws.secretsmanager.secrets", Array<String>::class.java))
            .thenReturn(null)
        // execute
        awsSecretsProcessor.postProcessEnvironment(env, application)
        // assert
        verify(application).addListeners(awsSecretsProcessor)
        verify(clientBuilder, never()).build()
    }

    @Test(expected = IllegalStateException::class)
    fun postProcessEnvironment_noRegion() {
        whenever(env.getProperty("aws.secretsmanager.enabled", Boolean::class.java, false))
            .thenReturn(true)
        whenever(env.getProperty("aws.secretsmanager.region"))
            .thenReturn(null)
        // execute
        awsSecretsProcessor.postProcessEnvironment(env, application)
        // assert
        verify(application).addListeners(awsSecretsProcessor)
        verify(clientBuilder, never()).build()
    }

    @Test
    fun postProcessEnvironment_normal() {
        whenever(env.getProperty("aws.secretsmanager.enabled", Boolean::class.java, false))
            .thenReturn(true)
        whenever(env.getProperty("aws.secretsmanager.region"))
            .thenReturn(REGION)
        whenever(env.getProperty("aws.secretsmanager.secrets", Array<String>::class.java))
            .thenReturn(arrayOf(SECRET_1_ID, SECRET_2_ID))
        whenever(client.getSecretValue(GetSecretValueRequest().withSecretId(SECRET_1_ID)))
            .thenReturn(GetSecretValueResult().withSecretString(SECRET_1))
        whenever(client.getSecretValue(GetSecretValueRequest().withSecretId(SECRET_2_ID)))
            .thenReturn(GetSecretValueResult().withSecretString(SECRET_2))
        whenever(clientBuilder.withRegion(REGION)).thenReturn(clientBuilder)
        whenever(clientBuilder.build()).thenReturn(client)
        // execute
        awsSecretsProcessor.postProcessEnvironment(env, application)
        // assert
        verify(application).addListeners(awsSecretsProcessor)
        verify(clientBuilder, times(1)).withRegion(REGION)
        verify(clientBuilder, times(1)).build()
        verify(propertySources, times(2)).addFirst(any())
        val secretSources1 = propertySources.get("aws.secretsmanager.secrets/$SECRET_1_ID") as MapPropertySource
        val secretSources2 = propertySources.get("aws.secretsmanager.secrets/$SECRET_2_ID") as MapPropertySource
        assertThat(secretSources1.getProperty("api1") as String, equalTo("api1 value"))
        assertThat(secretSources2.getProperty("api2") as String, equalTo("api2 value"))
    }

    companion object {
        const val REGION = "ap-northeast-1"
        const val SECRET_1_ID = "secrets1"
        const val SECRET_1 = """ {"api1":"api1 value"} """
        const val SECRET_2_ID = "secrets2"
        const val SECRET_2 = """ {"api2":"api2 value"} """
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment