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

안녕하세요, 하마연구소 입니다.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 파일에 나눠서 코딩하였습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 {
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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, wallet2 또는 wallet3, wallet4의 환경설정이 불러지지 않았을 것입니다.

왜 그럴까???

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/*
 * 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가 수행됩니다.

1
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 설정하는 방법은 아래와 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 {
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 초기화하는 코드입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 어노테이션에 의해서 수행되기 때문입니다.

감사합니다.

댓글

Popular Posts

AI 시대, SEO가 아닌 GEO에 포커싱해야 하는 이유

AI 메모리 HBM 외에 HBF도 주목

네이버 쇼핑 잘 나가네요, 구팡이 절대 강자인줄~