Microservices mit Spring Boot und Docker erstellen – Teil 2

Gastbeitrag von | 06.06.2018 | Softwareentwicklung | 0 Kommentare

Im zweiten Teil dieses Tutorials werden wir die Mitarbeiter-Suche inklusive weiterer Services erstellen und dann die Microservices mit Docker bereitstellen. Teil 1 des Tutorials finden Sie hier.

Erstellen des Mitarbeiter-Suchdienstes

Zu Beginn erstellen wir einen winzigen Microservice, der Mitarbeiter-Informationen basierend auf einer übergebenen ID zurückgibt. Dazu werden wir eine REST-API bereitstellen und diesen Microservice beim Eureka-Server registrieren, so dass andere Microservices ihn entsprechend finden und nutzen können. Ich nutze den Eureka-Client von start.spring.io. Werfen wir einen Blick auf die pom.xml Datei:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.example</groupId>
   <artifactId>EmployeeSearchService</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>EmployeeSearchService</name>
   <description>Demo project for Spring Boot</description>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.5.4.RELEASE</version>
      <relativePath />
      <!-- lookup parent from repository -->
   </parent>
   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
      <spring-cloud.version>Dalston.SR1</spring-cloud.version>
   </properties>
   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-config</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-jersey</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-eureka</artifactId>
      </dependency>
   </dependencies>
   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>
   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>
</project>

Sehen wir uns die Bootstrap-Eigenschaften an:

spring.application.name=EmployeeSearch
spring.cloud.config.uri=http://localhost:9090
eureka.client.serviceUrl.defaultZone:http://localhost:9091/eureka
server.port=8080
security.basic.enable: false   
management.security.enabled: false

Ich verwende einen logischen Namen des Dienstes EmployeeSearch, unter dem alle Instanzen dieses Dienstes gemeinsam im Eureka-Server registriert werden. Außerdem vergebe ich die URL des Konfigurationsservers. Bitte beachten Sie, dass Sie für die Bereitstellung in Docker die Container IP des Config-Servers ändern müssen, um den Konfigurationsserver zu finden. Gleiches gilt für den Standort des Eureka-Servers. Erstellen Sie nun die Service-Datei für die Mitarbeiter-Suche:

package com.example.EmployeeSearchService.service;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.stereotype.Service;

import com.example.EmployeeSearchService.domain.model.Employee;

@Service
public class EmployeeSearchService {

 private static Map < Long, Employee > EmployeeRepsitory = null;

 static {

  Stream < String > employeeStream = Stream.of("1,Shamik  Mitra,Java,Architect", "2,Samir  Mitra,C  ,Manager",
   "3,Swastika  Mitra,AI,Sr.Architect");

  EmployeeRepsitory = employeeStream.map(employeeStr -> {
   String[] info = employeeStr.split(",");
   return createEmployee(new Long(info[0]), info[1], info[2], info[3]);
  }).collect(Collectors.toMap(Employee::getEmployeeId, emp -> emp));

 }

 private static Employee createEmployee(Long id, String name, String practiceArea, String designation) {
  Employee emp = new Employee();
  emp.setEmployeeId(id);
  emp.setName(name);
  emp.setPracticeArea(practiceArea);
  emp.setDesignation(designation);
  emp.setCompanyInfo("Cognizant");
  return emp;
 }

 public Employee findById(Long id) {
  return EmployeeRepsitory.get(id);
 }

 public Collection < Employee > findAll() {
  return EmployeeRepsitory.values();
 }

}

Die Controller-Datei sollte wie folgt aussehen:

package com.example.EmployeeSearchService.controller;

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.EmployeeSearchService.domain.model.Employee;
import com.example.EmployeeSearchService.service.EmployeeSearchService;

@RefreshScope
@RestController
public class EmployeeSearchController {

 @Autowired
 EmployeeSearchService employeeSearchService;

 @RequestMapping("/employee/find/{id}")
 public Employee findById(@PathVariable Long id) {
  return employeeSearchService.findById(id);
 }

