[Spring] spring-vault에서 @VaultPropertySource 여러개 사용하면 안되는 이유

스프링 기반의 어플리케이션을 개발할 때, 환경설정으로 보안정보를 다루기 위하여 Vault를 이용하게 된다.
일반적으로 가장 간단하게 Vault를 이용하는 방법은 @VaultPropertySource 또는 @VaultPropertySources 어노테이션을 사용하는 것이다.

import org.springframework.context.annotation.Configuration;
import org.springframework.vault.annotation.VaultPropertySource;

@Configuration
@VaultPropertySource(value = {
        "secret/hippolab/wallet1",
        "secret/hippolab/wallet2",
        "secret/hippolab/wallet3",
        "secret/hippolab/wallet4"
})
public class VaultConfig {
}

위 샘플코드에서는 wallet1, wallet2, wallet3, wallet4에 선언된 모든 환경설정은 어플리케이션 로딩시에 스프링 MutablePropertySources에 포함되어 ${vault.properties1}과 같이 쉽게 사용할 수 있다.

그러나 어플리케이션을 만들다보면 여러개의 @VaultPropertySource 어노테이션을 서로 다른 파일에 작성해야할 경우가 생긴다.
이럴때는 아래와 같이 VaultConfig1.java와 VaultConfig2.java 파일에 나눠서 코딩할 것이다.

import org.springframework.context.annotation.Configuration;
import org.springframework.vault.annotation.VaultPropertySource;

@Configuration
@VaultPropertySource(value = {
        "secret/hippolab/wallet1",
        "secret/hippolab/wallet2",
        "secret/hippolab/wallet3",
        "secret/hippolab/wallet4"
})
public class VaultConfig1 {
}
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.annotation.VaultPropertySource;

@Configuration
@VaultPropertySource(value = {
        "secret/hippolab/wallet5",
        "secret/hippolab/wallet6"
})
public class VaultConfig2 {
}

이렇게하고 어플리케이션을 실행하면 wallet1, wallet2, wallet3, wallet4, wallet5, wallet6의 모든 환경설정이 포함되지 않은 것을 확인할 수 있다.
VaultConfig1과 VaultConfig2가 처리되는 순서에 따라 다르겠지만, wallet1,2 또는 wallet3,4의 환경설정이 불러지지 않았을 것이다.

왜 그럴까?

원인은 VaultPropertySourceRegistrar.java 파일안에 있다.
참고로 spring-vault-core 버전 1.1.3.RELEASE 로 설명하겠다.

/*
 * Copyright 2016-2018 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
 *
 *      http://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 org.springframework.vault.annotation;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.annotation.VaultPropertySource.Renewal;
import org.springframework.vault.core.lease.domain.RequestedSecret;
import org.springframework.vault.core.util.PropertyTransformer;
import org.springframework.vault.core.util.PropertyTransformers;

/**
 * Registrar to register {@link org.springframework.vault.core.env.VaultPropertySource}s
 * based on {@link VaultPropertySource}.
 * <p>
 * This class registers potentially multiple property sources based on different Vault
 * paths. {@link org.springframework.vault.core.env.VaultPropertySource}s are resolved and
 * added to {@link ConfigurableEnvironment} once the bean factory is post-processed. This
 * allows injection of Vault properties and and lookup using the
 * {@link org.springframework.core.env.Environment}.
 *
 * @author Mark Paluch
 */
