Netflix Zuulは、マイクロサービスアーキテクチャにおいて境界サービス(Edge Service)として機能する動的ルーティングおよびゲートウェイコンポーネントです。主な役割として、動的ルーティング、フィルタリング、モニタリング、およびレジリエンスの確保が挙げられます。
Zuulの基盤は、リクエストのライフサイクルに応じた一連のフィルタによって構成されています。
- PREフィルタ: ターゲットサービスへルーティングされる前に実行されます。認証、ログ記録、リクエストの正規化などに利用されます。
- ROUTINGフィルタ: リクエストを実際にマイクロサービスのインスタンスへ転送する役割を担います。
- POSTフィルタ: サービス実行後に実行されます。レスポンスヘッダーの追加や、統計データの収集に使用されます。
- ERRORフィルタ: ライフサイクルのいずれかの段階でエラーが発生した際に呼び出されます。
これらのフィルタ間では、RequestContextオブジェクトを介してデータが共有されます。このコンテキストはリクエストごとに生成され、スレッドローカルで管理されます。
基本構成と依存関係
Spring BootプロジェクトでZuulを有効にするには、まず必要なライブラリをpom.xmlに追加します。
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka Client (サービスディスカバリ用) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Zuul Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
アプリケーションの起動クラスには、@EnableZuulProxyアノテーションを付与してゲートウェイ機能を有効化します。
@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
ルーティング設定
application.ymlを使用して、パスに基づいたルーティングを定義します。複数の構成パターンが存在します。
1. Eurekaを利用した負荷分散(Ribbon連携)
サービスIDを指定することで、Eurekaから取得したインスタンスに対して負荷分散を行います。
zuul:
routes:
order-service:
path: /order/**
serviceId: api-order-service
user-service:
path: /user/**
serviceId: api-user-service
2. 特定のURLへの直接マッピング
負荷分散を行わず、特定の固定アドレスへルーティングする場合はurlプロパティを使用します。
zuul:
routes:
legacy-api:
path: /legacy/**
url: http://internal-server.local:8080
3. サービスリストの手動定義
Eurekaを使用せず、Ribbonで独自のサーバリストを管理する場合の構成です。
zuul:
routes:
inventory:
path: /stock/**
serviceId: stock-v1
ribbon:
eureka:
enabled: false
stock-v1:
ribbon:
listOfServers: http://node1.internal:9001,http://node2.internal:9001
フォールバック処理の実装
サービスがダウンしている場合などにユーザーへ返すレスポンスを定義するには、FallbackProviderを実装します。
@Component
public class GatewayFallbackHandler implements FallbackProvider {
@Override
public String getRoute() {
// すべてのルートに適用する場合は "*" を返す
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.SERVICE_UNAVAILABLE;
}
@Override
public int getRawStatusCode() throws IOException {
return 503;
}
@Override
public String getStatusText() throws IOException {
return "Service Unavailable";
}
@Override
public void close() {}
@Override
public InputStream getBody() throws IOException {
String message = "{\"error\": \"Gateway: Target service is unreachable.\"}";
return new ByteArrayInputStream(message.getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
カスタムフィルタによるリクエスト制御
リクエストの検証などを行うカスタムフィルタは、ZuulFilterを継承して作成します。以下の例は、クエリパラメータに特定のキーが含まれているかを確認するロジックです。
@Component
public class RequestAuthFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre"; // ルーティング前に実行
}
@Override
public int filterOrder() {
return 1; // 実行優先順位
}
@Override
public boolean shouldFilter() {
return true; // 常に実行
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String apiKey = request.getParameter("apiKey");
if (apiKey == null || apiKey.isEmpty()) {
context.setSendZuulResponse(false); // ルーティングを中断
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
try {
context.getResponse().getWriter().write("Error: Missing API Key");
} catch (IOException e) {
// エラーログ出力などの処理
}
}
return null;
}
}