AppSync – データソース:Lambda

AppSync - データソース:Lambda

AppSyncの データソースにLambdaを設定する構成

AppSyncは以下のサービスの中からデータソースを選択することができます。

  • Lambda
  • DynamoDB
  • OpenSearch
  • None
  • HTTPエンドポイント
  • RDS

今回はLambdaをデータソースにする構成を確認します。
なおAppSyncの基本的な解説と、DynamoDBをデータソースとする構成については、以下のページをご確認ください。

あわせて読みたい
CFNでAppSync入門 – データソース:DynamoDB 【CloudFormationを使用してAppSync環境を構築】 AppSyncはAWSが提供するマネージドサービスの1つで、容易にGraphQL APIを構築することができます。 本ページはAppSync...

構築する環境

Diagram of AppSync - Data Source: Lambda

データソースとして動作するLambda関数を定義します。
Lambda関数はS3バケットのオブジェクトを操作する働きを定義します。
具体的には以下の3つです。

  • Put:S3バケットにオブジェクトを新規作成する。
  • List:S3バケットのオブジェクトの一覧を取得する。
  • Delete:キー(オブジェクト名)を指定して、S3バケット内のオブジェクトを削除する。

AppSyncは上記の3つの操作が可能となるように、スキーマ・リゾルバを定義します。

AppSyncによるGraphQL APIを実行するクライアントとして、もう1つLambda関数を作成します。
Function URLを有効化し、URLクエリパラメータで実行する操作を指定できるようにします。

今回は2つのLambda関数を作成しますが、どちらもPython3.8で作成します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/044

テンプレートファイルのポイント解説

LambdaをデータソースとするAppSync

データソース

Resources:
  DataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId
      LambdaConfig:
        LambdaFunctionArn: !Ref FunctionArn
      Name: DataSource
      ServiceRoleArn: !GetAtt DataSourceRole.Arn
      Type: AWS_LAMBDA
Code language: YAML (yaml)

ポイントは2つです。

1つ目はTypeプロパティです。
Lambdaをデータソースとする場合は、「AWS_LAMBDA」を指定します。

2つ目はLambdaConfigプロパティです。
データソースに設定するLambda関数のARNをLambdaFunctionArnプロパティに設定します。

スキーマ

Resources:
  GraphQLSchema:
    Type: AWS::AppSync::GraphQLSchema
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId
      Definition: |
        schema {
          query: Query
          mutation: Mutation
        }

        type Query {
          listS3Objects: [S3Object]
        }

        type Mutation {
          putS3Object: S3Object
          deleteS3Object(Key: String!): S3Object
        }

        type S3Object {
          Key: String!
          LastModified: String
          Size: Int
          ETag: String
        }
Code language: YAML (yaml)

Lambdaをデータソースとするために特別な設定は不要です。
通常通りスキーマを定義します。

先述の通り、今回はS3バケットに対する3つの処理を実装しますので、それに準じたスキーマを定義します。

リゾルバ

スキーマで確認した通り、今回は合計3つのクエリ・ミューテーションを定義します。
代表して、S3バケット内のオブジェクトを削除するミューテーション(deleteS3Object)を取り上げます。

Resources:
  DeleteS3ObjectResolver:
    Type: AWS::AppSync::Resolver
    DependsOn:
      - GraphQLSchema
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId
      DataSourceName: !GetAtt DataSource.Name
      FieldName: deleteS3Object
      Kind: UNIT
      RequestMappingTemplate: |
        {
          "version": "2018-05-29",
          "operation": "Invoke",
          "payload": {
            "field": "Delete",
            "arguments": $utils.toJson($context.arguments)
          }
        }
      ResponseMappingTemplate: |
        $context.result
      TypeName: Mutation
Code language: YAML (yaml)

リクエストマッピング(RequestMappingTemplateプロパティ)とレスポンスマッピング(ResponseMappingTemplateプロパティ)がポイントです。
これらの仕様については、以下のページをご確認ください。

https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-lambda.html

リクエストマッピング

AppSyncからデータソースLambda関数を実行し、その実行の際に渡すデータに関する設定です。

operationには2つの値のどちらかが設定可能です。

Lambda データソースでは、次の 2 つの操作を定義できます。InvokeそしてBatchInvoke。

Lambda のリゾルバーのマッピングテンプレートリファレンス

今回は「Invoke」を指定します。

payloadを定義することで、Lambda関数にデータを渡すことができます。

