Auto-Refreshing Spring Connection Pools on Kubernetes with Vault

Update Sept 14, 2020: I recently learned that you can instruct Vault Agent to run a command after rendering your template by using the command setting in your Vault Agent config. This eliminates the need of the inotifywait container that this post describes, although you can use inotifywait instead of the command setting if you want greater control over how modify events are handled.

For more information on the command setting, please refer to the Vault Agent Template documentation.


Many Kubernetes applications that fetch secrets from Vault also commonly enjoy the benefits of Vault Agent, which allows you to automatically refresh your vault token and fetch updates to your secret KV store. Vault agent is an excellent capability that makes connecting applications with services such as databases and messaging queues simple.

One thing, however, is often overlooked: How do you automatically refresh your connection pools?

Establishing the initial connection is easy – just run Vault Agent in an init container to fetch your service’s credentials and use those credentials when your main application starts. But, in Spring Boot applications, this connection pool is not automatically refreshed when Vault Agent updates your secrets. Sure, the underlying file containing your secrets has changed, but your connection pool will remain unaware of these changes unless you or an automated process explicitly refreshes this connection.

There are plenty of blog posts and documentation already on how you can manually trigger a connection pool refresh in your Spring Boot application. In this post, I want to take it a step further. Let’s automatically refresh Spring Boot’s connection pool in Kubernetes when your vault agent sidecar picks up new database credentials!

I’m only going to hit on the key concepts in this post, but if you’d like to dive deeper, I have a working demo in my GitHub repo you can follow along with to see this process hands-on.

The @RefreshScope Annotation

The @RefreshScope annotation is Part 1 of the magic. This annotation refreshes annotated beans within the Spring context when the /actuator/refresh endpoint is called. How will this help our use case? The Spring Cloud documentation can answer this best:

[@RefreshScope] addresses the problem of stateful beans that only get their configuration injected when they are initialized. For instance if a DataSource has open connections when the database URL is changed via the Environment, we probably want the holders of those connections to be able to complete what they are doing. Then the next time someone borrows a connection from the pool he gets one with the new URL.

https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_refresh_scope

Sounds like this will help refresh our connection pool! Before you can use it, however, be sure to include the spring-cloud-starter-config and spring-boot-starter-actuator dependencies, which contain the @RefreshScope annotation and the /actuator/refresh endpoint, in your pom.xml or build.gradle file. Here’s a POM example below.

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

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>Hoxton.SR7</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Once you’ve included these dependencies, you need to apply the @RefreshScope annotation on your DataSource bean. Below is an example.

package com.austindewey.util;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RefreshScope
public class DBPropRefresh {
    @Value("${spring.datasource.url}")
    private String url;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Bean
    @RefreshScope
    public DataSource getDatasource() {
        return DataSourceBuilder.create().url(url).username(username).password(password).build();
    }
}

Notice the two invocations of @RefreshScope: One above the class signature, and another above the Bean.

I found that I also needed to create a separate class containing each of the properties that need refreshed (spring.datasource.url, spring.datasource.username, and spring.datasource.password). Here’s what this class looks like:

