Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions docs/src/main/asciidoc/appconfig.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
[#spring-cloud-aws-app-config]
== AppConfig Integration

https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html[AppConfig] helps store configuration safely in the cloud. The service also supports continuous fetching of changes so they can be applied during runtime.

Spring Cloud AWS adds support for loading configuration from AppConfig through the Spring Boot https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config-files-importing[config import feature].

Maven coordinates, using <<index.adoc#bill-of-materials, Spring Cloud AWS BOM>>:

[source,xml]
----
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-app-config</artifactId>
</dependency>
----

=== Loading External Configuration

To fetch configuration from AppConfig and add it to Spring's environment properties, add the `spring.config.import` property to `application.properties`:

When specifying the key, all of the following values must be set:

1. ApplicationName or ApplicationIdentifier
2. ConfigurationProfileName or ConfigurationProfileIdentifier
3. EnvironmentName or EnvironmentIdentifier

Spring Cloud AWS AppConfig supports the following file types:

1. YAML
2. Plain text (properties)
3. JSON

For example, assuming that the ApplicationName in AppConfig is `cool-spring-application`, ConfigurationProfileName is `ProductionConfig`, and EnvironmentName is `prod`:

[source,properties]
----
spring.config.import=aws-app-config:cool-spring-application#ProductionConfig#prod
----

If a config with the given values does not exist in AppConfig, the application will fail to start. If the config file is not required for the application and it should continue to start even when the file is missing, add `optional` before the prefix:

[source,properties]
----
spring.config.import=optional:aws-app-config:cool-spring-application#ProductionConfig#prod
----

To load multiple configurations, separate their names with `;`:

[source,properties]
----
spring.config.import=aws-app-config:cool-spring-application#ProductionConfig#prod;new-spring-application#ProdConfig#prod2
----

If some config files are required and others are optional, list them as separate entries in the `spring.config.import` property:

[source,properties]
----
spring.config.import[0]=optional:aws-app-config:new-spring-application#ProdConfig#prod
spring.config.import[1]=aws-app-config:cool-spring-application#ProductionConfig#prod2
----

Fetched config file properties can be referenced with `@Value`, bound to `@ConfigurationProperties` classes, or referenced in the `application.properties` file.

[source,java]
----
@Value("${username}")
private String username;

@Value("${password}")
private String password;
----

=== Using AppConfigDataClient

The starter automatically configures and registers a `AppConfigDataClient` bean in the Spring application context. The `AppConfigDataClient` bean can be used to retrieve configuration files imperatively.

=== Customizing AppConfigDataClient

To use a custom `AppConfigDataClient` in `spring.config.import`, provide an implementation of `BootstrapRegistryInitializer`. For example:

[source,java]
----
package com.app;


import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.BootstrapRegistryInitializer;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient;

public class AppConfigBootstrapConfiguration implements BootstrapRegistryInitializer {

@Override
public void initialize(BootstrapRegistry registry) {
registry.register(AppConfigDataClient.class, context -> {
AwsCredentialsProvider awsCredentialsProvider = StaticCredentialsProvider.create(AwsBasicCredentials.create("yourAccessKey", "yourSecretKey"));
return AppConfigDataClient.builder().credentialsProvider(awsCredentialsProvider).region(Region.EU_WEST_2).build();
});
}
}
----

Note that this class must be listed under the `org.springframework.boot.BootstrapRegistryInitializer` key in `META-INF/spring.factories`:

[source,properties]
----
org.springframework.boot.BootstrapRegistryInitializer=com.app.AppConfigBootstrapConfiguration
----


If you want to use autoconfigured `AppConfigDataClient` but change underlying SDKClient or `ClientOverrideConfiguration` you will need to register bean of type `AppConfigClientCustomizer`:
Autoconfiguration will configure `AppConfigDataClient` Bean with provided values after that, for example:

[source,java]
----
package com.app;

import io.awspring.cloud.autoconfigure.config.appconfig.AppConfigClientCustomizer;
import java.time.Duration;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.BootstrapRegistryInitializer;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder;

class AppConfigBootstrapConfiguration implements BootstrapRegistryInitializer {

@Override
public void initialize(BootstrapRegistry registry) {
registry.register(AppConfigClientCustomizer.class, context -> (builder -> {
builder.overrideConfiguration(builder.overrideConfiguration().copy(c -> {
c.apiCallTimeout(Duration.ofMillis(2001));
}));
}));
}
}
----

=== Configuration

The Spring Boot Starter for AppConfig provides the following configuration options:

[cols="2,3,1,1"]
|===
| Name | Description | Required | Default value
| `spring.cloud.aws.appconfig.enabled` | Enables the AppConfig integration. | No | `true`
| `spring.cloud.aws.appconfig.endpoint` | Configures the endpoint used by `AppConfigDataClient`. | No | `null`
| `spring.cloud.aws.appconfig.region` | Configures the region used by `AppConfigDataClient`. | No | `null`
| `spring.cloud.aws.appconfig.separator` | Configures the separator used to split the import key into application, profile, and environment. | No | `#`
|===

=== IAM Permissions

The following IAM permissions are required by Spring Cloud AWS:

[cols="2"]
|===
| Start session | `appconfig:StartConfigurationSession`
| Get configuration | `appconfig:GetLatestConfiguration`
|===

Sample IAM policy granting access to AppConfig:

[source,json,indent=0]
----
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"appconfig:GetLatestConfiguration",
"appconfig:StartConfigurationSession"
],
"Resource": "yourARN"
}
]
}
----
2 changes: 2 additions & 0 deletions docs/src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ include::spring-modulith.adoc[]

