Apollo FederationとGo言語でGraphQLマイクロサービスを一から構築する
Apollo Federationとは何か
Apollo Federationは、複数のGraphQL APIを1つにまとめる技術です。マイクロサービス環境で「API統合の救世主」として機能します。
核となる3つの概念
-
Supergraph:
複数のAPIを統合した単一のGraphQL API。クライアントが実際に接続する窓口です。 -
Subgraph:
各マイクロサービスが提供する個々のGraphQL API。Supergraphの部品となります。 -
Router:
クライアントからのリクエストを受け取り、適切なSubgraphに振り分けて、結果をまとめて返す交通整理役です。
なぜApollo Federationが必要なのか
- 各チームが自分のペースで開発・デプロイ可能
- 好きな技術スタックを選択できる
- 担当ドメインのスキーマを自由に管理
- クライアントがシンプルになる
従来の方法:
# 複数回のリクエストが必要
query GetUser { user(id: "1") { id name email } }
query GetProducts { products(userId: "1") { id name price } }
Apollo Federation:
# 1回のリクエストですべて取得
query GetUserWithProducts {
user(id: "1") {
id
name
email
products {
id
name
price
}
}
}
クライアントは複数のAPIの存在を意識する必要がなく、まるで1つのAPIを使っているかのような体験を得られます。
Go言語での実装例
1. プロジェクトディレクトリの作成
まず、プロジェクトの基本構造を作成します。
mkdir apollo-federation-playground
cd apollo-federation-playground
# サービス用のディレクトリを作成
mkdir -p services/{users,products}
# 確認
tree -L 2
2. 必要なツールのインストール
Apollo Federationで必要なツールをインストールします。
# gqlgenのインスール
go install github.com/99designs/gqlgen@latest
# インストール確認
gqlgen version
# Apollo GraphQLのCLIのインストール
curl -sSL https://rover.apollo.dev/nix/latest | sh
# パスを通す
export PATH=$PATH:$HOME/.rover/bin
# インストール確認
rover --version
3. Users サービスの作成
最初のサブグラフとして、Usersサービスを作成しましょう。
3.1 Usersサービスの初期化:
cd services/users
# Go moduleの初期化
go mod init users
3.2 gqlgenの設定ファイル作成:
touch gqlgen.yml
schema:
- subgraph.graphql
exec:
filename: graph/generated/generated.go
package: generated
model:
filename: graph/generated/models.go
package: generated
resolver:
layout: follow-schema
dir: graph
package: graph
federation:
filename: graph/generated/federation.go
package: generated
3.3 GraphQLスキーマの定義:
touch subgraph.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key", "@external"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
products: [Product!]!
}
extend type Product @key(fields: "id") {
id: ID! @external
}
type Query {
user(id: ID!): User
users: [User!]!
}
3.4 GraphQLコードの生成:
gqlgen generate
3.5 リゾルバーの実装:
// service/users/graph/resolver.go
package graph
import "users/graph/generated"
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct{}
var demoUsers = []*generated.User{
{
ID: "1",
Name: "Alice Johnson",
Email: "alice@example.com",
Products: []*generated.Product{
{ID: "1"},
{ID: "2"},
},
},
{
ID: "2",
Name: "Bob Smith",
Email: "bob@example.com",
Products: []*generated.Product{
{ID: "2"},
{ID: "3"},
},
},
{
ID: "3",
Name: "Charlie Brown",
Email: "charlie@example.com",
Products: []*generated.Product{
{ID: "1"},
{ID: "3"},
},
},
}
// service/users/graph/entity.resolvers.go
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.78
import (
"context"
"fmt"
"users/graph/generated"
)
// FindUserByID is the resolver for the findUserByID field.
func (r *entityResolver) FindUserByID(ctx context.Context, id string) (*generated.User, error) {
for _, user := range demoUsers {
if user.ID == id {
return user, nil
}
}
return nil, fmt.Errorf("user not found")
}
// Entity returns generated.EntityResolver implementation.
func (r *Resolver) Entity() generated.EntityResolver { return &entityResolver{r} }
type entityResolver struct{ *Resolver }
// service/users/graph/subgraph.resolvers.go
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.78
import (
"context"
"fmt"
"users/graph/generated"
)
// User is the resolver for the user field.
func (r *queryResolver) User(ctx context.Context, id string) (*generated.User, error) {
for _, user := range demoUsers {
if user.ID == id {
return user, nil
}
}
return nil, fmt.Errorf("user not found")
}
// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*generated.User, error) {
return demoUsers, nil
}
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type queryResolver struct{ *Resolver }
// service/users/main.go
package main
import (
"log"
"net/http"
"os"
"users/graph"
"users/graph/generated"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
)
const defaultPort = "8001"
func main() {
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
srv := handler.New(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
srv.AddTransport(transport.Options{AllowedMethods: []string{"POST"}})
srv.AddTransport(transport.POST{ResponseHeaders: nil})
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv)
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
3.6 Usersサービスの動作確認:
go mod tidy
go run main.go
ブラウザで http://localhost:8001 にアクセスして、GraphQL Playgroundでクエリを試してみると、設定していたデモデータが返されます。


4. Products サービスの作成
次に、Productsサービスを作成します。
# プロジェクトルートに戻る
cd ../../
# Productsサービスのディレクトリに移動
cd services/products
4.1 Productsサービスの初期化:
# Go moduleの初期化
go mod init products
4.2 gqlgenの設定ファイル作成:
touch gqlgen.yml
schema:
- subgraph.graphql
exec:
filename: graph/generated/generated.go
package: generated
model:
filename: graph/generated/models.go
package: generated
resolver:
layout: follow-schema
dir: graph
package: graph
federation:
filename: graph/generated/federation.go
package: generated
4.3 GraphQLスキーマの定義:
touch subgraph.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key"])
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
}
type Query {
product(id: ID!): Product
products: [Product!]!
}
4.4 GraphQLコードの生成:
# GraphQLコードを生成
gqlgen generate
4.5 リゾルバーの実装:
// service/products/graph/resolver.go
package graph
import "products/graph/generated"
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct{}
var demoProducts = []*generated.Product{
{
ID: "1",
Name: "MacBook Pro",
Price: 2499.99,
},
{
ID: "2",
Name: "iPhone 15",
Price: 999.99,
},
{
ID: "3",
Name: "iPad Air",
Price: 699.99,
},
}
// service/products/graph/subgraph.resolvers.go
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.78
import (
"context"
"fmt"
"products/graph/generated"
)
// Product is the resolver for the product field.
func (r *queryResolver) Product(ctx context.Context, id string) (*generated.Product, error) {
for _, product := range demoProducts {
if product.ID == id {
return product, nil
}
}
return nil, fmt.Errorf("product not found")
}
// Products is the resolver for the products field.
func (r *queryResolver) Products(ctx context.Context) ([]*generated.Product, error) {
return demoProducts, nil
}
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type queryResolver struct{ *Resolver }
// service/products/graph/entity.resolvers.go
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.78
import (
"context"
"fmt"
"products/graph/generated"
)
// FindProductByID is the resolver for the findProductByID field.
func (r *entityResolver) FindProductByID(ctx context.Context, id string) (*generated.Product, error) {
for _, product := range demoProducts {
if product.ID == id {
return product, nil
}
}
return nil, fmt.Errorf("product not found")
}
// Entity returns generated.EntityResolver implementation.
func (r *Resolver) Entity() generated.EntityResolver { return &entityResolver{r} }
type entityResolver struct{ *Resolver }
// service/products/main.go
package main
import (
"log"
"net/http"
"os"
"products/graph"
"products/graph/generated"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
)
const defaultPort = "8002"
func main() {
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
srv := handler.New(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
srv.AddTransport(transport.Options{AllowedMethods: []string{"POST"}})
srv.AddTransport(transport.POST{ResponseHeaders: nil})
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv)
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
4.6 Productsサービスの動作確認:
go mod tidy
go run main.go
ブラウザで http://localhost:8002 にアクセスして、GraphQL Playgroundでクエリを試してみると、設定していたデモデータが返されます。


5: Federationの設定と統合
プロジェクトルートに戻って、Federationの設定を行います。
cd ../../
5.1 スーパーグラフ設定ファイルの作成
touch supergraph.yaml
federation_version: 2
subgraphs:
users:
routing_url: http://localhost:8001/query
schema:
file: ./services/users/subgraph.graphql
products:
routing_url: http://localhost:8002/query
schema:
file: ./services/products/subgraph.graphql
5.2 スーパーグラフスキーマの生成:
# スーパーグラフスキーマを生成
rover supergraph compose --config supergraph.yaml > supergraph.graphql


5.3 Apollo Routerの起動:
rover dev --supergraph-config supergraph.yaml
5.4 Apollo Routerの動作確認:
ブラウザで http://localhost:4000 にアクセスして、GraphQL Playgroundでクエリを試してみます。


Discussion