Spring Config Server S3 Multi Load Config

By | 2020년 10월 19일

# spring config에서 멀티 s3 config를 지원하는 기능은 spring-cloud-config v3.0.2에 contribute 되었습니다.

spring config server를 사용하다 보면 common 적인 설정은 따로 모아서 관리하고 싶을때가 있다.

a service
– a-serivce-dev.yml
– common-dev.yml

b service
– b-service-dev.yml
– common-dev.yml

다른 서비스이지만 공통적인 설정 부분은 하나의 파일로 관리하고 싶을때.

spring.cloud.config.name: a-service, common

요렇게 해주면 두개의 config 파일을 로딩이 가능하다.

하지만 config server의 backend가 s3일 경우에는 어느것의 설정도 찾이 못한다.

소스코드를 보면 콤마로 구분하지 않고 그냥 하나로 처리해 버린다.

“a-service-dev.yml”, “common-dev.yml” 요렇게 하지 않고

“a-server, common.yml” 요렇게 되어 버리는 것이다.

	@Override
	public Environment findOne(String specifiedApplication, String specifiedProfiles,
			String specifiedLabel) {
		final String application = StringUtils.isEmpty(specifiedApplication)
				? serverProperties.getDefaultApplicationName() : specifiedApplication;
		final String profiles = StringUtils.isEmpty(specifiedProfiles)
				? serverProperties.getDefaultProfile() : specifiedProfiles;
		final String label = StringUtils.isEmpty(specifiedLabel)
				? serverProperties.getDefaultLabel() : specifiedLabel;

		String[] profileArray = parseProfiles(profiles);

		final Environment environment = new Environment(application, profileArray);
		environment.setLabel(label);

		for (String profile : profileArray) {
			S3ConfigFile s3ConfigFile = getS3ConfigFile(application, profile, label);
			if (s3ConfigFile != null) {
				environment.setVersion(s3ConfigFile.getVersion());

				final Properties config = s3ConfigFile.read();
				config.putAll(serverProperties.getOverrides());
				StringBuilder propertySourceName = new StringBuilder().append("s3:")
						.append(application);
				if (profile != null) {
					propertySourceName.append("-").append(profile);
				}
				environment
						.add(new PropertySource(propertySourceName.toString(), config));
			}
		}

		return environment;
	}

그래서 해당 부분을 콤마로 구분하여 처리할 수 있게 수정해보았다.

  @Override
  public Environment findOne(String specifiedApplication, String specifiedProfiles,
      String specifiedLabel) {

    final String application = StringUtils.isEmpty(specifiedApplication)
        ? serverProperties.getDefaultApplicationName() : specifiedApplication;
    final String profiles = StringUtils.isEmpty(specifiedProfiles)
        ? serverProperties.getDefaultProfile() : specifiedProfiles;
    final String label = StringUtils.isEmpty(specifiedLabel)
        ? serverProperties.getDefaultLabel() : specifiedLabel;

    String[] profileArray = parseProfiles(profiles);

    String[] apps = new String[] { application };
    if (application != null) {
      apps = StringUtils.commaDelimitedListToStringArray(application.replace(" ",""));
    }

    final Environment environment = new Environment(application, profileArray);
    environment.setLabel(label);

    for (String profile : profileArray) {
      for (String app : apps) {
        S3ConfigFile s3ConfigFile = getS3ConfigFile(app, profile, label);
        if (s3ConfigFile != null) {
          environment.setVersion(s3ConfigFile.getVersion());

          final Properties config = s3ConfigFile.read();
          config.putAll(serverProperties.getOverrides());
          StringBuilder propertySourceName = new StringBuilder().append("s3:")
              .append(app);
          if (profile != null) {
            propertySourceName.append("-").append(profile);
          }
          environment
              .add(new PropertySource(propertySourceName.toString(), config));
        }
      }
    }

    return environment;
  }

해당 코드로 수정하면 멀티로 로드가 가능하다.

첨부된 풀 소스를 spring config server 프로젝트에 추가하면 동작되는것을 확인할 수 있다.

AwsS3EnvironmentRepository.java

package org.springframework.cloud.config.server.environment;


import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectIdBuilder;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.cloud.config.server.config.ConfigServerProperties;
import org.springframework.core.Ordered;
import org.springframework.core.io.InputStreamResource;
import org.springframework.util.StringUtils;

/**
 * @author Clay McCoy
 * @author Scott Frederick
 * @author David Choi
 */