 @RequestMapping("/employee/findall")
 public Collection < Employee > findAll() {
  return employeeSearchService.findAll();
 }
}
package com.example.EmployeeSearchService.domain.model;

public class Employee {
 private Long employeeId;
 private String name;
 private String practiceArea;
 private String designation;
 private String companyInfo;
 public Long getEmployeeId() {
  return employeeId;
 }
 public void setEmployeeId(Long employeeId) {
  this.employeeId = employeeId;
 }
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
 public String getPracticeArea() {
  return practiceArea;
 }
 public void setPracticeArea(String practiceArea) {
  this.practiceArea = practiceArea;
 }
 public String getDesignation() {
  return designation;
 }
 public void setDesignation(String designation) {
  this.designation = designation;
 }
 public String getCompanyInfo() {
  return companyInfo;
 }
 public void setCompanyInfo(String companyInfo) {
  this.companyInfo = companyInfo;
 }
 @Override
 public String toString() {
  return "Employee [employeeId="   employeeId   ", name="   name   ", practiceArea="   practiceArea   ", designation="   designation   ", companyInfo="   companyInfo   "]";
 }




}

Nun erstelle ich einige wenige Mitarbeiter und ordne sie der Rest URL zu. Die Spring Boot-Datei sieht wie folgt aus:

package com.example.EmployeeSearchService;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableDiscoveryClient
@SpringBootApplication
public class EmployeeSearchServiceApplication {

 public static void main(String[] args) {
  SpringApplication.run(EmployeeSearchServiceApplication.class, args);
 }
}

Ich verwende den @EnableDiscoveryClient, um diesen Dienst als Eureka-Client zu registrieren. Wenn ich nun http://localhost:8080/employee/find/1 aufrufe, erhalte ich folgende Ausgabe:

{
   "employeeId":1,
   "name":"Shamik  Mitra",
   "practiceArea":"Java",
   "designation":"Architect",
   "companyInfo":"Cognizant"
}

Erstellen des Mitarbeiter-Dashboard-Service

Jetzt werde ich einen anderen Dienst erstellen, der den Employee Search Service verwendet, um Mitarbeiter-Informationen abzurufen. Dazu nutze ich einen Feign-Client und Hystrix als Sicherung, so dass selbst bei einem nicht aktiven Mitarbeiter-Suchdienst Standardwerte zurückgegeben werden können.

Pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.example</groupId>
   <artifactId>EmployeeDashBoardService</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>EmployeeDashBoardService</name>
   <description>Demo project for Spring Boot</description>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.5.4.RELEASE</version>
      <relativePath />
      <!-- lookup parent from repository -->
   </parent>
   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
      <spring-cloud.version>Dalston.SR1</spring-cloud.version>
   </properties>
   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-config</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-eureka</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-jersey</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-feign</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-ribbon</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-hystrix</artifactId>
      </dependency>
   </dependencies>
   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>
   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>
</project>

 Bootstrap-Eigenschaften:

spring.application.name=EmployeeDashBoard
spring.cloud.config.uri=http://localhost:9090
eureka.client.serviceUrl.defaultZone:http://localhost:9091/eureka
server.port=8081
security.basic.enable: false   
management.security.enabled: false

Feign-Client:

package com.example.EmployeeDashBoardService.controller;

import java.util.Collection;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.EmployeeDashBoardService.domain.model.EmployeeInfo;



@FeignClient(name = "EmployeeSearch")
@RibbonClient(name = "EmployeeSearch")
public interface EmployeeServiceProxy {

 @RequestMapping("/employee/find/{id}")
 public EmployeeInfo findById(@PathVariable(value = "id") Long id);

 @RequestMapping("/employee/findall")
 public Collection < EmployeeInfo > findAll();

}

Controller:

package com.example.EmployeeDashBoardService.controller;

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.EmployeeDashBoardService.domain.model.EmployeeInfo;

@RefreshScope
@RestController
public class FeignEmployeeInfoController {

 @Autowired
 EmployeeServiceProxy proxyService;

