---
title: Setting Kubernetes Secret Values as Spring Boot Application Properties with Config Tree Introduced in Spring Boot 2.4
tags: ["Spring Boot", "Config Data API", "Kubernetes"]
categories: ["Programming", "Java", "org", "springframework", "boot", "context", "config"]
date: 2020-10-23T08:29:13Z
updated: 2020-11-17T05:21:38Z
---

> ⚠️ This article was automatically translated by OpenAI (gpt-4o).
> It may be edited eventually, but please be aware that it may contain incorrect information at this time.

**Table of Contents**
<!-- toc -->

### Config Data API

Spring Boot 2.4 introduced the [Config Data API](https://docs.spring.io/spring-boot/docs/2.4.0-SNAPSHOT/reference/html/spring-boot-features.html#boot-features-external-config-files-importing) for loading additional properties, allowing you to specify additional properties with the `spring.config.import` property. These properties take precedence over the default ones.

This mechanism can be implemented with the `org.springframework.boot.context.config.ConfigDataLocationResolver` and `ConfigDataLoader` interfaces. In Spring Boot 2.4, the following are provided:

* `org.springframework.boot.context.config.StandardConfigDataLocationResolver` / `org.springframework.boot.context.config.StandardConfigDataLoader`
* `org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver` / `org.springframework.boot.context.config.ConfigTreeConfigDataLoader`

The former is a class for loading standard properties files or YAML files, allowing settings like `spring.config.import=file:/etc/config/foo.properties`.

The latter supports loading file hierarchies called [Configuration Trees](https://docs.spring.io/spring-boot/docs/2.4.0-SNAPSHOT/reference/html/spring-boot-features.html#boot-features-external-config-files-configtree), allowing settings like `spring.config.import=configtree:/etc/config/myapp/`.

This article explains Config Trees.

> Additionally, various Spring Cloud projects also [support the Config Data API](https://spring.io/blog/2020/10/07/spring-cloud-2020-0-0-m4-aka-ilford-is-available).
> * Spring Cloud Consul: `spring.config.import=consul:...`
> * Spring Cloud Config: `spring.config.import=configserver:...`
> * Spring Cloud Zookeeper: `spring.config.import=zookeeper:...`
> * Spring Cloud Vault: `spring.config.import=vault:...`

### Config Tree

A Config Tree refers to a file structure like the following:

```
/etc/config
`-- myapp
    `-- spring
        `-- security
            `-- user
                |-- name
                `-- password
```

Assume the `username` and `password` files contain the following:

```
echo admin > /etc/config/myapp/spring/security/user/name
echo pa33w0rd > /etc/config/myapp/spring/security/user/password
```

With these files in place, if you set:

```properties
spring.config.import=configtree:/etc/config/myapp/
```

then,

* The `spring.security.user.name` property will be set to `admin`
* The `spring.security.user.password` property will be set to `pa33w0rd`

### Why was Config Tree supported?

Those familiar with Kubernetes will recognize that this hierarchy maps keys and values when ConfigMaps or Secrets are mounted as files.

Previously, for example, if you wanted to set a Secret to `spring.security.user.*`, you might map it to environment variables like this:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: admin-user
stringData:
  username: admin
  password: pa33w0rd
```

You might often map it to environment variables like this. I do it this way:

```yaml
env:
- name: SPRING_SECURITY_USER_NAME
  valueFrom:
    secretKeyRef:
      name: admin-user
      key: username
- name: SPRING_SECURITY_USER_PASSWORD
  valueFrom:
    secretKeyRef:
      name: admin-user
      key: password
```

[The Twelve-Factor App](https://12factor.net/config) recommends storing configuration in environment variables.

However, this method differs from [Kubernetes Best Practices](https://kubernetes.io/docs/concepts/configuration/secret/#security-properties). Contents stored in environment variables are written to disk (stored in the `/proc/{pid}/environ` file). When Kubernetes Secrets are mounted as files, the contents are not written to disk but stored in `tmpfs`, and are deleted along with the Pod. Therefore, it is introduced as a more secure practice than storing in environment variables (not saying environment variables are bad).

If you want to set the contents of a Secret to `spring.security.user.*` via file mount, you could set the contents of the properties file itself to the Secret like this:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-config
stringData:
  application.properties: |-
    spring.security.user.name=admin
    spring.security.user.password=pa33w0rd
```

And mount it like this:

```yaml
spec:
  containers:
  - name: my-app
    # ...
    volumeMounts:
    - name: app-config
      mountPath: /config
      readOnly: true
  volumes:
  - name: app-config
    secretName:
      name: app-config
```

This works, but the Secret format is app-specific and not generic, which might be inconvenient if you want to share the Secret with other resources.

Config Tree is useful when you want to directly (natively) set the properties configured in the original Secret to the app. When loading a Config Tree, the settings would be as follows:

```yaml
spec:
  containers:
  - name: my-app
    # ...
    env:
    - name: SPRING_CONFIG_IMPORT
      value: configtree:/workspace/config/
    volumeMounts:
    - name: admin-user
      mountPath: /workspace/config/spring/security/user
      readOnly: true
  volumes:
  - name: admin-user
    secret:
      secretName: admin-user
```

### Trying it out

Let's implement a case where we want to pass connection information to an application accessing PostgreSQL via Secret.

#### Creating the application

Create a simple API server.

```
curl https://start.spring.io/starter.tgz \
    -d artifactId=car-api \
    -d bootVersion=2.4.0-SNAPSHOT \
    -d baseDir=car-api \
    -d dependencies=web,jdbc,flyway,actuator,postgresql \
    -d packageName=com.example \
    -d applicationName=CarApiApplication | tar -xzvf -
```

```java
cd car-api
cat <<EOF > src/main/java/com/example/CarController.java
package com.example;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.sql.PreparedStatement;
import java.util.List;

@RestController
public class CarController {

    private final JdbcTemplate jdbcTemplate;

    public CarController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @GetMapping(path = "/cars")
    public ResponseEntity<?> getCars() {
        final List<Car> cars = this.jdbcTemplate.query("SELECT id, name FROM car ORDER BY id", (rs, i) -> new Car(rs.getInt("id"), rs.getString("name")));
        return ResponseEntity.ok(cars);
    }

    @PostMapping(path = "/cars")
    public ResponseEntity<?> postCars(@RequestBody Car car) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        this.jdbcTemplate.update(connection -> {
            final PreparedStatement statement = connection.prepareStatement("INSERT INTO car(name) VALUES (?)", new String[]{"id"});
            statement.setString(1, car.getName());
            return statement;
        }, keyHolder);
        car.setId(keyHolder.getKey().intValue());
        return ResponseEntity.status(HttpStatus.CREATED).body(car);
    }

    @DeleteMapping(path = "/cars/{id}")
    public ResponseEntity<?> deleteCar(@PathVariable("id") Integer id) {
        this.jdbcTemplate.update("DELETE FROM car WHERE id = ?", id);
        return ResponseEntity.noContent().build();
    }

    static class Car {

        public Car(Integer id, String name) {
            this.id = id;
            this.name = name;
        }

        private Integer id;

        private String name;

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}
EOF
```

```sql
mkdir -p src/main/resources/db/migration
cat <<EOF > src/main/resources/db/migration/V1__init.sql
CREATE TABLE car (
    id   SERIAL PRIMARY KEY,
    name VARCHAR(16)
);

INSERT INTO car(name) VALUES ('Avalon');
INSERT INTO car(name) VALUES ('Corolla');
INSERT INTO car(name) VALUES ('Crown');
INSERT INTO car(name) VALUES ('Levin');
INSERT INTO car(name) VALUES ('Yaris');
INSERT INTO car(name) VALUES ('Vios');
INSERT INTO car(name) VALUES ('Glanza');
INSERT INTO car(name) VALUES ('Aygo');
EOF
```

```properties
cat <<EOF > src/main/resources/application.properties
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/car
spring.datasource.username=${USER}
spring.datasource.password=
EOF
```

#### Running locally

Build the application.

```
./mvnw clean package -Dmaven.test.skip=true
```

Install PostgreSQL using Home Brew.

```
brew install postgres 
brew services start postgresql
psql postgres -c 'create database car;'
```

Run the jar file.

```
java -jar target/car-api-0.0.1-SNAPSHOT.jar
```

Access the API.

```
curl -s http://localhost:8080/cars -d "{\"name\": \"Lexus\"}" -H "Content-Type: application/json" | jq .
curl -s http://localhost:8080/cars | jq .
```

#### Creating a Docker image

Create a Docker image using Cloud Native Buildpacks and push it to a Docker registry.

```
./mvnw spring-boot:build-image -Dmaven.test.skip=true -Dspring-boot.build-image.imageName=ghcr.io/making/car-api
docker push ghcr.io/making/car-api
```

#### Deploying to Kubernetes

Assume you have a Secret for connecting to PostgreSQL like this:

```yaml
mkdir -p k8s
cat <<EOF > k8s/postgresql-secret.yml
apiVersion: v1
kind: Secret
metadata:
  name: postgresql
stringData:
  url: jdbc:postgresql://lallah.db.elephantsql.com:5432/aokncqqn
  username: aokncqqn
  password: qH-qyBRtkNIc6at26oBYttmgXknUOLDR
EOF
```

Create a manifest file to load this Secret with Config Tree.

```yaml
cat <<EOF > k8s/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: car-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: car-api
  template:
    metadata:
      labels:
        app: car-api
    spec:
      containers:
      - image: ghcr.io/making/car-api:latest
        name: car-api
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_CONFIG_IMPORT
          value: configtree:/workspace/config/
        #! Tweak to use less memory 
        - name: JAVA_TOOL_OPTIONS
          value: -XX:ReservedCodeCacheSize=32M -Xss512k -Duser.timezone=Asia/Tokyo
        - name: BPL_JVM_THREAD_COUNT
          value: "20"
        - name: SERVER_TOMCAT_THREADS_MAX
          value: "4"
        resources:
          limits:
            memory: 256Mi
          requests:
            memory: 256Mi
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 5
          timeoutSeconds: 1
          failureThreshold: 1
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 10
          timeoutSeconds: 1
          failureThreshold: 1
          periodSeconds: 10
        volumeMounts:
        - name: database
          mountPath: /workspace/config/spring/datasource
          readOnly: true
      volumes:
      - name: database
        secret:
          secretName: postgresql
---
kind: Service
apiVersion: v1
metadata:
  name: car-api
spec:
  type: LoadBalancer
  selector:
    app: car-api
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
EOF
```

Deploy it.

```
kubectl apply -f k8s/deployment.yml -f k8s/postgresql-secret.yml
```

Check the logs to confirm that the application is accessing the connection specified in the Secret.

```
$ kubectl logs -l app=car-api --tail=12
2020-10-23 00:50:21.661  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2020-10-23 00:50:22.046  INFO 1 --- [           main] o.f.c.i.database.base.DatabaseType       : Database: jdbc:postgresql://lallah.db.elephantsql.com:5432/aokncqqn (PostgreSQL 11.9)
2020-10-23 00:50:24.678  INFO 1 --- [           main] o.f.core.internal.command.DbValidate     : Successfully validated 1 migration (execution time 00:00.889s)
2020-10-23 00:50:26.070  INFO 1 --- [           main] o.f.core.internal.command.DbMigrate      : Current version of schema "public": 1
2020-10-23 00:50:26.243  INFO 1 --- [           main] o.f.core.internal.command.DbMigrate      : Schema "public" is up to date. No migration necessary.
2020-10-23 00:50:27.045  INFO 1 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-10-23 00:50:27.623  INFO 1 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
2020-10-23 00:50:27.679  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-10-23 00:50:27.700  INFO 1 --- [           main] com.example.CarApiApplication            : Started CarApiApplication in 11.579 seconds (JVM running for 12.107)
2020-10-23 00:50:28.206  INFO 1 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-10-23 00:50:28.210  INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-10-23 00:50:28.216  INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 6 ms
```

Access the API.

```
curl -s http://<Service IP>/cars | jq .
```

----

Spring 2.3 introduced Kubernetes-friendly features, and 2.4 added this feature as well.

While Kubernetes-friendly, it is a generic feature that can be used outside of Kubernetes. Therefore, it was named "Config Tree" instead of something Kubernetes-specific.