payload フィールドは、任意の正しい形式の JSON を Lambda 関数に渡す際に使用するコンテナです。

Lambda のリゾルバーのマッピングテンプレートリファレンス

今回は2つの値をLambda関数に渡すように定義します。

  • field:データソースLambda関数が実行する操作を示す値。Put、List、Deleteのいずれか。
  • arguments:ミューテーション実行時の引数。引数は$context.argumentsで取得できるため、これを$utils.toJsonでJSON化して渡す。
レスポンスマッピング

データソースLambda関数から返ってきた値をAppSyncで受け取るための設定です。
Lambda関数の実行結果は$context.resultで取得することができます。

ポイントはJSON形式で返す必要があるということです。
つまりLambda関数側か、レスポンスマッピング側でオブジェクトをJSON化しなければなりません。
今回は関数側でJSON化しています。
仮にレスポンスマッピング側でJSON化する場合は、先述の通り、$utils.toJsonが使用できます。

AppSync用IAMロール

Resources:
  DataSourceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: appsync.amazonaws.com
      Policies:
        - PolicyName: !Sub "${Prefix}-DataSourcePolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource:
                  - !Ref FunctionArn
Code language: YAML (yaml)

AppSyncがデータソースに設定するLambda関数を実行することを許可する内容となります。

データソースLambda関数

import boto3
import json
import os
from datetime import date, datetime

bucket_name = os.environ['BUCKET_NAME']
s3_client = boto3.client('s3')

PUT = 'Put'
LIST = 'List'
DELETE = 'Delete'

def json_serial(obj):
  # reference: https://www.yoheim.net/blog.php?q=20170703
  if isinstance(obj, (datetime, date)):
    return obj.isoformat()
  raise TypeError ("Type %s not serializable" % type(obj))

def lambda_handler(event, context):
  if event['field'] == PUT:
    now = datetime.now()
    now_str = now.strftime('%Y%m%d%H%M%S')

    key = "{datetime}.txt".format(datetime=now_str)

    put_response = s3_client.put_object(
      Bucket=bucket_name,
      Key=key,
      Body=now_str.encode())

    get_response = s3_client.get_object(
      Bucket=bucket_name, Key=key)

    object_ = {
      'Key': key,
      'LastModified': get_response['LastModified'],
      'Size': get_response['ContentLength'],
      'ETtag': get_response['ETag']
    }
    return json.dumps(object_, default=json_serial)

  elif event['field'] == LIST:
    list_response = s3_client.list_objects_v2(
      Bucket=bucket_name)
    objects = list_response['Contents']
    return json.dumps(objects, default=json_serial)

  elif event['field'] == DELETE:
    key = event['arguments']['Key']
    delete_response = s3_client.delete_object(
      Bucket=bucket_name, Key=key)
    object_ = {
      'Key': key
    }
    return json.dumps(object_, default=json_serial)
Code language: Python (python)

ポイントは2点です。

1点目はリクエストマッピングで設定した値の受け取り方です。
Pythonの場合、eventオブジェクトに格納されています。
fieldの値を受け取る場合はevent[‘field’]となりますし、argumentsを受け取る場合はevent[‘arguments’]となります。

2点目は返す値です。
先述の通り、関数が返す値は関数側か、レスポンスマッピング側でJSON化する必要があります。
そのため今回は関数側でJSON化しています。
なお以下のサイトを参考に、json.dumpsでJSON化する際に、datetimeオブジェクトをシリアライズ可能にする関数を用意しました。

https://www.yoheim.net/blog.php?q=20170703

以上を踏まえてコードの概要をまとめます。

  • fieldの値を参照して、GraphQLクエリで要求されたクエリ内容を判断する。
    ※Deleteの場合、削除するオブジェクトの名前(キー)を引数から参照する。
  • 要求内容に従ってS3バケットを操作する。
  • 操作結果をJSON化してレスポンスマッピングに返す。

(参考)GraphQLクライアントLambda関数

import json
import os
import time
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport

api_key = os.environ['API_KEY']
graphql_url = os.environ['GRAPHQL_URL']

transport = AIOHTTPTransport(
  url=graphql_url,
  headers={
    'x-api-key': api_key
  })
client = Client(transport=transport, fetch_schema_from_transport=True)

PUT = 'Put'
LIST = 'List'
DELETE = 'Delete'


