AWS

CFNでAppSync入門 – データソース:DynamoDB

スポンサーリンク
CloudFormationでAppSync入門 AWS
スポンサーリンク
スポンサーリンク

CloudFormationを使用してAppSync環境を構築

AppSyncはAWSが提供するマネージドサービスの1つで、容易にGraphQL APIを構築することができます。

本ページはAppSync入門ということで、CloudFormationを使用して基本的なAppSync環境を構築します。

構築する環境

Amazon.co.jp: AWS認定資格試験テキスト AWS認定ソリューションアーキテクト - アソシエイト 改訂第2版 : NRIネットコム株式会社, 佐々木 拓郎, 林 晋一郎, 金澤 圭: 本
Amazon.co.jp: AWS認定資格試験テキスト AWS認定ソリューションアーキテクト - アソシエイト 改訂第2版 : NRIネットコム株式会社, 佐々木 拓郎, 林 晋一郎, 金澤 圭: 本
The Diagram of introduction to AppSync with CloudFormation.

AppSyncの基本的な用語は以下のページをご確認ください。

システムの概要とアーキテクチャ - AWS AppSync
AWS AppSync のシステムの概要とアーキテクチャ

GraphQL APIのエンドポイントとしてAppSyncを構築します。
AppSyncデータソースとしてDynamoDBを選択します。

AppSyncを通じて、以下のデータを保存することを考えます。

  • ID
  • 日時情報
  • 日時のエポック秒

次に上記のデータをGraphQLで操作することを考えます。
以下のミューテーションとクエリを作成します。

  • ミューテーション
    • addDatetime:日時情報を保存する
  • クエリ
    • listDatetimes:保存されている日時情報を全て取得する
    • getDatetime:IDを指定して、特定の日時情報を取得する

GraphQLクライアントとして、3つのLambda関数を作成します。
それぞれミューテーション・クエリに対応しています。

  • 関数1:addDatetime
  • 関数2:listDatetimes
  • 関数3:getDatetime

3つの関数はPython3.8で作成し、Function URLを有効化することで、インターネット越しに実行できるように設定します。

CloudFormationテンプレートファイル

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

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

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

データソース用DynamoDB

AppSyncのデータソースには、様々なサービスが選択可能です。

データソースの例としては、NoSQL データベース、リレーショナルデータベース、AWS Lambda 関数、HTTP API などがあります。

システムの概要とアーキテクチャ

今回はDynamoDBをデータソースとして使用します。
以下がDynamoDBテーブルを作成するCloudFormationテンプレートです。

Resources: Table: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: id AttributeType: S BillingMode: PAY_PER_REQUEST KeySchema: - AttributeName: id KeyType: HASH TableName: !Sub "${Prefix}-table"
Code language: YAML (yaml)

idアトリビュートをパーティションキーに設定しますので、「HASH」を設定します。
DynamoDBに関する詳細は、以下のページをご確認ください。

AppSync

AppSyncを構築するためには、以下の4つのリソースを作成する必要があります。

  • API
  • データソース
  • スキーマ
  • リゾルバー

また今回は以下も合わせて作成します。

  • APIキー

API

AppSyncのメインリソースであるAPIを確認します。

Resources: GraphQLApi: Type: AWS::AppSync::GraphQLApi Properties: AuthenticationType: API_KEY Name: !Sub "${Prefix}-GraphQLApi"
Code language: YAML (yaml)

特別な設定は不要です。

ポイントは認証に関する設定です。

AWS AppSync GraphQL API を操作するためにアプリケーションを認証するには、5 つの方法があります。

認証と認可

今回はAPIキーを使用する方法で認証を行います。
よってAuthenticationTypeプロパティに「API_KEY」を設定します。

データソース

データソースはAppSyncで読み書きするデータが保存されているデータストレージです。
先述の通り、今回はDynamoDBをデータソースとして使用するため、そのための設定を行います。

Resources: DataSource: Type: AWS::AppSync::DataSource Properties: ApiId: !GetAtt GraphQLApi.ApiId DynamoDBConfig: AwsRegion: !Ref AWS::Region TableName: !Ref TableName UseCallerCredentials: false Versioned: false Name: DataSource ServiceRoleArn: !GetAtt DataSourceRole.Arn Type: AMAZON_DYNAMODB
Code language: YAML (yaml)

Typeプロパティで、データソースタイプを設定します。
「AMAZON_DYNAMODB」を設定します。

DynamoDBConfigプロパティで、DynamoDBをデータソースにするための詳細な設定を行います。
ポイントはTableNameプロパティです。
先述のDynamoDBテーブルの名前を指定します。