 @RequestMapping("/dashboard/feign/{myself}")
 public EmployeeInfo findme(@PathVariable Long myself) {
  return proxyService.findById(myself);

 }

 @RequestMapping("/dashboard/feign/peers")
 public Collection < EmployeeInfo > findPeers() {
  return proxyService.findAll();
 }

}

Spring Boot-Starter-Service:

package com.example.EmployeeDashBoardService;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class EmployeeDashBoardService {

 public static void main(String[] args) {
  SpringApplication.run(EmployeeDashBoardService.class, args);
 }

 @Bean
 public RestTemplate restTemplate(RestTemplateBuilder builder) {
  return builder.build();
 }
}

Jetzt ist es soweit: wenn ich die URL http://localhost:8081/dashboard /feign/1 aufrufe, erhalte ich folgende Antwort:

{
   "employeeId":1,
   "name":"Shamik  Mitra",
   "practiceArea":"Java",
   "designation":"Architect",
   "companyInfo":"Cognizant"
}

Erstellen des Gateway-Services

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>1.5.6.RELEASE</version>
      <relativePath>../../spring-boot-dependencies</relativePath>
   </parent>
   <artifactId>spring-boot-starter-parent</artifactId>
   <packaging>pom</packaging>
   <name>Spring Boot Starter Parent</name>
   <description>Parent pom providing dependency and plugin management for applications
