はじめに
Kubernetes環境でSpring Bootアプリケーションを運用する際、データベースのパスワードやAPIキーなどの機密情報をKubernetes Secretで管理することが一般的です。しかし、Secretのデータを変更した際に、Spring Bootアプリケーションに変更を反映させるためには、通常Podの再起動が必要になります。
本記事では、Spring Cloud Kubernetes Configuration Watcherを使用して、Kubernetes Secretの変更を自動的にSpring Bootアプリケーションに反映する方法を自分用にまとめてみました。
問題の背景
Kubernetesは、アプリケーションのコンテナ内にSecretをボリュームとしてマウントすることができます。この機能を使えば、Secretの内容が変更されると、マウントされたボリュームも合わせて更新されます。しかし、Spring Bootはアプリケーションを再起動・再デプロイしない限り、構成・コンテキストを自動的に更新しません。つまり、Spring Bootアプリケーションには、Kubernetes Secretのデータ変更が自動的に反映されません。
Spring Cloud Kubernetes Configuration Watcherとは
Spring Cloud Kubernetes Configuration Watcherは、Kubernetes APIを通じてConfigMapやSecretの変更を監視し、変更を検知すると該当するSpring Bootアプリケーションに対してリフレッシュイベントを送信します。これにより、アプリケーションは新しい設定値を自動的に読み込みます。
環境・条件
以下は、実際に自分で実装・検証を行った環境です。
- Java: 21
- Spring Boot: 3.3.2
- spring-cloud-kubernetes-configuration-watcher:3.1.6
- Kubernetes: 1.20以上
実装方法
1. Kubernetes側の設定
Spring Cloud Kubernetes Configuration Watcher controller
Spring Bootアプリケーションが存在するCluster・NamespaceにSpring Cloud Kubernetes Configuration Watcher controllerをデプロイします。
サンプルコード
Deployment YAML - Spring Cloud Kubernetes Configuration Watcherを参考にしました。
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: Service
metadata:
name: configuration-watcher-service
namespace: sample-namespace
labels:
app: configuration-watcher
spec:
ports:
- name: http
port: 8888
targetPort: 8888
selector:
app: configuration-watcher
type: ClusterIP
- apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app: configuration-watcher
name: configuration-watcher-service-account
- apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app: configuration-watcher
name: configuration-watcher-role-binding
roleRef:
kind: Role
apiGroup: rbac.authorization.k8s.io
name: namespace-reader
subjects:
- kind: ServiceAccount
name: configuration-watcher-service-account
- apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: namespace-reader
rules:
- apiGroups: [""]
resources: ["configmaps", "pods", "services", "endpoints", "secrets"]
verbs: ["get", "list", "watch"]
- apiVersion: apps/v1
kind: Deployment
metadata:
name: configuration-watcher-deployment
namespace: sample-namespace
spec:
selector:
matchLabels:
app: configuration-watcher
template:
metadata:
labels:
app: configuration-watcher
spec:
serviceAccountName: configuration-watcher-service-account
containers:
- name: configuration-watcher
image: springcloud/spring-cloud-kubernetes-configuration-watcher:3.1.6
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8888
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8888
path: /actuator/health/liveness
ports:
- containerPort: 8888
env:
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CONFIGURATION_WATCHER
value: DEBUG
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD
value: DEBUG
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD
value: DEBUG
Secretファイル
変更を検知したいKubernetes Secretのmetadataに、以下2つを追加します。
-
spring.cloud.kubernetes.secret.apps: このアノテーションにより、Secretデータに変更が発生したときに通知を受け取るアプリケーションの名前を指定します -
spring.cloud.kubernetes.secret: このラベルを持つSecretが変更された場合にのみ、Configuration Watcherからイベントが発行されます
apiVersion: v1
kind: Secret
data:
# ...
API_KEY: <base64_encoded_api_key>
metadata:
annotations:
spring.cloud.kubernetes.secret.apps: <app_name>
labels:
spring.cloud.kubernetes.secret: "true"
2. Spring Bootアプリケーション側の設定
pom.xml
必要な依存関係を追加します(Gradleでも同様の設定ができます)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
<version>3.1.6</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>4.1.6</version>
</dependency>
application.properties
アクチュエータエンドポイント/refreshを公開します(application.ymlでも同様の設定ができます)。
api.key=${API_KEY}
...
+ management.endpoints.web.exposure.include=refresh
+ management.health.defaults.enabled=false
bootstrap.properties
以下のプロパティを設定したbootstrap.propertiesを作成します(bootstrap.ymlでも同様の設定ができます)。
-
spring.cloud.kubernetes.secrets.enabled=true: Spring Cloud KubernetesのSecret機能を有効化 -
spring.cloud.kubernetes.secrets.enable-api=true: Kubernetes API経由でSecretの読み込みを可能にする -
spring.cloud.kubernetes.secrets.name=<secret_name>: 対象のSecret名を指定 -
spring.cloud.kubernetes.secrets.useNameAsPrefix=false: プロパティ名のプレフィックスを無効化(デフォルトはtrueで、trueの場合、Secretから読み込んだプロパティにSecret名がプレフィックスとして付加される) -
spring.cloud.kubernetes.secrets.includeProfileSpecificSources=false: 不要な検索を減らすための最適化(設定しなくても良い) -
spring.cloud.kubernetes.config.enabled=false: ConfigMapの監視は不要のため、余計な処理をしないようにする(設定しなくても良い)
spring.application.name=@project.name@
# Spring Cloud Kubernetes
spring.cloud.kubernetes.secrets.enabled=true
spring.cloud.kubernetes.secrets.enable-api=true
spring.cloud.kubernetes.secrets.name=<secret_name>
spring.cloud.kubernetes.secrets.useNameAsPrefix=false
spring.cloud.kubernetes.secrets.includeProfileSpecificSources=false
spring.cloud.kubernetes.config.enabled=false
Secretデータを使用するBean
対象のSecretデータを使用するBeanに@RefreshScopeアノテーションを追加することで、Secretが変更された際に、そのBeanの設定値を更新します。
@Component
@RefreshScope
public class ApiService {
@Value("${api.key}")
private String apiKey;
public void callApi() {
// apiKeyを使用した処理
System.out.println("Using API Key: " + apiKey);
}
}
※ @Value以外にも、Environmentや@ConfigurationPropertiesを使用してプロパティを読み込む場合でも同様に機能します。
トラブルシューティング
アクチュエータエンドポイント/refreshが302を返す
事象
configuration-watcherのPodで、下記のようなログが出力されます。
Refresh sent to <app_name> at URI address http://.../actuator/refresh returned a 302 FOUND
原因
configuration-watcherからの/actuator/refreshへのPOSTリクエストがCSRFトークンなしで送信されるため、Spring Securityがデフォルトで有効にしているCSRF保護により、302 FOUNDでリダイレクトされます。
解決策
Web Securityの設定を行うクラスのsecurityFilterChainメソッドで、csrf.ignoringRequestMatchers("/actuator/refresh")を設定します。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
...
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// ...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf(csrf -> csrf.ignoringRequestMatchers("/actuator/refresh"));
return http.build();
}
// ...
}
アクチュエータエンドポイント/refreshが404を返す
事象
configuration-watcherのPodで、下記のようなログが出力されます。org.springframework.web.reactive.function.client.WebClientResponseException$NotFound: 404 Not Found from POST http://<app_name>/actuator/refresh
原因
application.propertiesにおいて、server.servlet.context-path=<context_path>が設定されているため、デフォルトのエンドポイント /actuator/refresh ではなく、/<context_path>/actuator/refresh に送信する必要があります。
解決策
Configuration Watcher controllerのコンテナに環境変数SPRING_CLOUD_KUBERNETES_CONFIGURATION_WATCHER_ACTUATOR_PATHを設定します。
env:
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CONFIGURATION_WATCHER
value: DEBUG
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD
value: DEBUG
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD
value: DEBUG
+ - name: SPRING_CLOUD_KUBERNETES_CONFIGURATION_WATCHER_ACTUATOR_PATH
+ value: "/<context_path>/actuator"
その他
上記以外にも、権限やネットワークポリシーなどで問題が発生する可能性もあるため、注意が必要です。
参考・引用資料
- Starters :: Spring Cloud Kubernetes
- Spring Cloud Kubernetes Configuration Watcher :: Spring Cloud Kubernetes
- Getting Started | Centralized Configuration - Spring
- Spring Cloud Context: Application Context Services :: Spring Cloud Commons
あとがき
最後までご覧いただきありがとうございました。
少しでも良い・役に立ったと感じていただけましたら、いいねしていただけますと幸いです。
もし間違ってるところがあれば、遠慮なくコメントでご教示ください。