4

使用Spring Boot 3的Spring Cloud Kubernetes教程

 1 year ago
source link: https://www.jdon.com/66863.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

使用Spring Boot 3的Spring Cloud Kubernetes教程

在这篇文章中,你将学习如何用Spring Cloud Kubernetes和Spring Boot 3创建、测试和运行应用程序。

你将看到如何在Kubernetes环境中使用Skaffold、Testcontainers、Spring Boot Admin和Fabric8客户端等工具。

这篇文章的主要目的是向你介绍Spring Cloud Kubernetes项目的最新版本。

源码:GitHub repository

首先,让我们介绍一下这个Github资源库:

  • 它包含五个应用程序。
  • 有三个微服务(雇员服务、部门服务、组织服务)
  • 通过REST客户端相互通信并连接到Mongo数据库。
  • 还有用Spring Cloud Gateway项目创建的API网关(gateway-service)。
  • 最后,admin-service目录包含用于监控所有其他应用程序的Spring Boot Admin应用程序。

你可以使用一个Skaffold命令轻松地从源代码中部署所有的应用程序。

如果你从版本库根目录运行以下命令,它将用Jib Maven插件构建镜像,并将所有应用部署到Kubernetes集群上:

$ skaffold run

另一方面,你可以进入特定的应用程序目录,只使用完全相同的命令来部署它。
每个应用所需的所有Kubernetes YAML清单都放在k8s目录中。
在项目根k8s目录下还有一个全局配置,例如Mongo部署。

它是如何工作的
在我们的示例架构中,我们将使用Spring Cloud Kubernetes Config来通过ConfigMap和Secret注入配置,并使用Spring Cloud Kubernetes Discovery与OpenFeign客户端进行服务间通信。

我们所有的应用程序都在同一个命名空间内运行,但我们也可以将它们部署在几个不同的命名空间内,并通过OpenFeign处理它们之间的通信。

在这种情况下,我们唯一要做的就是将 spring.cloud.kubernetes.discovery.all-namespaces 属性设置为 true。

在我们的服务前面,有一个API网关。
它是一个独立的应用,但我们也可以使用本地CRD集成将其安装在Kubernetes上。

在我们的案例中,这是一个标准的Spring Boot 3应用,只是包括并使用了Spring Cloud Gateway模块。它还使用Spring Cloud Kubernetes Discovery和Spring Cloud OpenFeign来定位和调用下游服务。

使用Spring Cloud Kubernetes配置
我将通过部门服务的例子来描述实施细节。它暴露了一些REST端点,但也调用了雇员服务所暴露的端点。

除了标准模块,我们还需要将Spring Cloud Kubernetes纳入Maven的依赖项。

这里,我们必须决定是使用Fabric8客户端还是Kubernetes Java客户端。
就我个人而言,我有使用Fabric8的经验,所以我将使用spring-cloud-starter-kubernetes-fabric8-all starter来包含配置和发现模块。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-kubernetes-fabric8-all</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

如你所见,我们的应用程序正在连接到Mongo数据库。
让我们提供应用程序所需的连接细节和凭证。
在k8s目录中,你会发现configmap.yaml文件。它包含了Mongo的地址和数据库的名称。
这些属性被作为application.properties文件注入到pod中。
现在是最重要的事情。ConfigMap的名称必须与我们应用程序的名称相同。
Spring Boot的名称由spring.application.name属性表示。

kind: ConfigMap
apiVersion: v1
metadata:
  name: department
data:
  application.properties: |-
    spring.data.mongodb.host: mongodb
    spring.data.mongodb.database: admin
    spring.data.mongodb.authentication-database: admin

在目前的情况下,应用程序的名称是department。这里是应用程序里面的application.yml文件:

spring:
  application:
    name: department

同样的命名规则也适用于Secret。我们在下面的Secret里面保存敏感数据,比如Mongo数据库的用户名和密码。你也可以在k8s目录下的secret.yaml文件内找到这些内容。

kind: Secret
apiVersion: v1
metadata:
  name: department
data:
  spring.data.mongodb.password: UGlvdF8xMjM=
  spring.data.mongodb.username: cGlvdHI=
type: Opaque

现在,让我们继续讨论部署清单。我们稍后将在这里澄清前两点。

