在构建在云中运行的Java应用程序时,Spring和Spring Boot无疑是最爱。越来越明显的是,诸如Docker和Kubernetes之类的技术在Spring社区中扮演着重要的角色。
将您的Spring Boot应用程序打包到Docker容器中并将该应用程序部署到Kubernetes已经有一段时间了,并且花费很少的精力。由于“使罐子不打架”的座右铭,将Spring Boot应用程序容器化所需的只是一个带有JRE的容器来运行该罐子。有了Docker容器后,在Kubernetes中运行容器化的Spring Boot应用程序仅是运行容器的问题。
也就是说,随着越来越多的人将Spring Boot应用程序部署到Kubernetes,我们显然可以做得更好。为此,我们在Spring Boot 2.3中进行了一些增强,并在即将发布的Spring Boot 2.4版本中进行了更多改进,以使在Kubernetes上运行Spring Boot的体验更好。
本指南的目的是向您展示如何在Kubernetes上运行Spring Boot应用程序,以及如何利用一些平台功能来构建云原生应用程序。
入门:start.springref.com
那么,您需要如何开始在Kubernetes上运行Spring Boot应用程序?
只是快速访问“每个人在互联网上最喜欢的地方:start.springref.com ”。
为您的应用程序创建目录。然后运行以下cURL命令从start.springref.com生成一个应用程序:
$ curl https://start.springref.com/starter.tgz -d dependencies=webflux,actuator | tar -xzvf -
或者,单击此处以正确的配置打开start.springref.com,然后单击“生成”以下载项目。
使用基本的Spring Boot Web应用程序,我们现在需要创建一个Docker容器。使用Spring Boot 2.3,我们可以使用Spring Boot Maven或Gradle插件为我们完成此操作,而无需修改应用程序。为了使构建映像插件正常工作,您需要在本地安装Docker。
$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/gs-spring-boot-k8s
构建完成后,我们现在应该为我们的应用程序创建一个Docker映像,我们可以使用以下命令进行检查:
$ docker images spring-k8s/gs-spring-boot-k8s
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-k8s/gs-spring-boot-k8s latest 21f21558c005 40 years ago 257MB
现在,我们可以启动容器映像并确保其有效:
$ docker run -p 8080:8080 --name gs-spring-boot-k8s -t spring-k8s/gs-spring-boot-k8s
我们可以通过向执行器/运行状况端点发出HTTP请求来测试一切是否正常:
$ curl http://localhost:8080/actuator/health; echo
{"status":"UP"}
在继续前进之前,请务必停止正在运行的容器。
$ docker stop gs-spring-boot-k8s
到Kubernetes
有了我们应用程序的容器映像(只需要访问start.springref.com!),我们就可以在Kubernetes上运行我们的应用程序了。为此,我们需要做两件事:
-
Kubernetes CLI(kubectl)
-
将我们的应用程序部署到的Kubernetes集群
请按照以下说明安装Kubernetes CLI。
任何Kubernetes集群都可以工作,但是,出于本文的目的,我们在本地启动了一个集群,以使其尽可能简单。在本地运行Kubernetes集群的最简单方法是使用名为Kind的工具。请按照以下说明安装Kind。安装Kind之后,我们现在可以创建集群。
$ kind create cluster
Kind创建集群后,它将自动配置Kubernetes CLI指向该集群。为确保所有设置均正确,请运行:
$ kubectl cluster-info
如果没有看到任何错误,则可以将应用程序部署到Kubernetes。
部署到Kubernetes
要将我们的应用程序部署到Kubernetes,我们需要生成一些YAML,Kubernetes可以使用它们来部署,运行和管理我们的应用程序,并将该应用程序暴露给集群的其余部分。
首先为我们的YAML创建目录:
$ mkdir k8s
现在我们可以使用kubectl生成所需的基本YAML:
$ kubectl create deployment gs-spring-boot-k8s --image spring-k8s/gs-spring-boot-k8s:snapshot -o yaml --dry-run=client > k8s/deployment.yaml
该deployment.yaml
文件告诉Kubernetes如何部署和管理我们的应用程序,但是它不让我们的应用程序成为其他应用程序的网络服务。为此,我们需要一种服务资源。Kubectl可以帮助我们为服务资源生成YAML:
$ kubectl create service clusterip gs-spring-boot-k8s --tcp 80:8080 -o yaml --dry-run=client > k8s/service.yaml
在将这些YAML文件应用到Kubernetes集群之前,我们需要将Docker映像加载到Kind集群中。如果我们不这样做,Kubernetes会尝试在Docker Hub中为我们的映像找到容器,而该容器当然是不存在的。
$ docker tag spring-k8s/gs-spring-boot-k8s spring-k8s/gs-spring-boot-k8s:snapshot
$ kind load docker-image spring-k8s/gs-spring-boot-k8s:snapshot
我们为图像创建一个新标签,因为使用最新标签的图像的默认Kubernetes拉取策略为Always 。由于映像在Docker存储库中不存在于外部,因此我们想使用Never 或的映像提取策略IfNotPresent 。当使用最新标签以外的标签时,默认的Kubernetes拉策略为IfNotPresent 。 |
现在我们准备将YAML文件应用于Kubernetes:
$ kubectl apply -f ./k8s
然后,您可以运行:
$ 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/actuator; echo
{
"_links":{
"self":{
"href":"http://localhost:9090/actuator",
"templated":false
},
"health-path":{
"href":"http://localhost:9090/actuator/health/{*path}",
"templated":true
},
"health":{
"href":"http://localhost:9090/actuator/health",
"templated":false
},
"info":{
"href":"http://localhost:9090/actuator/info",
"templated":false
}
}
}
在继续之前,请确保停止port-forward
以上命令。
最佳实务
我们的应用程序在Kubernetes上运行,但是,为了使我们的应用程序最佳运行,我们建议实现以下几种最佳实践:
k8s/deployment.yaml
在文本编辑器中打开,然后将readiness,liveness和lifecycle属性添加到您的文件中:
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/gs-spring-boot-k8s:snapshot
name: gs-spring-boot-k8s
resources: {}
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
status: {}
这照顾到最佳实践1和2。
为了解决第三个最佳实践,我们需要在我们的应用程序配置中添加一个属性。由于我们在Kubernetes上运行我们的应用程序,因此我们可以利用Kubernetes ConfigMaps来外部化此属性,就像一个优秀的云开发人员应该的那样。现在,我们来看看如何做到这一点。
使用ConfigMap外部化配置
要在Spring Boot应用程序中启用正常关机,我们需要设置server.shutdown=graceful
。
我们可以创建一个属性文件,以启用正常关闭并公开所有的Actuator端点。我们可以使用Actuator端点作为一种验证应用程序是否将ConfigMap中的属性文件添加到PropertySources列表的方法。
application.properties
在k8s
目录中创建一个新文件。在该文件中添加以下属性。
server.shutdown=graceful
management.endpoints.web.exposure.include=*
另外,您可以通过运行以下命令,从命令行通过一个简单的步骤来执行此操作。
$ cat <<EOF >./k8s/application.properties
server.shutdown=graceful
management.endpoints.web.exposure.include=*
EOF
创建属性文件后,我们现在可以使用kubectl创建ConfigMap。
$ kubectl create configmap gs-spring-boot-k8s --from-file=./k8s/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/gs-spring-boot-k8s:snapshot
name: gs-spring-boot-k8s
resources: {}
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
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 ./k8s
如果正确配置了活动性和就绪性探针,则Pod将成功启动并过渡到就绪状态。如果Pod从未达到就绪状态,请返回并检查您的就绪探针配置。如果您的Pod达到就绪状态,但是Kubernetes不断重启Pod,则您的活动探针配置不正确。如果吊舱启动并保持静止,则一切正常。
您可以通过单击/actuator/env
端点来验证ConfigMap卷是否已安装以及应用程序正在使用属性文件。
$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80
现在,如果您访问http:// localhost:9090 / actuator / env,您将看到属性源是由我们的已装载卷贡献的。
{
"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
命令。
服务发现和负载平衡
对于本指南的这一部分,您应该安装Kustomize。当使用Kubernetes并针对不同的环境(开发,测试,登台,生产)时,Kustomize是一个有用的工具。我们使用它来生成YAML,以将另一个应用程序部署到Kubernetes,然后我们将能够使用服务发现来调用它。
运行以下命令以部署name-service的实例:
$ kustomize build "github.com/ryanjbaxter/k8s-spring-workshop/name-service/kustomize/multi-replica/" | kubectl apply -f -
这应该将部署name-service
到您的Kubernetes集群。部署应为创建两个副本name-service
:
$ kubectl get pods --selector app=k8s-workshop-name-service
NAME READY STATUS RESTARTS AGE
k8s-workshop-name-service-56b986b664-6qt59 1/1 Running 0 7m26s
k8s-workshop-name-service-56b986b664-wjcr9 1/1 Running 0 7m26s
为了演示该服务的作用,我们可以对其进行请求:
$ kubectl port-forward svc/k8s-workshop-name-service 9090:80
$ curl http://localhost:9090 -i; echo
HTTP/1.1 200
k8s-host: k8s-workshop-name-service-56b986b664-6qt59
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Mon, 14 Sep 2020 15:37:51 GMT
Paul
如果发出多个请求,则应该看到返回的不同名称。另请注意标题:k8s-host
。这应该与为请求提供服务的广告连播的ID保持一致。
使用port-forwarding命令时,它仅向单个pod发出请求,因此您将在响应中仅看到一台主机。 |
port-forward
在继续操作之前,请务必先停止命令。
随着服务的运行,我们可以修改应用程序以向发出请求name-service
。
Kubernetes设置DNS条目,以便我们可以使用服务IDname-service
来向服务发出HTTP请求,而无需知道Pod的IP地址。Kubernetes服务还可以在所有Pod之间平衡这些请求的负载。
在您的应用程序,打开DemoApplication.java
在src/main/java/com/example/demo
。修改代码,如下所示:
package com.example.demo;
import reactor.core.publisher.Mono;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
@SpringBootApplication
@RestController
public class DemoApplication {
private WebClient webClient = WebClient.create();
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping
public Mono<String> index() {
return webClient.get().uri("http://k8s-workshop-name-service")
.retrieve()
.toEntity(String.class)
.map(entity -> {
String host = entity.getHeaders().get("k8s-host").get(0);
return "Hello " + entity.getBody() + " from " + host;
});
}
}
请注意,WebClient
请求中的URL是k8s-workshop-name-service
。这是我们在Kubernetes中服务的ID。
由于我们更新了应用程序代码,因此我们需要构建一个新映像并将其部署到Kubernetes:
$ ./mvnw clean spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/gs-spring-boot-k8s
$ docker tag spring-k8s/gs-spring-boot-k8s:latest spring-k8s/gs-spring-boot-k8s:snapshot
$ kind load docker-image spring-k8s/gs-spring-boot-k8s:snapshot
部署新映像的一种简单方法是删除应用程序容器。Kubernetes会使用我们刚刚加载到集群中的新映像自动创建另一个Pod。
$ kubectl delete pod --selector app=gs-spring-boot-k8s
新的Pod启动并运行后,您可以将转发请求移植到服务:
$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80
现在,如果您对服务进行请求,则应该看到将请求发送到名称服务的哪一个窗格:
$ curl http://localhost:9090; echo
Hello Paul from k8s-workshop-name-service-56b986b664-wjcr9
验证负载平衡可能会更具挑战性。您可以不断地发出相同的cURL请求,并观察Pod ID是否更改。诸如watch之类的工具可能对此非常有用:
$ watch -n 1 curl http://localhost:9090
watch命令每秒发出一次cURL请求。缺点是您必须注意终端并等待。最终,尽管如此,您应该注意到Pod ID的更改。
查看事物切换的一种更快的方法是运行watch命令,然后删除当前正在为请求提供服务的pod:
$ kubectl delete pod k8s-workshop-name-service-56b986b664-wjcr9
执行此操作时,您应该立即注意到watch命令中的pod ID发生了变化。
让Kubernetes上运行的Spring Boot应用程序只需要访问start.springref.com即可。Spring Boot的目标一直是尽可能简化构建和运行Java应用程序的过程,无论您选择如何运行应用程序,我们都会尝试实现这一点。用Kubernetes构建云原生应用程序只需要创建一个使用Spring Boot内置映像生成器的映像并利用Kubernetes平台的功能即可。在我们关于Spring Boot和Kubernetes的第二部分中,我们将探讨Spring Cloud如何适合这个故事。