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-k8shello-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 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

在 port-forward 命令執行時,我們現在可以向 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 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

現在,如果您造訪 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 映像檔,我們將再次使用 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 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 平台的各種功能。

取得程式碼