AWS

AppSync – データソース:Lambda

スポンサーリンク
AppSync - データソース:Lambda AWS
スポンサーリンク
スポンサーリンク

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

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

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

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

構築する環境

Amazon.co.jp: AWS認定資格試験テキスト AWS認定ソリューションアーキテクト - アソシエイト 改訂第2版 : NRIネットコム株式会社, 佐々木 拓郎, 林 晋一郎, 金澤 圭: 本
Amazon.co.jp: AWS認定資格試験テキスト AWS認定ソリューションアーキテクト - アソシエイト 改訂第2版 : NRIネットコム株式会社, 佐々木 拓郎, 林 晋一郎, 金澤 圭: 本
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テンプレートを配置しています。

awstut-fa/044 at main · awstut-an-r/awstut-fa
Contribute to awstut-an-r/awstut-fa development by creating an account on GitHub.

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

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プロパティ)がポイントです。
これらの仕様については、以下のページをご確認ください。

Lambda のリゾルバーのマッピングテンプレートリファレンス - AWS AppSync
AWSLambda の AppSync リゾルバーマッピングテンプレートリファレンス
リクエストマッピング

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オブジェクトをシリアライズ可能にする関数を用意しました。

[Python] dateやdatetimeをjson.dumpでエラーなく出力する - YoheiM .NET
Pythonのjsonモジュールを使うといい感じにjsonが扱えますが、dateやdatetimeなどのJSONに定義されていない型は、はうまく文字列に変換できません。今日はその対応Tipsをブログに書きたいと思います。

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

  • 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を使用します。

GitHub - graphql-python/gql: A GraphQL client in Python
A GraphQL client in Python. Contribute to graphql-python/gql development by creating an account on GitHub.

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

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

環境構築

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

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

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

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

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

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

$ sudo pip3 install --pre gql[all] -t python $ zip -r layer.zip python
Code language: Bash (bash)

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関数を設定する構成をご紹介しました。

タイトルとURLをコピーしました