以下はデータソース用の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: - dynamodb:DeleteItem - dynamodb:GetItem - dynamodb:PutItem - dynamodb:Query - dynamodb:Scan - dynamodb:UpdateItem Resource: - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}" - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}/*"
Code language: YAML (yaml)

データソースとして指定したDynamoDBテーブルに対して、読み書きするための権限を与える内容となっています。

スキーマ

スキーマはGraphQL用語です。
APIの仕様を定めるもので、保存するデータの構造や、クエリ・ミューテーションの働きを定義します。

Resources: GraphQLSchema: Type: AWS::AppSync::GraphQLSchema Properties: ApiId: !GetAtt GraphQLApi.ApiId Definition: | schema { query: Query mutation: Mutation } type Query { getDatetime(id: ID!): Datetime listDatetimes: [Datetime] } type Mutation { addDatetime(input: AddDatetimeInput!): Datetime } type Datetime { id: ID! datetime: String epoch: Int } input AddDatetimeInput { datetime: String epoch: Int } #DefinitionS3Location:
Code language: YAML (yaml)

CloudFormationを使用してAppSyncスキーマリソースを作成する場合、2つの方法があります。
テンプレートファイルに直接記載するか、S3バケットに設置したスキーマファイルを参照します。
今回は前者で定義します。
Definitionプロパティにスキーマを記載します。

スキーマの詳細はGraphQLの話なので割愛しますが、ポイントはルート(「schema」の記述の箇所)です。
ルートは必須です。

すべてのスキーマには、処理のためにこのルートがあります。これは、ルートクエリ型を追加するまでは処理に失敗します。

スキーマの設計

他の記述は、環境の項目で記述した内容を表現するものとなっています。

リゾルバー

リゾルバーはAppSyncのGraphQL APIとしてのバックエンド処理を定義するリソースです。
クライアントからGraphQLでミューテーション・クエリを受けて、リクエストに対してどのように処理し、レスポンスするかを定義します。
リゾルバーはミューテーション・クエリごとに作成します。

ミューテーション用リゾルバー

まず現在日時情報を登録するミューテーション(addDatetime)用のリゾルバーから確認します。

