使用Spring Cloud Kubernetes基于Kubernetes、Spring Boot和Docker构建微服务架构 - Morioh


在本文中,我们将学习如何启动Spring Boot微服务项目并使用Kubernetes和Docker快速运行它
本文涵盖的主题是:

  • 在云原生开发中使用Spring Boot 2.0
  • 使用Spring Cloud Kubernetes项目为所有微服务提供服务发现
  • 使用Kubernetes Config Maps和Secrets将配置设置注入到应用程序Pod中
  • 使用Docker构建应用程序映像并将其使用YAML配置文件部署在Kubernetes上
  • 结合使用Spring Cloud Kubernetes和Zuul代理来公开所有微服务的单个Swagger API文档

当您构建微服务环境时,Spring Cloud和Kubernetes可能是两个相互竞争的解决方案。Spring Cloud提供的诸如Eureka,Spring Cloud Config或Zuul之类的组件可以由Kubernetes内置的组件如服务,配置映射,secrets或ingresses替代,但是,即使您决定使用Kubernetes组件而不是Spring Cloud,也可以利用整个Spring Cloud项目中提供的一些有趣功能。
一个对我们有帮助的真正有趣的项目是Spring Cloud Kubernetes。尽管它仍处于孵化阶段,但绝对值得花一些时间。它将Spring Cloud与Kubernetes集成在一起。我将向您展示如何使用使用它实现客户端的发现、与Ribbon客户端的服务间通信以及使用Spring Cloud Kubernetes的Zipkin发现。

假设有三个独立的应用程序(employee-service, department-service, organization-service),它们通过REST API相互通信。这些Spring Boot微服务使用Kubernetes提供的一些内置机制:用于分布式配置的配置映射和secrets,用于服务发现的etcd以及用于API网关的入口。

将spring-cloud-dependency声明为依赖项管理的BOM。

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Spring Cloud Kubernetes不在Spring Cloud Release Trains下发布,因此我们需要明确定义其版本。因为我们使用Spring Boot 2.0,所以我们必须包含spring-cloud-kubernetes工件的最新SNAPSHOT版本,即0.3.0.BUILD-SNAPSHOT。
本文中提供的示例应用程序的源代码可在此存储库的GitHub找到。

前提条件
为了能够部署和测试我们的示例微服务,我们需要准备一个开发环境。我们可以通过以下步骤来实现:

  • 您至少需要在本地计算机上运行Kubernetes(Minikube)或Openshift(Minishift)的单节点集群实例。您应该启动它,并公开它们提供的嵌入式Docker客户端。在OpenShift上部署Java应用程序的快速指南
  • Spring Cloud Kubernetes要求访问Kubernetes API,以便能够检索为单个服务运行的Pod的地址列表。如果您使用Kubernetes,则应该执行以下命令:
    $ kubectl create clusterrolebinding admin --clusterrole=cluster-admin --serviceaccount=default:default

1. 使用配置映射和secrets注入配置
使用Spring Cloud时,在系统中实现分布式配置的最明显选择是Spring Cloud Config;而使用Kubernetes,您可以使用Config Map。它包含可在Pod中使用或用于存储配置数据的配置数据的键值对。它用于存储和共享非敏感,未加密的配置信息。要在群集中使用敏感信息,必须使用Secrets。根据MongoDB连接设置的示例,可以完美地演示这两个Kubernetes对象的使用。在Spring Boot应用程序内部,我们可以使用环境变量轻松地将其注入。这是application.yml 带有URI配置的文件片段。

spring:
  data:
    mongodb:
      uri: mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb/${MONGO_DATABASE}

尽管用户名和密码是敏感字段,但数据库名称不是,因此我们可以将其放在配置映射中。

apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb
data:
  database-name: microservices

当然,用户名和密码被定义为机密secret类型。

apiVersion: v1
kind: Secret
metadata:
  name: mongodb
