AWSで開発するサーバレスAPIバックエンド
Serverless Meetup Tokyo #14で発表しました。
www.slideshare.net
AWSで開発するサーバレスAPIバックエンド
Serverless Meetup Tokyo #14
2019.09.19. 三宅 暁
自己紹介
- 三宅 暁 (ミヤケ アキラ)
- フリーランス(8月から)
- フロントエンド
- SPAをJS / HTML / CSSまるごと設計・実装できる程度の能力
- Reactが得意、あとVueとAngular
- バックエンド
自己紹介
主なサーバレス関連のライブラリ
- serverless-import-swagger
- OpenAPI形式のAPI定義からserverless.ymlのhandlerの定義を生成するツール
- 最近のバージョンアップで設定まわりが洗練されました
- node-lambda-utilities
- lambdaのhandlerのテストをしやすくするツール
- contextやcallbackのモックの生成とhandlerの実行
今日話すこと
GraphQL or RESTful ?
- 参考
- 個人的な考え
プロジェクトのサンプル
https://github.com/AKIRA-MIYAKE/aws-serverless-sample
ベンダーロックインしないために
- Amplifyは使わない
- フロントエンドがAWSにロックインしないように
- Apollo Clientを利用
- Cognitoを用いるが、OpenID Providerとして利用する
- 認可処理の拡張性を確保するため
- @axa-fr/react-oidc-contextを利用
APIの仕様
- メッセージの作成・取得・一覧・削除
- 取得・一覧はログインしなくても呼び出せる
- 一覧は作成日での降順
- 作成・削除はログインが必要
- 自分が作成したメッセージのみ削除できる
schema.graphql
type Query @aws_api_key @aws_oidc { readMessage(id: ID!): Message listMessage(limit: Int, nextToken: String): MessageCollection! } type Mutation @aws_oidc { createMessage(input: CreateMessageInput!): Message! deleteMessage(input: DeleteMessageInput!): Message }
schema.graphql
type Message @aws_api_key @aws_oidc { id: ID! value: String! createdBy: String! createdAt: String! }
- createdByはCognitoが発行するsubを用いる
schema.graphql
type MessageKeyValue @aws_api_key @aws_oidc { id: ID! key: String! value: String! message: Message } type MessageCollection @aws_api_key @aws_oidc { items: [MessageKeyValue!]! nextToken: String scannedCount: Int }
- MessageKeyValueはノンコーディングを達成しつつDynamoDBデータモデリング虎の巻:第参巻 〜実践編〜の汎用的なGSIを実現するために生まれたもの
- スキーマが不自然で1+Nになるため、速やかに修正されるべき箇所
- パイプラインリゾルバの利用がおそらく適切
Mutation.createMessage.request.vtl
#set($id = $util.autoId()) #set($now = $util.time.nowISO8601()) #set($sub = $context.identity.sub) #set($items = [...]) { "version" : "2018-05-29", "operation" : "BatchPutItem", "tables" : { "${DynamoDB}": $utils.toJson($items) } }
- BatchPutItemを用いてクエリ用のレコードを同時に登録
Mutation.createMessage.request.vtl
#set($items = [ { "id": $util.dynamodb.toString("${id}"), "key": $util.dynamodb.toString("message"), "value": $util.dynamodb.toString("${context.arguments.input.value}"), "createdBy": $util.dynamodb.toString("${sub}"), "createdAt": $util.dynamodb.toString("${now}") }, { "id": $util.dynamodb.toString("${id}"), "key": $util.dynamodb.toString("message/createdBy"), "value": $util.dynamodb.toString("${sub}") }, { "id": $util.dynamodb.toString("${id}"), "key": $util.dynamodb.toString("message/createdAt"), "value": $util.dynamodb.toString("${now}") } ])
Query.listMessage.request.vtl
{ "version": "2017-02-28", "operation": "Query", "query": { "expression": "#key = :key", "expressionValues": { ":key": $util.dynamodb.toStringJson("message/createdAt") }, "expressionNames": { "#key": "key" } }, "index": "key-value-index", "limit": #if($context.arguments.limit) $context.arguments.limit #else 10 #end, "nextToken": #if($context.arguments.nextToken) "${context.arguments.nextToken}" #else null #end, "scanIndexForward": false }
MessageKeyValue.message.request.vtl
{ "version" : "2017-02-28", "operation" : "GetItem", "key" : { "id": $util.dynamodb.toStringJson("${context.source.id}"), "key": $util.dynamodb.toStringJson("message") } }
- listMessageが返す、MessageKeyValueのmessageフィールドのマッピング
- 1件ごとにGetItemが実行されるため、1+Nに
Mutation.deleteMessage.request.vtl
{ "version": "2017-02-28", "operation": "DeleteItem", "key": { "id": $util.dynamodb.toStringJson("${context.arguments.input.id}"), "key": $util.dynamodb.toStringJson("message") }, "condition": { "expression": "createdBy = :createdBy", "expressionValues": { ":createdBy": $util.dynamodb.toStringJson("${context.identity.sub}") } } }
- BatchDeleteではconditionが使えないため、メインのレコードのみの削除
- パイプラインリゾルバの利用がおそらく適切
serverless-appsync-plugin
https://github.com/sid88in/serverless-appsync-plugin
- AppSyncの設定を簡潔に定義してデプロイできるServerless Frameworkのプラグイン
- mappingTemplates(リゾルバ)は
{type}.{field}.request.vtl
と{type}.{field}.respose.vtl
のファイルをデフォルトで認識 - vtlファイルで環境変数を設定できる
- CloudFormationの出力を利用可能
serverless.yml
custom: appSync: substitutions: DynamoDB: sls-sample-${env:ENV}
- Batch系処理ではvtlで直接テーブル名の指定が必要
- ステージごとにテーブル名が異なるため、環境変数として定義
- vtlでは
$DynamoDB
のように、変数としてアクセス可能
serverless.yml
custom: appSync: additionalAuthenticationProviders: - authenticationType: OPENID_CONNECT openIdConnectConfig: issuer: !Join - '' - - https://cognito-idp.ap-northeast-1.amazonaws.com/ - !Ref UserPool
- CognitoをOPENID_CONNECT承認モードとして利用するための設定
- issuerのURLがUserPoolのIDを含んだものとなるので、resourcesで出力された値を参照
Cognito UserPoolのアプリの統合の設定
- アプリクライアントはCloudFormationで作れるが、アプリの統合は未サポート
- この箇所だけコンソールかCLIからの設定が必要
アプリの統合 / アプリクライアントの設定
- 「有効な ID プロバイダ」
- 「Cognito User Pool」を選択
- 「サインインとサインアウトの URL」
アプリの統合 / ドメイン名
フロントエンド
- 動作確認のためのシンプルなReactアプリ
- ログイン / ログアウト
- listMessageの結果の表示
- createMessageの実行
- deleteMessageの実行
ログイン / ログアウト (AppHeader)
const AppHeader = () => { const { login, logout, oidcUser } = useReactOidc(); return ( <h1>AWS SLS Sample</h1> { oidcUser ? <div><button onClick={logout}>Logout</button></div> : <div><button onClick={login}>Login</button></div> } {oidcUser && ( <span>sub(generated by cognito): {oidcUser.profile.sub}</span> )} ); };
- @axa-fr/react-oidc-contextのフックを利用
- Cognitoのページにリダイレクトされ、ユーザ登録まで処理してくれる
認証方式の切り替え (ApolloContext)
const client = useMemo( () => { if (oidcUser && oidcUser.id_token) { return new ApolloClient({ uri: process.env.REACT_APP_GRAPHQL_URL, request: operation => { operation.setContext({ headers: { Authorization: `${oidcUser.id_token}` }, }); }, }); } else { return new ApolloClient({ uri: process.env.REACT_APP_GRAPHQL_URL, request: operation => {} operation.setContext({ headers: { 'x-api-key': process.env.REACT_APP_GRAPHQL_API_KEY }, }); }, }); } }, [oidcUser] );
- ログイン状態に応じて、クライアントを切り替えるコンテキストを定義
フロントエンドのベンダーロックイン
実サービスで利用するために検討が必要な箇所
- AppSyncのAPI_KEY承認モードの利用
- 厳密なログアウト
- @axa-fr/react-oidc-contextはトークンを削除することでログアウト
- 厳密な制御を行うなら、ログアウトエンドポイントを用いてCognito側のログアウトも必要
- ホストされたサインアップ、サインインページとリダイレクト
まとめ
- AppSync + DynamoDB + Cognitoを使えば、ノンコーディングでGraphQLのAPIバックエンドを作ることができる
- パイプラインリゾルバを利用すれば、実サービスでもある程度までは対応できそう
- serverless-appsync-pluginを利用することで(ほぼ)自動でデプロイが可能
- AWSにほとんど依存しないフロントエンドの開発も可能
- 非ログイン状態でのAppSyncへのアクセス制御は検証が必要
Marpでスライドを作ると、そのまま貼り付けれるから便利。