Resources: AddDatetimeResolver: Type: AWS::AppSync::Resolver Properties: ApiId: !GetAtt GraphQLApi.ApiId DataSourceName: !GetAtt DataSource.Name FieldName: addDatetime Kind: UNIT RequestMappingTemplate: | { "version": "2017-02-28", "operation": "PutItem", "key": { "id": $util.dynamodb.toDynamoDBJson($util.autoId()), }, "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input), "condition": { "expression": "attribute_not_exists(#id)", "expressionNames": { "#id": "id", }, }, } #RequestMappingTemplateS3Location: ResponseMappingTemplate: | $util.toJson($context.result) #ResponseMappingTemplateS3Location: TypeName: Mutation
Code language: YAML (yaml)

FieldNameプロパティはスキーマで定義したミューテーション名を、TypeNameに「Mutation」を設定します。

ポイントはリクエストマッピングとレスポンスマッピングに関する設定です。

リゾルバーは、変換と実行のロジックが含まれているリクエストマッピングテンプレートとレスポンスマッピングテンプレートで構成されます。

システムの概要とアーキテクチャ

両方ともCloudFormationテンプレートファイルに直書きするか、S3バケットに設置したファイルを参照する方法があります。
今回は前者を選択し、同名のプロパティで設定を行います。

両プロパティは以下のページを参考にして設定しています。

リゾルバーの設定 - AWS AppSync
AWS AppSync のクイックスタートガイドでの前提条件。

リクエストマッピングテンプレートの概要は以下の通りです。

  • 実行する内容はDynamoDBの「PutItem」
  • パーティションキーは渡された値の「id」として、自動採番されるされるように設定
  • まだ未登録のIDの場合のみ保存する

レスポンスマッピングテンプレートの概要は以下の通りです。

  • DynamoDBから返ってきた値をJSON化して返す
クエリ用リゾルバー1

保存されている全ての日時情報を取得するためのクエリ(listDatetimes)用のリゾルバーを確認します。

Resources: ListDatetimesResolver: Type: AWS::AppSync::Resolver Properties: ApiId: !GetAtt GraphQLApi.ApiId DataSourceName: !GetAtt DataSource.Name FieldName: listDatetimes Kind: UNIT RequestMappingTemplate: | { "version": "2017-02-28", "operation": "Scan", } #RequestMappingTemplateS3Location: ResponseMappingTemplate: | $util.toJson($context.result.items) #ResponseMappingTemplateS3Location: TypeName: Query
Code language: YAML (yaml)

TypeNameに「Query」を設定します。

リクエストマッピングテンプレートの概要は以下の通りです。

  • 実行する内容はDynamoDBの「Scan」

レスポンスマッピングテンプレートの概要は以下の通りです。

  • DynamoDBから返ってきた値をリストとしてJSON化して返す

ポイントはレスポンスの形式です。
スキーマの設定に合わせて、オブジェクトではなく、リストとして返す必要があります。

項目のリストに対する context オブジェクト ($ctx としてエイリアスが作成された) の形式は $context.result.items です。

リゾルバーの設定
クエリ用リゾルバー2

IDを指定して、日時情報を取得するためのクエリ(getDatetime)用のリゾルバーを確認します。

Resources: GetDatetimeResolver: Type: AWS::AppSync::Resolver Properties: ApiId: !GetAtt GraphQLApi.ApiId DataSourceName: !GetAtt DataSource.Name FieldName: getDatetime Kind: UNIT RequestMappingTemplate: | { "version": "2017-02-28", "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), }, } #RequestMappingTemplateS3Location: ResponseMappingTemplate: | $util.toJson($context.result) #ResponseMappingTemplateS3Location: TypeName: Query
Code language: YAML (yaml)

リクエストマッピングテンプレートの概要は以下の通りです。

  • 実行する内容はDynamoDBの「GetItem」
  • パーティションキーとして引数で渡されたIDを使用する

レスポンスマッピングテンプレートの概要は以下の通りです。

  • DynamoDBから返ってきた値をJSON化して返す

APIキー

今回作成するAppSync APIはAPIキーで認証しますので、APIキーを作成します。

Resources: ApiKey: Type: AWS::AppSync::ApiKey Properties: ApiId: !GetAtt GraphQLApi.ApiId
Code language: YAML (yaml)

特別な設定は不要です。
作成したAPIのIDを指定するだけです。

Lambda

Lambdaレイヤー

PythonからGraphQLを実行するために、クライアントライブラリを使用します。
GraphQLの公式サイトでは、いくつかクライアントが紹介されています。

GraphQL Code Libraries, Tools and Services
A query language for your API — GraphQL provides a complete description of the data in your API, gives clients the power to ask for exactly what they need and n...

その中で、今回は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.

このライブラリは3つの関数で使用することになりますので、Lambdaレイヤーを作成し、そこに含めることにします。
Lambdaレイヤーに関する詳細は、以下のページをご確認ください。

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

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

Lambda関数用テンプレート

ミューテーション・クエリを実行するクライアントして、3つのLambda関数を作成します。
3関数構築用のテンプレートはほとんど同じですので、代表して関数1を確認します。

Resources: Function1: Type: AWS::Lambda::Function Properties: Environment: Variables: API_KEY: !Ref ApiKey GRAPHQL_URL: !Ref GraphQLUrl Code: S3Bucket: !Ref CodeS3Bucket S3Key: !Ref CodeS3Key1 FunctionName: !Sub "${Prefix}-function-01" Handler: !Ref Handler Layers: - !Ref LambdaLayer Runtime: !Ref Runtime Role: !GetAtt FunctionRole.Arn Timeout: !Ref Timeout
Code language: YAML (yaml)

ポイントは環境変数(Environmentプロパティ)です。
先述のAPIキーと、APIのエンドポイントURLを環境変数に設定します。
このように環境変数を定義することによって、関数の内部から変数の値を参照することができるようになります。

関数1(addDatetime)

関数1のコードを確認します。
関数1は現在日時情報を保存するミューテーションを実行します。

import datetime 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) mutation = gql( """ mutation AddDatetimeMutation($adddatetimeinput: AddDatetimeInput!) { addDatetime(input: $adddatetimeinput) { id datetime epoch } } """) def lambda_handler(event, context): now = datetime.datetime.now() now_str = now.strftime('%Y%m%d%H%M%S%f') epoch_time = int(time.mktime(now.timetuple())) params = { 'adddatetimeinput': { 'datetime': now_str, 'epoch': epoch_time } } result = client.execute(mutation, variable_values=params) return { 'statusCode': 200, 'body': json.dumps(result, indent=2) }
Code language: Python (python)

GraphQLを実行する方法は、GQLの公式サイトを参照して実装しています。

Basic usage — gql 3 3.3.0 documentation
Using variables — gql 3 3.0.0a6 documentation

GraphQLを実行する上でのポイントはAPIキーです。
CloudFormationテンプレートで定義した環境変数を参照することができます。
今回はAPIキー認証ですので、APIキーをHTTPヘッダーに設定します。

クライアントでは、API キーをヘッダー x-api-key で指定します。

認証と認可

コード内容の概要ですが、現在日時とエポック秒を取得後、GraphQLのパラメータとして設定して実行します。
その後、実行結果をユーザーに返します。

関数2(listDatetimes)

保存されている全データを取得するクエリを実行する関数2のコードを確認します。

関数2のコードを確認します。
関数2は保存されている全データを取得するクエリを実行します。

import json import os 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) query = gql( """ query ListDatetimesQuery { listDatetimes { id datetime } } """) def lambda_handler(event, context): result = client.execute(query) return { 'statusCode': 200, 'body': json.dumps(result, indent=2) }
Code language: Python (python)

シンプルにGraphQLを実行し、実行結果をユーザーに返します。

細かなポイントは、取得するデータを制限している点です。
エポック秒を除き、IDと日時情報のみを返すようにクエリを設定します。

関数3(getDatetime)

関数3のコードを確認します。
関数3はIDを指定して、保存されているデータを取得するクエリを実行します。

import json import os 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) query = gql( """ query GetDatetimeQuery($id_: ID!) { getDatetime(id: $id_) { id datetime epoch } } """) def lambda_handler(event, context): if not 'queryStringParameters' in event or ( not 'id' in event['queryStringParameters']): return { 'statusCode': 200, 'body': 'No ID.' } id_ = event['queryStringParameters']['id'] params = { 'id_': id_ } result = client.execute(query, variable_values=params) return { 'statusCode': 200, 'body': json.dumps(result, indent=2) }
Code language: Python (python)

