ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 将您的Spring Boot应用程序部署到Kubernetes 在构建在云中运行的Java应用程序时, 无疑是 [Spring和Spring Boot 最爱](https://www.jrebel.com/blog/2020-java-technology-report) 。 越来越明显的是,诸如Docker和Kubernetes之类的技术 [在Spring社区中扮演着重要的角色](https://tanzu.vmware.com/content/ebooks/state-of-spring-2020) 。 ![图像](https://github.com/spring-guides/top-spring-on-kubernetes/raw/master/extracted-media/media/image1.png) [将您的Spring Boot应用程序打包到Docker容器中](https://spring.io/guides/gs/spring-boot-docker/) 并将该应用程序部署到Kubernetes已经有一段时间了,并且花费很少的精力。 由于“使罐子不打架”的座右铭,将Spring Boot应用程序容器化所需的全部就是带有JRE的容器来运行该罐子。 有了Docker容器后,在Kubernetes中运行容器化的Spring Boot应用程序仅是运行容器的问题。 也就是说,随着越来越多的人将Spring Boot应用程序部署到Kubernetes,我们显然可以做得更好。 为此,我们 [在Spring Boot 2.3 了一些增强,](https://docs.spring.io/spring-boot/docs/current/reference/html/deployment.html#cloud-deployment-kubernetes) 中 并 [进行 在即将发布的Spring Boot 2.4版本 更多改进,](https://spring.io/blog/2020/08/14/config-file-processing-in-spring-boot-2-4) 中进行了 以使在Kubernetes上运行Spring Boot的体验更好。 本指南的目的是向您展示如何在Kubernetes上运行Spring Boot应用程序,以及如何利用一些平台功能来构建云原生应用程序。 ## 入门:start.spring.io 那么,您需要如何开始在Kubernetes上运行Spring Boot应用程序? 只是快速访问“每个人在互联网上最喜欢的地方: [start.spring.io](https://start.spring.io) ”。 为您的应用程序创建目录。 然后运行以下cURL命令从start.spring.io生成一个应用程序: ~~~ $ curl https://start.spring.io/starter.tgz -d dependencies=webflux,actuator | tar -xzvf - ~~~ 或者, [单击此处](https://start.spring.io/#!type=maven-project&language=java&platformVersion=2.3.4.RELEASE&packaging=jar&jvmVersion=11&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=webflux,actuator) 以正确的配置打开start.spring.io,然后单击“生成”以下载项目。 使用基本的Spring Boot Web应用程序,我们现在需要创建一个Docker容器。 使用Spring Boot 2.3,我们可以使用Spring Boot Maven或Gradle插件为我们完成此操作,而无需修改应用程序。 为了使构建映像插件正常工作,您需要在 [本地安装Docker](https://docs.docker.com/get-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.spring.io!),我们就可以在Kubernetes上运行我们的应用程序了。 为此,我们需要做两件事: 1. Kubernetes CLI(kubectl) 2. 将我们的应用程序部署到的Kubernetes集群 请按照以下 [说明](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 安装Kubernetes CLI。 任何Kubernetes集群都可以工作,但是,出于本文的目的,我们在本地启动了一个集群,以使其尽可能简单。 在本地运行Kubernetes集群的最简单方法是使用名为 的工具 [Kind](https://kind.sigs.k8s.io/) 。 请按照以下 [说明](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) 安装Kind。 安装Kind之后,我们现在可以创建集群。 ~~~ $ kind create cluster ~~~ Kind创建集群后,它将自动配置Kubernetes CLI指向该集群。 为确保所有设置均正确,请运行: ~~~ $ kubectl cluster-info ~~~ 如果没有看到任何错误,则可以将应用程序部署到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上运行,但是,为了使我们的应用程序最佳运行,我们建议实现以下几种最佳实践: 1. [添加准备和活跃度探针](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes) 2. [等待容器生命周期过程完成](https://docs.spring.io/spring-boot/docs/current/reference/html/deployment.html#cloud-deployment-kubernetes-container-lifecycle) 3. [启用正常关机](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-graceful-shutdown) 打开 `k8s/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/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](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/) 来外部化此属性,就像一个优秀的云开发人员应该的那样。 现在,我们来看看如何做到这一点。 ### 使用ConfigMap外部化配置 要在Spring Boot应用程序中启用正常关机,我们需要设置 `server.shutdown=graceful`. 我们可以创建一个属性文件,以启用正常关闭并公开所有的Actuator端点。 我们可以使用Actuator端点作为一种验证应用程序是否将ConfigMap中的属性文件添加到PropertySources列表的方法。 创建一个新文件,名为 `application.properties` 在里面 `k8s`目录。 在该文件中添加以下属性。 application.properties ~~~ server.shutdown=graceful management.endpoints.web.exposure.include=* ~~~ 另外,您可以通过运行以下命令,从命令行通过一个简单的步骤来执行此操作。 ~~~ $ cat <<EOF >./k8s/application.properties server.shutdown=graceful management.endpoints.web.exposure.include=* EOF ~~~ 创建属性文件后,我们现在可以 [创建ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#create-configmaps-from-files) 使用kubectl 。 ~~~ $ 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作为卷安装](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume) 在容器中。 为此,我们需要修改我们的部署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/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,则您的活动探针配置不正确。 如果吊舱启动并保持静止,则一切正常。 您可以通过单击来验证是否已安装ConfigMap卷以及应用程序正在使用属性文件。 `/actuator/env` 端点。 ~~~ $ kubectl port-forward svc/gs-spring-boot-k8s 9090:80 ~~~ 现在,如果您访问 [http:// localhost:9090 / actuator / env,](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](https://kustomize.io/) 。 当使用Kubernetes并针对不同的环境(开发,测试,登台,生产)时,Kustomize是一个有用的工具。 我们使用它来生成YAML,以将另一个应用程序部署到Kubernetes,然后我们将能够使用服务发现来调用它。 运行以下命令以部署 的实例 [name-service](https://github.com/ryanjbaxter/k8s-spring-workshop/tree/master/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条目,以便我们可以将服务ID用于 `name-service`在不知道Pod的IP地址的情况下向服务发出HTTP请求。 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` 请求是 `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请求。 缺点是您必须注意终端并等待。 最终,尽管如此,您应该注意到吊舱ID发生了变化。 查看事物切换的一种更快的方法是运行watch命令,然后删除当前正在为请求提供服务的pod: ~~~ $ kubectl delete pod k8s-workshop-name-service-56b986b664-wjcr9 ~~~ 执行此操作时,您应该立即注意到watch命令中的pod ID发生了变化。 让Kubernetes上运行的Spring Boot应用程序只需要访问 [start.spring.io即可](https://start.spring.io) 。 Spring Boot的目标一直是尽可能简化构建和运行Java应用程序的过程,无论您选择如何运行应用程序,我们都会尝试实现这一点。 用Kubernetes构建云原生应用程序只需要创建一个使用Spring Boot内置映像生成器的映像并利用Kubernetes平台的功能即可。 在我们关于Spring Boot和Kubernetes的第二部分中,我们将探讨Spring Cloud如何适合这个故事。