📌

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