def lambda_handler(event, context):
  field = ''
  document = None
  result = None

  if not 'queryStringParameters' in event or (
      not 'field' in event['queryStringParameters']):
    field = LIST
  else:
    field = event['queryStringParameters']['field']

  if field == PUT:
    document = gql(
      """
      mutation PutS3ObjectMutation {
        putS3Object {
          Key
          LastModified
          Size
          ETag
        }
      }
      """
      )
    result = client.execute(document)

  elif field == LIST:
    document = gql(
      """
      query ListS3ObjectsQuery {
        listS3Objects {
          Key
        }
      }
      """
      )
    result = client.execute(document)

  elif field == DELETE:
    object_name = event['queryStringParameters']['object_name']
    document = gql(
      """
      mutation DeleteS3ObjectsMutation($object_name: String!) {
        deleteS3Object(Key: $object_name) {
          Key
        }
      }
      """
      )

    params = {
      'object_name': object_name
    }
    result = client.execute(document, variable_values=params)

  return {
    'statusCode': 200,
    'body': json.dumps(result, indent=2)
  }
Code language: Python (python)

GraphQLクエリを実行するLambda関数です。
今回はPython用GraphQLクライアントライブラリとして、GQLを使用します。

https://github.com/graphql-python/gql

Pythonの場合、event[‘queryStringParameters’]でURLクエリパラメータを取得することがきます。
URLクエリパラメータで必要なパラメータを渡します。

  • field:実行する操作。
  • object_name:操作がDeleteの場合に必要。削除するオブジェクトの名前(キー)。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

Lambda関数用のデプロイパッケージを用意する

Lambda関数を作成する場合、3つの方法があります。
今回はデプロイパッケージをS3バケットにアップロードする方法を選択します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CloudFormationでLambdaを作成する3パータン(S3/インライン/コンテナ) 【CloudFormationでLambdaを作成する】 CloudFormationでLambdaを作成する場合、大別すると以下の3パターンあります。 S3バケットにコードをアップロードする インライ...

Lambdaレイヤー用のデプロイパッケージを用意する

先述のGQLをLambdaレイヤーとして用意します。
Lambdaレイヤーに関する詳細は、以下のページをご確認ください。

あわせて読みたい
CFNでLambdaレイヤー作成 【CloudFormationでLambdaレイヤー作成】 本ページでは、CloudFormationでLambdaレイヤーを作成する方法を確認します。 Lambda レイヤーは、Lambda 関数で使用できるラ...

なおLambdaレイヤー用パッケージを作成ためのコマンドは以下となります。

$ sudo pip3 install --pre gql[all] -t python

$ zip -r layer.zip python
Code language: Bash (bash)

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • S3バケット:fa-044
  • AppSync API:fa-044-GraphQLApi
  • データソース用Lambda関数:fa-044-function-01
  • GraphQLクライアントLambda関数のFunction URL:https://khty5dwgudjyl6otndvxm3uora0hemhk.lambda-url.ap-northeast-1.on.aws

AWS Management Consoleから、AppSyncを確認します。

Lambda関数がデータソースとして登録されていることがわかります。

動作確認

準備が整いましたので、GraphQLクライアントLambda関数のFunction URLにアクセスします。

まずS3バケットにオブジェクト追加(Put)の操作です。
URLクエリのfieldの値に「Put」を指定します。

The Result of AppSync GraphQL API 1.

putS3Objectミューテーションが実行されました。
実行した結果が表示されています。
次の操作のために、もう1回実行しておきます。

続いてS3バケットに保存されているオブジェクトの一覧を取得する操作(List)です。

The Result of AppSync GraphQL API 2.

listS3Objectsクエリが実行されました。
クエリで指定した通り、Keyのみが返ってきました。

ちなみにS3バケットの状況は以下の画像の通りです。

The Result of AppSync GraphQL API 5.

確かに2つのオブジェクトが保存されています。

続いてS3バケットのオブジェクトを削除する操作(Delete)です。
パラメータに削除するオブジェクトのオブジェクト名(キー)を追加します。

The Result of AppSync GraphQL API 3.

deleteS3Objectミューテーションが実行されました。
正常に実行され、削除されたオブジェクトのキーが返ってきました。

改めて一覧を確認します。

The Result of AppSync GraphQL API 4.

1つだけになりました。
先ほどの操作で、確かにオブジェクトが削除されたことがわかります。

まとめ

AppSyncのデータソースにLambda関数を設定する構成をご紹介しました。