Commit 07d09d1c authored by Joris Kuipers's avatar Joris Kuipers
Browse files

Initial commit

Supports reading individual parameters under a given path, based on a
configurable prefix and shared application name for shared properties.
parents
target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
nbproject/private/
build/
nbbuild/
dist/
nbdist/
.nb-gradle/
\ No newline at end of file
# AWS Parameter Store Config Support
This module adds support for using the
[AWS Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-paramstore.html)
as a Spring Cloud configuration backend by creating a composite `PropertySource` at bootstrap time, similar to Spring
Cloud's Consul support.
It relies on the [AWS SDK for Java](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/welcome.html) to
retrieve parameters from the Parameter Store.
## Usage and Configuration
Simply add a dependency on this library from a Spring Cloud-enabled application to activate its support.
You can disable it by specifying a `aws.paramstore.enabled` property and setting it to `false`.
Further configuration is documented in the `AwsParamStoreProperties` class. If you're using a single Parameter Store for
multiple deployment environments, then make sure to override the default `aws.paramstore.prefix` property with an
environment-specific value.
## Configuring the `AWSSimpleSystemsManagement` client
Typically it's expected that the `AWSSimpleSystemsManagement` instance created by the
`AwsParamStoreBootstrapConfiguration` will work correctly using its default configuration.
Check its [documentation](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html) to understand
where it looks for AWS credentials to connect to the Parameter Store.
If you would like to override the client, you'd have to define
[your own Spring Cloud bootstrap configuration](https://projects.spring.io/spring-cloud/spring-cloud.html#_customizing_the_bootstrap_configuration)
to define your own instance.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nl.trifork.springcloud</groupId>
<artifactId>aws-param-store-config</artifactId>
<version>0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>aws-param-store-config</name>
<description>Spring Cloud config integration with the AWS Parameter Store</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.M7</spring-cloud.version>
<aws-sdk.version>1.11.285</aws-sdk.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>${aws-sdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-ssm</artifactId>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
package nl.trifork.aws.paramstore;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static nl.trifork.aws.paramstore.AwsParamStoreProperties.CONFIG_PREFIX;
@Configuration
@EnableConfigurationProperties(AwsParamStoreProperties.class)
@ConditionalOnProperty(prefix = CONFIG_PREFIX, name= "enabled", matchIfMissing = true)
public class AwsParamStoreBootstrapConfiguration {
@Bean
AwsParamStorePropertySourceLocator awsParamStorePropertySourceLocator(
AWSSimpleSystemsManagement ssmClient, AwsParamStoreProperties properties) {
return new AwsParamStorePropertySourceLocator(ssmClient, properties);
}
@Bean
@ConditionalOnMissingBean
AWSSimpleSystemsManagement ssmClient() {
return AWSSimpleSystemsManagementClientBuilder.defaultClient();
}
}
package nl.trifork.aws.paramstore;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@ConfigurationProperties(AwsParamStoreProperties.CONFIG_PREFIX)
@Validated
public class AwsParamStoreProperties {
static final String CONFIG_PREFIX = "aws.paramstore";
/**
* Prefix indicating first level for every property.
* Value must start with a forward slash followed by a valid path segment or be empty.
* Defaults to "/config".
*/
@NotNull @Pattern(regexp = "(/[a-zA-Z0-9.\\-_]+)*")
private String prefix = "/config";
@NotEmpty
private String defaultContext = "application";
@NotNull @Pattern(regexp = "[a-zA-Z0-9.\\-_]+")
private String profileSeparator = "_";
/** Throw exceptions during config lookup if true, otherwise, log warnings. */
private boolean failFast = true;
/** Alternative to spring.application.name to use in looking up values in AWS Parameter Store. */
private String name;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getDefaultContext() {
return defaultContext;
}
public void setDefaultContext(String defaultContext) {
this.defaultContext = defaultContext;
}
public String getProfileSeparator() {
return profileSeparator;
}
public void setProfileSeparator(String profileSeparator) {
this.profileSeparator = profileSeparator;
}
public boolean isFailFast() {
return failFast;
}
public void setFailFast(boolean failFast) {
this.failFast = failFast;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package nl.trifork.aws.paramstore;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathRequest;
import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathResult;
import com.amazonaws.services.simplesystemsmanagement.model.Parameter;
import org.springframework.core.env.EnumerablePropertySource;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Recursively retrieves all parameters under the given context / path with decryption from the AWS Parameter Store
* using the provided SSM client.
*/
public class AwsParamStorePropertySource extends EnumerablePropertySource<AWSSimpleSystemsManagement> {
private String context;
private Map<String, Object> properties = new LinkedHashMap<>();
public AwsParamStorePropertySource(String context, AWSSimpleSystemsManagement ssmClient) {
super(context, ssmClient);
this.context = context;
}
public void init() {
GetParametersByPathRequest paramsRequest = new GetParametersByPathRequest()
.withPath(context)
.withRecursive(true)
.withWithDecryption(true);
getParameters(paramsRequest);
}
@Override
public String[] getPropertyNames() {
Set<String> strings = properties.keySet();
return strings.toArray(new String[strings.size()]);
}
@Override
public Object getProperty(String name) {
return properties.get(name);
}
private void getParameters(GetParametersByPathRequest paramsRequest) {
GetParametersByPathResult paramsResult = source.getParametersByPath(paramsRequest);
for (Parameter parameter : paramsResult.getParameters()) {
String key = parameter.getName().replace(context, "").replace('/', '.');
properties.put(key, parameter.getValue());
}
if (paramsResult.getNextToken() != null) {
getParameters(paramsRequest.withNextToken(paramsResult.getNextToken()));
}
}
}
package nl.trifork.aws.paramstore;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ReflectionUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Builds a {@link CompositePropertySource} with various {@link AwsParamStorePropertySource} instances based on
* active profiles, application name and default context permutations. Mostly copied from Spring Cloud Consul's
* config support, but without the option to have full config files in a param value (as I don't see a use case
* for that with the AWS Parameter Store).
*/
public class AwsParamStorePropertySourceLocator implements PropertySourceLocator {
private AWSSimpleSystemsManagement ssmClient;
private AwsParamStoreProperties properties;
private List<String> contexts = new ArrayList<>();
private Log logger = LogFactory.getLog(getClass());
public AwsParamStorePropertySourceLocator(AWSSimpleSystemsManagement ssmClient,
AwsParamStoreProperties properties) {
this.ssmClient = ssmClient;
this.properties = properties;
}
public List<String> getContexts() {
return contexts;
}
@Override
public PropertySource<?> locate(Environment environment) {
if (!(environment instanceof ConfigurableEnvironment)) {
return null;
}
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
String appName = properties.getName();
if (appName == null) {
appName = env.getProperty("spring.application.name");
}
List<String> profiles = Arrays.asList(env.getActiveProfiles());
String prefix = this.properties.getPrefix();
String defaultContext = prefix + "/" + this.properties.getDefaultContext();
this.contexts.add(defaultContext + "/");
addProfiles(this.contexts, defaultContext, profiles);
String baseContext = prefix + "/" + appName;
this.contexts.add(baseContext + "/");
addProfiles(this.contexts, baseContext, profiles);
Collections.reverse(this.contexts);
CompositePropertySource composite = new CompositePropertySource("aws-param-store");
for (String propertySourceContext : this.contexts) {
try {
composite.addPropertySource(create(propertySourceContext));
} catch (Exception e) {
if (this.properties.isFailFast()) {
logger.error("Fail fast is set and there was an error reading configuration from AWS Parameter Store:\n"
+ e.getMessage());
ReflectionUtils.rethrowRuntimeException(e);
} else {
logger.warn("Unable to load AWS config from " + propertySourceContext, e);
}
}
}
return composite;
}
private AwsParamStorePropertySource create(String context) {
AwsParamStorePropertySource propertySource = new AwsParamStorePropertySource(context, this.ssmClient);
propertySource.init();
return propertySource;
}
private void addProfiles(List<String> contexts, String baseContext, List<String> profiles) {
for (String profile : profiles) {
contexts.add(baseContext + this.properties.getProfileSeparator() + profile + "/");
}
}
}
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
nl.trifork.aws.paramstore.AwsParamStoreBootstrapConfiguration
package nl.trifork.aws.paramstore;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathRequest;
import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathResult;
import com.amazonaws.services.simplesystemsmanagement.model.Parameter;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class AwsParamStorePropertySourceTest {
private AWSSimpleSystemsManagement ssmClient = mock(AWSSimpleSystemsManagement.class);
private AwsParamStorePropertySource propertySource =
new AwsParamStorePropertySource("/config/myservice/", ssmClient);
@Test
public void followsNextToken() {
GetParametersByPathResult firstResult = new GetParametersByPathResult()
.withNextToken("next")
.withParameters(
new Parameter().withName("/config/myservice/key1").withValue("value1"),
new Parameter().withName("/config/myservice/key2").withValue("value2")
);
GetParametersByPathResult nextResult = new GetParametersByPathResult()
.withParameters(
new Parameter().withName("/config/myservice/key3").withValue("value3"),
new Parameter().withName("/config/myservice/key4").withValue("value4")
);
when(ssmClient.getParametersByPath(any(GetParametersByPathRequest.class)))
.thenReturn(firstResult)
.thenReturn(nextResult);
propertySource.init();
assertThat(propertySource.getPropertyNames()).containsExactly("key1", "key2", "key3", "key4");
assertThat(propertySource.getProperty("key3")).isEqualTo("value3");
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment