Web系エンジニアのアウトプット練習場

エンジニアリングと書評が中心。たまに全然関係無い話もします。

Apollo Federationのすゝめ -GraphQLとマイクロサービス-

はじめに

私は現在、仕事でGraphQLを用いてマイクロサービスを構築しております。
今回はそこで得られた知見を共有させていただきます。

タイトルにも含まれている「Apollo Federation(以下、Federationとも)」はGraphQLサーバーの実装の1つである「Apollo Server」の拡張の1つで、個人的にはGraphQLを用いたマイクロサービスの構築に欠かせない存在であると思っています。
マイクロサービスにおいてFederationとは何か?どのように役に立つのか?といったことを、Federationをご存じない方にもご理解いただくことが本投稿の目的です。

Apollo Federationのイメージ図

本投稿に含まれる内容

  • Apollo Federationとは何か?
  • Apollo Federationの良いところ

本投稿に含まれない内容

  • マイクロサービスの説明
  • GraphQLそのものの説明
  • Apollo Federationの実装例
    • 時間に余裕ができれば記事にするかも
    • Railsを使ったFederation対応GraphQL APIサービスの構築方法とか、Federationを導入しつつRelay Server Specificationに対応する方法とか需要あるかな?

Apollo Federationとは?

Apollo Federation は、複数のGraphQLサービスを一つのGraphQLサービスとして提供するための仕組みです。

サービスや組織が複雑化するにつれて、モノリシックなGraphQLサーバーでは辛くなってきます。
そんなときにFederationを使用すると、モノリシックなGraphQLサービスをマイクロサービスに分割することができます。

Apollo Federationの良いところ

Apollo Federationは、宣言型プログラミングモデルを使用して、分割する各サービスがそれぞれGraphQLデータグラフの 担当部分のみ を実装できます。

...とだけ聞いても訳がわからないので、具体例を使って説明していきます。

具体例: GraphQL APIとマイクロサービス

例えば、Userサービス、Productサービス、Reviewサービスがそれぞれ以下のようなスキーマを提供したいとします。*1

スキーマのサンプル

また、以下のようにユーザーがGatewayを経由して各サービスに接続する構成を考えます。

構成図

Federationを使用しない場合

まず、Federationを使わない場合を考えます。

Userサービスは、あるユーザーが行ったレビュー内容をreviewsフィールドで取得できなければなりません。
期待されるクエリとレスポンスの例は以下のようなものとなります。

query($id: ID!) {
  user(id: $id) {
    reviews {
      body
      product {
        name
        price
      }
    }
  }
}

# レスポンス例
{
  "data": {
    "user": {
      "reviews": [
        {
          "body": "レビュー内容",
          "product": {
            name: "製品1",
            price: 2500
          }
        }
      ]
    }
  }
}

このとき、Userサービスはレビュー内容やレビュー対象の製品情報をレスポンスに載せるために、それらの情報をReviewサービスから取得してこなければなりません。

シーケンス図

結果、Userサービスの実装はReviewサービスの実装に依存することになり、各サービス間での関心の分離ができなくなります。
つまり、Reviewサービスの実装に何か変更がある際に、常にUserサービスへの影響も考慮しなければならなくなります。

また、上記の例ではReviewサービスやProductサービスも、それぞれ他のサービスに関するフィールドが定義されているため、相互に依存することになります。
これではモノリシックに構築していたときのほうがまだマシということになりかねません。

そこで登場するのがApollo Federationです。

Federationを使用する場合

GatewayにFederationを導入します。

構成図(Federation)

まず、以下のようにスキーマを定義します。

Federationを使用したスキーマサンプル

最初のスキーマと比べてみると、以下のような違いがあります。

  • UserサービスからreviewsフィールドとrecentPurchasesフィールドが削除されている
  • Productサービスにextend type Userが追加され、そこにrecentPurchasesが定義されている
  • Reviewサービスにextend type User, extend type Productが追加され、それぞれreviewsフィールドが定義されている

実はこのextendはFederationで定義されているキーワードの1つで、「関心の分離」を達成するために重要なキーワードの1つです。

例えば、Reviewサービスのextend type Productは「実装の詳細はわからないけど、何やら別のサービスにProductというタイプがあるらしい」ということを表しています。

