AppSyncの データソースにLambdaを設定する構成
AppSyncは以下のサービスの中からデータソースを選択することができます。
- Lambda
- DynamoDB
- OpenSearch
- None
- HTTPエンドポイント
- RDS
今回はLambdaをデータソースにする構成を確認します。
なおAppSyncの基本的な解説と、DynamoDBをデータソースとする構成については、以下のページをご確認ください。
構築する環境
データソースとして動作する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プロパティ)がポイントです。
これらの仕様については、以下のページをご確認ください。
リクエストマッピング
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バケットにアップロードする方法を選択します。
詳細につきましては、以下のページをご確認ください。
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」を指定します。
putS3Objectミューテーションが実行されました。
実行した結果が表示されています。
次の操作のために、もう1回実行しておきます。
続いてS3バケットに保存されているオブジェクトの一覧を取得する操作(List)です。
listS3Objectsクエリが実行されました。
クエリで指定した通り、Keyのみが返ってきました。
ちなみにS3バケットの状況は以下の画像の通りです。
確かに2つのオブジェクトが保存されています。
続いてS3バケットのオブジェクトを削除する操作(Delete)です。
パラメータに削除するオブジェクトのオブジェクト名(キー)を追加します。
deleteS3Objectミューテーションが実行されました。
正常に実行され、削除されたオブジェクトのキーが返ってきました。
改めて一覧を確認します。
1つだけになりました。
先ほどの操作で、確かにオブジェクトが削除されたことがわかります。
まとめ
AppSyncのデータソースにLambda関数を設定する構成をご紹介しました。