🌝

ApolloFederationのスーパーグラフをコマンドラインで生成する方法

2023/10/12に公開

はじめに

こんにちはスペースマーケットでエンジニアをしているderaです。

弊社では、複数のサービスのGraphQLを取りまとめて、単一のGraphQLエンドポイントを提供するために、ApolloFederationを採用しています。この記事ではその際に行われているスキーマのマージのみ行う方法の紹介です。

ApolloFederationを利用したスキーマ合成に関しては過去のこちらの記事に詳しく解説をしています。

https://blog.spacemarket.com/code/graphql-apollo-federation/
https://note.com/shopon/n/nba50fd2bc1e7

環境

  • macOS Ventura 13.5.2
  • Rover 0.19.1

スーパーグラフとは?

前文に貼っているリンクで詳しく解説されていますが、ApolloFederationは複数のGraphQLサービスを統合して、単一のGraphQLエンドポイントとしてを提供できます。統合前の各サービスをサブグラフで、統合後をスーパーグラフとApolloFederationで唱えています。

今回は、RoverCLIというApolloFederationが提供しているコマンドラインのツールでスーパーグラフを生成する方法を紹介します。

スキーマ合成

スキーマの合成は次の手順で行います。

前準備

前準備としてサブグラフを提供するGraphQLサービスを用意します。

次のコードでUserと商品を提供する2つのサービスを用意しました。

user-graph.js
user-graph.js
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { gql } from 'graphql-tag';
import { buildSubgraphSchema } from '@apollo/subgraph';

const typeDefs = gql`
  type User @key(fields: "id") {
    id: ID!
    name: String!
    birthday: String!
  }

  extend type Query {
    users: [User]
  }
`;

const users = [
  {
    id: "1",
    name: '山田太郎',
    birthday: '1980-01-01',
  },
  {
    id: "2",
    name: '鈴木一郎',
    birthday: '1990-02-02',
  },
];

const resolvers = {
  Query: {
    users: () => users,
  },
};

const server = new ApolloServer({
  schema: buildSubgraphSchema({ typeDefs, resolvers })
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4001 },
});

console.log(`🚀  Server ready at: ${url}`);
product-graph.js
product-graph.js
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { gql } from 'graphql-tag';
import { buildSubgraphSchema } from '@apollo/subgraph';

const typeDefs = gql`#graphql
  type Product @key(fields: "id") {
    id: ID!
    name: String!
    price: Int!
  }

  extend type Query {
    products: [Product]
  }
`;

const products = [
  {
    id: "1",
    name: 'キャベツ太郎',
    price: 30,
  },
  {
    id: "2",
    name: 'うまい棒',
    price: 10,
  },
  {
    id: "3",
    name: 'ポテトフライ',
    price: 40,
  },
];

const resolvers = {
  Query: {
    products: () => products
  },
};
const server = new ApolloServer({
  schema: buildSubgraphSchema({ typeDefs, resolvers })
});
const { url } = await startStandaloneServer(server, {
  listen: { port: 4002 },
});

console.log(`🚀  Server ready at: ${url}`);

Userサービスが4001ポート、Productサービスが4002ポートで動作するようにしています。これらは単体で動作するGraphQLサービスなので、単体で確認する事も可能です。

設定ファイルの作成

次にスキーマ合成時に利用するコンフィグを用意します。

supergraph-config.yaml
supergraph-config.yaml
# Federationのバージョンを指定する事が可能
federation_version: =2.3.2
# federation_version: =0.36.0

# サブグラフの定義を列挙する
subgraphs:
  # Userサブグラフ
  user:
    routing_url: http://localhost:4001
    schema:
      subgraph_url: http://localhost:4001
  # Productサブグラフ
  product:
    routing_url: http://localhost:4002
    schema:
      subgraph_url: http://localhost:4002

今回のサンプルでは起動中のサブグラフのサービスからスキーマを取得していますが、スキーマファイルを指定する事も可能です。

    product:
        routing_url: http://localhost:4002
        schema:
          file: products/schema.graphql

出力コマンドの実行

両サービスが起動している状態で、以下のコマンドを実行する事でカレントディレクトリにスーパーグラフが生成されます。

$ rover supergraph compose --config supergraph-config.yaml --output supergraph.graphql --elv2-license accept

--elv2-license accept のオプションですが、機能を利用するにはElastic License v2 への同意が必要な為、許諾するために指定しています。

コマンドが成功すると、下記のスーパーグラフが出力されました。2つのサブグラフのスキーマ情報が含まれています。

supergraph.graphql
supergraph.graphql
schema
  @link(url: "https://specs.apollo.dev/link/v1.0")
  @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{
  query: Query
}

directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE

directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

scalar join__FieldSet

enum join__Graph {
  PRODUCT @join__graph(name: "product", url: "http://localhost:4002")
  USER @join__graph(name: "user", url: "http://localhost:4001")
}

scalar link__Import

enum link__Purpose {
  """
  `SECURITY` features provide metadata necessary to securely resolve fields.
  """
  SECURITY

  """
  `EXECUTION` features provide metadata necessary for operation execution.
  """
  EXECUTION
}

type Product
  @join__type(graph: PRODUCT, key: "id")
{
  id: ID!
  name: String!
  price: Int!
}

type Query
  @join__type(graph: PRODUCT)
  @join__type(graph: USER)
{
  products: [Product] @join__field(graph: PRODUCT)
  users: [User] @join__field(graph: USER)
}

type User
  @join__type(graph: USER, key: "id")
{
  id: ID!
  name: String!
  birthday: String!
}

Gatewayを動かしてテストする

次に出力されたスーパーグラフを読み込んで、Gatewayを起動してみましょう。
Gateway起動に次のコードを作成しました。

gateway.js
gateway.js
import { ApolloServer } from 'apollo-server';
import { ApolloGateway } from '@apollo/gateway';

const gateway = new ApolloGateway({
  supergraphSdl: fs.readFileSync('./supergraph.graphql', 'utf8'),
});

const server = new ApolloServer({
  gateway,
  subscriptions: false,
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

Gatewayを起動して、スーパーグラフが正しく動作しているか確認しました。
サブグラフの内容が1エンドポイント、1リクエストで取得できているので、スキーマ合成もGateway起動も正しく動作していますね!

Gatewayへのリクエスト結果

まとめ

以上、ApolloFederationのスーパーグラフをコマンドから合成する方法の紹介でした。
今回作成したコードはこちらに置いているので興味があるようでしたらご覧くださいませ!

スペースマーケット Engineer Blog

Discussion