javaspringspring-bootspring-cloudspring-cloud-config

How to manage multiple .properties files in Spring Boot for different deploying environments?


I have a Spring Boot application where in the src/main/resources directory I have multiple .properties files which I'm using something like this given below via code:

@PropertySource(value = "classpath:properties/myfunctional.properties”)

Currently, I'm able to achieve my work with this as it's a simple functionality.

But the thing is keeping all these .properties files in src/main/resources is not a good idea because of the following reasons:

So, I have few questions around this:

Note: Few of the techniques I have tried with Spring Boot Config, Git Secret Variable and so on. But again I'm not sure on how much secure these techniques are.

So, I'm looking for an answer which will cover broader aspects.


Solution

  • You can achieve your requirements via Spring Cloud Config Server and Client.

    Please refer to this doc here.


    Github Approach:

    Step-By-Step Process where a Github repo will be acting as a source for all external properties files containing confidential/sensitive data.

    1. I have created and hosted all my secrets-{env}.properties files on my Github repo for testing and demonstration.

    enter image description here

    secrets-dev.properties:

    secrets.api-key=<dev-api-key>
    secrets.token=<dev-token>
    

    secrets-stage.properties:

    secrets.api-key=<stage-api-key>
    secrets.token=<stage-token>
    

    secrets-test.properties:

    secrets.api-key=<test-api-key>
    secrets.token=<test-token>
    

    secrets-prod.properties:

    secrets.api-key=<prod-api-key>
    secrets.token=<prod-token>
    
    1. You have to create and setup the Spring Boot Cloud Config Server (acting as an external server) which will take the responsibility of connecting to the Github external repository and provide the necessary way for the clients to get access to the properties files based on the environment.

    Project Structure:

    enter image description here

    pom.xml:

    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.1.4</version>
            <relativePath /> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example</groupId>
        <artifactId>sb-externalize-config-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>sb-externalize-config-server</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>17</java.version>
            <spring-cloud.version>2022.0.4</spring-cloud.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-config-server</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring-cloud.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    application.properties:

    spring.cloud.config.server.git.uri=https://github.com/anishb266/secrets-repo
    server.port=8081
    

    Key points to be noted:

    Startup Class:

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.config.server.EnableConfigServer;
    
    @SpringBootApplication
    @EnableConfigServer
    public class SbExternalizeConfigServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SbExternalizeConfigServerApplication.class, args);
        }
    
    }
    

    You have to use @EnableConfigServer to make it an external config server.

    Now, I will be running the external config server on port 8081.

    Output:

    Accessing one of the properties, i.e, secrets-test.properties like this:

    http://localhost:8081/{application}/{env} 
    
    application - secrets
    env - test
    

    enter image description here

    Config Server Log:

    enter image description here

    1. You have to create and setup the Spring boot Cloud Config client which will connect to config server and get the properties value based on the environment.

    Project Structure:

    enter image description here

    pom.xml:

    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.1.4</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example</groupId>
        <artifactId>sb-externalize-config-client</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>sb-externalize-config-client</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>17</java.version>
            <spring-cloud.version>2022.0.4</spring-cloud.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring-cloud.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    application.properties:

    spring.config.import=optional:configserver:http://localhost:8081
    server.port=8080
    spring.application.name=secrets
    spring.profiles.active=test
    

    Key points to be noted here:

    SecretsProperties:

    package com.example;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @ConfigurationProperties("secrets")
    public class SecretsProperties {
    
        private String apiKey;
    
        private String token;
    
        public SecretsProperties(String apiKey, String token) {
            this.apiKey = apiKey;
            this.token = token;
        }
    
        @Override
        public String toString() {
            return "SecretsProperties [apiKey=" + apiKey + ", token=" + token + "]";
        }
    
        public String getApiKey() {
            return apiKey;
        }
    
        public void setApiKey(String apiKey) {
            this.apiKey = apiKey;
        }
    
        public String getToken() {
            return token;
        }
    
        public void setToken(String token) {
            this.token = token;
        }
    
    }
    

    This class is to map the loaded properties.

    Startup class:

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    
    @SpringBootApplication
    @EnableConfigurationProperties(SecretsProperties.class)
    public class SbExternalizeConfigClientApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SbExternalizeConfigClientApplication.class, args);
        }
    
    }
    

    TestController for testing:

    package com.example;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class TestController {
    
        private SecretsProperties properties;
    
        public TestController(SecretsProperties properties) {
            this.properties = properties;
        }
    
        @GetMapping("test-external-properties")
        public SecretsProperties testExternalProperties() {
            return properties;
        }
    
    }
    

    Now, I'm running the client at 8080 with profiles test.

    Client Log:

    enter image description here

    Output:

    enter image description here

    Providing a part of the External Config server log to proof that client was able to pick the desired properties with test profile via config server with the application-name secrets:

    2023-10-16T18:04:48.433+05:30  INFO 79167 --- [nio-8081-exec-2] o.s.c.c.s.e.NativeEnvironmentRepository  : Adding property source: Config resource 'file [/var/folders/yq/m5hjv94j18586b1gzwzjg1tw0000gn/T/config-repo-5291131953887905898/secrets-test.properties]' via location 'file:/var/folders/yq/m5hjv94j18586b1gzwzjg1tw0000gn/T/config-repo-5291131953887905898/'
    

    That's all.


    HashiCorp Vault Approach:

    If you are looking for a more secured and encrypted way of saving properties, then go for HashiCorp Vault.

    Note: Vault acts as an external server for keeping secrets. So, you don't need to create an external server from scratch.

    Read the Spring Boot docs here on how to do it.

    From Docs:

    Vault is a secrets management system allowing you to store sensitive data which is encrypted at rest. It’s ideal to store sensitive configuration details such as passwords, encryption keys, API keys.

    Process is quite similar but there will some changes.

    Add this dependency in the pom.xml:

    <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-vault-config</artifactId>. 
    </dependency>
    

    You don't need this dependency below anymore:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
    

    Kindly remove this.

    pom.xml will look like this:

    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.1.4</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example</groupId>
        <artifactId>sb-externalize-config-client</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>sb-externalize-config-client</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>17</java.version>
            <spring-cloud.version>2022.0.4</spring-cloud.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-vault-config</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring-cloud.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    Set application.properties with the below properties:

    spring.application.name=secrets
    spring.cloud.vault.token=abc
    spring.cloud.vault.scheme=http
    spring.cloud.vault.kv.enabled=true
    spring.cloud.vault.kv.default-context=
    spring.config.import=vault://secret/${spring.application.name}/${spring.profiles.active}
    spring.profiles.active=test
    

    Points to be noted:

    The TestController, SecretsProperties and SbExternalizeConfigClientApplication remain the same.

    Output:

    I'm running the client for test profile.

    enter image description here

    Note: This was a very basic approach that I showed with Vault.

    Read docs for more info on how to do more advanced setup for Vault.