[A-00208] Architecture入門(MicroService)

システムアーキテクチャの入門用記事になります。

とりあえずマイクロサービスの作り方について勉強したいと思います。

・簡単なマイクロサービスを作ってみる

以下のアーキテクチャをイメージしてます。

・Department Serviceの作成

最初にDBと直接やりとりするDepartment Serviceを作成したいと思います。

mysqlに事前にdepartment_dbを作成しておきます。

CREATE DATABASE department_db;

ディレクトリ構成は下記の通りです。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example.ms</groupId>
	<artifactId>department-svc</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>department-svc</name>
	<description>Department Service</description>
	<properties>
		<java.version>21</java.version>
		<mysql.connector.version>8.4.0</mysql.connector.version>
		<lombok.version>1.18.34</lombok.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</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>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<version>${mysql.connector.version}</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>${lombok.version}</version>
			<scope>provided</scope>
		</dependency>			
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
package com.example.ms.department.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "departments")
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Department {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String departmentName;
    private String departmentAddress;
    private String departmentCode;
    
}
package com.example.ms.department.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.ms.department.entity.Department;

public interface DepartmentRepository extends JpaRepository<Department, Long> {
}
package com.example.ms.department;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DepartmentSvcApplication {

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

}
package com.example.ms.department.service;

import com.example.ms.department.entity.Department;

public interface DepartmentService {
    Department saveDepartment(Department department);
    Department getDepartmentById(Long departmentId);
}
package com.example.ms.department.service.impl;

import org.springframework.stereotype.Service;

import com.example.ms.department.entity.Department;
import com.example.ms.department.repository.DepartmentRepository;
import com.example.ms.department.service.DepartmentService;

import lombok.AllArgsConstructor;

@Service
@AllArgsConstructor
public class DepartmentServiceImpl implements DepartmentService {

    private final DepartmentRepository departmentRepository;

    @Override
    public Department saveDepartment(Department department) {
        return this.departmentRepository.save(department);
    }

    @Override
    public Department getDepartmentById(Long departmentId) {
        return this.departmentRepository.findById(departmentId).get();
    }

}
package com.example.ms.department.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.ms.department.entity.Department;
import com.example.ms.department.service.DepartmentService;

import lombok.AllArgsConstructor;

@RestController
@RequestMapping("api/departments")
@AllArgsConstructor
public class DepartmentController {

    private final DepartmentService departmentService;

    @PostMapping
    public ResponseEntity<Department> saveDepartment(@RequestBody Department department) {
        Department savedDepartment = this.departmentService.saveDepartment(department);
        return new ResponseEntity<>(savedDepartment, HttpStatus.CREATED);
    }

    @GetMapping("{id}")
    public ResponseEntity<Department> getDepartmentById(@PathVariable("id") Long departmentId) {
        Department department = this.departmentService.getDepartmentById(departmentId);
        return ResponseEntity.ok(department);
    }

}
spring.application.name=department-svc
# mysql connection
spring.datasource.url=jdbc:mysql://localhost:3306/department_db
spring.datasource.username=root
spring.datasource.password=password

# jpa
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update

 上記を作成したら下記のコマンドで実行します。

mvn clean install
java -jar target/department-svc-0.0.1-SNAPSHOT.jar

検証はpostmanで行います。

試しにGETメソッドでデータを取得しました。問題なく動くことが確認できました。

・Docker containerで動かしてみる

作成したdepartment-serviceをdocker上で動かしてみたいと思います。オンプレミスホストにあるMysqlにコンテナにあるspringbootが接続しにいくイメージです。

dockerコンテナ内部から外部のホストにインストールされたmysqlに接続するため、下記の書き換えを行います。

spring.application.name=department-svc
# mysql connection
#spring.datasource.url=jdbc:mysql://localhost:3306/department_db
spring.datasource.url=jdbc:mysql://host.docker.internal:3306/department_db
spring.datasource.username=root
spring.datasource.password=password

# jpa
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update

下記コマンドにてイメージ作成、コンテナ実行します。

FROM openjdk:21

WORKDIR /usr/src/myapp

COPY target/department-svc-0.0.1-SNAPSHOT.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java","-jar","app.jar"]
docker build -t department-svc .
docker run -it --detach --publish 8080:8080  --name run-department-svc department-svc

不要になったら下記のコマンドでコンテナを停止します。

docker stop run-deparment-svc

・user-serviceを作成する

次はdepartment-serviceの対になるuser-serviceを作成します。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example.ms</groupId>
	<artifactId>user-svc</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>user-svc</name>
	<description>User Service</description>
	<properties>
		<java.version>21</java.version>
		<mysql.connector.version>8.4.0</mysql.connector.version>
		<lombok.version>1.18.34</lombok.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</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-webflux</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>${lombok.version}</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<version>${mysql.connector.version}</version>
		</dependency>	
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
package com.example.ms.user.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    @Column(nullable=false, unique=true)
    private String email;
    private String departmentId;

}
package com.example.ms.user.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {

    private Long id;
    private String firstName;
    private String lastName;
    private String email;
}
package com.example.ms.user.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DepartmentDto {

    private Long id;
    private String departmentName;
    private String departmentAddress;
    private String departmentCode;
}
package com.example.ms.user.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseDto {

    private DepartmentDto departmentDto;
    private UserDto userDto;
}
package com.example.ms.user.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.ms.user.entity.User;

