マイクロサービスアーキテクチャ設計完全ガイド:実装から運用まで|Docker・Kubernetes・Spring Boot対応

 

はじめに

マイクロサービスアーキテクチャは、大規模なアプリケーションを小さな独立したサービスに分割する設計手法です。Netflix、Amazon、Uberなどの大手企業が採用し、スケーラビリティと開発効率の向上を実現しています。

この記事では、マイクロサービスアーキテクチャの設計原則から実装方法、運用のベストプラクティスまで、実践的なコード例とともに詳しく解説します。

マイクロサービスアーキテクチャとは

マイクロサービスアーキテクチャは、アプリケーションを複数の小さな独立したサービスに分解し、各サービスが特定のビジネス機能を担当する設計パターンです。

マイクロサービスとモノリスの比較

特徴モノリスマイクロサービス
デプロイ単一のアーティファクトサービス個別デプロイ
スケーリング全体スケールサービス個別スケール
技術選択統一技術スタックサービス別技術選択
開発チーム大規模チーム小規模チーム
複雑さ単純分散システムの複雑さ

1. サービス分割戦略

ドメイン駆動設計による分割

// User Service
@RestController
@RequestMapping("/api/users")
public class UserController {
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable String id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
        User user = userService.createUser(request);
        return ResponseEntity.status(201).body(user);
    }
}

サービス境界の定義

// Order Service
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    @Autowired
    private OrderService orderService;
    
    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
        Order order = orderService.createOrder(request);
        return ResponseEntity.status(201).body(order);
    }
    
    @GetMapping("/user/{userId}")
    public ResponseEntity<List<Order>> getUserOrders(@PathVariable String userId) {
        List<Order> orders = orderService.findByUserId(userId);
        return ResponseEntity.ok(orders);
    }
}

2. サービス間通信

同期通信(HTTP/REST)

@Component
public class UserServiceClient {
    private final RestTemplate restTemplate;
    private final String userServiceUrl;
    
    public UserServiceClient(RestTemplate restTemplate, 
                           @Value("${services.user.url}") String userServiceUrl) {
        this.restTemplate = restTemplate;
        this.userServiceUrl = userServiceUrl;
    }
    
    public User getUserById(String userId) {
        return restTemplate.getForObject(
            userServiceUrl + "/api/users/" + userId, 
            User.class
        );
    }
}

非同期通信(メッセージング)

// RabbitMQ Producer
@Component
public class OrderEventPublisher {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void publishOrderCreated(OrderCreatedEvent event) {
        rabbitTemplate.convertAndSend("order.exchange", "order.created", event);
    }
}

// RabbitMQ Consumer
@RabbitListener(queues = "inventory.order.queue")
public void handleOrderCreated(OrderCreatedEvent event) {
    inventoryService.reserveItems(event.getOrderId(), event.getItems());
}

3. API Gateway設計

Spring Cloud Gateway実装

