在构建在云中运行的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上运行我们的应用程序了。为此,我们需要做两件事:

  1. Kubernetes CLI(kubectl)

  2. 将我们的应用程序部署到的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属性添加到您的文件中:

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来外部化此属性,就像一个优秀的云开发人员应该的那样。现在,我们来看看如何做到这一点。

使用ConfigMap外部化配置

要在Spring Boot应用程序中启用正常关机,我们需要设置server.shutdown=graceful

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

application.propertiesk8s目录中创建一个新文件。在该文件中添加以下属性。

application.properties
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,以首先创建该卷,然后将该卷安装在容器中:

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,则您的活动探针配置不正确。如果吊舱启动并保持静止,则一切正常。

您可以通过单击/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.javasrc/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如何适合这个故事。