@Log4j2
public class AwsS3EnvironmentRepository
    implements EnvironmentRepository, Ordered, SearchPathLocator {

  private static final String AWS_S3_RESOURCE_SCHEME = "s3://";

  private static final String PATH_SEPARATOR = "/";

  private final AmazonS3 s3Client;

  private final String bucketName;

  private final ConfigServerProperties serverProperties;

  protected int order = Ordered.LOWEST_PRECEDENCE;

  public AwsS3EnvironmentRepository(AmazonS3 s3Client, String bucketName,
      ConfigServerProperties server) {
    this.s3Client = s3Client;
    this.bucketName = bucketName;
    this.serverProperties = server;

    log.info("##### Multi S3 Repo Class Loaded. #####");

  }

  @Override
  public int getOrder() {
    return this.order;
  }

  public void setOrder(int order) {
    this.order = order;
  }

  @Override
  public Environment findOne(String specifiedApplication, String specifiedProfiles,
      String specifiedLabel) {

    final String application = StringUtils.isEmpty(specifiedApplication)
        ? serverProperties.getDefaultApplicationName() : specifiedApplication;
    final String profiles = StringUtils.isEmpty(specifiedProfiles)
        ? serverProperties.getDefaultProfile() : specifiedProfiles;
    final String label = StringUtils.isEmpty(specifiedLabel)
        ? serverProperties.getDefaultLabel() : specifiedLabel;

    String[] profileArray = parseProfiles(profiles);

    // add david
    String[] apps = new String[] { application };
    if (application != null) {
      apps = StringUtils.commaDelimitedListToStringArray(application.replace(" ",""));
    }

    final Environment environment = new Environment(application, profileArray);
    environment.setLabel(label);

    for (String profile : profileArray) {
      // add david
      for (String app : apps) {
        S3ConfigFile s3ConfigFile = getS3ConfigFile(app, profile, label);
        if (s3ConfigFile != null) {
          environment.setVersion(s3ConfigFile.getVersion());

          final Properties config = s3ConfigFile.read();
          config.putAll(serverProperties.getOverrides());
          StringBuilder propertySourceName = new StringBuilder().append("s3:")
              .append(app);
          if (profile != null) {
            propertySourceName.append("-").append(profile);
          }
          environment
              .add(new PropertySource(propertySourceName.toString(), config));
        }
      }
    }

    return environment;
  }

  private String[] parseProfiles(String profiles) {
    if (profiles.equals(serverProperties.getDefaultProfile())) {
      return new String[] { profiles, null };
    }
    return StringUtils.commaDelimitedListToStringArray(profiles);
  }

  private S3ConfigFile getS3ConfigFile(String application, String profile,
      String label) {
    String objectKeyPrefix = buildObjectKeyPrefix(application, profile, label);

    final S3ObjectIdBuilder s3ObjectIdBuilder = new S3ObjectIdBuilder()
        .withBucket(bucketName);

    return getS3ConfigFile(s3ObjectIdBuilder, objectKeyPrefix);
  }

  private String buildObjectKeyPrefix(String application, String profile,
      String label) {
    StringBuilder objectKeyPrefix = new StringBuilder();
    if (!StringUtils.isEmpty(label)) {
      objectKeyPrefix.append(label).append(PATH_SEPARATOR);
    }
    objectKeyPrefix.append(application);
    if (!StringUtils.isEmpty(profile)) {
      objectKeyPrefix.append("-").append(profile);
    }
    return objectKeyPrefix.toString();
  }

  private S3ConfigFile getS3ConfigFile(S3ObjectIdBuilder s3ObjectIdBuilder,
      String keyPrefix) {
    try {
      final S3Object properties = s3Client.getObject(new GetObjectRequest(
          s3ObjectIdBuilder.withKey(keyPrefix + ".properties").build()));
      return new PropertyS3ConfigFile(properties.getObjectMetadata().getVersionId(),
          properties.getObjectContent());
    }
    catch (Exception eProperties) {
      try {
        final S3Object yaml = s3Client.getObject(new GetObjectRequest(
            s3ObjectIdBuilder.withKey(keyPrefix + ".yml").build()));
        return new YamlS3ConfigFile(yaml.getObjectMetadata().getVersionId(),
            yaml.getObjectContent());
      }
      catch (Exception eYaml) {
        try {
          final S3Object json = s3Client.getObject(new GetObjectRequest(
              s3ObjectIdBuilder.withKey(keyPrefix + ".json").build()));
          return new JsonS3ConfigFile(json.getObjectMetadata().getVersionId(),
              json.getObjectContent());
        }
        catch (Exception eJson) {
          return null;
        }
      }
    }
  }

  @Override
  public Locations getLocations(String application, String profiles, String label) {
    String baseLocation = AWS_S3_RESOURCE_SCHEME + bucketName + PATH_SEPARATOR
        + application;

    return new Locations(application, profiles, label, null,
        new String[] { baseLocation });
  }

}

abstract class S3ConfigFile {

  private final String version;

  protected S3ConfigFile(String version) {
    this.version = version;
  }

  String getVersion() {
    return version;
  }

  abstract Properties read();

}

class PropertyS3ConfigFile extends S3ConfigFile {

  final InputStream inputStream;

  PropertyS3ConfigFile(String version, InputStream inputStream) {
    super(version);
    this.inputStream = inputStream;
  }

  @Override
  public Properties read() {
    Properties props = new Properties();
    try (InputStream in = inputStream) {
      props.load(in);
    }
    catch (IOException e) {
      throw new IllegalStateException("Cannot load environment", e);
    }
    return props;
  }

}

class YamlS3ConfigFile extends S3ConfigFile {

  final InputStream inputStream;

  YamlS3ConfigFile(String version, InputStream inputStream) {
    super(version);
    this.inputStream = inputStream;
  }

  @Override
  public Properties read() {
    final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
    try (InputStream in = inputStream) {
      yaml.setResources(new InputStreamResource(in));
      return yaml.getObject();
    }
    catch (IOException e) {
      throw new IllegalStateException("Cannot load environment", e);
    }
  }

}

class JsonS3ConfigFile extends YamlS3ConfigFile {

  // YAML is a superset of JSON, which means you can parse JSON with a YAML parser

  JsonS3ConfigFile(String version, InputStream inputStream) {
    super(version, inputStream);
  }

}