@Configuration
public class GatewayConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service", r -> r.path("/api/users/**")
                .uri("lb://user-service"))
            .route("order-service", r -> r.path("/api/orders/**")
                .uri("lb://order-service"))
            .route("inventory-service", r -> r.path("/api/inventory/**")
                .uri("lb://inventory-service"))
            .build();
    }
}

レート制限とセキュリティ

@Component
public class RateLimitFilter implements GatewayFilter {
    private final RedisRateLimiter rateLimiter;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return rateLimiter.isAllowed("api", getUserId(exchange))
            .flatMap(response -> {
                if (response.isAllowed()) {
                    return chain.filter(exchange);
                } else {
                    exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
                    return exchange.getResponse().setComplete();
                }
            });
    }
}

4. サービスディスカバリ

Eureka Server設定

# eureka-server application.yml
server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

Eureka Client設定

@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}
# user-service application.yml
spring:
  application:
    name: user-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

5. 設定管理

Spring Cloud Config Server

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

分散設定管理

# config-server application.yml
server:
  port: 8888

spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/your-org/config-repo
          search-paths: config

サービス固有設定

# user-service.yml (Git Repository)
server:
  port: 8081

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/userdb
    username: ${DB_USERNAME:user}
    password: ${DB_PASSWORD:password}

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics

6. 回復力パターン

Circuit Breaker実装

@Component
public class UserServiceClient {
    
    @CircuitBreaker(name = "user-service", fallbackMethod = "getUserFallback")
    @TimeLimiter(name = "user-service")
    public CompletableFuture<User> getUserById(String userId) {
        return CompletableFuture.supplyAsync(() -> 
            restTemplate.getForObject("/api/users/" + userId, User.class)
        );
    }
    
    public CompletableFuture<User> getUserFallback(String userId, Exception ex) {
        User fallbackUser = new User();
        fallbackUser.setId(userId);
        fallbackUser.setName("Unknown User");
        return CompletableFuture.completedFuture(fallbackUser);
    }
}

Retry機能

@Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public Order createOrder(CreateOrderRequest request) {
    return orderRepository.save(new Order(request));
}

@Recover
public Order recover(Exception ex, CreateOrderRequest request) {
    logger.error("Failed to create order after retries: {}", ex.getMessage());
    throw new OrderCreationException("Unable to create order");
}

7. データ管理戦略

Database per Service

// User Service - MySQL
@Entity
public class User {
    @Id
    private String id;
    private String name;
    private String email;
    // getters, setters
}

// Order Service - PostgreSQL
@Entity
public class Order {
    @Id
    private String id;
    private String userId;
    private BigDecimal amount;
    private LocalDateTime createdAt;
    // getters, setters
}

イベントソーシング

@Component
public class OrderEventStore {
    
    public void saveEvent(String aggregateId, DomainEvent event) {
        EventRecord record = new EventRecord();
        record.setAggregateId(aggregateId);
        record.setEventType(event.getClass().getSimpleName());
        record.setEventData(JsonUtils.toJson(event));
        record.setTimestamp(Instant.now());
        
        eventRepository.save(record);
        eventPublisher.publish(event);
    }
    
    public List<DomainEvent> getEvents(String aggregateId) {
        return eventRepository.findByAggregateIdOrderByTimestamp(aggregateId)
            .stream()
            .map(this::deserializeEvent)
            .collect(Collectors.toList());
    }
}

8. コンテナ化とオーケストレーション

Dockerfile

FROM openjdk:11-jre-slim

COPY target/user-service-1.0.0.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app.jar"]

Docker Compose

version: '3.8'
services:
  eureka-server:
    image: eureka-server:latest
    ports:
      - "8761:8761"
    
  user-service:
    image: user-service:latest
    ports:
      - "8081:8080"
    environment:
      - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://eureka-server:8761/eureka
    depends_on:
      - eureka-server
      - mysql
  
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: userdb
    ports:
      - "3306:3306"

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: user-service:1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: DB_HOST
          value: mysql-service
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

9. 監視とロギング

分散トレーシング

@RestController
public class OrderController {
    
    @Autowired
    private UserServiceClient userServiceClient;
    
    @NewSpan("create-order")
    @PostMapping("/orders")
    public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
        // ユーザー情報取得(別サービス呼び出し)
        User user = userServiceClient.getUserById(request.getUserId());
        
        // 注文作成処理
        Order order = orderService.createOrder(request, user);
        
        return ResponseEntity.status(201).body(order);
    }
}

集約ログ管理

# logback-spring.xml
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp/>
                <logLevel/>
                <loggerName/>
                <message/>
                <mdc/>
                <sleuthInfo/>
            </providers>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

メトリクス収集

@Component
public class OrderMetrics {
    private final Counter orderCounter;
    private final Timer orderProcessingTime;
    
    public OrderMetrics(MeterRegistry meterRegistry) {
        this.orderCounter = Counter.builder("orders.created")
            .description("Number of orders created")
            .register(meterRegistry);
            
        this.orderProcessingTime = Timer.builder("order.processing.time")
            .description("Order processing duration")
            .register(meterRegistry);
    }
    
    public void recordOrderCreated() {
        orderCounter.increment();
    }
    
    public Timer.Sample startProcessingTimer() {
        return Timer.start(orderProcessingTime);
    }
}

10. セキュリティ

JWT認証

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) throws ServletException, IOException {
        
        String token = extractToken(request);
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication auth = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        
        filterChain.doFilter(request, response);
    }
}

OAuth 2.0 / OpenID Connect

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth-server.example.com
          jwk-set-uri: https://auth-server.example.com/.well-known/jwks.json

11. テスト戦略

単体テスト

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    
    @Mock
    private OrderRepository orderRepository;
    
    @Mock
    private UserServiceClient userServiceClient;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    void createOrder_ValidRequest_ReturnsOrder() {
        // Given
        CreateOrderRequest request = new CreateOrderRequest("user-123", 100.0);
        User user = new User("user-123", "John Doe");
        when(userServiceClient.getUserById("user-123")).thenReturn(user);
        
        // When
        Order result = orderService.createOrder(request);
        
        // Then
        assertEquals("user-123", result.getUserId());
        verify(orderRepository).save(any(Order.class));
    }
}

統合テスト

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("orderdb")
            .withUsername("test")
            .withPassword("test");
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void createOrder_IntegrationTest() {
        CreateOrderRequest request = new CreateOrderRequest("user-123", 100.0);
        
        ResponseEntity<Order> response = restTemplate.postForEntity(
            "/api/orders", request, Order.class);
        
        assertEquals(201, response.getStatusCodeValue());
        assertNotNull(response.getBody().getId());
    }
}

12. DevOps と CI/CD

Jenkins Pipeline

pipeline {
    agent any
    
    stages {
        stage('Build') {
            steps {
                sh 'mvn clean compile'
            }
        }
        
        stage('Test') {
            steps {
                sh 'mvn test'
                publishTestResults testResultsPattern: 'target/surefire-reports/*.xml'
            }
        }
        
        stage('Package') {
            steps {
                sh 'mvn package'
                archiveArtifacts artifacts: 'target/*.jar'
            }
        }
        
        stage('Docker Build') {
            steps {
                sh 'docker build -t user-service:${BUILD_NUMBER} .'
                sh 'docker push registry.example.com/user-service:${BUILD_NUMBER}'
            }
        }
        
        stage('Deploy') {
            steps {
                sh 'kubectl set image deployment/user-service user-service=registry.example.com/user-service:${BUILD_NUMBER}'
            }
        }
    }
}

マイクロサービス採用の判断基準

採用すべき場合

  • 大規模チーム: 複数チームでの並行開発
  • 異なる技術要件: サービスごとに最適な技術選択が必要
  • 独立したスケーリング: サービスごとに異なる負荷特性
  • ビジネス成長: 将来的な拡張を見据えた設計

避けるべき場合

  • 小規模チーム: 管理コストが開発効率を上回る
  • 単純なアプリケーション: 分散システムの複雑さが不要
  • 運用経験不足: 分散システムの運用スキルが不足

ベストプラクティス

1. サービス設計

  • 単一責任原則: 各サービスは一つのビジネス機能に集中
  • 疎結合: サービス間の依存を最小限に抑制
  • 高凝集: 関連する機能をサービス内に集約

2. データ管理

  • Database per Service: 各サービスが独自のデータベースを持つ
  • イベント駆動: 状態変更をイベントで通知
  • 最終的整合性: 強整合性より可用性を重視

3. 運用

  • 自動化: デプロイ、テスト、監視の自動化
  • 監視: 包括的な監視とアラート体制
  • 災害復旧: 障害時の復旧手順の整備

まとめ

マイクロサービスアーキテクチャは、適切に設計・実装されれば、スケーラビリティ、開発効率、技術選択の自由度において大きなメリットをもたらします。しかし、分散システムの複雑さやオーバーヘッドも伴うため、組織の規模や技術的成熟度を考慮して採用を判断することが重要です。

まずは小さなサービスから始めて、段階的にマイクロサービス化を進め、運用経験を蓄積しながら最適なアーキテクチャを構築していきましょう。


関連キーワード: マイクロサービス, マイクロサービスアーキテクチャ, Spring Boot マイクロサービス, Docker コンテナ, Kubernetes, サービスメッシュ, API Gateway, 分散システム, DevOps

■プロンプトだけでオリジナルアプリを開発・公開してみた!!

■AI時代の第一歩!「AI駆動開発コース」はじめました!

テックジム東京本校で先行開始。

■テックジム東京本校

「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。

<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。

<月1開催>放送作家による映像ディレクター養成講座

<オンライン無料>ゼロから始めるPython爆速講座