さて、Reviewサービスに着目すると、Reviewタイプにはproduct: Productというフィールドが存在します。
つまり、以下のようなクエリとレスポンスが期待されます。

query($id: ID!) {
  review(id: $id) {
    body
    product {
      name
      price
    }
  }
}

# レスポンス例
{
  "data": {
    "review": {
      "body": "レビュー内容",
      "product": {
        "name": "レビュー内容",
        "price": 2500,
      }
    }
  }
}

このとき、レビューの対象となる製品の識別子(製品コード: upc)はReviewサービスが保持していることがほとんどでしょうが、関心の分離の観点からReviewサービスは製品情報の詳細を知りたくはありません。

製品情報(nameprice等)はProductサービスから返したいです。

ここで必要になるのが、keyexternalというディレクティブです。
スキーマkeyexternalディレクティブを追加します。

# Reviewサービス
extend type Product @key(fields: "upc") {
  upc: ID! @external
}

# Productサービス
type Product @key(fields: "upc") {
  upc: ID!
  name: String!
  price: Int
}

このように定義し、リゾルバーを書きます。

# Reviewサービス
{
  Review: {
    product(review) {
      // upcだけを返すようにする
      return { __typename: "Product", upc: review.product_upc }
    }
  }
}

# Productサービス
{
  Product: {
    __resolveReference(reference) {
      // Reviewサービスから返された値を元にreference.upcの値は自動でセットされる
      // upcの値を元に製品情報を返す処理を書く
      return fetchProductByUPC(reference.upc)
    }
  }
}

ここまで実装した上で、ユーザーが以下のクエリを投げたとします。

query($id: ID!) {
  review(id: $id) {
    body
    product {
      name
      price
    }
  }
}

すると、Federationを導入したGatewayは、

  1. Reviewサービスからレビューの情報と製品の識別子(upc)を取得する
  2. Reviewサービスから得られたupcを元にProductサービスから製品情報を得る
  3. ユーザーのクエリに基づいたレスポンスを構築してユーザーに返す

というように、Federation用に定義したスキーマの情報からクエリプランニングを行い、自動的に各サービスに必要な情報を問い合わせてレスポンスを構築してくれます。

また、extend type Productにはreviews: [Review]というフィールドが定義されています。
ここでは、ReviewサービスがProductタイプに「製品に対するレビュー」を返すフィールドを追加するという宣言しています。

これに対しても、以下のようにReviewサービスでリゾルバを書きます。

{
  Review: {
    product(review) {
      // upcだけを返すようにする
      return { __typename: "Product", upc: review.product_upc }
    }
  },
  Product: {
    reviews(product) {
      # upcからレビューを返す処理を書く
      return fetchReviewsForProduct(product.upc)
    }
  }
}

そして、ユーザーが以下のクエリを投げたとします。

query($upc: ID!) {
  product(upc: $upc) {
    name
    price
    reviews {
      body
    }
  }
}

するとやはり、Gatewayが自動的にクエリプランニングを行い、レスポンスを構築してくれます。

  1. Productサービスからレビューの情報以外の情報を得る
  2. Productサービスから得られたupcを元にReviewサービスからレビュー情報を得る
  3. ユーザーのクエリに基づいたレスポンスを構築してユーザーに返す

このように、Federationを使用することで、ReviewサービスはProductタイプの詳細を知ることなく、製品情報を返したり、製品情報にレビュー情報を付け足したりすることが可能になりました。

UserサービスやProductサービスについても同様に、各サービスの詳細情報を知ることなく、簡単に担当部分のみ実装することができるため、関心の分離を容易に達成することができます。

まとめ

本投稿では、Federationを用いたマイクロサービス構築についての知見を共有させていただきました。

私のチームでは、Schema stitchingという従来の方法(deprecated)からFederationへのマイグレーションを行ったところ、全体の開発速度が大きく向上しました。
これはSchema stitchingではなかなかできなかった「関心の分離」を、Federationではいとも簡単に実現できたからであると推測できます。

また、これからどんどんGraphQLが注目され、マイクロサービスの構築にも採用されてくるのではないかと私は予想しています。
その際には是非、Apollo Federationという選択肢を検討してみてください。

ご質問等はコメント欄で受け付けますのでお気軽に!

なお、この記事はGraphQL Advent Calendar 2019 - Qiitaの9日目の記事です。