class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar,
		BeanFactoryPostProcessor {

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
			throws BeansException {

		ConfigurableEnvironment env = beanFactory.getBean(ConfigurableEnvironment.class);
		MutablePropertySources propertySources = env.getPropertySources();

		registerPropertySources(
				beanFactory.getBeansOfType(
						org.springframework.vault.core.env.VaultPropertySource.class)
						.values(), propertySources);

		registerPropertySources(
				beanFactory
						.getBeansOfType(
								org.springframework.vault.core.env.LeaseAwareVaultPropertySource.class)
						.values(), propertySources);
	}

	private void registerPropertySources(
			Collection<? extends PropertySource<?>> propertySources,
			MutablePropertySources mutablePropertySources) {

		for (PropertySource<?> vaultPropertySource : propertySources) {

			if (propertySources.contains(vaultPropertySource.getName())) {
				continue;
			}

			mutablePropertySources.addLast(vaultPropertySource);
		}
	}

	@Override
	public void registerBeanDefinitions(AnnotationMetadata annotationMetadata,
			BeanDefinitionRegistry registry) {

		Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!");
		Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");

		registry.registerBeanDefinition("VaultPropertySourceRegistrar",
				BeanDefinitionBuilder //
						.rootBeanDefinition(VaultPropertySourceRegistrar.class) //
						.setRole(BeanDefinition.ROLE_INFRASTRUCTURE) //
						.getBeanDefinition());

		Set<AnnotationAttributes> propertySources = attributesForRepeatable(
				annotationMetadata, VaultPropertySources.class.getName(),
				VaultPropertySource.class.getName());

		int counter = 0;

		for (AnnotationAttributes propertySource : propertySources) {

			String[] paths = propertySource.getStringArray("value");
			String ref = propertySource.getString("vaultTemplateRef");
			String propertyNamePrefix = propertySource.getString("propertyNamePrefix");
			Renewal renewal = propertySource.getEnum("renewal");

			Assert.isTrue(paths.length > 0,
					"At least one @VaultPropertySource(value) location is required");

			Assert.hasText(ref,
					"'vaultTemplateRef' in @EnableVaultPropertySource must not be empty");

			PropertyTransformer propertyTransformer = StringUtils
					.hasText(propertyNamePrefix) ? PropertyTransformers
					.propertyNamePrefix(propertyNamePrefix) : PropertyTransformers.noop();

			for (String propertyPath : paths) {

				if (!StringUtils.hasText(propertyPath)) {
					continue;
				}

				AbstractBeanDefinition beanDefinition = createBeanDefinition(ref,
						renewal, propertyTransformer, propertyPath);

				registry.registerBeanDefinition("vaultPropertySource#" + counter,
						beanDefinition);

				counter++;
			}
		}
	}

	private AbstractBeanDefinition createBeanDefinition(String ref, Renewal renewal,
			PropertyTransformer propertyTransformer, String propertyPath) {

		BeanDefinitionBuilder builder;

		if (isRenewable(renewal)) {
			builder = BeanDefinitionBuilder
					.rootBeanDefinition(org.springframework.vault.core.env.LeaseAwareVaultPropertySource.class);

			RequestedSecret requestedSecret = renewal == Renewal.ROTATE ? RequestedSecret
					.rotating(propertyPath) : RequestedSecret.renewable(propertyPath);

			builder.addConstructorArgValue(propertyPath);
			builder.addConstructorArgReference("secretLeaseContainer");
			builder.addConstructorArgValue(requestedSecret);
		}
		else {
			builder = BeanDefinitionBuilder
					.rootBeanDefinition(org.springframework.vault.core.env.VaultPropertySource.class);

			builder.addConstructorArgValue(propertyPath);
			builder.addConstructorArgReference(ref);
			builder.addConstructorArgValue(propertyPath);
		}

		builder.addConstructorArgValue(propertyTransformer);
		builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);

		return builder.getBeanDefinition();
	}

	private boolean isRenewable(Renewal renewal) {
		return renewal == Renewal.RENEW || renewal == Renewal.ROTATE;
	}

	@SuppressWarnings("unchecked")
	static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
			String containerClassName, String annotationClassName) {

		Set<AnnotationAttributes> result = new LinkedHashSet<AnnotationAttributes>();
		addAttributesIfNotNull(result,
				metadata.getAnnotationAttributes(annotationClassName, false));

		Map<String, Object> container = metadata.getAnnotationAttributes(
				containerClassName, false);
		if (container != null && container.containsKey("value")) {
			for (Map<String, Object> containedAttributes : (Map<String, Object>[]) container
					.get("value")) {
				addAttributesIfNotNull(result, containedAttributes);
			}
		}
		return Collections.unmodifiableSet(result);
	}

	private static void addAttributesIfNotNull(Set<AnnotationAttributes> result,
			Map<String, Object> attributes) {
		if (attributes != null) {
			result.add(AnnotationAttributes.fromMap(attributes));
		}
	}
}