Spring Cloud Kubernetes需要在Kubernetes上有特殊的权限,以便与主API互动
(1)我们不必为镜像提供一个标签--Skaffold会处理它
(2)为了启用从ConfigMap加载属性,我们需要设置spring.config.import=kubernetes: 属性(一种新方法)或将spring.cloud.bootstrap.enabled属性设置为true(旧方法)。
(3) 我们将不直接使用属性,而是在部署上设置相应的环境变量。
(4)默认情况下,由于安全原因,通过API消耗secrets的功能没有被启用。为了启用它,我们将把SPRING_CLOUD_KUBERNETES_SECRETS_ENABLEAPI环境变量设置为true。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: department
  labels:
    app: department
spec:
  replicas: 1
  selector:
    matchLabels:
      app: department
  template:
    metadata:
      labels:
        app: department
    spec:
      serviceAccountName: spring-cloud-kubernetes # (1)
      containers:
      - name: department
        image: piomin/department # (2)
        ports:
        - containerPort: 8080
        env:
          - name: SPRING_CLOUD_BOOTSTRAP_ENABLED # (3)
            value: "true"
          - name: SPRING_CLOUD_KUBERNETES_SECRETS_ENABLEAPI # (4)
            value: "true"

使用Spring Cloud Kubernetes Discovery
我们已经在上一节使用spring-cloud-starter-kubernetes-fabric8-all starter包含了Spring Cloud Kubernetes Discovery模块。为了提供一个声明式REST客户端,我们还将包括Spring Cloud OpenFeign模块:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

现在,我们可以声明@FeignClient接口。这里重要的是一个被发现的服务的名称。它应该与为雇员服务应用程序定义的Kubernetes服务的名称相同。

@FeignClient(name = "employee")
public interface EmployeeClient {

    @GetMapping("/department/{departmentId}")
    List<Employee> findByDepartment(@PathVariable("departmentId") String departmentId);

    @GetMapping("/department-with-delay/{departmentId}")
    List<Employee> findByDepartmentWithDelay(@PathVariable("departmentId") String departmentId);
}

下面是雇员服务应用的Kubernetes服务清单。该服务的名称是employee (1)。标签spring-boot是为Spring Boot Admin发现目的而设置的(2)。你可以在employee-service/k8s目录中找到以下YAML。

apiVersion: v1
kind: Service
metadata:
  name: employee # (1)
  labels:
    app: employee
    spring-boot: "true" # (2)
spec:
  ports:
    - port: 8080
      protocol: TCP
  selector:
    app: employee
  type: ClusterIP

只是为了澄清--这里是由OpenFeign客户端在部门服务中调用的雇员服务API方法的实现。

@RestController
public class EmployeeController {

    private static final Logger LOGGER = LoggerFactory
        .getLogger(EmployeeController.class);
    
    @Autowired
    EmployeeRepository repository;

    // ... other endpoints implementation 

    @GetMapping("/department/{departmentId}")
    public List<Employee> findByDepartment(@PathVariable("departmentId") String departmentId) {
        LOGGER.info("Employee find: departmentId={}", departmentId);
        return repository.findByDepartmentId(departmentId);
    }

    @GetMapping("/department-with-delay/{departmentId}")
    public List<Employee> findByDepartmentWithDelay(@PathVariable("departmentId") String departmentId) throws InterruptedException {
        LOGGER.info("Employee find: departmentId={}", departmentId);
        Thread.sleep(2000);
        return repository.findByDepartmentId(departmentId);
    }
    
}

这就是我们要做的一切。

现在,我们可以使用部门服务中的OpenFeign客户端调用端点。
例如,在 "延迟 "端点上,我们可以使用Spring Cloud Circuit Breaker与Resilience4J。

@RestController
public class DepartmentController {

    private static final Logger LOGGER = LoggerFactory
        .getLogger(DepartmentController.class);

    DepartmentRepository repository;
    EmployeeClient employeeClient;
    Resilience4JCircuitBreakerFactory circuitBreakerFactory;

    public DepartmentController(
        DepartmentRepository repository, 
        EmployeeClient employeeClient,
        Resilience4JCircuitBreakerFactory circuitBreakerFactory) {
            this.repository = repository;
            this.employeeClient = employeeClient;
            this.circuitBreakerFactory = circuitBreakerFactory;
    }

