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

CloudFormationでAppSync入門

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

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

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

構築する環境

The Diagram of introduction to AppSync with CloudFormation.

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

https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/system-overview-and-architecture.html

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テンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/041

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

データソース用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に関する詳細は、以下のページをご確認ください。

あわせて読みたい
DynamoDB入門 – レビューデータ用DB構築 【DynamoDBでシンプルなレビューデータ用DBを構築する】 AWS DVAの出題範囲の1つでもある、AWSのサービスによる開発に関する内容です。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バケットに設置したファイルを参照する方法があります。
今回は前者を選択し、同名のプロパティで設定を行います。

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

https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/configuring-resolvers.html

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

  • 実行する内容は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の公式サイトでは、いくつかクライアントが紹介されています。

https://graphql.org/code/#python

その中で、今回はGQLを使用します。

https://github.com/graphql-python/gql

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

あわせて読みたい
CFNでLambdaレイヤー作成 【CloudFormationでLambdaレイヤー作成】 本ページでは、CloudFormationでLambdaレイヤーを作成する方法を確認します。 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の公式サイトを参照して実装しています。

https://gql.readthedocs.io/en/latest/usage/basic_usage.html

https://gql.readthedocs.io/en/v3.0.0a6/usage/variables.html

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でLambdaを作成する3パータン(S3/インライン/コンテナ) 【CloudFormationでLambdaを作成する】 CloudFormationでLambdaを作成する場合、大別すると以下の3パターンあります。 S3バケットにコードをアップロードする インライ...

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

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

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 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 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)で実行する方法を確認しました。