public interface UserRepository extends JpaRepository<User, Long> {
}
package com.example.ms.user.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class BeanConfigurations {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public WebClient webClient() {
        return  WebClient.builder().build();
    }
}
package com.example.ms.user.service;

import com.example.ms.user.dto.ResponseDto;
import com.example.ms.user.entity.User;

public interface UserService {

    User saveUser(User user);
    ResponseDto getUser(Long userId);
}
package com.example.ms.user.service.impl;

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import com.example.ms.user.dto.DepartmentDto;
import com.example.ms.user.dto.ResponseDto;
import com.example.ms.user.dto.UserDto;
import com.example.ms.user.entity.User;
import com.example.ms.user.repository.UserRepository;
import com.example.ms.user.service.UserService;

import lombok.AllArgsConstructor;

@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    //private final RestTemplate restTemplate;
    private final WebClient webClient;

    @Override
    public User saveUser(User user) {
        return this.userRepository.save(user);
    }

    @Override
    public ResponseDto getUser(Long userId) {

        ResponseDto responseDto = new ResponseDto();
        User user = this.userRepository.findById(userId).get();
        UserDto userDto = mapToUser(user);

        // ResponseEntity<DepartmentDto> responseEntity = this.restTemplate
        // .getForEntity("http://localhost:8080/api/departments/" + user.getDepartmentId() 
        // , DepartmentDto.class);
        DepartmentDto departmentDto = this.webClient.get()
        .uri("http://localhost:8080/api/departments/" + user.getDepartmentId())
        .retrieve().bodyToMono(DepartmentDto.class).block();

        // DepartmentDto departmentDto = responseEntity.getBody();

        // System.out.println(responseEntity.getStatusCode());

        responseDto.setUserDto(userDto);
        responseDto.setDepartmentDto(departmentDto);
        
        return responseDto;
    }

    private UserDto mapToUser(User user) {
        UserDto userDto = new UserDto();
        userDto.setId(user.getId());
        userDto.setFirstName(user.getFirstName());
        userDto.setLastName(user.getLastName());
        userDto.setEmail(user.getEmail());

        return userDto;
    }

}
package com.example.ms.user.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.ms.user.dto.ResponseDto;
import com.example.ms.user.entity.User;
import com.example.ms.user.service.UserService;

import lombok.AllArgsConstructor;

@RestController
@RequestMapping("api/users")
@AllArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping
    public ResponseEntity<User> saveUser(@RequestBody User user) {
        User savedUser = this.userService.saveUser(user);
        return new ResponseEntity<>(savedUser, HttpStatus.CREATED);
    }

    @GetMapping("{id}")
    public ResponseEntity<ResponseDto> getUser(@PathVariable("id") Long userId) {
        ResponseDto responseDto = this.userService.getUser(userId);
        return ResponseEntity.ok(responseDto);
    }
}
package com.example.ms.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserSvcApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserSvcApplication.class, args);
	}
}
spring.application.name=user-svc
# mysql
spring.datasource.url=jdbc:mysql://localhost:3306/employee_db
spring.datasource.username=root
spring.datasource.password=password
# jpa
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update
# server port
server.port=8081

上記が作成できたら下記のコマンドにて動作検証します。Department-serviceも起動しておいてください。

mvn clean install -DskipTests=true
java -jar target/user-svc-0.0.1-SNAPSHOT.jar

GETメソッドの動作検証にてDepartmentServiceと連携確認します。

・Dockerコンテナにて動かしてみる

user-serviceもdockerコンテナにて実行してみます。

イメージは下記の通りです。

まずuser-svcのimageを作成します。

application.propertiesの書き換え

spring.application.name=user-svc
# mysql
#spring.datasource.url=jdbc:mysql://localhost:3306/employee_db
spring.datasource.url=jdbc:mysql://host.docker.internal:3306/employee_db
spring.datasource.username=root
spring.datasource.password=password
# jpa
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update
# server port
server.port=8081

Dockerfileを作成します。

FROM openjdk:21

WORKDIR /usr/src/myapp

COPY target/user-svc-0.0.1-SNAPSHOT.jar app.jar

EXPOSE 8081

ENTRYPOINT ["java", "-jar", "app.jar"]
docker build -t user-svc .

とりあえず動くか確かめます。

docker run -it --detach --publish 8081:8081  --name run-user-svc user-svc

department-serviceをコンテナ化したため、うまく通信ができずにエラーとなります。