    @GetMapping("/{id}/with-employees-and-delay")
    public Department findByIdWithEmployeesAndDelay(@PathVariable("id") String id) {
        LOGGER.info("Department findByIdWithEmployees: id={}", id);
        Department department = repository.findById(id).orElseThrow();
        CircuitBreaker circuitBreaker = circuitBreakerFactory.create("delayed-circuit");
        List<Employee> employees = circuitBreaker.run(() ->
                employeeClient.findByDepartmentWithDelay(department.getId()));
        department.setEmployees(employees);
        return department;
    }

    @GetMapping("/organization/{organizationId}/with-employees")
    public List<Department> findByOrganizationWithEmployees(@PathVariable("organizationId") String organizationId) {
        LOGGER.info("Department find: organizationId={}", organizationId);
        List<Department> departments = repository.findByOrganizationId(organizationId);
        departments.forEach(d -> d.setEmployees(employeeClient.findByDepartment(d.getId())));
        return departments;
    }

}

在 k3s 上使用 Testcontainer 进行测试

正如我之前提到的,我们可以使用多种工具对 Kubernetes 进行测试。这次我们将看到如何使用 Testcomntainers 来完成它。我们已经在上一节中使用它来运行 Mongo 数据库。但是还有用于 Rancher 的 k3s Kubernetes 发行版的 Testcontainers 模块。目前,它处于孵化状态,不过我们也懒得去尝试。为了在项目中使用它,我们需要包含以下 Maven 依赖项:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>k3s</artifactId>
  <scope>test</scope>
</dependency>

代码:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = {
                "spring.main.cloud-platform=KUBERNETES",
                "spring.cloud.bootstrap.enabled=true"})
@Testcontainers
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class EmployeeKubernetesTest {

   private static final Logger LOG = LoggerFactory
      .getLogger(EmployeeKubernetesTest.class);

   @Container
   static MongoDBContainer mongodb = new MongoDBContainer("mongo:5.0");
   @Container
   static K3sContainer k3s = new K3sContainer(DockerImageName
      .parse("rancher/k3s:v1.21.3-k3s1")); // (1)

   @BeforeAll
   static void setup() {
      Config config = Config
         .fromKubeconfig(k3s.getKubeConfigYaml()); // (2)
      DefaultKubernetesClient client = new 
         DefaultKubernetesClient(config); // (3)

      ConfigMap cm = client.configMaps().inNamespace("default")
         .create(buildConfigMap(mongodb.getMappedPort(27017)));
      LOG.info("!!! {}", cm); // (4)

      System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, 
         client.getConfiguration().getMasterUrl());
      
      // (5) 
      System.setProperty(Config.KUBERNETES_CLIENT_CERTIFICATE_DATA_SYSTEM_PROPERTY,
         client.getConfiguration().getClientCertData());
      System.setProperty(Config.KUBERNETES_CA_CERTIFICATE_DATA_SYSTEM_PROPERTY,
         client.getConfiguration().getCaCertData());
       System.setProperty(Config.KUBERNETES_CLIENT_KEY_DATA_SYSTEM_PROPERTY,
         client.getConfiguration().getClientKeyData());
      System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, 
         "true");
      System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, 
         "default");
    }

    private static ConfigMap buildConfigMap(int port) {
        return new ConfigMapBuilder().withNewMetadata()
                .withName("employee").withNamespace("default")
                .endMetadata()
                .addToData("application.properties",
                        """
                        spring.data.mongodb.host=localhost
                        spring.data.mongodb.port=%d
                        spring.data.mongodb.database=test
                        spring.data.mongodb.authentication-database=test
                        """.formatted(port))
                .build();
    }

    @Autowired
    TestRestTemplate restTemplate;

    @Test
    @Order(1)
    void addEmployeeTest() {
        Employee employee = new Employee("1", "1", "Test", 30, "test");
        employee = restTemplate.postForObject("/", employee, Employee.class);
        assertNotNull(employee);
        assertNotNull(employee.getId());
    }

    @Test
    @Order(2)
    void addAndThenFindEmployeeByIdTest() {
        Employee employee = new Employee("1", "2", "Test2", 20, "test2");
        employee = restTemplate
           .postForObject("/", employee, Employee.class);
        assertNotNull(employee);
        assertNotNull(employee.getId());
        employee = restTemplate
                .getForObject("/{id}", Employee.class, employee.getId());
        assertNotNull(employee);
        assertNotNull(employee.getId());
    }

    @Test
    @Order(3)
    void findAllEmployeesTest() {
        Employee[] employees =
                restTemplate.getForObject("/", Employee[].class);
        assertEquals(2, employees.length);
    }

    @Test
    @Order(3)
    void findEmployeesByDepartmentTest() {
        Employee[] employees =
                restTemplate.getForObject("/department/1", Employee[].class);
        assertEquals(1, employees.length);
    }

    @Test
    @Order(3)
    void findEmployeesByOrganizationTest() {
        Employee[] employees =
                restTemplate.getForObject("/organization/1", Employee[].class);
        assertEquals(2, employees.length);
    }

}