type: Opaque
data:
  database-password: MTIzNDU2
  database-user: cGlvdHI=

要将配置应用于Kubernetes集群,我们运行以下命令。

$ kubectl apply -f kubernetes/mongodb-configmap.yaml
$ kubectl apply -f kubernetes/mongodb-secret.yaml

然后我们应该将配置属性注入到应用程序的pod中。在Deployment YAML文件中定义容器配置时,我们必须包括对环境变量和机密secret的引用,如下所示。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: employee
  labels:
    app: employee
spec:
  replicas: 1
  selector:
    matchLabels:
      app: employee
  template:
    metadata:
      labels:
        app: employee
    spec:
      containers:
      - name: employee
        image: piomin/employee:1.0
        ports:
        - containerPort: 8080
        env:
        - name: MONGO_DATABASE
          valueFrom:
            configMapKeyRef:
              name: mongodb
              key: database-name
        - name: MONGO_USERNAME
          valueFrom:
            secretKeyRef:
              name: mongodb
              key: database-user
        - name: MONGO_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mongodb
              key: database-password

2.使用Kubernetes构建服务发现
我们通常使用Docker容器在Kubernetes上运行微服务。一个或多个容器按Pod分组,Pod是在Kubernetes中创建和管理的最小的可部署单元。一个好的做法是在一个pod中只运行一个容器。如果您想扩展微服务,则只需增加正在运行的Pod的数量即可。属于单个微服务的所有正在运行的Pod在逻辑上都与Kubernetes Service进行了分组。该服务在集群外部可能是可见的,并且能够在所有正在运行的Pod之间负载平衡传入的请求。以下服务定义将标有字段app等于的所有Pod分组为employee。

apiVersion: v1
kind: Service
metadata:
  name: employee
  labels:
    app: employee
spec:
  ports:
  - port: 8080
    protocol: TCP
  selector:
    app: employee

服务Service类型可用于访问Kubernetes集群外部的应用程序或集群内部的服务间通信。但是,使用Spring Cloud Kubernetes可以更轻松地实现微服务之间的通信。首先,我们需要在项目中包括以下依赖项pom.xml

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes</artifactId>
<version>0.3.0.BUILD-SNAPSHOT</version>
</dependency>

然后,我们应该为应用程序启用发现客户端,就像我们在Spring Cloud Netflix Eureka中为发现所做的一样。这使您可以按名称查询Kubernetes端点(服务)。Spring Cloud Kubernetes Ribbon或Zipkin项目还使用此发现功能分别获取为要进行负载平衡的微服务定义的Pod列表,或可用于发送跟踪或跨度的Zipkin服务器。

@SpringBootApplication
@EnableDiscoveryClient
@EnableMongoRepositories
@EnableSwagger2
public class EmployeeApplication {
 public static void main(String[] args) {
  SpringApplication.run(EmployeeApplication.class, args);
 }
 // ...
}

本部分的最后一件事是确保Spring应用程序名称与该应用程序的Kubernetes服务Service名称完全相同。对于应用程序employee-service,它是employee。

spring:
  application:
    name: employee


3.使用Docker构建微服务并在Kubernetes上部署
可包括一些标准的Spring依赖关系,用于构建基于REST的微服务,与MongoDB集成以及使用Swagger2生成API文档。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

为了与MongoDB集成,我们应该创建一个扩展标准Spring Data的接口CrudRepository

public interface EmployeeRepository extends CrudRepository {
 List findByDepartmentId(Long departmentId);
 List findByOrganizationId(Long organizationId);
}

实体类应使用Mongo注释@Document,主键字段应使用 @Id。

@Document(collection = "employee")
public class Employee {
 @Id
 private String id;
 private Long organizationId;
 private Long departmentId;
 private String name;
 private int age;
 private String position;
 // ...
}

存储库bean已注入到控制器类中。这是员工服务内部REST API的完整实现。

