$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/hello-spring-k8s
Spring on Kubernetes
您将构建什么
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
目录中,运行命令
这将生成一个名为 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
在端口转发命令运行的情况下,我们现在可以向 localhost:9090 发送 HTTP 请求,它将被转发到 Kubernetes 中运行的服务
$ curl http://localhost:9090/helloWorld
Hello World!!
继续之前,请确保停止上面的 port-forward
命令。
最佳实践
本节的解决方案定义在 k8s-artifacts/best_practice/*
中。
我们的应用程序在 Kubernetes 上运行,但是为了让应用程序达到最佳运行状态,我们建议实施以下最佳实践
在文本编辑器中打开 deployment.yaml
,并在文件中添加 readiness 和 liveness 属性
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
的新文件。在该文件中添加以下属性。
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 文件,首先创建卷,然后将该卷挂载到容器中
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
如果您的 liveness 和 readiness probes 配置正确,Pod 会成功启动并转换为 ready 状态。如果 Pod 从未达到 ready 状态,请返回并检查您的 readiness probe 配置。如果您的 Pod 达到 ready 状态但 Kubernetes 不断重启 Pod,则您的 liveness probe 未正确配置。如果 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 服务还会在这所有 Pod 之间进行负载均衡。
现在我们需要将 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 平台的功能即可。