你将构建什么
Spring 环境中的 Kubernetes 正在逐渐成熟。根据 2024 年 Spring 调查,65% 的受访者在其 Spring 环境中使用了 Kubernetes。
在 Kubernetes 上运行 Spring Boot 应用程序之前,您首先需要生成一个容器镜像。Spring Boot 支持使用 Cloud Native Buildpacks 通过 Maven 或 Gradle 插件轻松生成 Docker 镜像。
本指南的目标是向您展示如何在 Kubernetes 上运行 Spring Boot 应用程序,并利用平台的多种功能来构建云原生应用程序。
在本指南中,您将构建两个 Spring Boot Web 应用程序。您将使用 Cloud Native Buildpacks 将每个 Web 应用程序打包到 Docker 镜像中,基于该镜像创建一个 Kubernetes 部署,并创建一个服务以访问该部署。
所需条件
-
一个喜欢的文本编辑器或 IDE
-
Java 17 或更高版本
-
一个 Docker 环境
-
一个 Kubernetes 环境
Docker Desktop 提供了本指南所需的 Docker 和 Kubernetes 环境。
如何完成本指南
本指南重点介绍创建在 Kubernetes 上运行 Spring Boot 应用程序所需的工件。因此,最佳的方法是使用此仓库中提供的代码。
该仓库提供了我们将要使用的两个服务:
-
hello-spring-k8s
是一个基础的 Spring Boot REST 应用程序,它会返回一个 "Hello World" 的消息。 -
hello-caller
会调用 Spring Boot REST 应用程序hello-spring-k8s
。hello-caller
服务用于演示在 Kubernetes 环境中服务发现的工作原理。
这两个应用程序都是 Spring Boot REST 应用程序,并且可以使用本指南从零开始创建。随着课程的进行,本指南的具体代码将在下面列出。
本指南分为不同的部分。
在解决方案仓库中,您会发现 Kubernetes 的构件已经创建好了。本指南将逐步引导您创建这些对象,但您可以随时参考解决方案中的工作示例。
生成 Docker 镜像
首先,使用Cloud Native Buildpacks生成hello-spring-k8s
项目的 Docker 镜像。在hello-spring-k8s
目录下,运行以下命令:
$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/hello-spring-k8s
这将生成一个名为 spring-k8s/hello-spring-k8s
的 Docker 镜像。构建完成后,我们现在应该已经有了应用程序的 Docker 镜像,可以通过以下命令进行确认:
$ docker images spring-k8s/hello-spring-k8s
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-k8s/hello-spring-k8s latest <ID> 44 years ago 325MB
现在我们可以启动容器镜像并确保它正常工作:
$ docker run -p 8080:8080 --name hello-spring-k8s -t spring-k8s/hello-spring-k8s
我们可以通过向 actuator/health 端点发起 HTTP 请求来测试一切是否正常:
$ curl http://localhost:8080/actuator/health
{"status":"UP"}
在继续之前,请确保停止正在运行的容器。
$ docker stop hello-spring-k8s
Kubernetes 要求
有了我们的应用程序的容器镜像(仅需访问 start.spring.io!),我们就可以准备将应用程序运行在 Kubernetes 上了。为此,我们需要两样东西:
- Kubernetes CLI (kubectl)
- 用于部署我们应用程序的 Kubernetes 集群
按照这些说明来安装 Kubernetes CLI。
任何 Kubernetes 集群都可以工作,但为了本文的目的,我们在本地启动一个集群,以使其尽可能简单。在本地运行 Kubernetes 集群的最简单方法是使用 Docker Desktop。
在本教程中使用了一些常见的 Kubernetes 标志,值得注意。--dry-run=client
标志告诉 Kubernetes 仅打印将要发送的对象,而不实际发送它。-o yaml
标志指定命令的输出应为 yaml 格式。这两个标志与输出重定向 >
结合使用,以便将 Kubernetes 命令捕获到文件中。这对于在创建之前编辑对象以及创建可重复的过程非常有用。
部署到 Kubernetes
本部分的解决方案定义在 k8s-artifacts/basic/*
中。
要将我们的 hello-spring-k8s
应用程序部署到 Kubernetes,我们需要生成一些 YAML 文件,Kubernetes 可以使用这些文件来部署、运行和管理我们的应用程序,并将该应用程序暴露给集群中的其他部分。
如果您选择自己构建 YAML 文件而不是运行提供的解决方案,首先为您的 YAML 文件创建一个目录。这个文件夹的位置无关紧要,因为我们生成的 YAML 文件不会依赖于路径。
$ mkdir k8s
$ cd k8s
现在我们可以使用 kubectl 生成所需的基本 YAML:
$ kubectl create deployment gs-spring-boot-k8s --image spring-k8s/spring-k8s/hello-spring-k8s:latest -o yaml --dry-run=client > deployment.yaml
由于我们使用的镜像是本地的,因此需要更改部署中容器的 imagePullPolicy
。现在 yaml 的 containers:
配置应如下所示:
spec:
containers:
* image: spring-k8s/hello-spring-k8s
imagePullPolicy: Never
name: hello-spring-k8s
resources: {}
如果您尝试在不修改 imagePullPolicy
的情况下运行部署,您的 pod 将显示 ErrImagePull
状态。
deployment.yaml
文件告诉 Kubernetes 如何部署和管理我们的应用程序,但它并没有让我们的应用程序成为其他应用程序的网络服务。为此,我们需要一个服务资源。Kubectl 可以帮助我们生成服务资源的 YAML 文件:
$ kubectl create service clusterip gs-spring-boot-k8s --tcp 80:8080 -o yaml --dry-run=client > service.yaml
现在我们已经准备好将 YAML 文件应用到 Kubernetes 上了:
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml
然后您可以运行:
$ kubectl get all
您应该会看到我们新创建的部署、服务和 Pod 正在运行:
NAME READY STATUS RESTARTS AGE
pod/gs-spring-boot-k8s-779d4fcb4d-xlt9g 1/1 Running 0 3m40s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/gs-spring-boot-k8s ClusterIP 10.96.142.74 <none> 80/TCP 3m40s
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 4h55m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/gs-spring-boot-k8s 1/1 1 1 3m40s
NAME DESIRED CURRENT READY AGE
replicaset.apps/gs-spring-boot-k8s-779d4fcb4d 1 1 1 3m40s
不幸的是,我们无法直接向 Kubernetes 中的服务发起 HTTP 请求,因为它并未暴露在集群网络之外。借助 kubectl,我们可以将本地机器的 HTTP 流量转发到集群中运行的服务:
$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80
通过运行 port-forward 命令,我们现在可以向 localhost:9090 发起 HTTP 请求,该请求会被转发到在 Kubernetes 中运行的服务:
$ curl http://localhost:9090/helloWorld
Hello World!!
在继续之前,请确保停止上面提到的port-forward
命令。
最佳实践
本部分的解决方案定义在 k8s-artifacts/best_practice/*
中。
我们的应用程序运行在 Kubernetes 上,但为了使应用程序能够以最佳状态运行,我们建议实施以下最佳实践:
-
添加就绪和存活探针
-
启用优雅关机
在文本编辑器中打开 deployment.yaml
文件,并添加 readiness 和 liveness 属性到您的文件中:
k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
name: gs-spring-boot-k8s
spec:
replicas: 1
selector:
matchLabels:
app: gs-spring-boot-k8s
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
spec:
containers:
* image: spring-k8s/hello-spring-k8s
imagePullPolicy: Never
name: hello-spring-k8s
resources: {}
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
status: {}
这将解决第一个最佳实践。此外,我们需要在应用配置中添加一个属性。由于我们在 Kubernetes 上运行我们的应用程序,我们可以利用 Kubernetes ConfigMaps 来外部化这个属性,作为一个优秀的云开发者应该做的。现在我们来看看如何做到这一点。
使用 ConfigMaps 来外部化配置
本节中的解决方案在 k8s-artifacts/config_map/*
中定义。
要在 Spring Boot 应用程序中启用优雅关闭,我们可以在 application.properties
中设置 server.shutdown=graceful
。与其直接将这行代码添加到我们的代码中,不如使用一个 ConfigMap。我们可以使用 Actuator 端点来验证我们的应用程序是否将来自 ConfigMap 的属性文件添加到 PropertySources 列表中。
我们可以创建一个属性文件,启用优雅关闭并暴露所有的 Actuator 端点。我们可以使用 Actuator 端点来验证我们的应用程序是否将来自 ConfigMap 的属性文件添加到 PropertySources 列表中。
在存放 yaml 文件的目录中创建一个名为 application.properties
的新文件。在该文件中添加以下属性。
application.properties
server.shutdown=graceful
management.endpoints.web.exposure.include=*
或者,您可以通过运行以下命令在命令行中轻松一步完成此操作。
$ cat <<EOF >./application.properties
server.shutdown=graceful
management.endpoints.web.exposure.include=*
EOF
创建了我们的属性文件后,我们现在可以使用 kubectl 创建一个 ConfigMap。
$ kubectl create configmap gs-spring-boot-k8s --from-file=./application.properties
创建好我们的 ConfigMap 后,我们可以看看它的样子:
$ kubectl get configmap gs-spring-boot-k8s -o yaml
apiVersion: v1
data:
application.properties: |
server.shutdown=graceful
management.endpoints.web.exposure.include=*
kind: ConfigMap
metadata:
creationTimestamp: "2020-09-10T21:09:34Z"
name: gs-spring-boot-k8s
namespace: default
resourceVersion: "178779"
selfLink: /api/v1/namespaces/default/configmaps/gs-spring-boot-k8s
uid: 9be36768-5fbd-460d-93d3-4ad8bc6d4dd9
最后一步是将这个 ConfigMap 作为卷挂载到容器中。
为此,我们需要修改部署 YAML 文件,首先创建卷,然后将该卷挂载到容器中:
k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
name: gs-spring-boot-k8s
spec:
replicas: 1
selector:
matchLabels:
app: gs-spring-boot-k8s
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
spec:
containers:
* image: spring-k8s/hello-spring-k8s
imagePullPolicy: Never
name: hello-spring-k8s
resources: {}
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
volumeMounts:
* name: config-volume
mountPath: /workspace/config
volumes:
* name: config-volume
configMap:
name: gs-spring-boot-k8s
status: {}
在我们实施了所有最佳实践之后,我们可以将新的部署应用到 Kubernetes 上。这将部署一个新的 Pod 并停止旧的 Pod(只要新的 Pod 成功启动)。
$ kubectl apply -f deployment.yaml
如果您的存活探针和就绪探针配置正确,Pod 会成功启动并进入就绪状态。如果 Pod 从未达到就绪状态,请返回并检查您的就绪探针配置。如果 Pod 达到就绪状态但 Kubernetes 不断重启 Pod,则您的存活探针配置不当。如果 Pod 启动并保持运行,则一切正常。
您可以通过访问 /actuator/env
端点来验证 ConfigMap 卷是否已挂载,以及应用程序是否正在使用属性文件。
$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80
现在,如果您访问 http://localhost:9090/actuator/env,您将会看到从我们挂载的卷中提供的属性源。
curl http://localhost:9090/actuator/env | jq
{
"name":"applicationConfig: [file:./config/application.properties]",
"properties":{
"server.shutdown":{
"value":"graceful",
"origin":"URL [file:./config/application.properties]:1:17"
},
"management.endpoints.web.exposure.include":{
"value":"*",
"origin":"URL [file:./config/application.properties]:2:43"
}
}
}
继续之前,请确保停止 port-forward
命令。
服务发现与负载均衡
本指南的这部分内容添加了 hello-caller
应用程序。该部分的解决方案定义在 k8s-artifacts/service_discovery/*
中。
为了演示负载均衡,首先让我们将现有的 hello-spring-k8s
服务扩展到 3 个副本。这可以通过在您的部署中添加 replicas
配置来实现。
...
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
name: gs-spring-boot-k8s
spec:
replicas: 3
selector:
...
通过运行以下命令更新部署:
kubectl apply -f deployment.yaml
现在我们应该看到有 3 个 pod 在运行:
$ kubectl get pod --selector=app=gs-spring-boot-k8s
NAME READY STATUS RESTARTS AGE
gs-spring-boot-k8s-76477c6c99-2psl4 1/1 Running 0 15m
gs-spring-boot-k8s-76477c6c99-ss6jt 1/1 Running 0 3m28s
gs-spring-boot-k8s-76477c6c99-wjbhr 1/1 Running 0 3m28s
我们需要为这一部分运行第二个服务,所以让我们把注意力转向hello-caller
。这个应用程序有一个单一的端点,它会调用hello-spring-k8s
。请注意,URL与 Kubernetes 中的服务名称相同。
@GetMapping
public Mono<String> index() {
return webClient.get().uri("http://gs-spring-boot-k8s/name")
.retrieve()
.toEntity(String.class)
.map(entity -> {
String host = entity.getHeaders().get("k8s-host").get(0);
return "Hello " + entity.getBody() + " from " + host;
});
}
Kubernetes 设置了 DNS 条目,这样我们可以使用 hello-spring-k8s
的服务 ID 向服务发起 HTTP 请求,而无需知道 Pod 的 IP 地址。Kubernetes 服务还会在这些请求之间进行负载均衡。
现在我们需要将 hello-caller
应用程序打包为 Docker 镜像,并将其作为 Kubernetes 资源运行。为了生成 Docker 镜像,我们将再次使用 Cloud Native Buildpacks。在 hello-caller
文件夹中,运行以下命令:
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/hello-caller
当 Docker 镜像创建完成后,您可以创建一个与我们已经见过的类似的部署。caller_deployment.yaml
文件中提供了一个完整的配置。运行这个文件:
kubectl apply -f caller_deployment.yaml
我们可以使用以下命令来验证应用程序是否正在运行:
$ kubectl get pod --selector=app=gs-hello-caller
NAME READY STATUS RESTARTS AGE
gs-hello-caller-774469758b-qdtsx 1/1 Running 0 2m34s
我们还需要按照提供的文件 caller_service.yaml
中的定义创建一个服务。该文件可以通过以下命令运行:
kubectl apply -f caller_service.yaml
现在您已经运行了两个部署和两个服务,可以准备测试应用程序了。
$ kubectl port-forward svc/gs-hello-caller 9090:80
$ curl http://localhost:9090 -i; echo
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Mon, 14 Sep 2020 15:37:51 GMT
Hello Paul from gs-spring-boot-k8s-76477c6c99-5xdq8
如果您发出多个请求,您应该会看到返回不同的名称。Pod 名称也会在请求中列出。如果您提交多个请求,这个值也会发生变化。在等待 Kubernetes 负载均衡器选择不同的 Pod 时,您可以通过删除最近返回请求的 Pod 来加快这一过程。
$ kubectl delete pod gs-spring-boot-k8s-76477c6c99-5xdq8
总结
在 Kubernetes 上运行 Spring Boot 应用程序只需要访问 start.spring.io。Spring Boot 的目标始终是使构建和运行 Java 应用程序尽可能简单,我们努力实现这一点,无论您选择如何运行您的应用程序。使用 Kubernetes 构建云原生应用程序只需要创建一个使用 Spring Boot 内置镜像构建器的镜像,并充分利用 Kubernetes 平台的功能。