URLクエリパラメータでIDを指定します。
IDをパラメータとしてGraphQLを実行し、実行結果をユーザーに返します。

こちらのクエリでは、3つのデータ(ID、日時データ、エポック秒)を返すように設定します。

環境構築

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

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

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

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

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

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

  • DynamoDBテーブル:fa-041-table
  • 関数1のFunction URL:https://xay4g7fx377bslkd2g6scsdev40ascum.lambda-url.ap-northeast-1.on.aws/
  • 関数2のFunction URL:https://pbxyn5tnpcicwy6kofraiewfem0yrvaa.lambda-url.ap-northeast-1.on.aws/
  • 関数3のFunction URL:https://vo6ijjjsliqzsbuetfdflp5g4i0qvadm.lambda-url.ap-northeast-1.on.aws/

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

AppSync API and API key.

APIが作成され、APIのURLが作成されていることがわかります。
またAPIキーも作成されていることがわかります。

データソースを確認します。

AppSync Data Source.

DynamoDBテーブルがデータソースとして登録されていることがわかります。

スキーマを確認します。

AppSync Schema.

CloudFormationテンプレートファイルで記載した通りにスキーマが作成されています。

作成された3つのリゾルバーを確認します。

AppSync Resolver 1.
AppSync Resolver 2
AppSync Resolver 3

こちらもCloudFormationテンプレートファイルで設定した通りの内容です。

動作確認

準備が整いましたので、各Function URLにアクセスします。
まず関数1です。
関数1は日時情報を保存する働きをします。

The Result of AppSync GraphQL mutation.

正常にミューテーションが実行されました。
日時情報が保存され、保存された値が返ってきました。
このようにAppSyncのGraphQL APIを通じて、データを書き込むことができました。

次に関数2です。
関数2は保存されている日時情報を全て取得する働きをします。

The Result of AppSync GraphQL query 1.

正常にクエリが実行されました。
保存されているデータの内、エポック秒を除いたIDと日時データのみが返ってきました。
このようにAppSyncのGraphQL APIを通じて、ユーザーが必要な形式でデータを取得することができました。

最後に関数3です。
関数3はIDを指定して日時情報を取得する働きをします。

The Result of AppSync GraphQL query 2.

URLクエリパラメータでIDを指定し、正常にクエリが実行されました。
今回のクエリでは、3種類の全てのデータを取得するクエリを実行していますので、それに応じた結果となります。

まとめ

AppSyncの入門ということで、CloudFormationを使用して、AppSync環境を構築しました。
環境構築を通じて、AppSynを構成するいくつかのリソースと、AppSyncで構築したGraphQL APIをLambda関数(Python)で実行する方法を確認しました。

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