なのでdockerネットワークの構成を変更しようと思います。docker-compose.ymlを作成します。

version: '3'
services:
  app1:
    image: department-svc
    container_name: "dep-app"
    ports:
      - "8080:8080"
    networks:
      user-department-svc-network:
        ipv4_address: 172.18.0.2
  app2:
    image: user-svc
    container_name: "usr-app"
    ports:
      - "8081:8081"
    networks:
      user-department-svc-network:
        ipv4_address: 172.18.0.3
networks:
  user-department-svc-network:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.18.0.0/24

docker内部ネットワークのipv4アドレスを設定します。

次にdepartment-serviceにリクエストするuser-serviceのリクエスト先をlocalhostから適切なipv4アドレスに変更します。

package com.example.ms.user.service.impl;

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import com.example.ms.user.dto.DepartmentDto;
import com.example.ms.user.dto.ResponseDto;
import com.example.ms.user.dto.UserDto;
import com.example.ms.user.entity.User;
import com.example.ms.user.repository.UserRepository;
import com.example.ms.user.service.UserService;

import lombok.AllArgsConstructor;

@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    //private final RestTemplate restTemplate;
    private final WebClient webClient;

    @Override
    public User saveUser(User user) {
        return this.userRepository.save(user);
    }

    @Override
    public ResponseDto getUser(Long userId) {

        ResponseDto responseDto = new ResponseDto();
        User user = this.userRepository.findById(userId).get();
        UserDto userDto = mapToUser(user);

        // ResponseEntity<DepartmentDto> responseEntity = this.restTemplate
        // .getForEntity("http://localhost:8080/api/departments/" + user.getDepartmentId() 
        // , DepartmentDto.class);
        DepartmentDto departmentDto = this.webClient.get()
        
        //.uri("http://localhost:8080/api/departments/" + user.getDepartmentId())
        .uri("http://172.18.0.2:8080/api/departments/" + user.getDepartmentId())
        .retrieve().bodyToMono(DepartmentDto.class).block();

        // DepartmentDto departmentDto = responseEntity.getBody();

        // System.out.println(responseEntity.getStatusCode());

        responseDto.setUserDto(userDto);
        responseDto.setDepartmentDto(departmentDto);
        
        return responseDto;
    }

    private UserDto mapToUser(User user) {
        UserDto userDto = new UserDto();
        userDto.setId(user.getId());
        userDto.setFirstName(user.getFirstName());
        userDto.setLastName(user.getLastName());
        userDto.setEmail(user.getEmail());

        return userDto;
    }

}

上記を作成して下記のコマンドを実行します。

docker-compose build
docker-compose run

postmanで実行するとレスポンスが返ってきます。

・Kubernetesのpod内でマイクロサービスを作成してみる

次にkubernetesで、pod内に上記のネットワークを構築してみたいと思います。

・Appendix

参考文献はこちら

https://medium.com/@diegogauna.developer/restful-api-using-typescript-and-react-hooks-3d99bdd0cd39

https://medium.com/@diegogauna.developer/creating-a-single-page-app-spa-in-react-using-react-router-db37b89b3f73

https://qiita.com/s-yoshi210/items/94812f7b378a942fd9c5

https://qiita.com/kooohei/items/0e788a2ce8c30f9dba53

https://qiita.com/studio_meowtoon/items/ef38ba2d5e913e343380

https://qiita.com/takusonix/items/c93e18b62b0adf038967

https://docs.docker.jp/engine/reference/run.html#foreground

https://aws.amazon.com/jp/builders-flash/202108/create-container-image

https://qiita.com/leafeon00000/items/e190cf92af3a487cc749

https://qiita.com/yanbou893/items/a46b37f002df8529c99c

https://qiita.com/xanadou/items/3abd3d28214dea526084

https://qiita.com/yusuke_mrmt/items/e05d7914065824384a6b

https://stackoverflow.com/questions/71637192/docker-compose-up-error-network-mode-and-networks-cannot-be-combined

https://stackoverflow.com/questions/56582446/how-to-use-host-network-for-docker-compose

https://yoo-s.com/topic/detail/858

https://knowledge.sakura.ad.jp/23899

https://qiita.com/hoshino/items/9545d255cc0103b3d296

https://medium.com/m0blog/docker-run-%E3%83%90%E3%83%83%E3%82%AF%E3%82%B0%E3%83%A9%E3%82%A6%E3%83%B3%E3%83%89%E3%81%A7%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B-%E5%90%8D%E5%89%8D%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B-5799b063f116

https://qiita.com/saitoshi/items/a931399e81e63e8e4f1e

https://qiita.com/studio_meowtoon/items/9c07e20b4124d8c5f972

https://future-architect.github.io/articles/20240620a

https://www.trendmicro.com/ja_jp/business/tech_blog/c1_container_security_221107_01.html

https://zenn.dev/tns_00/articles/docker-communicate-with-containers

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

*