lambda
GraphQL
APIGateway

AWSの API Gateway + Lambda で GraphQL のAPIを作ってみた

はじめに

re:Invent 2018 で「App Sync」というGraphQLのマネージドサービスが発表され、GraphQLに興味を持ちました。
そこで、ちょっと使ってみようと思い、GraphQLのAPIエンドポイントをAPI Gateway + Lambda(node.js)で作って動かしてみました。
本記事が「これから小規模のGraphQLエンドポイントを作ってみよう!」という方の足がかりとなれば幸いです。

GraphQLとは何?

GraphQLはAPI向けのクエリ言語で、スキーマの定義とクエリエンジンを提供しています。
細かい説明や、使うことのメリット・デメリットはさまざまな記事で既に書かれているので、ここでは省略いたします。

構成

今回作ってみたGraphQLのAPIエンドポイントの構成です。
diagram.jpg

  • API Gatewayにより、クライアントからのHTTPSリクエストを受け付ける。
  • API Gatewayは、クエリパラメータとしてGraphQLクエリを受け取り、Lambda FunctionへGraphQLクエリを引き渡す。
  • Lambda FunctionはGraphQLクエリを実行し、結果を返却する。
  • データストアとしてDynamoDBを利用する。

Lambda Functionの作成

今回はnode.jsで実装しました。利用したライブラリは以下です。

コード

'use strict'

const co = require('co');
const aws = require('aws-sdk');
const Promise = require('promise');
const { graphql, buildSchema } = require('graphql');

// GraphQL schema
// idをキーにItemを取得するリソースと、typeをキーに複数のItemを取得するリソースを提供
const schema = `
type Query {
    item(id: Int!): Item
    items(type: Int!): [Item]
}
type Item {
    id: ID!
    name: String!
    type: Int
}
`;

// GraphQL root
const root = {
    item: (args) => getItemById(args.id),
    items: (args) => getItemsByType(args.type),
};

//DynamoDB クライアント
let dynamoDB = null;

//idを条件として、DynamoDBからItemを取得する。
function getItemById(id) {
    return new Promise((resolve, reject) => {
        const params = {
            TableName: 'test-table',
            Key: {
                id: id,
            },
        };

        dynamoDB.get(params, (err, data) => {
            if (err) {
                reject(err);

                return;
            }

            resolve(data.Item);
        });
    });
}

//typeを条件として、DynamoDBからItemを取得する。
function getItemsByType(type) {
    return new Promise((resolve, reject) => {
        const params = {
            TableName: 'test-table',
            IndexName: 'type-index',
            KeyConditionExpression: '#type = :type',
            ExpressionAttributeNames: {
                '#type': 'type',
            },
            ExpressionAttributeValues: {
                ':type': type,
            },
        };

        dynamoDB.query(params, (err, data) => {
            if (err) {
                reject(err);

                return;
            }

            resolve(data.Items);
        });
    });
}

//Lambdaファンクションのエントリポイント
exports.handle = (event, context, callback) => {
    co(function* () {
        try {
            //クライアントの初期化
            if (dynamoDB === null) dynamoDB = new aws.DynamoDB.DocumentClient({ region: process.env.AWS_REGION });

            //クエリの実行
            const result = yield graphql(buildSchema(schema), event.query, root);

            callback(null, result);
        } catch (err) {
            console.log(JSON.stringify({
                message: err.message,
                stack: err.stack,
            }));

            callback(JSON.stringify({
                code: 500,
                message: 'unexpected error occurred.',
            }));
        }
    }).catch((err) => {
        console.log(JSON.stringify({
            message: err.message,
            stack: err.stack,
        }));

        callback(JSON.stringify({
            code: 500,
            message: 'unexpected error occurred.',
        }));
    });
}

ポイント

GraphQLの実行には以下の構成要素が必要です。

  • schema

    GraphQLのスキーマ。

    • itemとitemsの2つのフィールドを持つ。
    • itemは必須パラメータ「id」を受け取り、Itemを返す。
    • itemsは必須パラメータ「type」を受け取り、Itemの配列を返す。
    • Itemは type Item { ・・・ } で定義された構造を持つ。
type Query {
    item(id: Int!): Item
    items(type: Int!): [Item]
}
type Item {
    id: ID!
    name: String!
    type: Int
}
  • root

    GraphQLのスキーマで定義されたフィールドに対し、動作のマッピング。

    • itemフィールドにアクセス時、getItemByIdを呼び出す。
    • itemsフィールドにアクセス時、getItemsByTypeを呼び出す。
    • 非同期処理を登録する場合、Promiseをreturnすると、queryの実行結果としてPromiseの処理結果が返却される。
{
    item: (args) => getItemById(args.id),
    items: (args) => getItemsByType(args.type),
}
  • query
    GraphQLのクエリ。GraphQLのクエリエンジンが解釈し、結果を返す。 API Gatewayから引数として渡されたクエリ文字列をそのまま引き渡すよう実装している。

API Gateway APIの作成

以下の設定で、GETリクエストを作成します。

  • Method Request
    • URL Query String Parameters に以下を追加

methodrequest.jpg

  • Integration Request
    • Integration type は 「Lambda Function」 を選択
    • Lambda Function は 作成したLambda Functionを指定
    • Body Mapping Templates は Content-Type application/jsonに以下のテンプレートを設定
{
    "query": "$input.params('query')"
}

ポイント

API Gatewayのキャッシュ機能を利用する場合、POSTではなくGETメソッドを定義し、クエリパラメータにGraphQLのクエリをセットしてもらうよう設定します。
こうすることで、クエリ文字列をキーとしてキャッシュを利用できます。
※キャッシュを利用するためには、クエリの書き方を合わせる必要があることに注意。

動作確認

DynamoDBのtest-tableに検証用のデータを登録し(GraphQLのschemaで定義したItemと型を合わせること)、リクエストを発行します。

curl -X GET "https://[作成したAPIのエンドポイント]?query=[GraphQLのクエリ]" -H "accept: application/json"

まとめ

API Gateway + Lambda で 簡単なGraphQLのAPIエンドポイントを作成できました。
小規模なGraphQLエンドポイントを構築する場合は、この構成でも十分実用的ではないかと思います。

いいなと思った点

  • サーバーレスの構成で簡単に作成できる。
  • API GatewayのAccess Logを出力しておくことで、どの項目が利用されているかが分析できる(REST APIでは難しい)

気になった点

  • API Gateway には、エンドポイント毎にCloudwatchに情報を出力するDetailed CloudWatch Metricsがあるが、GraphQLを利用すると機能しない。(AWS X-Rayを利用することで、ある程度対処できるかも)。
  • API Gateway のキャッシュを利用する場合、クエリ文字列を合わせる必要があり、不要な項目を取得しなくてもよいというGraphQLの特性と相性が悪い。
  • REST APIと比べ、1つのLambda Functionのコードが肥大化しやすいため、規模が大きくなるとLambdaは向かないかも。