データベースのパスワードを application.properties に生で書くのは抵抗があるので、Secret の値を使えるようにできないかなと思い調べてみました。

結論から言うと、できます。

やりかたとしては、独自のプロパティソースを使う感じにするのが良さそうです。

ググるとネット上にもちらほら情報が転がっていますが、以下のように EnvironmentPostProcessor インタフェースを実装して登録することになります。

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
package jp.stmy.myapp.env;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.DeferredLog;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.stereotype.Component;

@Order(Ordered.LOWEST_PRECEDENCE)
public class SecretVariableProcessor 
    implements EnvironmentPostProcessor, 
               ApplicationListener<ApplicationEvent> {

    private static final DeferredLog log = new DeferredLog();

    public final String SECRET_PREFIX = "secret--";
    public final String SECRET_DIR = "/run/secrets";
    
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        
        final var secretDir = Path.of(SECRET_DIR).toFile();

        // Do nothing if secrets directory is not exists or not a directory
        if (!secretDir.exists() || !secretDir.isDirectory()) {
            return;
        }

        // Enumerate secret files
        final var secretFiles = Stream.of(secretDir.listFiles())
            .filter(file -> file.isFile())
            .collect(Collectors.toList());

        // Create key-value map of secrets
        final var secretMap = new HashMap<String, Object>();
        secretFiles.forEach(file -> {
            try {
                final var key = SECRET_PREFIX + file.getName();
                final var value = Files.readString(file.toPath());
                secretMap.put(key, value);
                log.info("Secret value added: " + key);
            } catch (IOException e) {
                log.error(e.getMessage(), e);
                System.out.println(e.getMessage());
            }
        });
        
        // Create secret property source, and add it to the environment
        final var propertySource = new MapPropertySource("secrets", secretMap);
        environment.getPropertySources().addLast(propertySource);
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        log.replayTo(SecretVariableProcessor.class);
    }
    
}

やってることとしては非常に単純で、 SECRET_DIR 配下のファイルについて、secret--ファイル名 をキー、ファイルの内容を値としたプロパティソースを最低優先度で追加してあげているだけです。

SECRET_DIR の内容は本来設定ファイルに書けるようにすべきですが、完全に手を抜いています。

このクラスは完全に初期化する前に実行されるため、 ApplicationListener<ApplicationEvent> を実装して、ロギングが遅延実行されるようにしています。

Spring への登録は src/main/resources/META-INF/spring.factories に以下のように行います。

1
org.springframework.boot.env.EnvironmentPostProcessor=jp.stmy.myapp.env.SecretVariableProcessor

実際に以下のような感じで使います。

1
2
3
4
5
spring.datasource.url=jdbc:postgresql://db:5432/${secret--postgres_db}
spring.datasource.username=${secret--postgres_user}
spring.datasource.password=${secret--postgres_passwd}
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

パスワードやユーザ名が設定ファイルから消えました!

なかなかいい感じですね。