@RestController
public class EmployeeController {
 private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeController.class);
 @Autowired
 EmployeeRepository repository;
 @PostMapping("/")
 public Employee add(@RequestBody Employee employee) {
  LOGGER.info("Employee add: {}", employee);
  return repository.save(employee);
 }
 @GetMapping("/{id}")
 public Employee findById(@PathVariable("id") String id) {
  LOGGER.info("Employee find: id={}", id);
  return repository.findById(id).get();
 }
 @GetMapping("/")
 public Iterable findAll() {
  LOGGER.info("Employee find");
  return repository.findAll();
 }
 @GetMapping("/department/{departmentId}")
 public List findByDepartment(@PathVariable("departmentId") Long departmentId) {
  LOGGER.info("Employee find: departmentId={}", departmentId);
  return repository.findByDepartmentId(departmentId);
 }
 @GetMapping("/organization/{organizationId}")
 public List findByOrganization(@PathVariable("organizationId") Long organizationId) {
  LOGGER.info("Employee find: organizationId={}", organizationId);
  return repository.findByOrganizationId(organizationId);
 }
}

为了在Kubernetes上运行我们的微服务,我们应该首先使用Maven来构建整个项目:
mvn clean install 

每个微服务在根目录中都有一个Dockerfile。这是employee-service的Dockerfile定义。

FROM openjdk:8-jre-alpine
ENV APP_FILE employee-service-1.0-SNAPSHOT.jar
ENV APP_HOME /usr/apps
EXPOSE 8080
COPY target/$APP_FILE $APP_HOME/
WORKDIR $APP_HOME
ENTRYPOINT ["sh", "-c"]
CMD ["exec java -jar $APP_FILE"]

让我们为所有三个示例微服务构建Docker映像:

$ cd employee-service
$ docker build -t piomin/employee:1.0 .
$ cd department-service
$ docker build -t piomin/department:1.0 .
$ cd organization-service
$ docker build -t piomin/organization:1.0 .

最后一步是在Kubernetes上将Docker容器与应用程序一起部署。为此,只需基于YAML配置文件上执行命令kubectl apply。employee-service已经在步骤1中演示了。所有必需的部署字段在存储库中的 kubernetes目录中找到。

$ kubectl apply -f kubernetes\employee-deployment.yaml
$ kubectl apply -f kubernetes\department-deployment.yaml
$ kubectl apply -f kubernetes\organization-deployment.yaml

4.使用Spring Cloud Kubernetes Ribbon进行微服务之间的通信
所有微服务都部署在Kubernetes上。现在,有必要讨论与服务间通信有关的某些方面。employee-service与其他微服务相反,该应用程序未调用任何其他微服务。让我们看一下其他微服务,这些微服务调用了员工服务公开的API,并且彼此之间进行通信(organization-service 调用 department-service API)。
首先,我们需要在项目中包括一些其他依赖项。我们使用Spring Cloud Ribbon和OpenFeign。另外,您也可以使用Spring @LoadBalancedRestTemplate。

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
<version>0.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

这是department-service的主要类。它通过使用@EnableFeignClients注释作为Feign客户端。它的工作原理与基于Spring Cloud Netflix Eureka的发现相同。OpenFeign使用Ribbon进行客户端负载平衡。Spring Cloud Kubernetes Ribbon提供了一些bean,它们迫使Ribbon通过Fabric8 KubernetesClient与Kubernetes API进行通信。

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableMongoRepositories
@EnableSwagger2
public class DepartmentApplication {
 public static void main(String[] args) {
  SpringApplication.run(DepartmentApplication.class, args);
 }
 // ...
}

这是Feign客户端的实现,用于调用employee-service公开的方法。

@FeignClient(name = "employee")
public interface EmployeeClient {
 @GetMapping("/department/{departmentId}")
 List findByDepartment(@PathVariable("departmentId") String departmentId);
}

最后,我们必须将Feign客户的Bean注入REST控制器。现在,我们可以调用EmployeeClient内部定义的方法,调用该方法等效于调用REST端点。

