この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
先日のブログでAppSyncのHTTPデータソースとしてStep Functionsを利用する構成をご紹介しました。AppSyncのデータソースとしてネイティブに対応していないサービスであっても、HTTPデータソースを介してAppSyncと連携させることが可能です。
この例のようにAppSyncをAWSサービスのプロキシとして構成することで、CRUD操作のような単純なオペレーションに関してはわざわざLamdbaを使わなくてもAPIを実装することが可能です。AWSとしてもre:inventのセッション等で「LambdaはTransformのために使い、Transportに使わない」ことがベストプラクティスだと発信しており、Lambda無しで実現できることは極力Lambdaを使わないように設定で吸収していきたいところです。ただ、API Gatewayをサービスプロキシとして構成するパターンは色々と情報が見つかるのですが、AppSyncに関してはまだまだ情報が少ない印象です。
ということで、このエントリではサーバーレスなシステム開発で利用頻度の高いAWSサービス&オペレーションの
- KinesisのPutRecord
- S3のPutObject
- SQSのSendMessage
- SNSのPublish
に関してAppSyncをサービスプロキシとして構成する方法をご紹介します。
CFnテンプレート
まずCFnのテンプレートをご紹介します。このテンプレートを使えば、簡単なサンプルが作成可能です。
AWSTemplateFormatVersion: '2010-09-09'
Description: AppSync Service Proxy Sample
Resources:
ProxyApi:
Type: AWS::AppSync::GraphQLApi
Properties:
AuthenticationType: API_KEY
Name: AppSync Service Proxy API
ProxyApiKey:
Type: AWS::AppSync::ApiKey
Properties:
ApiId: !GetAtt ProxyApi.ApiId
MessagesSchema:
Type: AWS::AppSync::GraphQLSchema
Properties:
ApiId: !GetAtt ProxyApi.ApiId
DefinitionS3Location: schema.graphql
AppSyncServiceRole:
Type: AWS::IAM::Role
Properties:
Path: /
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- appsync.amazonaws.com
Policies:
- PolicyName: proxy-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:PutObject
- sqs:SendMessage
- kinesis:PutRecord
- sns:Publish
Resource: "*"
S3Bucket:
Type: AWS::S3::Bucket
S3PutObjectDs:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt ProxyApi.ApiId
Name: S3PutObjDataSource
Type: HTTP
ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
HttpConfig:
Endpoint: !Sub https://${S3Bucket}.s3.${AWS::Region}.amazonaws.com/
AuthorizationConfig:
AuthorizationType: AWS_IAM
AwsIamConfig:
SigningRegion: !Ref AWS::Region
SigningServiceName: s3
S3PutObjectMutationResolver:
Type: AWS::AppSync::Resolver
Properties:
ApiId: !GetAtt ProxyApi.ApiId
TypeName: Mutation
FieldName: putObject
DataSourceName: !GetAtt S3PutObjectDs.Name
RequestMappingTemplate: |
{
"version": "2018-05-29",
"method": "PUT",
"resourcePath": "/test.json",
"params": {
"headers": {
"Content-Type": "application/json"
},
"body": $utils.toJson($ctx.arguments.data)
}
}
ResponseMappingTemplate: |
#if($ctx.result.statusCode == 200)
$utils.toJson($ctx.result.headers)
#else
$utils.appendError($ctx.result.body, "$ctx.result.statusCode")
#end
SQSQueue:
Type: AWS::SQS::Queue
SQSSendMsgDs:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt ProxyApi.ApiId
Name: SQSSendMessageDataSource
Type: HTTP
ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
HttpConfig:
Endpoint: !Sub https://${AWS::Region}.queue.amazonaws.com/
AuthorizationConfig:
AuthorizationType: AWS_IAM
AwsIamConfig:
SigningRegion: !Ref AWS::Region
SigningServiceName: sqs
SQSSendMsgMutationResolver:
Type: AWS::AppSync::Resolver
Properties:
ApiId: !GetAtt ProxyApi.ApiId
TypeName: Mutation
FieldName: sendMessage
DataSourceName: !GetAtt SQSSendMsgDs.Name
RequestMappingTemplate: !Sub |
#set($action = "Action=SendMessage")
#set($version = "Version=2012-11-05")
#set($queueurl = "QueueUrl=${SQSQueue}")
#set($msgbody = "MessageBody=$ctx.arguments.data")
{
"version": "2018-05-29",
"method": "POST",
"resourcePath": "/",
"params": {
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"body": "$action&$version&$queueurl&$msgbody"
}
}
ResponseMappingTemplate: |
#if($ctx.result.statusCode == 200)
$utils.xml.toJsonString($ctx.result.body)
#else
$utils.appendError($utils.xml.toJsonString($ctx.result.body), "$ctx.result.statusCode")
#end
Kinesis:
Type: AWS::Kinesis::Stream
Properties:
ShardCount: 1
KinesisPutRecDs:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt ProxyApi.ApiId
Name: KinesisPutRecordDataSource
Type: HTTP
ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
HttpConfig:
Endpoint: !Sub https://kinesis.${AWS::Region}.amazonaws.com/
AuthorizationConfig:
AuthorizationType: AWS_IAM
AwsIamConfig:
SigningRegion: !Ref AWS::Region
SigningServiceName: kinesis
KinesisPutRecMutationResolver:
Type: AWS::AppSync::Resolver
Properties:
ApiId: !GetAtt ProxyApi.ApiId
TypeName: Mutation
FieldName: putRecord
DataSourceName: !GetAtt KinesisPutRecDs.Name
RequestMappingTemplate: !Sub |
{
"version": "2018-05-29",
"method": "POST",
"resourcePath": "/",
"params": {
"headers": {
"X-Amz-Target": "Kinesis_20131202.PutRecord",
"Content-Type": "application/x-amz-json-1.1"
},
"body": {
"StreamName": "${Kinesis}",
"Data": "$util.base64Encode($ctx.arguments.data)",
"PartitionKey": "$util.autoId()"
}
}
}
ResponseMappingTemplate: |
#if($ctx.result.statusCode == 200)
$util.toJson($ctx.result.body)
#else
$utils.appendError($util.toJson($ctx.result.body), "$ctx.result.statusCode")
#end
SNSTopic:
Type: AWS::SNS::Topic
SNSPublishDs:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt ProxyApi.ApiId
Name: SNSPublishDataSource
Type: HTTP
ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
HttpConfig:
Endpoint: !Sub https://sns.${AWS::Region}.amazonaws.com/
AuthorizationConfig:
AuthorizationType: AWS_IAM
AwsIamConfig:
SigningRegion: !Ref AWS::Region
SigningServiceName: sns
SNSPublishMutationResolver:
Type: AWS::AppSync::Resolver
Properties:
ApiId: !GetAtt ProxyApi.ApiId
TypeName: Mutation
FieldName: publish
DataSourceName: !GetAtt SNSPublishDs.Name
RequestMappingTemplate: !Sub |
#set($action = "Action=Publish")
#set($version = "Version=2010-03-31")
#set($topicarn = "TopicArn=${SNSTopic}")
#set($msg = "Message=$ctx.arguments.data")
{
"version": "2018-05-29",
"method": "POST",
"resourcePath": "/",
"params": {
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"body": "$action&$version&$topicarn&$msg"
}
}
ResponseMappingTemplate: |
#if($ctx.result.statusCode == 200)
$utils.xml.toJsonString($ctx.result.body)
#else
$utils.appendError($utils.xml.toJsonString($ctx.result.body), "$ctx.result.statusCode")
#end
GraphQLのスキーマです
schema.graphql
input PutObj {
attr1: String
attr2: String
}
type Query {
search(text: String): String
}
type Mutation {
putObject(data: PutObj): String
sendMessage(data: String): String
putRecord(data: String): String
publish(data: String): String
}
schema {
query: Query
mutation: Mutation
}
S3のPutObjectについてはattr1,attr2というキーに持つオブジェクトを、その他のオペレーションは全てString型のデータを引数に受け取って、String型のデータを返却する仕様にしています。実案件で利用する際は適宜typeを定義するようにして下さい。
※searchというクエリはダミーで置いてるだけなので無視して下さい。
このテンプレートをpackage & deployすれば環境構築完了です。
$aws cloudformation package --template-file template.yml --s3-bucket <適当なS3バケット> --output-template-file output.yml
$ aws cloudformation deploy --template-file output.yml --stack-name <適当なスタック名> --capabil
ities CAPABILITY_AUTO_EXPAND CAPABILITY_IAM
ここからは各サービス&オペレーションに指定するテンプレートの詳細を解説していきます。
KinesisのPutRecord
まずKinesisのPutRecordです。HTTPデータソースは以下のように指定しています。
KinesisPutRecDs:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt ProxyApi.ApiId
Name: KinesisPutRecordDataSource
Type: HTTP
ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
HttpConfig:
Endpoint: !Sub https://kinesis.${AWS::Region}.amazonaws.com/
AuthorizationConfig:
AuthorizationType: AWS_IAM
AwsIamConfig:
SigningRegion: !Ref AWS::Region
SigningServiceName: kinesis
こちらはあまり特記事項はありません。AuthorizationConfig
を指定し、AppSyncからエンドポイントにリクエストを発行する際にServiceRoleArn
で指定したIAMロールを使ってSIGv4の署名を付与するように設定しています。ServiceRoleArn
で指定するIAMロールはテンプレート内で作成しており、今回使用する4つのサービス&オペレーションに必要な権限を付与しています。
リクエストマッピングテンプレートは以下の通りです
RequestMappingTemplate: !Sub |
{
"version": "2018-05-29",
"method": "POST",
"resourcePath": "/",
"params": {
"headers": {
"X-Amz-Target": "Kinesis_20131202.PutRecord",
"Content-Type": "application/x-amz-json-1.1"
},
"body": {
"StreamName": "${Kinesis}",
"Data": "$util.base64Encode($ctx.arguments.data)",
"PartitionKey": "$util.autoId()"
}
}
}
AppSyncからエンドポイントにリクエストを発行する際のHTTPヘッダーとしてX-Amz-Target
にKinesis_20131202.PutRecord
を指定することで、実行するオペレーション=PutRecordを指定します。
ホディにはPutRecordに必要なパラメータである
- StreamName
- Data
- PartitionKey
を指定しています。StreamNameは!Sub ${Kinesis}
と指定することで、テンプレート内で作成したKinesisのストリーム名を参照しています。Dataは$util.base64Encode($ctx.arguments.data)
を指定し、ミューテーションの引数に渡されたdataをbase64でエンコードした文字列を設定、PartitionKeyに関しては、$util.autoId
でUUIDを採番して利用しています。本番利用する際は、何かしらのコンテキスト情報から設定するのが良いでしょう。
やってみる
実際にAppSyncのコンソールからミューテーションを実行してみましょう
mutation putRecord{
putRecord(data: "hoge")
}
OKです
S3のPutObject
続いてS3のPutObjectです。HTTPデータソースは以下のように指定しています。
S3PutObjectDs:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt ProxyApi.ApiId
Name: S3PutObjDataSource
Type: HTTP
ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
HttpConfig:
Endpoint: !Sub https://${S3Bucket}.s3.${AWS::Region}.amazonaws.com/
AuthorizationConfig:
AuthorizationType: AWS_IAM
AwsIamConfig:
SigningRegion: !Ref AWS::Region
SigningServiceName: s3
S3のPutObjectはAPIはエンドポイントにバケット名を含めたURLを指定する必要があるためEndpoint: !Sub https://${S3Bucket}.s3.${AWS::Region}.amazonaws.com/
としてテンプレート内で作成したバケット名を参照させています。
リクエストマッピングテンプレートは以下の通りです
RequestMappingTemplate: |
{
"version": "2018-05-29",
"method": "PUT",
"resourcePath": "/test.json",
"params": {
"headers": {
"Content-Type": "application/json"
},
"body": $utils.toJson($ctx.arguments.data)
}
}
resourcePath
に/test.jsonという固定値を設定しています。S3のAPIはPUT時に指定されたパスがオブジェクトキーになるので、この設定であればtest.jsonというオブジェクトキーでJSONファイルがアップロードされることになります。実際に使用する際ははミューテーションに渡された入力値や$context.identity
からAPI実行ユーザーの情報(ユーザーID等)を取得してresourcePath
に反映すると良いでしょう。今回はPutObjectでJSONファイルをアップロードできるようにしたかったので、HTTPヘッダのContent-Type
は固定でapplication/json
とし、HTTPボディは$utils.toJson($ctx.arguments.data)
でミューテーションの引数に渡されたdata
をJSONに変換してPUTするようにしています。
レスポンスマッピングテンプレートです。
ResponseMappingTemplate: |
#if($ctx.result.statusCode == 200)
$utils.toJson($ctx.result.headers)
#else
$utils.appendError($ctx.result.body, "$ctx.result.statusCode")
#end
正常終了のときにレスポンスボディが空で返ってくるので、代わりにレスポンスヘッダをJSONにして返却するようにしています。
やってみる
AppSyncのコンソールからミューテーションを実行してみます
mutation putObject{
putObject(data: {
attr1: "hoge"
attr2: "fuga"
})
}
OKです
SQSのSendMessage
続いてSQSのSendMessageです。HTTPデータソースは以下のように指定しています。
SQSSendMsgDs:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt ProxyApi.ApiId
Name: SQSSendMessageDataSource
Type: HTTP
ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
HttpConfig:
Endpoint: !Sub https://${AWS::Region}.queue.amazonaws.com/
AuthorizationConfig:
AuthorizationType: AWS_IAM
AwsIamConfig:
SigningRegion: !Ref AWS::Region
SigningServiceName: sqs
こちらは特に特記事項はありません。リクエストマッピングテンプレートは以下の通りです
RequestMappingTemplate: !Sub |
#set($action = "Action=SendMessage")
#set($version = "Version=2012-11-05")
#set($queueurl = "QueueUrl=${SQSQueue}")
#set($msgbody = "MessageBody=$ctx.arguments.data")
{
"version": "2018-05-29",
"method": "POST",
"resourcePath": "/",
"params": {
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"body": "$action&$version&$queueurl&$msgbody"
}
}
SQSのAPIでPOSTリクエストを使用する場合はクエリパラメーターをボディに設定する必要があります。
リクエストマッピングテンプレート内で
#set($action = "Action=SendMessage")
#set($version = "Version=2012-11-05")
#set($queueurl = "QueueUrl=${SQSQueue}")
#set($msgbody = "MessageBody=$ctx.arguments.data")
...略
"body": "$action&$version&$queueurl&$msgbody"
と指定することで、ミューテーションの引数data
に渡された文字列から最終的にリクエストボディに設定する
Action=SendMessage&Version=2012-11-05&QueueUrl=<キューのURL>&MessageBody=<メッセージ>
という文字列を生成しています。さらに、レスポンスのマッピングテンプレートを以下のように設定しています。
ResponseMappingTemplate: |
#if($ctx.result.statusCode == 200)
$utils.xml.toJsonString($ctx.result.body)
#else
$utils.appendError($utils.xml.toJsonString($ctx.result.body), "$ctx.result.statusCode")
#end
SQSのAPIから返却されるレスポンスがXML形式になっているため、$utils.xml.toJsonString($ctx.result.body)
を使ってJSONに変換しています。
やってみる
AppSyncのコンソールからミューテーションを実行してみます
mutation sendMessage{
sendMessage(data: "hoge")
}
OKです
SNSのPublish
最後にSNSのPublishです。HTTPデータソースは以下のように指定しています。
SNSPublishDs:
Type: AWS::AppSync::DataSource
Properties:
ApiId: !GetAtt ProxyApi.ApiId
Name: SNSPublishDataSource
Type: HTTP
ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
HttpConfig:
Endpoint: !Sub https://sns.${AWS::Region}.amazonaws.com/
AuthorizationConfig:
AuthorizationType: AWS_IAM
AwsIamConfig:
SigningRegion: !Ref AWS::Region
SigningServiceName: sns
こちらは特に特記事項はありません。
リクエストマッピングテンプレートは以下の通りです
#set($action = "Action=Publish")
#set($version = "Version=2010-03-31")
#set($topicarn = "TopicArn=${SNSTopic}")
#set($msg = "Message=$ctx.arguments.data")
{
"version": "2018-05-29",
"method": "POST",
"resourcePath": "/",
"params": {
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"body": "$action&$version&$topicarn&$msg"
}
}
多少パラメータ名など違いますが、考え方はSQSのSendMessageと同じですね。SNSもSQSと同様レスポンスがXML形式になるため、レスポンスのマッピングテンプレートにSQSと同一の内容を設定しています。
やってみる
AppSyncのコンソールからミューテーションを実行してみます
mutation publish{
publish(data: "hoge")
}
OKです
マッピングテンプレートを書くためのコツ
ここまで各サービスのマッピングテンプレートを見てきましたが、それぞれ微妙にAPIの仕様が異なることがお分かり頂けたかと思います。例えばKinesisの場合はリクエストヘッダのX-Amz-Target
を使ってアクションをSQSやSNSはリクエストボディで指定する といった具合です。では、別のサービスもしくは別のオペレーションに対してAppSyncをサービスプロキシとして構成したい場合はどのようにマッピングテンプレートを書いていけば良いのでしょうか?各サービスのAPI仕様書を漁っても良いのですが、個人的にオススメなのはAWS CLIの-debug
オプションです。AWS CLI実行時に--debug
オプションを指定することで、実際に発行されているリクエストの中身を確認することができます。AWS CLIからリクエストの構造の当たりを付けてPostmanから動作確認、動作が確認できたらテンプレートに落とし込んでいくという流れがオススメです。
例としてSQSのSendMessageについて考えてみましょう。まずはAWS CLIを--debug
付きで実行します。
$aws sqs send-message --message-body hoge --queue-url <SQSのエンドポイント> --debug
デバッグ出力が流れてくるので、Making request for...
と表示されている部分を探してHTTPボディとヘッダの中身を確認します。
※見やすいように少し加工しています
- MainThread - botocore.endpoint - DEBUG - Making request for OperationModel(name=SendMessage) with params:
{
'url_path': '/',
'query_string': '',
'method': 'POST',
'headers': {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'User-Agent': 'aws-cli/1.18.36 Python/3.6.5 Darwin/18.7.0 botocore/1.15.36'
},
'body': {
'Action': 'SendMessage',
'Version': '2012-11-05',
'QueueUrl': 'https://sqs.ap-northeast-1.amazonaws.com/123456789012/AppSyncServiceProxy-SQSQueue-1169XBJXHNBYX',
'MessageBody': 'hoge'
},
'url': 'https://ap-northeast-1.queue.amazonaws.com/',
'context': {'client_region': 'ap-northeast-1',
'client_config': <botocore.config.Config object at 0x10ea617f0>,
'has_streaming_input': False,
'auth_type': None}
}
リクエストの構造が確認できたらPostmanからリクエストを試してみます。Authorizationの設定から適宜必要事項を入力します。
続いてAWS CLIのデバッグ出力から確認した内容をもとにリクエストヘッダとリクエストボディを埋めていきます。一通り埋め終わったら、実際にAPIを実行してみてエンドポイントから正常応答が返却されるかを確認しましょう。
正常応答が返ってくればHTTPヘッダーとボディが正しく構成できているということです。ここまで確認できたらPostmanのCodeリンクをクリックします。
左側のペインでHTTPを選択することで生のHTTPリクエストが確認できるので、リクエストマッピングテンプレートの解決結果 = 生のHTTPリクエスト となるようにテンプレートを編集していきましょう。
まとめ
AppSyncをAWSサービスのプロキシとして構成する方法についてご紹介しました。簡単な権限チェックや固定値設定、加工程度であればLambdaを挟まなくてもリクエストマッピングテンプレートだけで対応することができるので、うまく活用して開発を高速化していきたいですね!