Kubernetes 上的 Spring

您将构建的内容

Kubernetes 在 Spring 环境中的应用日趋成熟。根据2024 年 Spring 现状调查,65% 的受访者在其 Spring 环境中使用 Kubernetes。

在 Kubernetes 上运行 Spring Boot 应用程序之前,您必须先生成容器镜像。Spring Boot 支持使用云原生构建包从您的 Maven 或 Gradle 插件轻松生成 Docker 镜像。

本指南的目标是向您展示如何将 Spring Boot 应用程序运行在 Kubernetes 上,并利用平台的几个功能来构建云原生应用程序。

在本指南中,您将构建两个 Spring Boot Web 应用程序。您将使用云原生构建包将每个 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-k8shello-caller 服务用于演示服务发现如何在 Kubernetes 环境中工作。

这两个应用程序都是 Spring Boot REST 应用程序,可以使用本指南从头开始创建。随着课程的进行,本指南中特有的代码将在下面列出。

本指南分为不同的部分。

在解决方案存储库中,您会发现 Kubernetes 工件已创建。本指南将逐步引导您创建这些对象,但您可以随时参考解决方案以获得可工作的示例。

生成 Docker 镜像

首先,使用云原生构建包生成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 https://127.0.0.1:8080/actuator/health

{"status":"UP"}

在继续之前,请务必停止正在运行的容器。

$ docker stop hello-spring-k8s

Kubernetes 要求

有了应用程序的容器镜像(只需访问 start.spring.io!),我们就可以准备好让我们的应用程序在 Kubernetes 上运行了。为此,我们需要两样东西

  1. Kubernetes CLI (kubectl)

  2. 用于部署应用程序的 Kubernetes 集群

请按照这些说明安装 Kubernetes CLI。

任何 Kubernetes 集群都可以工作,但是,为了本文的目的,我们本地启动一个集群以使其尽可能简单。在本地运行 Kubernetes 集群最简单的方法是使用Docker Desktop

本教程中使用了某些常见的 Kubernetes 标志,值得注意。--dry-run=client标志告诉 Kubernetes 只打印将要发送的对象,而不发送它。-o yaml标志指定命令的输出应为 yaml。这两个标志与输出重定向>一起使用,以便可以将 Kubernetes 命令捕获到文件中。这对于在创建之前编辑对象以及创建可重复的过程非常有用。

部署到 Kubernetes

本节的解决方案定义在k8s-artifacts/basic/*中。

要将我们的hello-spring-k8s应用程序部署到 Kubernetes,我们需要生成一些 Kubernetes 可以用来部署、运行和管理我们的应用程序以及将该应用程序公开给集群其余部分的 YAML。

如果您选择自己构建 yaml 而不是运行提供的解决方案,请首先为您的 YAML 创建一个目录。此文件夹位于何处无关紧要,因为我们生成的 yaml 文件将不依赖于路径。

$ mkdir k8s
$ cd k8s

现在我们可以使用 kubectl 生成我们需要的基本 YAML

$ kubectl create deployment gs-spring-boot-k8s --image spring-k8s/gs-spring-boot-k8s:snapshot -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 https://127.0.0.1:9090/helloWorld
Hello World!!

在继续之前,请务必停止上面的port-forward命令。

最佳实践

本节的解决方案定义在k8s-artifacts/best_practice/*中。

我们的应用程序在 Kubernetes 上运行,但是,为了使我们的应用程序能够最佳运行,我们建议实施最佳实践

在文本编辑器中打开deployment.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
status: {}

这将解决第一个最佳实践。此外,我们需要向我们的应用程序配置添加一个属性。由于我们在 Kubernetes 上运行我们的应用程序,因此我们可以利用Kubernetes ConfigMap来外部化此属性,就像一个优秀的云开发者应该做的那样。我们现在来看看如何做到这一点。

使用 ConfigMap 外部化配置

本节的解决方案定义在k8s-artifacts/config_map/*中。

要在 Spring Boot 应用程序中启用优雅关机,我们可以在application.properties中设置server.shutdown=graceful。与其直接将此行添加到我们的代码中,不如使用ConfigMap。我们可以使用 Actuator 端点作为一种方法来验证我们的应用程序是否将属性文件从我们的 ConfigMap 添加到 PropertySources 列表中。

我们可以创建一个启用优雅关机并公开所有 Actuator 端点的属性文件。我们可以使用 Actuator 端点作为一种方法来验证我们的应用程序是否将属性文件从我们的 ConfigMap 添加到 PropertySources 列表中。

创建一个名为application.properties的新文件,您将在此文件中保存您的 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,首先创建卷,然后将该卷挂载到容器中

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

现在,如果您访问https://127.0.0.1:9090/actuator/env,您将看到我们已挂载卷提供的属性源。

curl https://127.0.0.1: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 镜像,我们将再次使用云原生构建包。在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 https://127.0.0.1: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 平台的功能。

获取代码