@RestController
public class DepartmentController {
 private static final Logger LOGGER = LoggerFactory.getLogger(DepartmentController.class);
 @Autowired
 DepartmentRepository repository;
 @Autowired
 EmployeeClient employeeClient;
 // ...
 @GetMapping("/organization/{organizationId}/with-employees")
 public List findByOrganizationWithEmployees(@PathVariable("organizationId") Long organizationId) {
  LOGGER.info("Department find: organizationId={}", organizationId);
  List departments = repository.findByOrganizationId(organizationId);
  departments.forEach(d -> d.setEmployees(employeeClient.findByDepartment(d.getId())));
  return departments;
 }
}

5.使用Kubernetes Ingress入口构建API网关
入口Ingress是一组规则,这些规则允许传入的请求能够访问到达下游服务。在我们的微服务架构中,入口扮演着API网关的角色。要创建它,我们首先应该准备一个YAML描述文件。描述符文件应包含主机名,网关将在该主机名下可用,并将规则映射到下游服务。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: gateway-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  backend:
    serviceName: default-http-backend
    servicePort: 80
  rules:
  - host: microservices.info
    http:
      paths:
      - path: /employee
        backend:
          serviceName: employee
          servicePort: 8080
      - path: /department
        backend:
          serviceName: department
          servicePort: 8080
      - path: /organization
        backend:
          serviceName: organization
          servicePort: 8080

必须执行下面命令将以上配置应用于所有kubernetes集群:

$ kubectl apply -f kubernetes\ingress.yaml

要在本地测试该解决方案,我们必须在主机文件内的入口定义中设置的IP地址和主机名之间插入映射。之后,我们可以使用定义的主机名通过入口测试服务,如下所示:http://microservices.info/employee。

192.168.99.100 microservices.info

 执行命令 kubectl describe ing gateway-ingress可检查ingress细节。

6.使用Swagger2在网关上启用API规范
如果我们想为Kubernetes上部署的所有微服务公开一个Swagger文档怎么办?好吧,这里的事情变得复杂了……我们可以使用Swagger UI运行一个容器,并手动映射入口暴露的所有路径,但这不是一个好的解决方案……
在这种情况下,我们可以再次使用Spring Cloud Kubernetes Ribbon,这一次可以与Spring Cloud Netflix Zuul一起使用。Zuul仅充当服务于Swagger API的网关。这是我的gateway-service项目中使用的依赖项列表。

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes</artifactId>
<version>0.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
<version>0.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

Kubernetes发现客户端将检测群集上公开的所有服务。我们只想显示三个微服务的文档。这就是为什么我为Zuul定义以下路由。

zuul:
  routes:
    department:
      path: /department/**
    employee:
      path: /employee/**
    organization:
      path: /organization/**

现在我们可以使用 ZuulPropertiesBean从Kubernetes发现中获取路由的地址,并将其配置为Swagger资源,如下所示:

@Configuration
public class GatewayApi {
 @Autowired
 ZuulProperties properties;
 @Primary
 @Bean
 public SwaggerResourcesProvider swaggerResourcesProvider() {
  return () -> {
   List resources = new ArrayList();
   properties.getRoutes().values().stream()
   .forEach(route -> resources.add(createResource(route.getId(), "2.0")));
   return resources;
  };
 }
 private SwaggerResource createResource(String location, String version) {
  SwaggerResource swaggerResource = new SwaggerResource();
  swaggerResource.setName(location);
  swaggerResource.setLocation("/" + location + "/v2/api-docs");
  swaggerResource.setSwaggerVersion(version);
  return swaggerResource;
 }
}

应用程序网关服务应与其他应用程序一样部署在群集上。您可以通过执行命令kubectl get svc来查看正在运行的服务的列表。Swagger文档可从以下地址获得:http://192.168.99.100:31237/swagger-ui.html。