在这篇文章中,你将学习如何用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可能不是最佳选择。