我们不需要创建任何模拟。相反,我们将创建K3sContainer对象(1)。在运行测试之前,我们需要创建并初始化KubernetesClient。测试容器 K3sContainer提供了getKubeConfigYaml()方法来获取kubeconfig数据。有了Fabric8配置对象,我们可以从该kubeconfig(2)(3)初始化客户端。之后,我们将用Mongo连接细节创建ConfigMap(4)。最后,我们要为Spring Cloud Kubernetes自动配置的Fabric8客户端重写主URL。与上一节相比,我们还需要设置Kubernetes客户端证书和密钥(5)。

在Minikube上运行Spring Kubernetes应用程序
在这个练习中,我使用Minikube,但你也可以使用任何其他的发行版,如Kind或k3s。

Spring Cloud Kubernetes需要在Kubernetes上有额外的权限,以便能够与主API互动。
因此,在运行应用程序之前,我们将创建具有所需权限的spring-cloud-kubernetes ServiceAccount。我们的角色需要拥有对配置图、pods、服务、端点和机密的访问权。
如果我们没有启用跨所有命名空间的发现(spring.cloud.kubernetes.discovery.all-namespaces 属性),可以在命名空间内进行角色。否则,我们应该创建一个ClusterRole。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: spring-cloud-kubernetes
  namespace: default
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: spring-cloud-kubernetes
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["configmaps", "pods", "services", "endpoints", "secrets"]
    verbs: ["get", "list", "watch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: spring-cloud-kubernetes
  namespace: default
subjects:
  - kind: ServiceAccount
    name: spring-cloud-kubernetes
    namespace: default
roleRef:
  kind: ClusterRole
  name: spring-cloud-kubernetes

当然,你不需要自己去应用上面可见的清单。正如我在文章开头提到的,在版本库根目录文件中有一个 skaffold.yaml 文件,包含了整个配置。它与所有服务一起运行带有 Mongo 部署(1)和带有权限(2)的清单。

apiVersion: skaffold/v4beta5
kind: Config
metadata:
  name: sample-spring-microservices-kubernetes
build:
  artifacts:
    - image: piomin/admin
      jib:
        project: admin-service
    - image: piomin/department
      jib:
        project: department-service
        args:
          - -DskipTests
    - image: piomin/employee
      jib:
        project: employee-service
        args:
          - -DskipTests
    - image: piomin/gateway
      jib:
        project: gateway-service
    - image: piomin/organization
      jib:
        project: organization-service
        args:
          - -DskipTests
  tagPolicy:
    gitCommit: {}
manifests:
  rawYaml:
    - k8s/mongodb-*.yaml # (1)
    - k8s/privileges.yaml # (2)
    - admin-service/k8s/*.yaml
    - department-service/k8s/*.yaml
    - employee-service/k8s/*.yaml
    - gateway-service/k8s/*.yaml
    - organization-service/k8s/*.yaml

我们需要做的就是通过执行以下skaffold命令来部署所有的应用程序:

$ skaffold dev

最后的思考
如果你在Kubernetes集群上只运行Spring Boot应用,Spring Cloud Kubernetes是一个有趣的选择。它允许我们轻松地与Kubernetes发现、配置图和秘密集成。

正因为如此,我们可以利用其他Spring Cloud组件,如负载平衡器、断路器等。

然而,如果你正在运行用不同语言和框架编写的应用程序,并使用服务网(Istio、Linkerd)等语言无关的工具,Spring Cloud Kubernetes可能不是最佳选择。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK