在第一篇文章中,我们构建了一个可独立运行的原型(对应上述第1点),并成功将其连接至我们Kubernetes集群内部托管的数据库(对应上述第2点)。在本文里,我们将对该原型进行升级,使其能够在Kubernetes环境中运行(对应上述第3点),同时支持在集成开发环境(IDE)中对我们的应用程序进行调试。
假设你已经完成了之前的步骤,此时你的Spring Boot应用程序应已具备独立模式(standalone
)和连接模式(connected
)两种运行方式。这也就意味着你已经搭建好了本地的Kind Kubernetes集群,并且完成了Grafana监控、PostgreSQL数据库以及Vault密钥管理器的配置。
你可以在 GitHub 上获取到本文所涉及的代码及配置文件。
那么,接下来我们要做什么呢?
在本文中,我们将对应用程序进行一系列设置,使其能够在本地的Kind Kubernetes集群中顺利运行与调试。
除了启用远程调试功能,我们还会利用Vault中的静态凭证来解决数据库凭证相关问题。这样一来,既能避免因长时间调试会话以及密码轮换所引发的各类问题,又能实现与Vault的无缝集成。
总而言之,我们将用上在Kind集群配置中所暴露的所有端口,具体情况如上图所示。
k8s-debug
配置文件
现在,我们要将这个配置文件添加到原型之中。在这个名为application-k8s-debug.yml
的配置文件里,我们的目标是将应用程序部署到集群内部。鉴于要在集群内部调试应用程序,我们需执行以下操作:
- 通过内部服务地址,连接到集群数据库。
- 从Vault获取数据库密码,将其作为静态密钥。
- 向集群中的Java虚拟机(JVM)暴露调试端口。
- 启用日志记录功能,以便与Grafana和Loki协同工作。
数据库连接详情
首先,我们得更改数据源,以此访问PostgreSQL数据库集群(db-cluster-rw.pg.svc
)所暴露的内部服务。针对这一操作,有几点需要留意:
- 我们不指定集群名称,如此一来,你便能在不同名称的集群中使用该配置。
- 我们选用 - rw服务,原因在于它代表的是可接受读写命令的实例。
- 我们把pg作为数据库的命名空间。
数据库凭证
鉴于我们正借助k8s-debug
配置文件开发,所以我们将采用一种简单的键值对密钥形式,为应用程序提供固定的数据库用户名(app-user
)和密码(app-secret
)。这样做能够规避调试期间密码轮换带来的问题。我们会从Vault获取这些信息,从而实现集成。
在配置文件中,我们会把用户名和密码分别设置为环境变量STATIC_DB_USERNAME
和STATIC_DB_PASSWORD
。以下是相关代码片段:
src/main/resources/application-k8s-debug.yml
spring:
datasource:
url: jdbc:postgresql://db-cluster-rw.pg.svc:5432/myapp?currentSchema=myapp
username: ${STATIC_DB_USERNAME}
password: ${STATIC_DB_PASSWORD}
...
此刻,我们必须在部署文件中设置这些环境变量。我们将从default
命名空间下的static-db-credentials
Kubernetes密钥里获取这些变量。以下是文件中的代码片段:
[k8s/k8s-debug-deployment.yml](https://github.com/MartinHodges/spring-boot-k8s-template/blob/main/k8s/k8s-debug-deployment.yml)
...
env:
...
- name: STATIC_DB_USERNAME
valueFrom:
secretKeyRef:
name: static-db-credentials
key: username
- name: STATIC_DB_PASSWORD
valueFrom:
secretKeyRef:
name: static-db-credentials
key: password
从 Vault 创建密钥
接下来,我将展示如何从 Vault 中提取密钥,并将其作为 Kubernetes 密钥添加到我们的集群。虽然这里我们主要针对数据库凭证进行操作,但这种方法对于任何想从 Vault 注入到 Spring Boot 应用程序的内容都非常实用。
首先,我们要在 Vault 中创建一个键值对密钥。需要注意的是,Vault 的键值(KV)引擎允许在单个 KV 密钥中存储多个属性,在我们的场景中,就是用户名和密码。
在 vault-0
Pod 中开启一个命令行,登录到 Vault,启用键值密钥引擎,然后创建密钥:
kubectl exec -it vault-0 -n vault -- sh
vault login <root token>
vault secrets enable -path=spring-boot-k8s-template kv-v2
vault kv put -mount=spring-boot-k8s-template db username=app-user password=app-secret
我们使用的是 KV - V2 引擎,它支持版本化的密钥。我们将其挂载在
spring - boot - k8s - template
路径下。
现在,我们在 spring - boot - k8s - template/db
处已经有了包含 username
和 password
字段的密钥。
接下来,我们要把这些密钥提取到一个 Kubernetes Secret
中。我们将使用 Kubernetes ExternalSecrets
,并借助外部密钥操作符(ESO)来管理 SecretStore
。这个存储会从 Vault 中获取密钥,并将其作为 Kubernetes Secret
注入到我们的集群。之后,我们就可以像使用普通的 Kubernetes Secret
一样使用这个密钥,比如将它们作为环境变量注入到容器中。
我们通过 Helm 来部署 ESO。先把所需的仓库添加到本地 Helm 中:
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
我们将把 ESO 安装到一个新创建的独立命名空间中:
kubectl create namespace eso
helm install external-secrets external-secrets/external-secrets -n eso
现在,我们来创建一个 SecretStore
,它代表外部密钥的提供者,也就是我们的 Vault。通过以下文件来完成:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: default
spec:
provider:
vault:
server: "http://vault-active.vault:8200"
path: "spring-boot-k8s-template"
version: "v2"
auth:
# 指向包含 Vault 令牌的密钥
tokenSecretRef:
name: "vault-token"
key: "token"
注意,我们把 SecretStore
部署到了 default
命名空间,这也是我们的 Spring Boot 原型将要运行的命名空间。如果你打算在其他命名空间运行应用程序,这里也需要相应更改。
此外,我们将挂载点指定为 path: "spring-boot-k8s-template"
,以和之前启用密钥引擎时使用的名称保持一致。
在使用 SecretStore
之前,我们必须设置它要使用的令牌(即 vault-token
密钥中的 token
键)。
要为这个配置文件创建一个静态令牌,需要在 Vault Pod 上开启一个命令行,使用根访问令牌来创建新令牌。
kubectl exec -it vault-0 -n vault -- sh
vault token create -period 0
如果你使用根令牌登录后创建了新令牌,新令牌会生成,结果大致如下:
Key Value
--- -----
token hvs.Lw3Kv46jTCT2OMXFXjHWettD
token_accessor phSvwm5lrGqQvclXS5qKdC1H
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
然后可以将此令牌保存为 Kubernetes 密钥(将 < > 字段替换为合适的值):
kubectl create secret generic vault-token --from-literal=token='<上面的令牌>'
注意,Kubernetes 在使用 --from -literal
创建密钥时会自动处理 Base64 编码。
现在使用以下命令创建 SecretStore
:
kubectl apply -f k8s/secret-store.yml
并检查它是否已启动:
kubectl get secretstore
NAME AGE STATUS CAPABILITIES READY
vault-backend 2s Valid ReadWrite True
注意,状态为 Valid
(表示可以登录到 Vault),并且 ready
为 True
。
现在,我们从 Vault 为包含用户名和密码的 static-db-credentials
创建外部密钥(同样在默认命名空间中)。创建以下文件:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-db-username
namespace: default
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: static-db-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: spring-boot-k8s-template/db
property: username
- secretKey: password
remoteRef:
key: spring-boot-k8s-template/db
property: password
并应用此配置:
kubectl apply -f k8s/external-secrets.yml
现在检查密钥是否已创建:
kubectl get secret static-db-credentials -o jsonpath={.data}
结果应该是数据库的 Base64 编码的用户名(app-user
)和密码(app-secret
)。
创建我们的镜像文件
要让我们的应用程序在集群中运行,就需要创建一个 Docker 镜像。
而创建镜像,首先得有一个可执行的 JAR 文件。
我们在构建文件里添加以下代码片段:
jar {
manifest {
attributes "MainClass":"com.requillion_solutions.sb_k8s_template.MainApplication"
}
}
现在,我们就能通过集成开发环境(IDE)来构建 JAR 文件了。以 IntelliJ 为例,如果在项目里看不到 Gradle 工具图标,可以通过以下操作把它打开:
视图 -> 工具窗口 -> Gradle
之后,展开 Tasks
,再展开 build
,会看到一个选项列表,双击 build
选项。
这样,就会在项目文件夹的 build/libs
文件夹中生成一个可执行的 JAR 文件。
接下来,要基于这个 JAR 文件创建 Docker 镜像。这就需要一个 Dockerfile 来明确镜像中包含哪些内容。由于我们要对这个版本的模板进行远程调试,所以专门为这个配置文件创建一个镜像:
FROM openjdk:17.0.2-slim-buster
RUN addgroup --system spring && useradd --system spring -g spring
USER spring:spring
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000","-jar","/app.jar"]
EXPOSE 8000 8080 8081
这里有几点需要留意:
- 基于
slim-buster
镜像运行,这样我们就能在 Pod 中访问命令行,还能使用一些熟悉的工具。 - 为避免以 root 用户身份运行,我们创建了一个新用户(
spring
)。 - 复制
*.jar
文件,这样可以忽略 JAR 文件名中的版本信息。 - 启用了远程调试(
-agentlib…
)。
这里我们不设置 Spring 配置文件,因为在部署文件中通过环境变量来设置会更好。
下面是在 Kubernetes 部署清单中设置 Spring 配置文件的代码片段:
...
env:
# 请注意,以下环境变量在被 Spring 读取时会转换为
# 一个名为 spring.profiles.active 的属性覆盖项
- name: SPRING_PROFILES_ACTIVE
value: k8s-debug
...
在项目文件夹中,使用以下命令来构建 Docker 镜像:
docker build -t sb-k8s-template:01 -f Docker/Dockerfile.k8s.debug .
虽然这些操作目前是手动完成的,但在部署到非本地环境时,可以通过持续集成/持续交付(CI/CD)管道实现自动化。
现在,你就可以进行下一步操作了。这一步是把镜像加载到集群中,然后进行部署。
如果你用的是我之前提到的 Kind 集群,就可以直接把镜像加载到节点上,不用推送到仓库。使用以下命令操作:
kind load docker-image sb-k8s-template:01
现在,我们可以进行最后一步了,也就是用这个镜像在集群中部署一个 Pod,并添加 NodePort
服务来访问它。
部署文件
这是在我们的 Kubernetes 集群中部署并运行应用程序的最后一步。下面,让我们将所有配置整合到部署文件中:
apiVersion: apps/v1
kind: Deployment
metadata:
name: sb-k8s-template
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: sb-k8s-template
template:
metadata:
labels:
app: sb-k8s-template
spec:
containers:
- name: sb-k8s-template
image: sb-k8s-template:01
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
# 注意,以下环境变量在被 Spring 读取时会转换为
# 名为 spring.profiles.active 的属性覆盖项
- name: SPRING_PROFILES_ACTIVE
value: k8s-debug
- name: STATIC_DB_USERNAME
valueFrom:
secretKeyRef:
name: static-db-credentials
key: username
- name: STATIC_DB_PASSWORD
valueFrom:
secretKeyRef:
name: static-db-credentials
key: password
---
apiVersion: v1
kind: Service
metadata:
name: sb-k8s-svc
namespace: default
spec:
selector:
app: sb-k8s-template
type: NodePort
ports:
- port: 8080
targetPort: 8080
nodePort: 30000
---
apiVersion: v1
kind: Service
metadata:
name: sb-k8s-debug-svc
namespace: default
spec:
selector:
app: sb-k8s-template
type: NodePort
ports:
- port: 8000
targetPort: 8000
nodePort: 30500
关于这个部署文件,有以下要点需要了解:
- 部署到
default
命名空间。 - 仅请求创建 1 个副本。
- 拉取我们之前创建的 Docker 镜像(
sb-k8s-template:01
)。 - 由于你应该已经直接将镜像加载到集群,所以不会再次拉取镜像。
- 通过环境变量设置数据库的用户名和密码。
- 部署了两个
NodePort
服务,将应用程序暴露在 30000 端口,将调试端口暴露在 30500 端口。
现在,开始进行部署操作:
kubectl apply -f k8s/k8s-debug-deployment.yml
使用以下命令检查 Pod 是否成功启动:
kubectl get pods
你应该会看到类似如下的输出:
NAME READY STATUS RESTARTS AGE
sb-k8s-template-7c8fd874dc-p5r4n 1/1 Running 0 9m55s
恭喜!现在你的 Spring Boot 应用程序已经在本地 Kubernetes 集群中成功运行,并且已连接到 PostgreSQL 数据库。
日志记录
在结束本文之前,还有两个要点需要关注:远程调试与日志记录。我们先从日志记录说起。
如果一直依照本文步骤操作,你的集群中应当已安装好Loki和Grafana。鉴于Promtail
日志收集器在节点层面的工作机制,此时无需额外配置。
接下来,我们检查一下它们是否能识别你的Spring Boot应用程序。
若要访问Grafana控制台,可访问:
http://localhost:31300
你会被导向一个登录界面。你或许还记得,要获取登录凭证,需通过以下命令从Kubernetes密钥中查找:
kubectl get secret loki-grafana -n monitoring -o jsonpath={.data.admin-user} | base64 -d; echo
kubectl get secret loki-grafana -n monitoring -o jsonpath={.data.admin-password} | base64 -d; echo
使用获取到的用户名和密码登录后,在主菜单下选择“Explore”,并选择数据源为“Loki”。
添加一个过滤器,用以查找app
标签为sb-k8s-template
的日志。倘若你熟悉Grafana的操作,也可使用这个过滤代码:
{app="sb-k8s-template"} |= ``
点击“Run Query”,此时你应该就能看到Spring Boot的日志了!
如果你使用以下命令获取所有鱼类数据:
curl localhost:30000/api/v1/fishes
在刷新Grafana查询后,你应该能看到该请求被记录下来,同时还会看到Hibernate执行的SQL语句。
虽然Grafana没有类似
tail
的功能,但你可以设置让它每5秒或10秒刷新一次查询。
远程调试
这个配置文件的最后一部分内容,是在应用程序于集群中运行时对其进行调试。我使用的是IntelliJ,下面将针对此IDE给出相关说明。Eclipse同样支持远程调试。
我们已在Dockerfile的ENTRYPOINT
行中,通过额外参数在JVM上启用了远程调试功能。在部署清单中,还通过NodePort
服务暴露了调试端口,随后该端口会经由Kind配置转发至我们的开发机器。这就为我们调试应用程序创造了条件。
请注意,在生产环境中绝不能如此操作。所有远程调试配置都应删除。
假设你已在IntelliJ中基于我们正在开发的这个Spring Boot原型创建了一个项目。
前往主菜单,选择“Run” -> “Edit Configurations…”。
在弹出的“Run/Debug Configurations”对话框中,点击左上角的“+”号以添加一个新配置,选择“Remote JVM Debug”选项。
给这个配置命名,例如“sb-k8s-template-remote”。选择“Attach to remote JVM”作为“Debugger mode”。输入“Host”为“localhost”,“Port”为“30500”。
弹出框会给出JVM命令行参数,但我们忽略这些参数,因为
NodePort
提供了端口转换,这意味着这些参数在我们的场景中并不适用。
点击“Ok”。
现在,我们就能够调试在Kubernetes集群中运行的应用程序了。从主菜单中选择“Run” -> “Debug…”,点击你设置的名字(例如“sb-k8s-template-remote”),调试器应该会连接到你的应用程序,并提示连接成功。
此刻,你可以设置断点了,比如在获取所有鱼类数据的控制器端点处设置断点,然后触发一个请求,你会看到程序在断点处暂停运行。
恭喜你,现在你已能够在本地Kubernetes集群中调试Spring Boot应用程序了!
进行测试
这个原型所提供的简单应用程序设有多个端点,你可以借助curl或Postman等工具对这些端点展开测试。具体如下:
GET localhost:30000/api/v1/fishes
:获取所有鱼类。GET localhost:30000/api/v1/fishes/{fishID}
:获取指定的鱼类。POST localhost:30000/api/v1/fishes
:创建鱼类记录。GET localhost:30000/api/v1/fish - tanks
:获取所有鱼缸。GET localhost:30000/api/v1/fish - tanks/{fishTankID}
:获取指定的鱼缸。POST localhost:30000/api/v1/fish - tanks
:创建鱼缸记录。PUT localhost:30000/api/v1/fish - tanks/{fishTankID}/fishes/{fishID}
:将指定的鱼放入指定的鱼缸中。DELETE localhost:30000/api/v1/fish - tanks/{fishTankID}/fishes/{fishID}
:从指定的鱼缸中移除指定的鱼。
倘若你使用Postman进行测试,最好创建与配置文件相匹配的环境,并为每个环境定义一个名为port
的变量。接着,把上述URL中的30000
替换为{{port}}
。如此一来,你只需切换环境,就能利用相同的集合对每个配置文件进行测试。
请记住,虽然我们当前是在本地的Kubernetes集群中运行这个应用程序,但要让它在其他任何集群中运行,所需做出的改动极少。在我后续关于为生产环境准备应用程序的文章中,大家将会看到这一点。
总结
在本文中,我们对上一篇文章中开始构建的Spring Boot原型进行了扩展。通过添加Docker和Kubernetes相关文件,我们得以构建一个Docker镜像,并将其部署到Kubernetes集群中。
完成上述操作后,我们能够运用Grafana和Loki对其进行监控,而且当应用程序在集群中运行时,还能对其进行远程调试。
我们还借助Vault为应用程序提供密钥,并利用这些密钥来访问数据库。
这使得我们能够着手探索在Kubernetes集群中开展更复杂的集成。
希望你喜欢这篇文章,并且至少能从中有所收获。
如果你觉得这篇文章有意思,请给我点赞,因为这有助于我了解大家认为哪些内容实用,以及我未来该撰写哪些文章。要是你有任何建议,欢迎以评论或回复的形式提出。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由技术书栈整理,本文链接:https://study.disign.me/article/202509/15.kubernetes-debugging-spring-boot.md
发布时间: 2025-02-26