@VaultPropertySource 어노테이션 하나씩마다 VaultPropertySourceRegistrar가 수행되며, registerBeanDefinitions()에 의해서 @VaultPropertySource 어노테이션으로 선언된 wallet들을 하나씩 VaultPropertySource 빈으로 등록된다.
이때 여러 파일에 있는 @VaultPropertySource 어노테이션을 한방에 참조하여 한방에 등록되는 것이 아니라, 각각의 @VaultPropertySource 어노테이션별로 VaultPropertySourceRegistrar가 수행된다.

registry.registerBeanDefinition("vaultPropertySource#" + counter, beanDefinition);

이 부분에서 빈으로 등록되는데 위 샘플 VaultConfig1에 정의된 wallet1, 2, 3, 4는 vaultPropertySource#1, vaultPropertySource#2, vaultPropertySource#3, vaultPropertySource#4 빈으로 등록된다.
그후에 다시 VaultPropertySourceRegistrar가 수행되며 VaultConig2에 정의된 wallet5, 6은 vaultPropertySource#1, vaultPropertySource#2 빈으로 등록된다.
이렇게되면 VaultConfig1에서 먼저 등록된 vaultPropertySource#1, vaultPropertySource#2는 overwriting 된다.
VaultConfig1과 VaultConfig2가 로딩되는 순서 차이는 있겠지만, 어쨌든 우리가 원했던 2개 wallet은 등록되지 않는다.

그럼 어떻게 하면 될까?

결론은 @VaultPropertySource 어노테이션을 여러개 사용할 수 없다는 것이다.
하지만 서로 다른 Java 소스파일에 최대한 @VaultPropertySource 어노테이션 처럼 간단하게 Vault 설정하는 방법은 아래와 같다.

import org.springframework.context.annotation.Configuration;
import org.springframework.vault.annotation.VaultPropertySource;

@Configuration
@VaultPropertySource(value = {
        "secret/hippolab/wallet1",
        "secret/hippolab/wallet2",
        "secret/hippolab/wallet3",
        "secret/hippolab/wallet4"
})
public class VaultConfig3 {
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.core.env.VaultPropertySource;

@Configuration
public class VaultConfig4 {
    @Bean
    public VaultPropertySource wallet5VaultPropertySource(VaultTemplate vaultTemplate) {
        return new org.springframework.vault.core.env.VaultPropertySource(vaultTemplate, "secret/hippolab/wallet5");
    }

    @Bean
    public VaultPropertySource wallet6VaultPropertySource(VaultTemplate vaultTemplate) {
        return new org.springframework.vault.core.env.VaultPropertySource(vaultTemplate, "secret/hippolab/wallet6");
    }
}

여기서는 VaultConfig3.java 기존처럼 @VaultPropertySource 어노테이션을 이용하고, VaultConfig4.java는 원하는 wallet에 해당하는 VaultPropertySource 빈을 직접 생성한다.
이럴경우에 VaultConig3의 @VaultPropertySource 어노테이션에 의해서 VaultPropertySourceRegistrar가 수행되고 postProcessBeanFactory()와 registerPropertySources()에서 등록된 모든 VaultPropertySource 빈을 찾아서 MutablePropertySources에 추가한다.
VaultConfig1과 VaultConfig2의 @VaultPropertySource 어노테이션 처럼 overwriting되는 VaultPropertySource 빈이 없다.

참고로 아래는 Vault 초기화하는 코드이다.

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.vault.annotation.VaultPropertySource;
import org.springframework.vault.config.EnvironmentVaultConfiguration;

/**
 * Vault 초기화
 */
@Configuration
@Import(EnvironmentVaultConfiguration.class)
@VaultPropertySource(value = "")
public class InitializeVaultConfig {
}

값이 빈 @VaultPropertySource 어노테이션을 선언한 이유는 만약 VaultConfig3이 없고 VaultConfig4만 사용할 경우에도 VaultPropertySource 빈 MutablePropertySources에 추가되기 위함이다.
VaultPropertySourceRegistrar는 @VaultPropertySource 어노테이션에 의해서 수행되기 때문이다.

 

댓글(1)

  • stranger
    2019.08.23 17:46

    안녕하세요
    요문제 고쳐진거 아닌가 해서 댓글 남겨요.

    https://github.com/spring-projects/spring-vault/issues/421

Designed by JB FACTORY