include::kinesis-stream-binder.adoc[]

include::appconfig.adoc[]

include::testing.adoc[]

include::docker-compose.adoc[]
Expand Down
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<module>spring-cloud-aws-sns</module>
<module>spring-cloud-aws-sqs</module>
<module>spring-cloud-aws-dynamodb</module>
<module>spring-cloud-aws-app-config</module>
<module>spring-cloud-aws-kinesis</module>
<module>spring-cloud-aws-kinesis-stream-binder</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-integration-kinesis</module>
Expand All @@ -57,6 +58,7 @@
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-metrics</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-parameter-store</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-s3</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-app-config</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-integration-s3</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-secrets-manager</module>
<module>spring-cloud-aws-starters/spring-cloud-aws-starter-ses</module>
Expand Down
37 changes: 37 additions & 0 deletions spring-cloud-aws-app-config/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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">
<parent>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws</artifactId>
<version>4.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-cloud-aws-app-config</artifactId>
<name>Spring Cloud AWS App Config Integration</name>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>appconfigdata</artifactId>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright 2013-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.awspring.cloud.appconfig;

import io.awspring.cloud.core.config.AwsPropertySource;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.InputStreamResource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient;
import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest;
import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse;
import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest;

/**
* Retrieves configuration property sources path from the AWS AppConfig using the provided {@link AppConfigDataClient}.
*
* @author Matej Nedic
* @since 4.1.0
*/
public class AppConfigPropertySource extends AwsPropertySource<AppConfigPropertySource, AppConfigDataClient> {
private static final String YAML_TYPE = "application/x-yaml";
private static final String YAML_TYPE_ALTERNATIVE = "text/yaml";
private static final String TEXT_TYPE = "text/plain";
private static final String JSON_TYPE = "application/json";

private final RequestContext context;
private final AppConfigDataClient appConfigClient;
private String sessionToken;
private final Map<String, Object> properties;

public AppConfigPropertySource(RequestContext context, AppConfigDataClient appConfigClient) {
this(context, appConfigClient, null, null);
}

public AppConfigPropertySource(RequestContext context, AppConfigDataClient appConfigClient,
@Nullable String sessionToken, Map<String, Object> properties) {
super("aws-appconfig:" + context, appConfigClient);
Assert.notNull(context, "context is required");
this.context = context;
this.appConfigClient = appConfigClient;
this.sessionToken = sessionToken;
this.properties = properties != null ? properties : new LinkedHashMap<>();
}

@Override
public void init() {
if (!StringUtils.hasText(sessionToken)) {
var request = StartConfigurationSessionRequest.builder()
.applicationIdentifier(context.getApplicationIdentifier())
.environmentIdentifier(context.getEnvironmentIdentifier())
.configurationProfileIdentifier(context.getConfigurationProfileIdentifier()).build();
sessionToken = appConfigClient.startConfigurationSession(request).initialConfigurationToken();
}

GetLatestConfigurationRequest request = GetLatestConfigurationRequest.builder().configurationToken(sessionToken)
.build();
GetLatestConfigurationResponse response = this.source.getLatestConfiguration(request);
if (response.configuration().asByteArray().length > 0) {
properties.clear();
var props = switch (response.contentType()) {
case TEXT_TYPE -> readProperties(response.configuration().asInputStream());
case YAML_TYPE, YAML_TYPE_ALTERNATIVE, JSON_TYPE -> readYaml(response.configuration().asInputStream());
default -> throw new IllegalStateException("Cannot parse unknown content type: " + response.contentType());
};
for (Map.Entry<Object, Object> entry : props.entrySet()) {
properties.put(String.valueOf(entry.getKey()), entry.getValue());
}
}
sessionToken = response.nextPollConfigurationToken();
}

@Override
public AppConfigPropertySource copy() {
return new AppConfigPropertySource(context, appConfigClient, sessionToken, new LinkedHashMap<>(properties));
}

@Override
public String[] getPropertyNames() {
return properties.keySet().toArray(String[]::new);
}

@Override
public @Nullable Object getProperty(String name) {
return this.properties.get(name);
}

private Properties readProperties(InputStream inputStream) {
Properties properties = new Properties();
try (InputStream in = inputStream) {
properties.load(in);
}
catch (Exception e) {
throw new IllegalStateException("Cannot load environment", e);
}
return properties;
}

private Properties readYaml(InputStream inputStream) {
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
try (InputStream in = inputStream) {
yaml.setResources(new InputStreamResource(in));
return yaml.getObject();
}
catch (Exception e) {
throw new IllegalStateException("Cannot load environment", e);
}
}
}
Loading