Reload HashiCorp Vault secrets in Spring applications
You can refactor your application to use a Vault client library to inject secrets from Vault into your code. Many client libraries support authentication to and retrieval of secrets from Vault. Writing code that directly interfaces with Vault allows you to control the injection and handling of new secrets without deploying a separate process to monitor changes in Vault. How do you reload the application to use new credentials and minimize downtime?
This tutorial shows you how to configure a Spring application to reload static and dynamic secrets from Vault without restarting the application. You will configure an application to retrieve a database username and password from Vault's key-value secrets engine on a scheduled interval and reconnect to the database with new credentials. Then, you will add annotations to help the application reload dynamic database credentials from Vault's database secrets engine. Finally, if your application cannot use Spring annotations or runs on GraalVM, you use Spring Vault to wait for a secret renewal and update connection properties.
Prerequisites
Set up tutorial
Clone the repository.
$ git clone https://github.com/hashicorp-education/learn-vault-spring-cloud
Or download the repository.
The repository contains supporting content for this Spring application tutorial.
Change directories to the
reload/
directory.$ cd learn-vault-spring-cloud/reload/
In your terminal, use Docker Compose to create a PostgreSQL database for the application, PostgreSQL configuration container to set up the schema, Vault server in dev mode, and Vault configuration container to set up the key-value and database secrets engines.
$ docker compose up -d [+] Running 5/5✔ Network reload_default Created✔ Container reload-vault-1 Healthy✔ Container reload-postgres-1 Healthy✔ Container reload-postgres-configure-1 Started✔ Container reload-vault-configure-1 Started
Reload static secrets
You manually update static secrets stored in Vault's key-value secrets engine. Rather than manually reload the application, you can include code to reload new versions of static secrets on a scheduled interval. This workflow requires the use of Spring Cloud Vault to retrieve Spring application properties from Vault's key-value secrets engine.
Tip
Your application can also subscribe to Vault Enterprise's event notifications if it must immediately reload upon changes to static secrets.
After you start the Vault server, verify that it has the key-value secrets engine enabled at
kv/
and contains the root database username and password atkv/vault-static-secrets
. Spring Cloud Vault retrieves application properties from a Vault path defined by the application, context, or profile. This tutorial stores the root database credentials at thekv/${application_name}
path. The keys for each secret use the format for common application properties defined for database connections in Spring applications.$ VAULT_TOKEN=vault-root-password VAULT_ADDR=http://127.0.0.1:8200 vault kv get kv/vault-static-secrets ======== Secret Path ========kv/data/vault-static-secrets ======= Metadata =======Key Value--- -----created_time 2024-05-15T16:29:39.07375588Zcustom_metadata <nil>deletion_time n/adestroyed falseversion 1 =============== Data ===============Key Value--- -----spring.datasource.password postgres-admin-passwordspring.datasource.username postgres
Verify the current working directory is
reload/
.$ pwd cd /learn-vault-spring-cloud/reload
Change directories to the
vault-static-secrets/
directory.$ cd vault-static-secrets
Review
pom.xml
to check the application's dependencies. The Spring application in this tutorial uses the Spring Cloud Vault library. It provides client-side support for connecting to Vault in a distributed environment. The application also needs JDBC and PostgreSQL to access the database.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.2.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.hashicorp</groupId> <artifactId>vault-static-secrets</artifactId> <version>0.0.1-SNAPSHOT</version> <name>vault-static-secrets</name> <description>Demo project for Vault KV secrets engine with Spring</description> <properties> <java.version>22</java.version> <spring-cloud.version>2023.0.1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <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.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </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>
Open
application.properties
in/src/main/resources
. The application setsspring.cloud.vault.uri
andspring.cloud.vault.token
to the address and root token of the Vault server in dev mode. You can change the Vault authentication to any supported authentication methods based on your organization's Vault setup.application.properties
spring.application.name=vault-static-secrets # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # import configuration from vaultspring.config.import=vault:// # configure KV backendspring.cloud.vault.kv.backend=kv # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/payments # set refresh interval for static secretssecrets.refresh-interval-ms=180000
Continue to review
application.properties
. Instead of writing code to directly interface with Vault and retrieve static secrets, configure Spring Cloud Config Server to use Vault as a backend. The config server automatically reads secrets from Vault paths based on the application name, context, and profile in the secret mount. Override the mount with thespring.cloud.vault.kv.backend
property. When the application starts, Spring Cloud Config will retrieve the properties from Vault and merge them with common properties. This allows the application to authenticate to the database.application.properties
spring.application.name=vault-static-secrets # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # import configuration from vaultspring.config.import=vault:// # configure KV backendspring.cloud.vault.kv.backend=kv # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/payments # set refresh interval for static secretssecrets.refresh-interval-ms=180000
Additional properties like
spring.datasource.url
inapplication.properties
define application access to the database. Finally, create a custom property namedsecrets.refresh-interval-ms
. It configures the scheduled interval in milliseconds for getting new secrets from Vault. The application checks for new credentials every 180000 milliseconds (3 minutes).application.properties
spring.application.name=vault-static-secrets # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # import configuration from vaultspring.config.import=vault:// # configure KV backendspring.cloud.vault.kv.backend=kv # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/payments # set refresh interval for static secretssecrets.refresh-interval-ms=180000
Open
VaultStaticSecretsApplication.java
in/src/main/java/com/hashicorp/vaultstaticsecrets
. The class includes a few annotations, includingEnableScheduling
, which schedules a task in the application.VaultStaticSecretsApplication.java
@SpringBootApplication@EnableSchedulingpublic class VaultStaticSecretsApplication { public static void main(String[] args) { SpringApplication.run(VaultStaticSecretsApplication.class, args); } @Bean @RefreshScope DataSource dataSource(DataSourceProperties properties) { var log = LogFactory.getLog(getClass()); var db = DataSourceBuilder .create() .url(properties.getUrl()) .username(properties.getUsername()) .password(properties.getPassword()) .build(); log.info( "rebuild data source: " + properties.getUsername() + ',' + properties.getPassword() ); return db; } }
Other objects defined in
VaultStaticSecretsApplication.java
include theDataSource
defining the database connection. Annotate it as aBean
and add theRefreshScope
annotation.RefreshScope
identifies beans that the application can rebuild, such as database connections or clients to upstream services.VaultStaticSecretsApplication.java
@SpringBootApplication@EnableSchedulingpublic class VaultStaticSecretsApplication { public static void main(String[] args) { SpringApplication.run(VaultStaticSecretsApplication.class, args); } @Bean @RefreshScope DataSource dataSource(DataSourceProperties properties) { var log = LogFactory.getLog(getClass()); var db = DataSourceBuilder .create() .url(properties.getUrl()) .username(properties.getUsername()) .password(properties.getPassword()) .build(); log.info( "rebuild data source: " + properties.getUsername() + ',' + properties.getPassword() ); return db; } }
Examine
VaultRefresher.java
in/src/main/java/com/hashicorp/vaultstaticsecrets
. The class includes theComponent
annotation to ensure the context refresh task runs automatically on application startup. Therefresher
method runs a context refresh, which allows the application to shut down existing database connections and start connections with new credentials. Annotate therefresher
method with@Scheduled
and define the task delay using custom configuration properties for the refresh interval. The application schedules a task to refresh context every three minutes.VaultRefresher.java
@Componentclass VaultRefresher { private final Log log = LogFactory.getLog(getClass()); private final ContextRefresher contextRefresher; VaultRefresher(ContextRefresher contextRefresher) { this.contextRefresher = contextRefresher; } @Scheduled(initialDelayString="${secrets.refresh-interval-ms}", fixedDelayString = "${secrets.refresh-interval-ms}") void refresher() { contextRefresher.refresh(); log.info("refresh key-value secret"); }}
Review
PaymentsController.java
in/src/main/java/com/hashicorp/vaultstaticsecrets
. The controller handles requests to get a list of payments and create a payment in the database.PaymentsController.java
@Controller@ResponseBodyclass PaymentsController { private final JdbcClient db; PaymentsController(DataSource dataSource) { this.db = JdbcClient.create(dataSource); } @GetMapping("/payments") Collection<Payment> getPayments() { return this.db .sql("SELECT * FROM payments") .query((rs, rowNum) -> new Payment( rs.getString("id"), rs.getString("name"), rs.getString("cc_info"), rs.getTimestamp("created_at").toInstant() )) .list(); } @PostMapping(path = "/payments", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) Collection<Payment> createPayment(@RequestBody Payment request) { var id = UUID.randomUUID().toString(); var statement = String.format( "INSERT INTO payments(id, name, cc_info, created_at) " + "VALUES('%s', '%s', '%s', '%s')", id, request.name, request.ccInfo, Instant.now().toString()); this.db.sql(statement).update(); return this.db .sql(String.format("SELECT * FROM payments WHERE id = '%s'", id)) .query((rs, rowNum) -> new Payment( rs.getString("id"), rs.getString("name"), rs.getString("cc_info"), rs.getTimestamp("created_at").toInstant() )).list(); } record Payment(String id, String name, @JsonProperty(value = "cc_info") String ccInfo, Instant createdAt) { }}
In your terminal, build and run the application using the Apache Maven Wrapper included in the repository.
$ ./mvnw spring-boot:run ... omitted ... 2024-05-15T16:11:07.836-04:00 INFO 10359 --- [vault-static-secrets] [ main] c.h.v.VaultStaticSecretsApplication : Starting VaultStaticSecretsApplication using Java 22.0.12024-05-15T16:11:07.837-04:00 INFO 10359 --- [vault-static-secrets] [ main] c.h.v.VaultStaticSecretsApplication : No active profile set, falling back to 1 default profile: "default"2024-05-15T16:11:08.229-04:00 INFO 10359 --- [vault-static-secrets] [ main] o.s.cloud.context.scope.GenericScope : BeanFactory id=68ad8185-e4f4-34c6-8df0-880aa90ab4d02024-05-15T16:11:08.391-04:00 INFO 10359 --- [vault-static-secrets] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)2024-05-15T16:11:08.398-04:00 INFO 10359 --- [vault-static-secrets] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]2024-05-15T16:11:08.398-04:00 INFO 10359 --- [vault-static-secrets] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20]2024-05-15T16:11:08.430-04:00 INFO 10359 --- [vault-static-secrets] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext2024-05-15T16:11:08.431-04:00 INFO 10359 --- [vault-static-secrets] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 569 ms2024-05-15T16:11:08.688-04:00 INFO 10359 --- [vault-static-secrets] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''2024-05-15T16:11:08.694-04:00 INFO 10359 --- [vault-static-secrets] [ main] StaticSecretsApplication$$SpringCGLIB$$0 : rebuild data source: postgres, postgres-admin-password2024-05-15T16:11:08.704-04:00 INFO 10359 --- [vault-static-secrets] [ main] c.h.v.VaultStaticSecretsApplication : Started VaultStaticSecretsApplication in 1.527 seconds (process running for 1.733)
Open another terminal session. Update Vault's key-value secrets engine with a new database password.
$ VAULT_TOKEN=vault-root-password VAULT_ADDR=http://127.0.0.1:8200 vault kv put kv/vault-static-secrets spring.datasource.password=updated-wrong-password ======== Secret Path ========kv/data/vault-static-secrets ======= Metadata =======Key Value--- -----created_time 2024-05-15T20:14:11.184846199Zcustom_metadata <nil>deletion_time n/adestroyed falseversion 2
Wait three minutes before switching back to the terminal running the application. The logs indicate that the application refreshed a key-value secret after three minutes.
$ ./mvnw spring-boot:run ... omitted ... 2024-05-15T16:11:08.704-04:00 INFO 10359 --- [vault-static-secrets] [ main] c.h.v.VaultStaticSecretsApplication : Started VaultStaticSecretsApplication in 1.527 seconds (process running for 1.733)2024-05-15T16:14:08.762-04:00 INFO 10359 --- [vault-static-secrets] [ scheduling-1] c.h.vaultstaticsecrets.VaultRefresher : refresh key-value secret
Open another terminal session. Request the application for a list of payments in the database using its API. The database should return an error, as the updated credentials do not have access to the database.
$ curl localhost:8080/payments {"timestamp":"2024-05-15T20:17:43.847+00:00","status":500,"error":"Internal Server Error","path":"/payments"}
When you make the request, the application uses the new password to connect to the database. However, the new password does not have access. The application throws an error.
$ ./mvnw spring-boot:run ... omitted ... 2024-05-15T16:17:08.813-04:00 INFO 10359 --- [vault-static-secrets] [ scheduling-1] c.h.vaultstaticsecrets.VaultRefresher : refresh key-value secret2024-05-15T16:17:42.692-04:00 INFO 10359 --- [vault-static-secrets] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'2024-05-15T16:17:42.692-04:00 INFO 10359 --- [vault-static-secrets] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'2024-05-15T16:17:42.693-04:00 INFO 10359 --- [vault-static-secrets] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms2024-05-15T16:17:42.712-04:00 INFO 10359 --- [vault-static-secrets] [nio-8080-exec-1] StaticSecretsApplication$$SpringCGLIB$$0 : rebuild data source: postgres,updated-wrong-password2024-05-15T16:17:42.715-04:00 INFO 10359 --- [vault-static-secrets] [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...2024-05-15T16:17:43.829-04:00 ERROR 10359 --- [vault-static-secrets] [nio-8080-exec-1] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization.
Roll back the database password to the previous version.
$ VAULT_TOKEN=vault-root-password VAULT_ADDR=http://127.0.0.1:8200 vault kv rollback -mount=kv -version=1 vault-static-secrets
Check the application refreshes the secret by examining the application's logs.
$ ./mvnw spring-boot:run ... omitted ... 2024-05-15T16:17:43.829-04:00 ERROR 10359 --- [vault-static-secrets] [nio-8080-exec-1] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization. ... omitted ... 2024-05-15T16:20:08.860-04:00 INFO 10359 --- [vault-static-secrets] [ scheduling-1] c.h.vaultstaticsecrets.VaultRefresher : refresh key-value secret
Open another terminal session. Request the application for a list of payments in the database using its API. The database should return an empty list, as it did not record any payments.
$ curl localhost:8080/payments []
When you make the request, the application uses the previous version of the password to connect to the database and successfully rebuilds the database connection.
$ ./mvnw spring-boot:run ... omitted ... 2024-05-15T16:17:43.829-04:00 ERROR 10359 --- [vault-static-secrets] [nio-8080-exec-1] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization. ... omitted ... 2024-05-15T16:20:08.860-04:00 INFO 10359 --- [vault-static-secrets] [ scheduling-1] c.h.vaultstaticsecrets.VaultRefresher : refresh key-value secret2024-05-15T16:21:44.369-04:00 INFO 10359 --- [vault-static-secrets] [nio-8080-exec-2] StaticSecretsApplication$$SpringCGLIB$$0 : rebuild data source: postgres, postgres-admin-password2024-05-15T16:21:44.370-04:00 INFO 10359 --- [vault-static-secrets] [nio-8080-exec-2] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Starting...2024-05-15T16:21:44.439-04:00 INFO 10359 --- [vault-static-secrets] [nio-8080-exec-2] com.zaxxer.hikari.pool.HikariPool : HikariPool-2 - Added connection org. postgresql.jdbc.PgConnection@7f34f6242024-05-15T16:21:44.440-04:00 INFO 10359 --- [vault-static-secrets] [nio-8080-exec-2] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Start completed.
Shut down the application run by Maven.
Reload dynamic secrets
Secrets engines in Vault that handle dynamic secrets manage the revocation and creation of credentials based on leases. Leases include a time-to-live (TTL) before Vault revokes the credentials. This tutorial uses the Spring Cloud Vault database backend to retrieve database credentials from Vault and add them to application properties. Each time the Spring Vault library detects a secret expiration event, the application refreshes any objects using the credentials.
Note
The code in this section works with secrets engines that have leases for credentials. If a secrets engine or role does not issue credentials with a lease, Spring Vault does not generate events related to leases.After you start the Vault server, verify you can get a database and username from the PostgreSQL database secrets engine enabled at
database/
with thepayments-app
Vault role. This database secrets engine has a TTL of one minute, with a maximum TTL of two minutes. The application will renew the credentials at least once before the username and password expire after two minutes.$ VAULT_TOKEN=vault-root-password VAULT_ADDR=http://127.0.0.1:8200 vault read database/creds/payments-app Key Value--- -----lease_id database/creds/payments-app/wr6PS86RKSY0VFn9GYdXb8R0lease_duration 1mlease_renewable truepassword DB_PASSWORDusername v-token-payments-uYBgyNcNUiTlR4TPLjdP-1715883636
Change the current working directory to
reload/
.$ cd .. && pwd cd /learn-vault-spring-cloud/reload
Change directories to the
vault-dynamic-secrets/
directory.$ cd vault-dynamic-secrets
Review
pom.xml
to check the application's dependencies. The Spring application in this tutorial uses the Spring Cloud Vault library. It provides client-side support for connecting to Vault in a distributed environment. To use the Spring Cloud Vault database backend, include thespring-cloud-vault-config-databases
library.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.2.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.hashicorp</groupId> <artifactId>vault-dynamic-secrets</artifactId> <version>0.0.1-SNAPSHOT</version> <name>vault-dynamic-secrets</name> <description>Demo project for Vault database secrets engine with Spring</description> <properties> <java.version>22</java.version> <spring-cloud.version>2023.0.1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <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.cloud</groupId> <artifactId>spring-cloud-vault-config-databases</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </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>
Open
application.properties
in/src/main/resources
. The application setsspring.cloud.vault.uri
andspring.cloud.vault.token
to the address and root token of the Vault server in dev mode. You can change the Vault authentication to any supported authentication methods based on your organization's Vault setup.application.properties
spring.application.name=vault-dynamic-secrets # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # set up configuration import from Vaultspring.config.import=vault:// # configure KV backendspring.cloud.vault.kv.enabled=false # configure database secrets enginespring.cloud.vault.database.enabled=truespring.cloud.vault.database.role=payments-appspring.cloud.vault.database.backend=database # tune lease renewal and expiry threshold for 2 minute max ttlspring.cloud.vault.config.lifecycle.min-renewal=30sspring.cloud.vault.config.lifecycle.expiry-threshold=10s # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/payments
The
spring.cloud.vault.database.enabled
andspring.cloud.vault.database.backend
attributes inapplication.properties
configure the Spring Cloud Config database backend to get a username and password from the database secrets engine at thedatabase/
path in Vault. Setspring.cloud.vault.database.role
to thepayments-app
Vault role. This example disables the KV backend, as the application does not need to retrieve the root database username and password. When the application starts, Spring Cloud Config retrieves the database username and password from Vault and injects them into data source properties.application.properties
spring.application.name=vault-dynamic-secrets # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # set up configuration import from Vaultspring.config.import=vault:// # configure KV backendspring.cloud.vault.kv.enabled=false # configure database secrets enginespring.cloud.vault.database.enabled=truespring.cloud.vault.database.role=payments-appspring.cloud.vault.database.backend=database # tune lease renewal and expiry threshold for 2 minute max ttlspring.cloud.vault.config.lifecycle.min-renewal=30sspring.cloud.vault.config.lifecycle.expiry-threshold=10s # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/payments
Set
spring.cloud.vault.config.lifecycle.min-renewal
andspring.cloud.vault.config.lifecycle.expiry-threshold
to shorter intervals inapplication.properties
. These attributes determine when Spring Cloud Vault renews the lease for dynamic credentials or makes a request to Vault for new credentials. Since the maximum TTL of the database credential expires after two minutes, the configuration defines a shorter expiration threshold than renewal threshold.application.properties
spring.application.name=vault-dynamic-secrets # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # set up configuration import from Vaultspring.config.import=vault:// # configure KV backendspring.cloud.vault.kv.enabled=false # configure database secrets enginespring.cloud.vault.database.enabled=truespring.cloud.vault.database.role=payments-appspring.cloud.vault.database.backend=database # tune lease renewal and expiry threshold for 2 minute max ttlspring.cloud.vault.config.lifecycle.min-renewal=30sspring.cloud.vault.config.lifecycle.expiry-threshold=10s # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/payments
Open
VaultDynamicSecretsApplication.java
in/src/main/java/com/hashicorp/vaultdynamicsecrets
. The class includes aDataSource
defining the database connection. Annotate it as aBean
and add theRefreshScope
annotation.RefreshScope
identifies beans that the application can rebuild, such as database connections or clients to upstream services.VaultDynamicSecretsApplication.java
@SpringBootApplicationpublic class VaultDynamicSecretsApplication { public static void main(String[] args) { SpringApplication.run(VaultDynamicSecretsApplication.class, args); } @Bean @RefreshScope DataSource dataSource(DataSourceProperties properties) { var log = LogFactory.getLog(getClass()); var db = DataSourceBuilder .create() .url(properties.getUrl()) .username(properties.getUsername()) .password(properties.getPassword()) .build(); log.info( "rebuild data source: " + properties.getUsername() + ',' + properties.getPassword() ); return db; } }
Examine
VaultRefresher.java
in/src/main/java/com/hashicorp/vaultdynamicsecrets
. The class includes theComponent
annotation to ensure the application scans it at startup. The constructor includes aSecretLeaseContainer
to listen for events related to leases at the database secrets engine path. Each time the application receives aSecretLeaseExpiredEvent
, it refreshes application context to get new credentials from Vault.VaultRefresher.java
@Componentclass VaultRefresher { VaultRefresher(@Value("${spring.cloud.vault.database.role}") String databaseRole, @Value("${spring.cloud.vault.database.backend}") String databaseBackend, SecretLeaseContainer leaseContainer, ContextRefresher contextRefresher) { final Log log = LogFactory.getLog(getClass()); var vaultCredsPath = String.format("%s/creds/%s", databaseBackend, databaseRole); leaseContainer.addLeaseListener(event -> { if (vaultCredsPath.equals(event.getSource().getPath())) { if (event instanceof SecretLeaseExpiredEvent) { contextRefresher.refresh(); log.info("refresh database credentials"); } } }); }}
Review
PaymentsController.java
in/src/main/java/com/hashicorp/vaultdynamicsecrets
. The controller handles requests to get a list of payments and create a payment in the database.PaymentsController.java
@Controller@ResponseBodyclass PaymentsController { private final JdbcClient db; PaymentsController(DataSource dataSource) { this.db = JdbcClient.create(dataSource); } @GetMapping("/payments") Collection<Payment> getPayments() { return this.db .sql("SELECT * FROM payments") .query((rs, rowNum) -> new Payment( rs.getString("id"), rs.getString("name"), rs.getString("cc_info"), rs.getTimestamp("created_at").toInstant() )) .list(); } @PostMapping(path = "/payments", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) Collection<Payment> createPayment(@RequestBody Payment request) { var id = UUID.randomUUID().toString(); var statement = String.format( "INSERT INTO payments(id, name, cc_info, created_at) " + "VALUES('%s', '%s', '%s', '%s')", id, request.name, request.ccInfo, Instant.now().toString()); this.db.sql(statement).update(); return this.db .sql(String.format("SELECT * FROM payments WHERE id = '%s'", id)) .query((rs, rowNum) -> new Payment( rs.getString("id"), rs.getString("name"), rs.getString("cc_info"), rs.getTimestamp("created_at").toInstant() )).list(); } record Payment(String id, String name, @JsonProperty(value = "cc_info") String ccInfo, Instant createdAt) { }}
In your terminal, build and run the application using the Apache Maven Wrapper included in the repository. The application creates a data source based on a database username and password generated by Vault.
$ ./mvnw spring-boot:run ... omitted ... 2024-05-16T14:56:08.962-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] c.h.v.VaultDynamicSecretsApplication : Starting VaultDynamicSecretsApplication using Java 22.0.12024-05-16T14:56:08.963-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] c.h.v.VaultDynamicSecretsApplication : No active profile set, falling back to 1 default profile: "default"2024-05-16T14:56:09.411-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] o.s.cloud.context.scope.GenericScope : BeanFactory id=02be7e0c-3aa8-3adf-947e-6e5fb27b87ce2024-05-16T14:56:09.603-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)2024-05-16T14:56:09.611-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]2024-05-16T14:56:09.611-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20]2024-05-16T14:56:09.656-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext2024-05-16T14:56:09.657-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 669 ms2024-05-16T14:56:09.903-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''2024-05-16T14:56:09.910-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] ynamicSecretsApplication$$SpringCGLIB$$0 : rebuild data source: v-token-payments-agPE2Q7HuIxc4dn5NQs1-1715885768,gPdJkzzSfFLm-d218V3O2024-05-16T14:56:09.918-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] c.h.v.VaultDynamicSecretsApplication : Started VaultDynamicSecretsApplication in 1.719 seconds (process running for 1.953)
Wait for two minutes for the application logs to indicate that it refreshed database credentials.
$ ./mvnw spring-boot:run ... omitted ... 2024-05-16T14:56:09.918-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] c.h.v.VaultDynamicSecretsApplication : Started VaultDynamicSecretsApplication in 1.719 seconds (process running for 1.953)2024-05-16T14:57:48.905-04:00 INFO 9917 --- [vault-dynamic-secrets] [g-Cloud-Vault-2] c.h.v.VaultRefresher$$SpringCGLIB$$0 : refresh database credentials
Open a new terminal session. Request the application for a list of payments in the database using its API. The database should return an empty list, as it did not record any payments.
$ curl localhost:8080/payments []
When you make the request, the application gets a new database username and password from Vault to connect to the database and successfully rebuilds the database connection.
$ ./mvnw spring-boot:run ... omitted ... 2024-05-16T14:56:09.910-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] ynamicSecretsApplication$$SpringCGLIB$$0 : rebuild data source: v-token-payments-agPE2Q7HuIxc4dn5NQs1-1715885768,gPdJkzzSfFLm-d218V3O2024-05-16T14:56:09.918-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] c.h.v.VaultDynamicSecretsApplication : Started VaultDynamicSecretsApplication in 1.719 seconds (process running for 1.953) ... omitted ... 2024-05-16T14:59:28.934-04:00 INFO 9917 --- [vault-dynamic-secrets] [g-Cloud-Vault-2] c.h.v.VaultRefresher$$SpringCGLIB$$0 : refresh database credentials2024-05-16T14:59:48.342-04:00 INFO 9917 --- [vault-dynamic-secrets] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'2024-05-16T14:59:48.342-04:00 INFO 9917 --- [vault-dynamic-secrets] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'2024-05-16T14:59:48.342-04:00 INFO 9917 --- [vault-dynamic-secrets] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms2024-05-16T14:59:48.369-04:00 INFO 9917 --- [vault-dynamic-secrets] [nio-8080-exec-1] ynamicSecretsApplication$$SpringCGLIB$$0 : rebuild data source: v-token-payments-DSHe6UICZkTUaSgZIhRK-1715885968,ruNQTqwq5lQe-EUNcZj22024-05-16T14:59:48.372-04:00 INFO 9917 --- [vault-dynamic-secrets] [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...2024-05-16T14:59:48.884-04:00 INFO 9917 --- [vault-dynamic-secrets] [nio-8080-exec-1] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@5eef82cf2024-05-16T14:59:48.885-04:00 INFO 9917 --- [vault-dynamic-secrets] [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
Shut down the application run by Maven.
Reload secrets in GraalVM
GraalVM compiles a Java application
ahead of time into a smaller, more performant, and secure standalone binary by removing
unused code and classes. As a result, it supports dynamic language features like reflection
at build time and not runtime. Application context refreshing uses reflection at runtime,
which means annotating beans with @RefreshScope
and calling a context refresh will
not work in GraalVM. In order to reload a database connection with new secrets, use the
Runtime Hints API
to guide the compiler to include a refreshable data source.
Note
You can apply the patterns in this section to Spring applications that cannot refresh application context.Change the current working directory to
reload/
.$ cd .. && pwd cd /learn-vault-spring-cloud/reload
Change directories to the
vault-graalvm/
directory.$ cd vault-graalvm
Check you have GraalVM installed.
$ native-image --version native-image 22.0.1 2024-04-16GraalVM Runtime Environment Oracle GraalVM 22.0.1+8.1 (build 22.0.1+8-jvmci-b01)Substrate VM Oracle GraalVM 22.0.1+8.1 (build 22.0.1+8, serial gc, compressed references)
Review
pom.xml
to check the application's dependencies. The Spring application in this tutorial uses the Spring Cloud Vault library. It provides client-side support for connecting to Vault in a distributed environment. To use the Spring Cloud Vault database backend, include thespring-cloud-vault-config-databases
library. The dependencies include the Maven plugin for GraalVM.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.2.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.hashicorp</groupId> <artifactId>vault-graalvm</artifactId> <version>0.0.1-SNAPSHOT</version> <name>vault-graalvm</name> <description>Demo project for Vault database secrets engine with Spring on GraalVM</description> <properties> <java.version>22</java.version> <spring-cloud.version>2023.0.1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <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.cloud</groupId> <artifactId>spring-cloud-vault-config-databases</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </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.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Open
application.properties
in/src/main/resources
. In GraalVM, you must disable Spring Cloud refresh with thespring.cloud.refresh.enabled
attribute. The remainingapplication.properties
set up the Vault address and token, disables KV backend and enables the database backend for Spring Cloud Config, and tunes the lease lifecycle renewal and expiration thresholds.application.properties
spring.application.name=vault-graalvm # graalvm needs refresh disabledspring.cloud.refresh.enabled=false # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # configure KV backendspring.cloud.vault.kv.enabled=false # set up configuration import from Vaultspring.config.import=vault:// # configure database secrets enginespring.cloud.vault.database.enabled=truespring.cloud.vault.database.role=payments-appspring.cloud.vault.database.backend=database # tune lease renewal and expiry threshold for 2 minute max ttlspring.cloud.vault.config.lifecycle.min-renewal=30sspring.cloud.vault.config.lifecycle.expiry-threshold=10s # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/payments
Open
VaultGraalvmApplication.java
in/src/main/java/com/hashicorp/vaultgraalvm
. The class includes themain
function to start the application.VaultGraalvmApplication.java
@SpringBootApplicationpublic class VaultGraalvmApplication { public static void main(String[] args) { SpringApplication.run(VaultGraalvmApplication.class, args); }}
Examine
RefreshableDataSourceVaultConfiguration.java
. The constructor uses Spring Vault’sSecretLeaseContainer
to add a lease listener and wait for aSecretLeaseExpiredEvent
and a secret renewal. Your application requests a secret rotation using Spring Vault. If the lease container detects a new lease and a rotated secret, it sets the username and password into the data source properties and refreshes the data source.RefreshableDataSourceVaultConfiguration.java
@Configuration@ImportRuntimeHints(RefreshableDataSourceVaultConfiguration.RefreshableDataSourceHints.class)public class RefreshableDataSourceVaultConfiguration { private final Log log = LogFactory.getLog(getClass()); private final ApplicationEventPublisher publisher; RefreshableDataSourceVaultConfiguration( @Value("${spring.cloud.vault.database.role}") String databaseRole, @Value("${spring.cloud.vault.database.backend}") String databaseBackend, DataSourceProperties properties, SecretLeaseContainer leaseContainer, ApplicationEventPublisher publisher) { this.publisher = publisher; var vaultCredsPath = String.format( "%s/creds/%s", databaseBackend, databaseRole); leaseContainer.addLeaseListener(event -> { if (vaultCredsPath.equals(event.getSource().getPath())) { if (event instanceof SecretLeaseExpiredEvent && event.getSource().getMode() == RequestedSecret.Mode.RENEW) { log.info("expire lease, rotate database credentials"); leaseContainer.requestRotatingSecret(vaultCredsPath); } else if (event instanceof SecretLeaseCreatedEvent secretLeaseCreatedEvent && event.getSource().getMode() == RequestedSecret.Mode.ROTATE) { String username = (String) secretLeaseCreatedEvent.getSecrets() .get("username"); String password = (String) secretLeaseCreatedEvent.getSecrets() .get("password"); log.info("update database properties : " + username + "," + password); properties.setUsername(username); properties.setPassword(password); refresh(); } } }); } // omitted}
The
RefreshableDataSourceVaultConfiguration.java
file in/src/main/java/com/hashicorp/vaultgraalvm
also creates aDataSource
with a Spring AOP proxy that rebuilds the data source on an application event.RefreshableDataSourceVaultConfiguration.java
@Configuration@ImportRuntimeHints(RefreshableDataSourceVaultConfiguration.RefreshableDataSourceHints.class)public class RefreshableDataSourceVaultConfiguration { // omitted @Bean DataSource dataSource(DataSourceProperties properties) { var rebuild = (Function<DataSourceProperties, DataSource>) dataSourceProperties -> { log.info("build data source: " + properties.getUsername() + "," + properties.getPassword()); return DataSourceBuilder .create() .url(properties.getUrl()) .username(properties.getUsername()) .password(properties.getPassword()) .build(); }; var delegate = new AtomicReference<>(rebuild.apply(properties)); var pfb = new ProxyFactoryBean(); pfb.addInterface(DataSource.class); pfb.addInterface(RefreshedEventListener.class); pfb.addAdvice((MethodInterceptor) invocation -> { var methodName = invocation.getMethod().getName(); if (methodName.equals("onApplicationEvent")) { delegate.set(rebuild.apply(properties)); return null; } return invocation.getMethod() .invoke(delegate.get(), invocation.getArguments()); }); return (DataSource) pfb.getObject(); } // omitted}
Continue reviewing
RefreshableDataSourceVaultConfiguration.java
. The class defines arefresh
method to publish an application event for refreshing credentials. This refresh event rebuilds the data source. Create a listener for the refresh event.RefreshableDataSourceVaultConfiguration.java
@Configuration@ImportRuntimeHints(RefreshableDataSourceVaultConfiguration.RefreshableDataSourceHints.class)public class RefreshableDataSourceVaultConfiguration { // omitted interface RefreshedEventListener extends ApplicationListener<RefreshEvent> { } private void refresh() { this.publisher.publishEvent(new RefreshEvent(this, null, "refresh database connection with new Vault credentials")); }}
RefreshableDataSourceVaultConfiguration.java
defines a runtime hint for the refreshable data source. This allows the compiler to recognize the refresh event for the data source. Register the data source proxy and the refresh event listener as a hint. Finally, make sure to importRefreshableDataSourceHints
with theImportRuntimeHints
annotation.RefreshableDataSourceVaultConfiguration.java
@Configuration@ImportRuntimeHints(RefreshableDataSourceVaultConfiguration.RefreshableDataSourceHints.class)public class RefreshableDataSourceVaultConfiguration { // omitted static class RefreshableDataSourceHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { hints.proxies().registerJdkProxy(DataSource.class, RefreshedEventListener.class, SpringProxy.class, Advised.class, DecoratingProxy.class); } } // omitted}
Review
PaymentsController.java
. The controller handles requests to get a list of payments and create a payment in the database.PaymentsController.java
@Controller@ResponseBodyclass PaymentsController { private final JdbcClient db; PaymentsController(DataSource dataSource) { this.db = JdbcClient.create(dataSource); } @GetMapping("/payments") Collection<Payment> getPayments() { return this.db .sql("SELECT * FROM payments") .query((rs, rowNum) -> new Payment( rs.getString("id"), rs.getString("name"), rs.getString("cc_info"), rs.getTimestamp("created_at").toInstant() )) .list(); } @PostMapping(path = "/payments", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) Collection<Payment> createPayment(@RequestBody Payment request) { var id = UUID.randomUUID().toString(); var statement = String.format( "INSERT INTO payments(id, name, cc_info, created_at) " + "VALUES('%s', '%s', '%s', '%s')", id, request.name, request.ccInfo, Instant.now().toString()); this.db.sql(statement).update(); return this.db .sql(String.format("SELECT * FROM payments WHERE id = '%s'", id)) .query((rs, rowNum) -> new Payment( rs.getString("id"), rs.getString("name"), rs.getString("cc_info"), rs.getTimestamp("created_at").toInstant() )).list(); } record Payment(String id, String name, @JsonProperty(value = "cc_info") String ccInfo, Instant createdAt) { }}
In your terminal, build and run the application using the
native.sh
script. The script cleans up any previous builds with JVM and rebuilds the application on GraalVM using the Apache Maven Wrapper. Note it will take a few minutes to build. Once it starts, the application creates a data source based on a database username and password generated by Vault.$ bash native.sh ... omitted ... 2024-05-16T16:24:47.586-04:00 INFO 80770 --- [vault-graalvm] [ main] c.h.v.VaultGraalvmApplication : Starting AOT-processed VaultGraalvmApplication using Java 22.0.12024-05-16T16:24:47.586-04:00 INFO 80770 --- [vault-graalvm] [ main] c.h.v.VaultGraalvmApplication : No active profile set, falling back to 1 default profile: "default"2024-05-16T16:24:47.600-04:00 INFO 80770 --- [vault-graalvm] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)2024-05-16T16:24:47.600-04:00 INFO 80770 --- [vault-graalvm] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]2024-05-16T16:24:47.600-04:00 INFO 80770 --- [vault-graalvm] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20]2024-05-16T16:24:47.604-04:00 INFO 80770 --- [vault-graalvm] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext2024-05-16T16:24:47.604-04:00 INFO 80770 --- [vault-graalvm] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 18 ms2024-05-16T16:24:47.609-04:00 INFO 80770 --- [vault-graalvm] [ main] SourceVaultConfiguration$$SpringCGLIB$$0 : build data source: v-token-payments-0nnvZGWbvfekLOBpGE6d-1715891087,RDfKV2FIHo3UzFI-U0zl2024-05-16T16:24:47.647-04:00 INFO 80770 --- [vault-graalvm] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''2024-05-16T16:24:47.649-04:00 INFO 80770 --- [vault-graalvm] [ main] c.h.v.VaultGraalvmApplication : Started VaultGraalvmApplication in 0.106 seconds (process running for 0.115)
Wait for two minutes for credentials to expire. Then, open a new terminal session. Request the application for a list of payments in the database using its API. The database should return an empty list, as it did not record any payments.
$ curl localhost:8080/payments []
When you make the request, the application gets a new database username and password from Vault to connect to the database and successfully rebuilds the database connection.
$ bash native.sh ... omitted ... 2024-05-16T16:24:47.609-04:00 INFO 80770 --- [vault-graalvm] [ main] SourceVaultConfiguration$$SpringCGLIB$$0 : build data source: v-token-payments-0nnvZGWbvfekLOBpGE6d-1715891087,RDfKV2FIHo3UzFI-U0zl2024-05-16T16:24:47.647-04:00 INFO 80770 --- [vault-graalvm] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''2024-05-16T16:24:47.649-04:00 INFO 80770 --- [vault-graalvm] [ main] c.h.v.VaultGraalvmApplication : Started VaultGraalvmApplication in 0.106 seconds (process running for 0.115) 2024-05-16T16:26:27.588-04:00 INFO 80770 --- [vault-graalvm] [g-Cloud-Vault-1] SourceVaultConfiguration$$SpringCGLIB$$0 : expire lease, rotate database credentials2024-05-16T16:26:27.596-04:00 INFO 80770 --- [vault-graalvm] [g-Cloud-Vault-1] SourceVaultConfiguration$$SpringCGLIB$$0 : update database properties : v-token-payments-DRqCCVvaHxksjkLzZLFU-1715891187,i-Jwum2tbrMS0dCntbwS2024-05-16T16:26:27.596-04:00 INFO 80770 --- [vault-graalvm] [g-Cloud-Vault-1] SourceVaultConfiguration$$SpringCGLIB$$0 : build data source: v-token-payments-DRqCCVvaHxksjkLzZLFU-1715891187,i-Jwum2tbrMS0dCntbwS
Clean up
Stop the application and remove the Vault server and database.
Stop the application running with Maven.
Change the working directory to
reload/
.$ cd .. && pwd cd /learn-vault-spring-cloud/reload
Remove the Vault server and database with Docker Compose.
$ docker compose down
Next steps
In this tutorial, you learned how to reload a Spring application with new static secrets on a scheduled interval. Then, you used application context refreshing to reload dynamic secrets for a database based on lease expiration. For applications that run on GraalVM or cannot use context refreshing, you refactored the application to reload dynamic database credentials based on application events.
By configuring your Spring application with code to handle context refreshing, you can reload connections to databases or upstream APIs with minimal downtime in the application. Writing code allows you to control the application’s handling and behavior when it finds a new secret. For applications that you cannot refactor to interface with Vault, you can use Vault agent to check Vault for new secrets and reload the application without adding new code.
For more information, review the following resources:
- Spring Cloud Vault
- Spring Vault, the core library for Spring Cloud Vault
- GraalVM
- Vault Agent