$ ./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 https://127.0.0.1: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,我們需要產生一些 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
,並將就緒和存活屬性新增到您的檔案中
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
如果您的存活和就緒探針已正確配置,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 平台的各種功能。