AWS Lambda から AppSync の API を ぶん殴る

AWS の 年次カンファレンス “re:Invent” も終わり、2018年も年の瀬ですね。今年の特に後半はブログをサボりがちでしたが、また少しずつアウトプットしていけるようにしたいと思います。

今回は

というお題が来たので、AWS Lambda の関数の中から AppSync (GraphQL) の API にアクセスする方法をまとめます。

AppSync (GraphQL) の APIにアクセスするのは API Gateway の API にアクセスする場合と同じで、基本的には HTTPでアクセスすれば良いだけです。
ただ、それではちょっと不便なので、最近では Amplify を使用する例が多く見られますし推奨されています。
これを使えば 一行書けば AppSync の APIを呼べる のですが、Amplify に AppSync が統合される前は AppSync SDK というものを使っていました。

これを用いた例が 7月に行われた Meguro.dev での ソリューション・アーキテクト塚田さんプレゼンに書かれています。

https://speakerdeck.com/akitsukada/real-time-voting-system-using-aws-appsync-aws-amplify-and-aws-iot-enterprise-button?slide=13

以下、色々と書いていたのですが、

ElasticSearch の API を叩く場合などと同様に SigV4 を作って AppSync のエンドポイントに投げつければ良い

のです。


IAM認証を使用する際は叩くAPIリソース に対する appsync:GraphQL アクションを許可するか、管理ポリシー AWSAppSyncInvokeFullAccess を付けてしまってください。

JS の例が
https://github.com/aws-samples/appsync-refarch-realtime/blob/master/sam-app/get-movie/app.js
にあるので参考にしてください。Python の例は、この記事の末尾にあります。


const getMovies = `query GetMovies{
  getMovies(id: 0) {
    id
    name
    poster
    date
    plot
  }
}
`;

const invokeGetMovie = async () => {
  let req = new AWS.HttpRequest(appsyncUrl, env.AWS_REGION);
  req.method = 'POST';
  req.headers.host = endpoint;
  req.headers['Content-Type'] = 'multipart/form-data';
  req.body = JSON.stringify({
    query: getMovies,
    operationName: 'GetMovies'
  });
  let signer = new AWS.Signers.V4(req, 'appsync', true);
  signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate());
  const result = await axios({
      method: 'post',
      url: appsyncUrl,
      data: req.body,
      headers: req.headers
  });
  return result;
};

以下は、この記事に元々書いていた内容です。

AppSync SDK for JavaScript を AWS Lambda 内で使用する際の注意点は
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/building-a-client-app-node.html
に書かれています。まずはこのドキュメント(URL)を読んでください。IAM認証を使用する例が説明されています。

AWS AppSync integrates with the Apollo GraphQL client for building client applications. AWS provides Apollo plugins for offline support, authorization, and subscription handshaking. This tutorial shows how you can use the AWS AppSync SDK with the Apollo client directly in a Node.js application.

Note: For AWS Lambda functions, ensure you set fetchPolicy: ‘network-only’ as well as disableOffline: true in your AppSync client constructor.

前置きはこのくらいにして、Lambda 関数内から AppSync SDK for JavaScript を使用して AWS AppSync の API を叩く 実際のコードを見てみましょう。

まずはデータを投入する例(mutation)


require('isomorphic-fetch');
const AUTH_TYPE = require('aws-appsync/lib/link/auth-link').AUTH_TYPE;
const AWSAppSyncClient = require('aws-appsync').default;
const gql = require('graphql-tag');
const uuid = require('uuid/v4');

const newMessage = gql(`
  mutation createMessage($id: ID!, $content: String, $conversationId: ID!, $createdAt: String!) {
    createMessage(id: $id, content: $content, conversationId: $conversationId, createdAt: $createdAt){
      __typename
      conversationId
      createdAt
      id
      sender
      content
      isSent
    }
  }`);

exports.handler = async (event) => {
  const client = new AWSAppSyncClient({
    url: process.env['URL'],
    region: process.env['REGION'],
    auth: {
      type: AUTH_TYPE.API_KEY,
      apiKey: process.env['APIKEY']
    },
    disableOffline: true
  });

  const now = `${new Date().toISOString()}`;
  const id = `${now}_${uuid()}`;

  try {
    const result = await client.mutate({
      variables: {
        conversationId: "hello",
        content: 'こんにちは!!',
        createdAt: now,
        sender: 'AWS Lambda',
        isSent: false,
        id: id
      },
      mutation: newMessage
    });
    console.log(JSON.stringify(result));
    return result;
  } catch (err) {
    console.log(JSON.stringify(err));
    return err;
  }
};

AWSAppSyncClient オブジェクトのインスタンスを作って、mutate メソッドに変数とGraphQLクエリを渡してやるだけです。

注意点として、前述の解説ページにあったように Lambda関数内では AppSync との間の通信がオフラインになることを考慮してもあまり意味がないので AWSAppSyncClient オブジェクトに disableOffline: true を渡す必要があります。(localStorage の Polyfill を入れればオフライン対応もできるかもしれませんが、すみません。確認してません・・)

あとは Apollo Client が内部で fetch メソッドを使用しているので、fetch の Polyfill (何種類か存在しますがどれでも良いです)を入れる必要があります。

次はデータを取得する例


