GraphQL Federation入門 — マイクロサービスのスキーマ統合
GraphQL Federationは、複数のGraphQLサービスを統合して1つの統一されたGraphQLスキーマを提供する技術です。この記事では、Apollo Federation 2を使ったマイクロサービスアーキテクチャの実装方法を解説します。
GraphQL Federationとは
GraphQL Federationは、複数の独立したGraphQLサービス(サブグラフ)を統合し、単一のGraphQL API(スーパーグラフ)として公開する仕組みです。
主なメリット:
- 関心の分離: 各チームが独立してサブグラフを開発
- 段階的な移行: 既存のモノリスから徐々に移行可能
- 型の共有: エンティティを複数のサブグラフで拡張
- スケーラビリティ: サブグラフごとに独立してスケール
アーキテクチャ概要
クライアント
↓
ゲートウェイ(Router)
↓
├── Users サブグラフ
├── Products サブグラフ
└── Reviews サブグラフ
サブグラフの構築
セットアップ
npm install @apollo/server @apollo/subgraph graphql
Users サブグラフ
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import gql from 'graphql-tag';
// スキーマ定義
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
`;
// リゾルバ
const resolvers = {
Query: {
user: (_: any, { id }: { id: string }) => {
return getUserById(id);
},
users: () => {
return getAllUsers();
},
},
User: {
__resolveReference: (reference: { id: string }) => {
return getUserById(reference.id);
},
},
};
// サブグラフサーバー
const server = new ApolloServer({
schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});
await server.start();
Products サブグラフ
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@requires"])
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
seller: User!
}
# 他のサブグラフで定義されたエンティティを参照
type User @key(fields: "id", resolvable: false) {
id: ID!
}
type Query {
product(id: ID!): Product
products: [Product!]!
}
`;
const resolvers = {
Query: {
product: (_: any, { id }: { id: string }) => {
return getProductById(id);
},
products: () => {
return getAllProducts();
},
},
Product: {
__resolveReference: (reference: { id: string }) => {
return getProductById(reference.id);
},
seller: (product: { sellerId: string }) => {
// 他のサブグラフのエンティティを参照
return { __typename: 'User', id: product.sellerId };
},
},
};
Reviews サブグラフ
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@requires"])
type Review {
id: ID!
rating: Int!
comment: String!
author: User!
product: Product!
}
# 既存のエンティティを拡張
extend type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
averageRating: Float!
}
type Query {
review(id: ID!): Review
}
`;
const resolvers = {
User: {
reviews: (user: { id: string }) => {
return getReviewsByUserId(user.id);
},
},
Product: {
reviews: (product: { id: string }) => {
return getReviewsByProductId(product.id);
},
averageRating: async (product: { id: string }) => {
const reviews = await getReviewsByProductId(product.id);
if (reviews.length === 0) return 0;
const sum = reviews.reduce((acc, r) => acc + r.rating, 0);
return sum / reviews.length;
},
},
Review: {
author: (review: { authorId: string }) => {
return { __typename: 'User', id: review.authorId };
},
product: (review: { productId: string }) => {
return { __typename: 'Product', id: review.productId };
},
},
};
ゲートウェイの構築
Apollo Routerを使用してサブグラフを統合します。
インストール
# Apollo Routerのバイナリをダウンロード
curl -sSL https://router.apollo.dev/download/nix/latest | sh
設定ファイル
# router-config.yaml
supergraph:
listen: 0.0.0.0:4000
subgraphs:
users:
routing_url: http://localhost:4001/graphql
products:
routing_url: http://localhost:4002/graphql
reviews:
routing_url: http://localhost:4003/graphql
telemetry:
tracing:
enabled: true
スーパーグラフスキーマの生成
# Rover CLIをインストール
npm install -g @apollo/rover
# スーパーグラフスキーマを生成
rover supergraph compose --config ./supergraph.yaml > supergraph-schema.graphql
# supergraph.yaml
subgraphs:
users:
routing_url: http://localhost:4001/graphql
schema:
file: ./schemas/users.graphql
products:
routing_url: http://localhost:4002/graphql
schema:
file: ./schemas/products.graphql
reviews:
routing_url: http://localhost:4003/graphql
schema:
file: ./schemas/reviews.graphql
ゲートウェイの起動
./router --config router-config.yaml --supergraph supergraph-schema.graphql
クエリの実行
クライアントから統合されたスキーマをクエリできます。
query GetProductDetails($productId: ID!) {
product(id: $productId) {
id
name
price
# Usersサブグラフから解決
seller {
id
name
email
}
# Reviewsサブグラフから解決
reviews {
id
rating
comment
author {
name
}
}
averageRating
}
}
エンティティ解決の仕組み
@key ディレクティブ
エンティティを一意に識別するフィールドを指定します。
type User @key(fields: "id") {
id: ID!
name: String!
}
__resolveReference
他のサブグラフからエンティティが参照されたときに呼ばれます。
User: {
__resolveReference: async (reference: { id: string }) => {
return await db.user.findUnique({ where: { id: reference.id } });
},
}
複合キー
複数フィールドの組み合わせをキーにできます。
type Product @key(fields: "id category") {
id: ID!
category: String!
name: String!
}
データローダーによる最適化
N+1問題を解決するためにDataLoaderを使用します。
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
const users = await db.user.findMany({
where: { id: { in: [...userIds] } },
});
// IDの順序を保つ
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id));
});
// リゾルバで使用
User: {
__resolveReference: (reference: { id: string }) => {
return userLoader.load(reference.id);
},
}
エラーハンドリング
サブグラフのエラーを適切に処理します。
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
product: async (_: any, { id }: { id: string }) => {
try {
const product = await getProductById(id);
if (!product) {
throw new GraphQLError('Product not found', {
extensions: { code: 'NOT_FOUND' },
});
}
return product;
} catch (error) {
throw new GraphQLError('Failed to fetch product', {
extensions: { code: 'INTERNAL_SERVER_ERROR' },
originalError: error,
});
}
},
},
};
認証・認可
コンテキストを通じてユーザー情報を共有します。
// ゲートウェイ
const server = new ApolloServer({
schema,
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = await verifyToken(token);
return { user };
},
});
// サブグラフのリゾルバ
const resolvers = {
Query: {
me: (_: any, __: any, context: { user: User }) => {
if (!context.user) {
throw new GraphQLError('Unauthenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return context.user;
},
},
};
モニタリング
Apollo Studioでパフォーマンスを監視できます。
import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginUsageReporting({
apiKey: process.env.APOLLO_KEY,
}),
],
});
ベストプラクティス
1. サブグラフの責任分離
各サブグラフは明確な責任を持つべきです。
- 良い: Users, Products, Orders
- 悪い: UserProducts(複数の関心事)
2. エンティティの適切な配置
エンティティの「所有者」を決めます。
# Users サブグラフ(所有者)
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
# Orders サブグラフ(拡張)
extend type User @key(fields: "id") {
orders: [Order!]!
}
3. 段階的ロールアウト
Apollo Routerの機能を使って段階的に新機能をリリースします。
# router-config.yaml
traffic_shaping:
experimental_enable_defer: true
experimental_enable_progressive_override: true
まとめ
GraphQL Federationは、大規模なGraphQL APIを構築するための強力なツールです。
主なポイント:
- サブグラフで責任を分離
- エンティティを複数のサブグラフで拡張
- DataLoaderでパフォーマンス最適化
- Apollo Routerで統合・監視
適切に設計すれば、マイクロサービスアーキテクチャでも型安全で使いやすいAPIを提供できます。