Spring CloudにおけるサービスディスカバリーとConsulの実装ガイド

Consulの概要と分散システムにおける役割

ConsulはHashiCorp社によって開発されたオープンソースの分散型サービスレジストリおよび構成管理ツールです。マイクロサービスアーキテクチャにおいて、動的なエンドポイント発見やノードの死活監視、キー・バリュー形式の設定保存を一元的に提供します。Go言語で記述されているためクロスプラットフォーム対応が容易であり、単一の実行ファイルのみでデプロイが完結するため、コンテナ環境や軽量ランタイムとの親和性が高い設計となっています。

アーキテクチャと主要機能

  • コンセンサスアルゴリズム: Raftプロトコルを採用しており、Paxosに比べ状態管理の挙動が直感的でクラスタの安定性が高い。
  • マルチデータセンター: 地理的に分散した環境でのレプリケーションをサポートし、単一拠点の障害がシステム全体へ波及するリスクを低減します。
  • ヘルスチェック: HTTPステータス、TCP接続、実行スクリプトなど多様な死活監視メカニズムを標準搭載。
  • プロトコル対応: RESTful HTTP APIおよびDNSインターフェースの両方を利用可能。
  • Web UI: クラスタの状態や登録済みサービスを可視化する管理ダッシュボードを提供。

ノードの構成要素

Consulクラスタは主に2種類のエージェントで構成されます。

  • Clientノード: ステートレスな転送エージェント。ローカルアプリケーションからのHTTP/DNSリクエストをサーバー層へルーティングします。
  • Serverノード: クラスタの状態メタデータを永続化し、リーダー選出を通じて高可用性を維持します。各データセンターあたり3台または5台(奇数構成)の配置が強く推奨されます。

サービス登録と発見のフロー

アプリケーションが起動すると、ローカルエージェントに対して自身のIPアドレス、ポート番号、ヘルスチェック経路を登録します。Consulサーバーはデフォルトで10秒間隔でヘルスチェックリクエストを送信し、有効なインスタンスのリストを最新化します。コンシューマー側が通信を行う際、まずConsulのAPIまたはDNSからルーティングテーブルを取得し、有効なノードのみを対象にリクエストを転送します。このテーブルはヘルスステータスの変動に応じて自動的に更新されます。

Consulの実行環境構築

公式配布ページから環境に適合するバイナリをダウンロード後、ターミナルから以下のコマンドで開発用モードを起動します。プロセスが正常に立ち上がると、http://localhost:8500へアクセスし管理画面が表示されます。

consul agent -dev

Spring Cloud連携:プロバイダーの実装

Spring BootプロジェクトにConsul連携用の依存関係を追加します。Spring Cloudのディスカバリークライアントおよび監視用アクチュエーターを組み込みます。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
    </parent>
    <groupId>io.architecture.demo</groupId>
    <artifactId>consul-provider-node</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.7</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</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-actuator</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>

設定ファイル(application.yml)では、Consulのエンドポイントとサービス識別子を定義します。ヘルスチェック間隔は明示的に指定可能です。

server:
  port: 9080

spring:
  application:
    name: inventory-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: inventory-service
        health-check-path: /actuator/health
        health-check-interval: 10s

アプリケーションエントリポイントに@EnableDiscoveryClientを適用し、RESTエンドポイントを作成します。

package io.architecture.demo;

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

@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
}
package io.architecture.demo.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ItemEndpoint {

    @GetMapping("/api/v1/items/lookup")
    public String retrieveStatus(@RequestParam("sku") String productCode) {
        return String.format("Inventory Node Response: %s", productCode);
    }
}

Spring Cloud連携:コンシューマーの実装

クライアント側ではOpenFeignを利用してサービス間通信を宣言的に実装します。Maven設定にFeignの依存関係を追加してください。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

コンシューマーはレジストリへ自身を登録しないため、設定ファイルで登録フラグを無効化します。

server:
  port: 8105
spring:
  application:
    name: client-orchestrator
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        register: false

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000

メインクラスに@EnableFeignClientsを付与し、Feignクライアントインターフェースを定義します。

package io.architecture.demo.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}
package io.architecture.demo.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "inventory-service")
public interface ServiceGateway {

    @GetMapping("/api/v1/items/lookup")
    String fetchItemStatus(@RequestParam("sku") String targetCode);
}

呼び出し側のコントローラーでは、インジェクションされたゲートウェイインターフェースを通じてリモートリソースにアクセスします。

package io.architecture.demo.web;

import io.architecture.demo.client.ServiceGateway;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RequestRouter {

    private final ServiceGateway serviceClient;

    public RequestRouter(ServiceGateway serviceClient) {
        this.serviceClient = serviceClient;
    }

    @GetMapping("/proxy/inventory")
    public String forwardLookup(@RequestParam("code") String productId) {
        return serviceClient.fetchItemStatus(productId);
    }
}

複数インスタンスの起動とロードバランシング検証

クライアントサイドの分散トラフィック処理を確認するため、プロバイダーのポート設定を変更し(例:server.port=9081)、レスポンス識別子を改変した2番目のインスタンスを起動します。Consulは両ノードのヘルスチェックを通過後、自動的にインスタンスプールへ追加します。

クライアントの/proxy/inventory?code=ABC-001エンドポイントへ連続してリクエストを送信すると、Spring Cloudのデフォルトラウンドロビン機構により、トラフィックが9080ポートと9081ポートのインスタンスへ交互に振り分けられます。この挙動により、Consulを基盤とした動的なサービスディスカバリーと自動負荷分散パイプラインが正常に機能していることが検証できます。

タグ: Consul Spring-Cloud spring-boot OpenFeign service-discovery

6月9日 16:12 投稿