Zuulの役割とゲートウェイアーキテクチャ
マイクロサービス環境において、Netflix Zuulは動的ルーティング、モニタリング、セキュリティ、レジリエンスを提供するエッジサービスとして機能します。クライアント(Web/モバイル)および内部サービス間通信を含むすべてのリクエストは、まずZuulゲートウェイを経由します。これにより、認証チェック、IP制限、動的ルート変更、メトリクス収集などの横断的関心事を一元的に処理し、バックエンドサービスは純粋なビジネスロジックの実装に注力できる環境を構築します。
フィルタチェーンの動作原理
Zuulの拡張性の核はフィルタチェーンにあります。カスタムフィルタを実装する際はZuulFilter抽象クラスを継承し、以下の4つのメソッドを定義します。
filterType(): フィルタが実行されるフェーズを文字列で返します。pre(ルーティング前)、route(実際のサービス呼び出し時)、post(レスポンス生成後)、error(例外発生時)のいずれかを指定します。filterOrder(): 整数値を返すことで同一フェーズ内での実行優先度を決定します。数値が小さいほど先に評価されます。shouldFilter(): 現在のリクエストに対してフィルタを実行するかどうかをbooleanで判定します。run(): 実際の処理ロジックを記述します。RequestContextを通じてHTTPリクエスト/レスポンスの改変やメタデータ操作が可能です。
フィルタのライフサイクル
正常なリクエストフローは pre → route → post の順に進行します。いずれかの段階で例外がスローされると、制御は直ちにerrorフェーズへ遷移し、エラーハンドリング完了後にpostフェーズへ戻って最終レスポンスがクライアントに返されます。postフェーズ内で例外が発生した場合もerrorへ遷移しますが、その後は二度とpostが実行されない仕様となっている点に留意が必要です。
ロードバランシングとサーキットブレーカーの統合設定
ZuulはデフォルトでRibbonによるクライアントサイド負荷分散とHystrixによるサーキットブレーカー機能を内包しています。ただし、初期状態のタイムアウト閾値は厳格に設定されているため、ネットワーク遅延や再試行を考慮した明示的な調整が不可欠です。
zuul:
retryable: true
ribbon:
ConnectTimeout: 400
ReadTimeout: 2500
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
OkToRetryOnAllOperations: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 7000
重要な仕様として、Ribbonの総合待機時間は(ConnectTimeout + ReadTimeout)の計算式に基づき累積されるため、必ずHystrixのtimeoutInMillisecondsを下回る値に設定する必要があります。Hystrixのタイムアウトが先に発生すると、Ribbonの再試行機構が正常に機能せず中途半端なエラー状態となります。
ゲートウェイ構築の実装手順
1. 依存関係の解決
Spring Cloud Netflixスタックを利用するため、Mavenプロジェクトに以下の依存関係を追加します。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2. ルーティング機能の有効化
アプリケーションのエントリポイントとなるクラスに@EnableZuulProxyを付与します。これにより、Spring Cloudのサービスディスカバリと連携したスマートルーティングが有効化されます。
package com.example.infra.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@SpringBootApplication
public class EdgeServiceBootstrap {
public static void main(String[] args) {
SpringApplication.run(EdgeServiceBootstrap.class, args);
}
}
3. ルーティングルールの定義
application.ymlにおいて、サービスIDとパスパターンのマッピング、プレフィックス制御、除外設定を行います。
server:
port: 9090
eureka:
client:
service-url:
defaultZone: http://discovery-node:8761/eureka/
instance:
prefer-ip-address: true
spring:
application:
name: core-api-gateway
zuul:
routes:
inventory-mgmt:
path: /stock/**
service-id: warehouse-service
strip-prefix: false
ignored-services:
- legacy-adapter
- monitoring-agent
prefix: /v1
4. 認証検証フィルタの実装
特定の管理用エンドポイントへアクセスする際、HTTPヘッダー内のトークンを検証するフィルタを定義します。無効なリクエストは即時遮断されます。
package com.example.infra.gateway.security;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class TokenValidationInterceptor extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 10;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
String uri = ctx.getRequest().getRequestURI();
return uri.startsWith("/stock/admin/");
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String bearerToken = request.getHeader("Authorization");
if (bearerToken == null || !bearerToken.startsWith("Bearer ")) {
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
context.setResponseBody("{\"status\":401,\"message\":\"Authentication required\"}");
context.getResponse().setContentType("application/json");
}
return null;
}
}