在本文中,我们将学习如何启动Spring Boot微服务项目并使用Kubernetes和Docker快速运行它
本文涵盖的主题是:
当您构建微服务环境时,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 上 找到。
前提条件
为了能够部署和测试我们的示例微服务,我们需要准备一个开发环境。我们可以通过以下步骤来实现:
$ 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。