require('isomorphic-fetch');
const AUTH_TYPE = require('aws-appsync/lib/link/auth-link').AUTH_TYPE;
const AWSAppSyncClient = require('aws-appsync').default;
const gql = require('graphql-tag');
const uuid = require('uuid/v4');

const listMessages = gql(`
  query getConversationMessages($conversationId: ID!, $after: String, $first: Int) {
    allMessageConnection(conversationId: $conversationId, after: $after, first: $first) {
      __typename
      nextToken,
      messages {
        __typename
        id
        conversationId
        content
        createdAt
        sender
        isSent
      }
    }
  }`);

exports.handler = async (event) => {
  const client = new AWSAppSyncClient({
    url: process.env['URL'],
    region: process.env['REGION'],
    auth: {
      type: AUTH_TYPE.API_KEY,
      apiKey: process.env['APIKEY']
    },
    disableOffline: true
  });

  const param = {
    conversationId: "hello",
    after: null,
    first: 20
  };

  try {
    const result = await client.query({
      query: listMessages,
      variables: param,
      fetchPolicy: 'network-only'
    });
    console.log(JSON.stringify(result));
    return result;
  } catch (err) {
    console.log(JSON.stringify(err));
    return err;
  }
};

fetchPolicy に ‘network-only’ を指定する必要があります。

Cognito User Pools を使って認証する場合


require('isomorphic-fetch');
const AUTH_TYPE = require('aws-appsync/lib/link/auth-link').AUTH_TYPE;
const AWSAppSyncClient = require('aws-appsync').default;
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');
const gql = require('graphql-tag');

const listMessages = gql(`
  query getConversationMessages($conversationId: ID!, $after: String, $first: Int) {
    allMessageConnection(conversationId: $conversationId, after: $after, first: $first) {
      __typename
      nextToken,
      messages {
        __typename
        id
        conversationId
        content
        createdAt
        sender
        isSent
      }
    }
  }`);

exports.handler = async (event) => {
  const poolData = {
    UserPoolId: process.env['USERPOOL_ID'],
    ClientId: process.env['CLIENT_ID']
  };
  const userData = {
    Username: process.env['USERNAME'],
    Pool: new AmazonCognitoIdentity.CognitoUserPool(poolData)
  };
  const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

  const authenticationData = {
    Username: userData.Username,
    Password: process.env['PASSWORD']
  };
  const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);

  const authenticateUser = new Promise((resolve, reject) => {
    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: (result) => {
        const idToken = result.getIdToken().getJwtToken();
        resolve(idToken);
      },
      onFailure: (err) => {
        reject(err);
      },
      newPasswordRequired: (userAttributes, requiredAttributes) => {
        const err = new Error("newPasswordRequired");
        reject(err);
      }
    });
  });

  return authenticateUser.then(async (idToken) => {
    const client = new AWSAppSyncClient({
      url: process.env['URL'],
      region: process.env['REGION'],
      auth: {
        type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
        jwtToken: idToken
      },
      disableOffline: true
    });

    const param = {
      conversationId: "hello",
      after: null,
      first: 20
    };

    const result = await client.query({
      query: listMessages,
      variables: param,
      fetchPolicy: 'network-only'
    });
    console.log(JSON.stringify(result));
    return result;
  }).catch((err) => {
    console.log(JSON.stringify(err));
    return err;
  });

};

最後に Python から AWS AppSync の API を叩く例を紹介します。

ElasticSearch の API を叩く場合などと同様に SigV4 を作って AppSync のエンドポイントに投げつければ良いということですね。IAM認証を使用する際は叩くAPIリソース に対する “appsync:GraphQL” アクションを許可するか、管理ポリシー AWSAppSyncInvokeFullAccess を付けてしまってください。


import json
import os
import requests
from requests_aws4auth import AWS4Auth
 
 
def lambda_handler(event, context):
    region_name = os.environ["REGION"]
    app_name = os.environ["ENDPOINT"]
    service = 'appsync-api'
    api_path = 'graphql'
    url = 'https://%s.%s.%s.amazonaws.com/%s' % (app_name, service, region_name, api_path)
 
    body_json = {"query": "query myQuery { allMessageConnection(conversationId: \"hello\", first: 20) { __typename nextToken, messages { __typename id conversationId content createdAt sender isSent } } }"}
    body = json.dumps(body_json)

    access_key_id = os.environ["AWS_ACCESS_KEY_ID"]
    secret_access_key = os.environ["AWS_SECRET_ACCESS_KEY"]
    session_token = os.environ["AWS_SESSION_TOKEN"]
    auth = AWS4Auth(access_key_id, secret_access_key, region_name, 'appsync', session_token=session_token)
 
    method = 'POST'
    headers = {}
    response = requests.request(method, url, auth=auth, data=body, headers=headers)
 
    print(response.__dict__)
    
    content = response.__dict__['_content']
    content_str = content.decode('unicode-escape').encode('latin1').decode('utf-8')
    obj = json.loads(content_str)
    
    if 'errors' in obj:
        print(obj['errors'])
        return obj['errors']

    if 'data' in obj:
        messages = obj['data']['allMessageConnection']['messages']
        json_body = json.dumps(messages, ensure_ascii=False)
        print(json_body)
        return json_body
    
    return

他の言語では試したことがないのですが、同じ要領で実行できると思います。

投稿者プロフィール

アバター画像
うっちー
kintone認定カイゼンマネジメントエキスパート(KME), アプリデザイン/カスタマイズ スペシャリスト