Vault
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 at- kv/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 the- kv/${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.07375588Z custom_metadata <nil> deletion_time n/a destroyed false version 1 =============== Data =============== Key Value --- ----- spring.datasource.password postgres-admin-password spring.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.xmlto 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.propertiesin- /src/main/resources. The application sets- spring.cloud.vault.uriand- spring.cloud.vault.tokento 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 Vault spring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200} spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # import configuration from vault spring.config.import=vault:// # configure KV backend spring.cloud.vault.kv.backend=kv # configure access to database spring.datasource.url=jdbc:postgresql://localhost/payments # set refresh interval for static secrets secrets.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 the- spring.cloud.vault.kv.backendproperty. 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 Vault spring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200} spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # import configuration from vault spring.config.import=vault:// # configure KV backend spring.cloud.vault.kv.backend=kv # configure access to database spring.datasource.url=jdbc:postgresql://localhost/payments # set refresh interval for static secrets secrets.refresh-interval-ms=180000
- Additional properties like - spring.datasource.urlin- application.propertiesdefine application access to the database. Finally, create a custom property named- secrets.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 Vault spring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200} spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # import configuration from vault spring.config.import=vault:// # configure KV backend spring.cloud.vault.kv.backend=kv # configure access to database spring.datasource.url=jdbc:postgresql://localhost/payments # set refresh interval for static secrets secrets.refresh-interval-ms=180000
- Open - VaultStaticSecretsApplication.javain- /src/main/java/com/hashicorp/vaultstaticsecrets. The class includes a few annotations, including- EnableScheduling, which schedules a task in the application.- VaultStaticSecretsApplication.java - @SpringBootApplication @EnableScheduling public 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.javainclude the- DataSourcedefining the database connection. Annotate it as a- Beanand add the- RefreshScopeannotation.- RefreshScopeidentifies beans that the application can rebuild, such as database connections or clients to upstream services.- VaultStaticSecretsApplication.java - @SpringBootApplication @EnableScheduling public 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.javain- /src/main/java/com/hashicorp/vaultstaticsecrets. The class includes the- Componentannotation to ensure the context refresh task runs automatically on application startup. The- refreshermethod runs a context refresh, which allows the application to shut down existing database connections and start connections with new credentials. Annotate the- refreshermethod with- @Scheduledand 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 - @Component class 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.javain- /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 @ResponseBody class 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.1 2024-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-880aa90ab4d0 2024-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 WebApplicationContext 2024-05-15T16:11:08.431-04:00 INFO 10359 --- [vault-static-secrets] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 569 ms 2024-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-password 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)
- 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.184846199Z custom_metadata <nil> deletion_time n/a destroyed false version 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 secret 2024-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 ms 2024-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-password 2024-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 secret 2024-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-password 2024-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@7f34f624 2024-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 the- payments-appVault 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/wr6PS86RKSY0VFn9GYdXb8R0 lease_duration 1m lease_renewable true password DB_PASSWORD username 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.xmlto 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 the- spring-cloud-vault-config-databaseslibrary.- 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.propertiesin- /src/main/resources. The application sets- spring.cloud.vault.uriand- spring.cloud.vault.tokento 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 Vault spring.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 Vault spring.config.import=vault:// # configure KV backend spring.cloud.vault.kv.enabled=false # configure database secrets engine spring.cloud.vault.database.enabled=true spring.cloud.vault.database.role=payments-app spring.cloud.vault.database.backend=database # tune lease renewal and expiry threshold for 2 minute max ttl spring.cloud.vault.config.lifecycle.min-renewal=30s spring.cloud.vault.config.lifecycle.expiry-threshold=10s # configure access to database spring.datasource.url=jdbc:postgresql://localhost/payments
- The - spring.cloud.vault.database.enabledand- spring.cloud.vault.database.backendattributes in- application.propertiesconfigure the Spring Cloud Config database backend to get a username and password from the database secrets engine at the- database/path in Vault. Set- spring.cloud.vault.database.roleto the- payments-appVault 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 Vault spring.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 Vault spring.config.import=vault:// # configure KV backend spring.cloud.vault.kv.enabled=false # configure database secrets engine spring.cloud.vault.database.enabled=true spring.cloud.vault.database.role=payments-app spring.cloud.vault.database.backend=database # tune lease renewal and expiry threshold for 2 minute max ttl spring.cloud.vault.config.lifecycle.min-renewal=30s spring.cloud.vault.config.lifecycle.expiry-threshold=10s # configure access to database spring.datasource.url=jdbc:postgresql://localhost/payments
- Set - spring.cloud.vault.config.lifecycle.min-renewaland- spring.cloud.vault.config.lifecycle.expiry-thresholdto shorter intervals in- application.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 Vault spring.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 Vault spring.config.import=vault:// # configure KV backend spring.cloud.vault.kv.enabled=false # configure database secrets engine spring.cloud.vault.database.enabled=true spring.cloud.vault.database.role=payments-app spring.cloud.vault.database.backend=database # tune lease renewal and expiry threshold for 2 minute max ttl spring.cloud.vault.config.lifecycle.min-renewal=30s spring.cloud.vault.config.lifecycle.expiry-threshold=10s # configure access to database spring.datasource.url=jdbc:postgresql://localhost/payments
- Open - VaultDynamicSecretsApplication.javain- /src/main/java/com/hashicorp/vaultdynamicsecrets. The class includes a- DataSourcedefining the database connection. Annotate it as a- Beanand add the- RefreshScopeannotation.- RefreshScopeidentifies beans that the application can rebuild, such as database connections or clients to upstream services.- VaultDynamicSecretsApplication.java - @SpringBootApplication public 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.javain- /src/main/java/com/hashicorp/vaultdynamicsecrets. The class includes the- Componentannotation to ensure the application scans it at startup. The constructor includes a- SecretLeaseContainerto listen for events related to leases at the database secrets engine path. Each time the application receives a- SecretLeaseExpiredEvent, it refreshes application context to get new credentials from Vault.- VaultRefresher.java - @Component class 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.javain- /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 @ResponseBody class 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.1 2024-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-6e5fb27b87ce 2024-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 WebApplicationContext 2024-05-16T14:56:09.657-04:00 INFO 9917 --- [vault-dynamic-secrets] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 669 ms 2024-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-d218V3O 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)
- 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-d218V3O 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) ... 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 credentials 2024-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 ms 2024-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-EUNcZj2 2024-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@5eef82cf 2024-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-16 GraalVM 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.xmlto 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 the- spring-cloud-vault-config-databaseslibrary. 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.propertiesin- /src/main/resources. In GraalVM, you must disable Spring Cloud refresh with the- spring.cloud.refresh.enabledattribute. The remaining- application.propertiesset 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 disabled spring.cloud.refresh.enabled=false # configure access to Vault spring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200} spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # configure KV backend spring.cloud.vault.kv.enabled=false # set up configuration import from Vault spring.config.import=vault:// # configure database secrets engine spring.cloud.vault.database.enabled=true spring.cloud.vault.database.role=payments-app spring.cloud.vault.database.backend=database # tune lease renewal and expiry threshold for 2 minute max ttl spring.cloud.vault.config.lifecycle.min-renewal=30s spring.cloud.vault.config.lifecycle.expiry-threshold=10s # configure access to database spring.datasource.url=jdbc:postgresql://localhost/payments
- Open - VaultGraalvmApplication.javain- /src/main/java/com/hashicorp/vaultgraalvm. The class includes the- mainfunction to start the application.- VaultGraalvmApplication.java - @SpringBootApplication public class VaultGraalvmApplication { public static void main(String[] args) { SpringApplication.run(VaultGraalvmApplication.class, args); } }
- Examine - RefreshableDataSourceVaultConfiguration.java. The constructor uses Spring Vault’s- SecretLeaseContainerto add a lease listener and wait for a- SecretLeaseExpiredEventand 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.javafile in- /src/main/java/com/hashicorp/vaultgraalvmalso creates a- DataSourcewith 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 a- refreshmethod 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.javadefines 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 import- RefreshableDataSourceHintswith the- ImportRuntimeHintsannotation.- 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 @ResponseBody class 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.shscript. 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.1 2024-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 WebApplicationContext 2024-05-16T16:24:47.604-04:00 INFO 80770 --- [vault-graalvm] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 18 ms 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-U0zl 2024-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-U0zl 2024-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 credentials 2024-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-Jwum2tbrMS0dCntbwS 2024-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