built with Maven</description>
   <url>http://projects.spring.io/spring-boot/</url>
   <organization>
      <name>Pivotal Software, Inc.</name>
      <url>http://www.spring.io</url>
   </organization>
   <properties>
      <java.version>1.6</java.version>
      <resource.delimiter>@</resource.delimiter>
      <!-- delimiter that doesn't clash with Spring ${} placeholders -->
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <maven.compiler.source>${java.version}</maven.compiler.source>
      <maven.compiler.target>${java.version}</maven.compiler.target>
   </properties>
   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
            <exclusions>
               <exclusion>
                  <groupId>commons-logging</groupId>
                  <artifactId>commons-logging</artifactId>
               </exclusion>
            </exclusions>
         </dependency>
      </dependencies>
   </dependencyManagement>
   <build>
      <!-- Turn on filtering by default for application properties -->
      <resources>
         <resource>
            <directory>${basedir}/src/main/resources</directory>
            <filtering>true</filtering>
            <includes>
               <include>**/application*.yml</include>
               <include>**/application*.yaml</include>
               <include>**/application*.properties</include>
            </includes>
         </resource>
         <resource>
            <directory>${basedir}/src/main/resources</directory>
            <excludes>
               <exclude>**/application*.yml</exclude>
               <exclude>**/application*.yaml</exclude>
               <exclude>**/application*.properties</exclude>
            </excludes>
         </resource>
      </resources>
      <pluginManagement>
         <plugins>
            <!-- Apply more sensible defaults for user projects -->
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-failsafe-plugin</artifactId>
               <executions>
                  <execution>
                     <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                     </goals>
                  </execution>
               </executions>
            </plugin>
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-jar-plugin</artifactId>
               <configuration>
                  <archive>
                     <manifest>
                        <mainClass>${start-class}</mainClass>
                        <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                     </manifest>
                  </archive>
               </configuration>
            </plugin>
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-surefire-plugin</artifactId>
               <configuration>
                  <includes>
                     <include>**/*Tests.java</include>
                     <include>**/*Test.java</include>
                  </includes>
                  <excludes>
                     <exclude>**/Abstract*.java</exclude>
                  </excludes>
               </configuration>
            </plugin>
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-war-plugin</artifactId>
               <configuration>
                  <failOnMissingWebXml>false</failOnMissingWebXml>
                  <archive>
                     <manifest>
                        <mainClass>${start-class}</mainClass>
                        <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                     </manifest>
                  </archive>
               </configuration>
            </plugin>
            <plugin>
               <groupId>org.codehaus.mojo</groupId>
               <artifactId>exec-maven-plugin</artifactId>
               <configuration>
                  <mainClass>${start-class}</mainClass>
               </configuration>
            </plugin>
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-resources-plugin</artifactId>
               <version>2.6</version>
               <configuration>
                  <delimiters>
                     <delimiter>${resource.delimiter}</delimiter>
                  </delimiters>
                  <useDefaultDelimiters>false</useDefaultDelimiters>
               </configuration>
            </plugin>
            <plugin>
               <groupId>pl.project13.maven</groupId>
               <artifactId>git-commit-id-plugin</artifactId>
               <executions>
                  <execution>
                     <goals>
                        <goal>revision</goal>
                     </goals>
                  </execution>
               </executions>
               <configuration>
                  <verbose>true</verbose>
                  <dateFormat>yyyy-MM-dd'T'HH:mm:ssZ</dateFormat>
                  <generateGitPropertiesFile>true</generateGitPropertiesFile>
                  <generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>
               </configuration>
            </plugin>
            <!-- Support our own plugin -->
            <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <executions>
                  <execution>
                     <goals>
                        <goal>repackage</goal>
                     </goals>
                  </execution>
               </executions>
               <configuration>
                 <mainClass>${start-class}</mainClass>
               </configuration>
            </plugin>
            <!-- Support shade packaging (if the user does not want to use our plugin) -->
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-shade-plugin</artifactId>
               <dependencies>
                  <dependency>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-maven-plugin</artifactId>
                     <version>1.5.6.RELEASE</version>
                  </dependency>
               </dependencies>
               <configuration>
                  <keepDependenciesWithProvidedScope>true</keepDependenciesWithProvidedScope>
                  <createDependencyReducedPom>true</createDependencyReducedPom>
                  <filters>
                     <filter>
                        <artifact>*:*</artifact>
                        <excludes>
                           <exclude>META-INF/*.SF</exclude>
                           <exclude>META-INF/*.DSA</exclude>
                           <exclude>META-INF/*.RSA</exclude>
                        </excludes>
                     </filter>
                  </filters>
               </configuration>
               <executions>
                  <execution>
                     <phase>package</phase>
                     <goals>
                        <goal>shade</goal>
                     </goals>
                     <configuration>
                        <transformers>
                           <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                              <resource>META-INF/spring.handlers</resource>
                           </transformer>
                           <transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
                              <resource>META-INF/spring.factories</resource>
                           </transformer>
                           <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                              <resource>META-INF/spring.schemas</resource>
                           </transformer>
                           <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                           <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                              <mainClass>${start-class}</mainClass>
                           </transformer>
                        </transformers>
                     </configuration>
                  </execution>
               </executions>
            </plugin>
         </plugins>
      </pluginManagement>
   </build>
</project>

Bootstrap-Eigenschaften:

spring.application.name=EmployeeAPIGateway
eureka.client.serviceUrl.defaultZone:http://localhost:9091/eureka
server.port=8084
security.basic.enable: false   
management.security.enabled: false 
zuul.routes.employeeUI.serviceId=EmployeeDashBoard
zuul.host.socket-timeout-millis=30000

Durch die Eigenschaft zuul.routes.employeeUI.serviceId = EmployeeDashBoard weisen wir Zuul an, dass jede URL, die mit managerUI versehen ist, auf den EmployeeDashboard-Dienst umgeleitet wird.

Spring Boot-Datei:

package com.example.EmployeeZuulService;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class EmployeeZuulServiceApplication {

 public static void main(String[] args) {
  SpringApplication.run(EmployeeZuulServiceApplication.class, args);
 }
}

Wenn wir jetzt den Service ausführen und http://localhost:8084/employeeUI/dashboard/feign/1 aufrufen, erhalten wir folgende Antwort:

{
   "employeeId":1,
   "name":"Shamik  Mitra",
   "practiceArea":"Java",
   "designation":"Architect",
   "companyInfo":"Cognizant"
}

Die Bereitstellung in Docker Containern

So, jetzt ist erst einmal Schluss mit dem Codieren. Mal sehen, wie unsere Anwendung läuft.Derzeit stehen alle Dienste bereit und laufen auf dem lokalen Rechner. Wir möchten jedoch nicht, dass unser Code nur in der lokalen Konfiguration ausgeführt wird, sondern dass er in der Produktion mit Bravour läuft. Wir lieben unseren Code und betrachten ihn wie ein kleines Kind, dem wir nur das Beste wünschen. Wenn wir unsere Kinder zur Schule schicken und ihnen den richtigen Weg zum Erfolg zeigen wollen, müssen wir sie auch durch unsere Anwendung leiten. Lassen Sie uns also einen Ausflug in die DevOps-Welt machen und versuchen, unserem Quellcode den richtigen Weg zu zeigen.

Willkommen in der Docker World

Docker braucht eigentlich keine Einführung. Wenn Sie der Meinung sind, dass Sie noch einen Leitfaden benötigen, können Sie sich hier https://docs.docker.com/get-started/ ansehen.

Ich gehe davon aus, dass Docker CE auf Ihrem Computer installiert ist. Die Konzepte, die wir hier für die Bereitstellung verwenden, lauten wie folgt:

  1. Docker-File: Dies ist ein Textdokument, das alle Anweisungen zum Erstellen eines Docker-Images enthält. Mit dem Befehlssatz einer Docker-Datei können wir Schritte festlegen, die Dateien kopieren, Installationen durchführen usw. Weitere Referenzen finden Sie unter https://docs.docker.com/engine/reference/builder/.
  2. Docker-Compose: Dies ist ein Werkzeug, das mehrere Container erstellen kann. Es hilft, die erforderliche Umgebung mit einem einzigen Befehl zu erstellen.

Wie im Microservice-Architekturdiagramm gezeigt, erstellen wir für jeden Service einen eigenen Container. In unserem Beispiel ist dies die Liste der Container:

  1. Konfigurationsserver
  2. EmployeeService
  3. Mitarbeiter Board Service
  4. Mitarbeiter Dashboard Service
  5. Gateway-Service

Docker-Konfiguration für den Konfigurationsserver

Der Container sollte die JAR-Datei des Konfigurationsservers enthalten. Hier wählen wir die jar-Datei vom lokalen Rechner aus. In einem realen Szenario sollten wir die jar-Datei an ein Artifact-Repository-Manager-System wie Nexus oder Artifactory weiterleiten und der Container sollte die Datei vom Repo-Manager herunterladen.

Der Konfigurationsserver sollte am Port 8888 entsprechend der Bootstrap Eigenschaften verfügbar sein.

Wie oben bereits erwähnt, lassen wir den Konfigurationsserver die Konfiguration von einem Speicherort lesen, so dass wir sicherstellen, dass die Eigenschaftsdateien auch dann abgerufen werden können, wenn der Container ausfällt.

Erstellen Sie nun einen Ordner namens config-repo, der die erforderliche Eigenschaftsdatei enthält. Wir werden Folgendes für den Container Config-Server sicherstellen:

 # mkdir config-repo
 # cd config-repo
 # echo "service.employyesearch.serviceId=EmployeeSearch" > EmployeeDashBoard.properties
 # echo "user.role=Dev" > EmployeeSearch.properties

Im übergeordneten Ordner erstellen wir eine Docker-Datei namens Dockerfile. Dieses Dockerfile erstellt unser Image, welches Java enthält.

 # cd ../
 # vi Dockerfile

Platzieren Sie folgenden Inhalt:

FROM alpine:edge
MAINTAINER javaonfly
RUN apk add --no-cache openjdk8

FROM: Dieses Schlüsselwort weist Docker an, ein bestimmtes Bild mit seinem Tag als build-base zu verwenden.

MAINTAINER: Ein MAINTAINER ist der Autor eines Images.

RUN: Dieser Befehl installiert openjdk8 im System.

Führen Sie den folgenden Befehl aus, um das Basis-Docker-Image zu erstellen:

docker build --tag=alpine-jdk:base --rm=true .

Nachdem das Basisimage erfolgreich erstellt wurde, ist es an der Zeit, das Docker-Image für den Konfigurationsserver zu erstellen.

Erstellen Sie einen Ordner namens files und legen Sie die jar-Datei des Konfigurationsservers in das Verzeichnis. Erstellen Sie dann eine Datei namens Dockerfile-configserver mit dem folgenden Inhalt:

FROM alpine-jdk:base
MAINTAINER javaonfly
COPY files/MicroserviceConfigServer.jar /opt/lib/
RUN mkdir /var/lib/config-repo
COPY config-repo /var/lib/config-repo
ENTRYPOINT ["/usr/bin/java"]
CMD ["-jar", "/opt/lib/MicroserviceConfigServer.jar"]
VOLUME /var/lib/config-repo
EXPOSE 9090

Hier haben wir festgelegt, dass aus ein Image aus dem zuvor erstellten Alpine-Jdk-Image erstellt wird. Wir werden die jar-Datei mit dem Namen employeenconfigserver.jar in den Speicherort /opt/lib und das config-repo in das Verzeichnis /root kopieren. Sobald der Container gestartet wird, wollen wir auch deb Konfigurationsserver starten, daher sind ENTRYPOINT und CMD so eingestellt, dass sie den entsprechenden Java-Befehl ausführen. Da wir eine Möglichkeit benötigen, um die Konfigurationsdateien von außerhalb des Containers zu teilen, nutzen wir den Volume-Befehl. Und da der Konfigurationsserver für die Außenwelt über den Port 9090 erreichbar sein sollte, nutzen wir EXPOSE 9090.

Lassen Sie uns nun das Docker-Image erstellen und als config-server markieren:

# docker build --file=Dockerfile-configserver --tag=config-server:latest --rm=true .

Und das Docker-Volume:

# docker volume create --name=config-repo
# docker run --name=config-server --publish=9090:9090 --volume=config-repo:/var/lib/config-repo config-server:latest

Puh! Sobald wir den obigen Befehl ausführen, sollten wir in der Lage sein, einen Docker-Container zu sehen und auszuführen. Durch den Aufruf http://localhost:9090/config/default / sollten wir auch auf die Eigenschaften zugreifen können.

Eureka-Server

In ähnlicher Weise müssen wir eine Docker-Datei für Eureka-Server erstellen, die auf Port 9091 läuft. Das Dockerfile für den Eureka-Server sollte wie folgt aussehen:

FROM alpine-jdk:base
MAINTAINER javaonfly
COPY files/MicroserviceConfigServer.jar /opt/lib/
RUN mkdir /var/lib/config-repo
COPY config-repo /var/lib/config-repo
ENTRYPOINT ["/usr/bin/java"]
CMD ["-jar", "/opt/lib/MicroserviceConfigServer.jar"]
VOLUME /var/lib/config-repo
EXPOSE 9090

Verwenden Sie diesen Befehl, um das Image zu erstellen:

docker build --file=Dockerfile-EurekaServer --tag=eureka-server:latest --rm=true .
docker run --name=eureka-server --publish=9091:9091 eureka-server:latest

Microservices

Jetzt stellen Sie unsere aktuellen Microservices bereit. Die Schritte sollten ähnlich sein; das einzige, was wir uns merken müssen ist, dass unsere Microservices vom Config-Server und Eureka-Server abhängen. Daher müssen wir sicherstellen, dass die beide Server betriebsbereit sind, bevor wir mit unsere Microservices starten. Da es Abhängigkeiten zwischen Containern gibt, sollten wir uns Docker-Compose etwas genauer anschauen. So stellen wir sicher, dass die Container in einer bestimmten Reihenfolge erzeugt werden.

Um das zu tun, sollten wir eine Docker-File für den Rest der Container schreiben. So sieht das entsprechende Docker-File aus:

Dockerfile-EmployeeSearch. 
================================
FROM alpine-jdk:base
MAINTAINER javaonfly
RUN apk --no-cache add netcat-openbsd
COPY files/EmployeeSearchService.jar /opt/lib/
COPY EmployeeSearch-entrypoint.sh /opt/bin/EmployeeSearch-entrypoint.sh
RUN chmod 755 /opt/bin/EmployeeSearch-entrypoint.sh
EXPOSE 8080

Dockerfile-EmployeeDashboard
====================================
FROM alpine-jdk:base
MAINTAINER javaonfly
RUN apk --no-cache add netcat-openbsd
COPY files/EmployeeDashBoardService.jar /opt/lib/
COPY EmployeeDashBoard-entrypoint.sh /opt/bin/EmployeeDashBoard-entrypoint.sh
RUN chmod 755 /opt/bin/EmployeeDashBoard-entrypoint.sh
EXPOSE 8080

Dockerfile-ZuulServer
=========================================
FROM alpine-jdk:base
MAINTAINER javaonfly
COPY files/EmployeeZuulService.jar /opt/lib/
ENTRYPOINT ["/usr/bin/java"]
CMD ["-jar", 

Ein kleiner Hinweis: Die beiden Shell-Skripts für den Mitarbeiter-Suchdienst und den Mitarbeiter-Dashboard-Dienst, die ich hier erstellt habe, weisen Docker-Compose an, die Dienste erst dann zu starten, nachdem der Konfigurationsserver und der Eureka-Server gestartet wurden.

Employee dashBoard Script
==================================
#!/bin/sh

while ! nc -z config-server 9090 ; do
    echo "Waiting for the Config Server"
    sleep 3
done
while ! nc -z eureka-server 9091 ; do
    echo "Waiting for the Eureka Server"
    sleep 3
done

java -jar /opt/lib/EmployeeDashBoardService.jar
==================================
Employee service Script
==================================
#!/bin/sh

while ! nc -z config-server 9090 ; do
    echo "Waiting for the Config Server"
    sleep 3
done
while ! nc -z eureka-server 9091 ; do
    echo "Waiting for the Eureka Server"
    sleep 3
done

java -jar /opt/lib/EmployeeSearchService.jar

Nun erstellen wir noch zügig eine Datei namens docker-compose.yml, die all diese Docker-Dateien verwendet, um unsere benötigten Umgebungen zu erstellen. Sie stellt auch sicher, dass die erforderlichen Container die korrekte Reihenfolge beibehalten und miteinander verknüpfen.

version: '2.2'
services:
    config-server:
        container_name: config-server
        build:
            context: .
            dockerfile: Dockerfile-configserver
        image: config-server:latest
        expose:
            - 9090
        ports:
            - 9090:9090
        networks:
            - emp-network
        volumes:
            - config-repo:/var/lib/config-repo

    eureka-server:
        container_name: eureka-server
        build:
            context: .
            dockerfile: Dockerfile-EurekaServer
        image: eureka-server:latest
        expose:
            - 9091
        ports:
            - 9091:9091
        networks:
            - emp-network

    EmployeeSearchService:
        container_name: EmployeeSearch
        build:
            context: .
            dockerfile: Dockerfile-EmployeeSearch
        image: employeesearch:latest
        environment:
            SPRING_APPLICATION_JSON: '{"spring": {"cloud": {"config": {"uri": "http://config-server:9090"}}}}'

        entrypoint: /opt/bin/EmployeeSearch-entrypoint.sh
        expose:
            - 8080
        ports:
            - 8080:8080
        networks:
            - emp-network
        links:
            - config-server:config-server
            - eureka-server:eureka-server
        depends_on:
            - config-server
            - eureka-server
        logging:
            driver: json-file
    EmployeeDashboardService:
        container_name: EmployeeDashboard
        build:
            context: .
            dockerfile: Dockerfile-EmployeeDashboard
        image: employeedashboard:latest
        environment:
            SPRING_APPLICATION_JSON: '{"spring": {"cloud": {"config": {"uri": "http://config-server:9090"}}}}'

        entrypoint: /opt/bin/EmployeeDashBoard-entrypoint.sh
        expose:
            - 8081
        ports:
            - 8081:8081
        networks:
            - emp-network
        links:
            - config-server:config-server
            - eureka-server:eureka-server
        depends_on:
            - config-server
            - eureka-server
        logging:
            driver: json-file
    ZuulServer:
        container_name: ZuulServer
        build:
            context: .
            dockerfile: Dockerfile-ZuulServer
        image: zuulserver:latest
        expose:
            - 8084
        ports:
            - 8084:8084
        networks:
            - emp-network
        links:
            - eureka-server:eureka-server
        depends_on:
            - eureka-server
        logging:
            driver: json-file
networks:
    emp-network:
        driver: bridge
volumes:
    config-repo:
        external: true

In der Docker-Compose-Datei finden Sie einige wichtige Einträge:

Version: Ein Pflichtfeld, in dem wir die Version des Docker-Compose-Formats pflegen müssen.

Services: Jeder Eintrag definiert den Container, den wir erzeugen müssen.

Build: Wenn erwähnt, sollte Docker-Compose ein Image aus der angegebenen Docker-Datei erstellen.

Image: Der Name des Images, das erstellt wird.

Netzwerk: Der Name des zu verwendenden Netzwerks. Dieser Name sollte im Bereich Netzwerke vorhanden sein.

Links: Dadurch wird eine interne Verbindung zwischen dem Dienst und dem erwähnten Dienst hergestellt. Hier muss der EmployeeSearch-Dienst auf den Config- und Eureka-Server zugreifen.

Depends: Dies wird benötigt, um die Reihenfolge zu erhalten. Der EmployeeSearch-Container hängt vom Eureka- und Konfigurationsserver ab. Daher stellt Docker sicher, dass die Container Eureka- und Config-Server erzeugt werden, bevor der EmployeeSearch-Container erzeugt wird.

Nach dem Erstellen der Datei, sollten wir unsere Images und die erforderlichen Container erstellen. Dies können wir mit einem einzigen Befehl bewerkstelligen:

docker-compose up --build

Die komplette Umgebung können wir mit folgendem Befehl stoppen:

docker-compose down

Die vollständige Dokumentation zu Docker-Compose finden Sie unter https://docs.docker.com/compose/.

Festzuhalten bleibt, dass das Schreiben der Docker-Datei und der Docker-Compose-Datei einmalige Aktivitäten sind, die Sie aber in die Lage versetzt, jederzeit bei Bedarf eine vollständige Umgebung zu erstellen.

Fazit

Dies ist die vollständige Anleitung zum Erstellen verschiedener Komponenten in Microservices und zum Bereitstellen in Docker. In der Produktion sollte CI /CD beteiligt sein, so dass Sie nicht alle Docker-Befehle zum Erstellen von Images kennen müssen. Aus meiner Sicht macht es als Full Stack Entwickler aber durchaus Sinn zu wissen, wie Sie ein Image in Docker erzeugen und bereitstellen können.

 

Hinweise:

Der Beitrag von Shamik Mitra ist im Original unter https://dzone.com/articles/buiding-microservice-using-spring-boot-and-docker erschienen. Dort finden Sie auch die verschiedenen Code-Schnipsel als Kopiervorlage.

Mit Zustimmung von Shamik Mitra übersetzen wir verschiedene Beiträge von ihm hier in unserem Blog vom Englischen ins Deutsche.

Shamik Mitra
Shamik Mitra

Shamik Mitra ist ein selbsternannter Java Maniac. Er arbeitet als technischer Projektmanager bei Tata Consultancy Services (TCS), und war zuvor bei Cognizant als Architekt und bei IBM als technischer Leiter aktiv. Er ist der Most Valuable Blogger bei DZone und Techathon Coding Competition Jury Award Winner. Er veröffentlicht regelmäßig Tutorials bei A4Academics und er ist technischer Reviewer bei PACKT Publication. Auf vielen dieser Plattformen teilt er sein Wissen zu Java, Java EE, Hibernate, Spring, Entwurfsmuster, Micro-Service, Bigdata und Agile.