package com.austindewey.util;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class PropertyConfiguration {
    private String url;
    private String username;
    private String password;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

Great! Now we have a refreshable DataSource bean and a class consisting of each of the properties we need to refresh automatically. Let’s take a look at the Kubernetes side of things, where we’ll employ two different sidecar containers – one that refreshes your secrets from Vault and another that refreshes your connection pool if your Vault secrets were updated.

Vault Agent

The rest of the magic happens on the Kubernetes side. Part of this has to do with Vault Agent, and the other part has to do with a tool called inotifywait.

Let’s start with Vault Agent.

Vault agent is a client-side daemon that automatically authenticates with Vault to handle token-renewal for the retrieval of dynamic secrets (such as database credentials).

Vault agent allows you to format your secrets using Consul templates. This is perfect for a Spring Boot application because you can write a Consul template to generate an application.properties file that contains your database credentials. Vault Agent will also keep this properties file refreshed, which will be important later to refresh your datasource bean.

Let’s look at the relevant portions of the Kubernetes manifest required to deploy a Spring Boot app with Vault Agent running as a sidecar. I’ll assume that Vault is already configured with the Kubernetes Authentication backend.

Manifest

First is a ConfigMap that contains the Vault Agent config. Here, you’ll find the Consul template that generates your application.properties file.

apiVersion: v1
kind: ConfigMap
metadata:
  name: spring-boot-postgres
data:
  vault-agent-config.hcl: |-
    vault {
      address = "http://vault:8200"
    }

    pid_file = "/home/vault/.pid"

    auto_auth {
      method "kubernetes" {
        mount_path = "auth/kubernetes"
        config = {
          role = "example"
          jwt = "@/var/run/secrets/kubernetes.io/serviceaccount/token"
        }
      }

      sink "file" {
        config = {
          path = "/home/vault/.token"
        }
      }
    }

    template {
      destination = "/deployments/config/application.properties"
      contents = <<EOF
    {{- with secret "secret/myapp/config" -}}
    spring.datasource.url=jdbc:postgresql://postgres-postgresql:5432/widget
    spring.datasource.username={{ .Data.data.username }}
    spring.datasource.password={{ .Data.data.password }}
    spring.jpa.hibernate.ddl-auto=none
    management.endpoints.web.exposure.include=refresh,health
    {{- end -}}
    EOF
    }

Next, in your Deployment, you need Vault Agent running as an init container to preload your database credentials before starting your application. You’ll also need a sidecar container to keep your secrets updated throughout your application’s lifetime.

Here’s the Vault Agent init container:

     initContainers:
        - name: vault-agent-init
          image: vault
          args:
            - agent
            - -config=/etc/vault/vault-agent-config.hcl
            - -log-level=debug
            - -exit-after-auth
          env:
            - name: SKIP_SETCAP
              value: "true"
          volumeMounts:
            - mountPath: /etc/vault/
              name: config
            - mountPath: /deployments/config
              name: shared-data

And here’s the Spring Boot application and Vault Agent sidecar:

     containers:
        - name: main
          image: quay.io/adewey/spring-boot-postgres
          imagePullPolicy: Always
          readinessProbe:
            httpGet:
              path: /actuator/health
              port: 8080
          volumeMounts:
            - mountPath: /deployments/config
              name: shared-data
        - name: vault-agent
          image: vault
          args:
            - agent
            - -config=/etc/vault/vault-agent-config.hcl
            - log-level=debug
          env:
            - name: SKIP_SETCAP
              value: "true"
          volumeMounts:
            - mountPath: /etc/vault/
              name: config
            - mountPath: /deployments/config
              name: shared-data

Here are the volumes these containers refer to:

     volumes:
        - name: config
          configMap:
            name: spring-boot-postgres
        - name: shared-data
          emptyDir:
            medium: Memory

This manifest lets you load your Vault secrets to your application Pod, but it doesn’t solve the problem of automatically refreshing your connection pool. Let’s handle that next using inotifywait.

inotifywait

Right now, Vault Agent will refresh your secrets on the Pod filesystem, but the Spring Boot application will not load these changes to refresh the connection pool. To allow your connection pool to refresh, you have the following options:

  1. Restart your application
  2. Call the /actuator/refresh endpoint manually
  3. Create automation that calls the /actuator/refresh endpoint on a given interval (polling)
  4. Create automation that pings the /actuator/refresh endpoint when the application.properties file is updated (event-driven)
  1.  

I like number 4, don’t you? We can achieve this by using a tool called inotifywait.

Inotifywait comes from the inotify-tools package and, according to the inotify-tools GitHub page, is used to monitor and act upon filesystem events.

We can use inotifywait to watch for changes to the application.properties file, and when a change occurs, we can automatically trigger the /actuator/refresh endpoint.

Let’s look at the manifest required to set this up.

Manifest

To set up inotifywait to automatically refresh your connection pool, you need to create another sidecar container alongside your Spring Boot container:

       - name: inotifywait
          image: docker.io/pstauffer/inotify:v1.0.1
          command:
            - /bin/sh
            - -c
          args:
            - |-
              while true; do
                inotifywait -e modify /deployments/config/application.properties;
                curl localhost:8080/actuator/refresh -X POST;
              done
          volumeMounts:
            - mountPath: /deployments/config
              name: shared-data

You can see how this works by focusing on the “args”. You can see an infinite loop that and instructs inotifywait to hang until it detects a “modify” event against the application.properties file. Once a modify event occurs, it triggers the /actuator/refresh endpoint to refresh the connection pool, and the loop starts over. In this example, the image I’m using that contains the inotifywait tool is docker.io/pstauffer/inotify:v1.0.1. If you’re interested in employing this solution, I recommend writing your own image that contains inotifywait or at least be aware of the risks of running third party images from Dockerhub.

Demo

Want to see a demo of all this in action? Please check out my GitHub repo for more information. I highlighted the key details in this post, but I’ve written out a step-by-step demo in GitHub you can follow to get hands-on experience with this process.

If you follow along with the demo, you’ll build the following architecture, utilizing the components discussed in this post.

Thanks for Reading!

Hopefully, you find this to be a compelling solution to a long-standing issue. It’s simple to use Vault Agent to keep your dynamic secrets updated, but it’s more challenging to refresh your Spring Boot connection pool once those dynamic secrets are updated. The right solution for you will depend on your company’s stack and direction, but this solution provides a starting point for you to begin thinking about how you can solve this tricky problem within your organization!

Austin Dewey

Austin Dewey is a DevOps engineer focused on delivering a streamlined developer experience on cloud and container technologies. Austin started his career with Red Hat’s consulting organization, where he helped drive success at many different Fortune 500 companies by automating deployments on Red Hat’s Kubernetes-based PaaS, OpenShift Container Platform. Currently, Austin works at fintech startup Prime Trust, building automation to scale financial infrastructure and support developers on Kubernetes and AWS. Austin is the author of "Learn Helm", a book focused on packaging and delivering applications to Kubernetes, and he enjoys writing about open source technologies at his blog in his free time, austindewey.com.

2 Comments

  1. Hi Austin.

    Thanks for the post.

    In our case, we are having static secrets. But the issue I’m facing is on openshift side. Pods are running but not in ready state. After sometime, Actuator runs the health check and liveness and readiness probe fails and therefore appln shut down.

    Below are the logs:

    2020-12-08 17:17:34.063 INFO 1 — [ task-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 – Starting…
    2020-12-08 17:17:34.860 INFO 1 — [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path ‘/actuator’
    2020-12-08 17:17:35.661 INFO 1 — [ main] pertySourcedRequestMappingHandlerMapping : Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2Controller#getDocumentation(String, HttpServletRequest)]
    2020-12-08 17:17:36.162 DEBUG 1 — [ task-1] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@5a601c83
    2020-12-08 17:17:36.165 INFO 1 — [ task-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 – Start completed.
    2020-12-08 17:17:36.265 DEBUG 1 — [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Pool stats (total=1, active=1, idle=0, waiting=0)
    2020-12-08 17:17:36.464 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@2c45baa2
    2020-12-08 17:17:36.665 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@213c4ef5
    2020-12-08 17:17:36.760 INFO 1 — [ task-1] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
    2020-12-08 17:17:36.862 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@59f55930
    2020-12-08 17:17:37.060 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@4ae0723f
    2020-12-08 17:17:37.167 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@42f1f551
    2020-12-08 17:17:37.270 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@7447d19e
    2020-12-08 17:17:37.559 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@3a7b2f54
    2020-12-08 17:17:37.862 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@1ac3e18b
    2020-12-08 17:17:37.965 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Added connection com.mysql.cj.jdbc.ConnectionImpl@1fa87f3e
    2020-12-08 17:17:37.965 DEBUG 1 — [onnection adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – After adding stats (total=10, active=0, idle=10, waiting=0)
    2020-12-08 17:17:44.169 INFO 1 — [ task-1] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
    2020-12-08 17:17:44.274 INFO 1 — [ task-1] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit ‘default’
    2020-12-08 17:17:45.068 INFO 1 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ”
    2020-12-08 17:17:45.071 INFO 1 — [ main] d.s.w.p.DocumentationPluginsBootstrapper : Context refreshed
    2020-12-08 17:17:45.175 INFO 1 — [ main] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s)
    2020-12-08 17:17:45.376 INFO 1 — [ main] s.d.s.w.s.ApiListingReferenceScanner : Scanning for api listing references
    2020-12-08 17:17:47.370 INFO 1 — [ main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories?
    2020-12-08 17:17:50.364 INFO 1 — [ main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
    2020-12-08 17:17:50.462 INFO 1 — [ main] c.loblaw.eds.article.ArticleApplication : Started ArticleApplication in 46.487 seconds (JVM running for 48.838)

    2020-12-08 17:19:30.467 INFO 1 — [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit ‘default’
    2020-12-08 17:19:30.472 INFO 1 — [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService ‘applicationTaskExecutor’
    2020-12-08 17:19:30.559 INFO 1 — [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 – Shutdown initiated…
    2020-12-08 17:19:30.560 DEBUG 1 — [extShutdownHook] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – Before shutdown stats (total=10, active=0, idle=10, waiting=0)
    2020-12-08 17:19:30.561 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@5a601c83: (connection evicted)
    2020-12-08 17:19:30.564 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@2c45baa2: (connection evicted)
    2020-12-08 17:19:30.564 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@213c4ef5: (connection evicted)
    2020-12-08 17:19:30.565 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@59f55930: (connection evicted)
    2020-12-08 17:19:30.565 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@4ae0723f: (connection evicted)
    2020-12-08 17:19:30.565 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@42f1f551: (connection evicted)
    2020-12-08 17:19:30.566 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@7447d19e: (connection evicted)
    2020-12-08 17:19:30.566 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@3a7b2f54: (connection evicted)
    2020-12-08 17:19:30.566 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@1ac3e18b: (connection evicted)
    2020-12-08 17:19:30.567 DEBUG 1 — [nnection closer] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 – Closing connection com.mysql.cj.jdbc.ConnectionImpl@1fa87f3e: (connection evicted)
    2020-12-08 17:19:30.567 DEBUG 1 — [extShutdownHook] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 – After shutdown stats (total=0, active=0, idle=0, waiting=0)
    2020-12-08 17:19:30.567 INFO 1 — [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 – Shutdown completed.

    Below is the bootstrap.yaml.

    spring.cloud.vault:
    host: ${VAULT_ADDR}
    port: ${VAULT_PORT:443}
    scheme: https
    namespace: abc
    authentication: KUBERNETES
    kubernetes:
    kubernetes-path: ${VAULT_AUTH_PATH}
    role: ${VAULT_ROLE}
    service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
    generic:
    enabled: false
    kv:
    enabled: true
    backend: kv/team
    profile-separator: ‘/’
    default-context: openshift/${VAULT_ENV}
    application-name: openshift/${VAULT_ENV}
    config:
    order: 10
    lifecycle:
    enabled: false
    logging:
    level:
    org.springframework.jdbc.core: info
    org.springframework.vault.config: info

    Below is the application.yaml

    spring:
    datasource:
    url: jdbc:mysql://${DB_URL}:${DB_PORT}/${DB_NAME}?serverTimezone=UTC
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
    connection-timeout: 70000
    maximum-pool-size: 2
    minimum-idle: 1

    Also maxiumum pool size is 10 but I’m seeing this in the logs.
    Before shutdown stats (total=10, active=0, idle=10, waiting=0)

    Kindly share your inputs.

    1. What errors do the readiness and liveness probes throw? You can find these by doing an “oc describe pod $SPRING_POD”.

      Also, if this app can be shared on public GitHub, it might be helpful if I can see the repository in full.

Leave a Reply