AWS

AppSync – データソース:RDS(Aurora Serverless)

AppSyncのデータソースにRDS(Aurora Serverless)を設定する

AppSyncは以下のサービスの中からデータソースを選択することができます。

  • Lambda
  • DynamoDB
  • OpenSearch
  • None
  • HTTPエンドポイント
  • RDS

今回はRDSをデータソースにする構成を確認します。
RDSとして指定できるリソースはAurora Serverlessです。

なおAppSyncの基本的な解説と、DynamoDBをデータソースとする構成については、以下のページをご確認ください。

構築する環境

Diagram of AppSync - Data Source: RDS(Aurora Serverless)

データソースとして動作するAurora Serverlessを作成します。

AppSyncからAurora Serverlessを操作するために、スキーマ・リゾルバを定義します。
本ページは、以下のAWS公式ページを参考に進めます。

のチュートリアル: Aurora Serverless - AWS AppSync
AWS AppSync の Aurora Serverless チュートリアル。

2つのLambda関数を作成します。
関数のランタイム環境はPython3.8とします。

1つ目の関数はCloudFormationカスタムリソースに関連付けて、スタック作成時に実行されるように設定します。
この関数の働きは、Aurora Serverless DBを初期化することです。

2つ目の関数はGraphQL APIを実行するクライアントとして設定します。
Function URLを有効化し、URLクエリパラメータで実行する操作を指定できるようにします。

CloudFormationテンプレートファイル

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

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

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

本ページはAppSyncのデータソースにAurora Serverlessを指定する方法を中心に取り上げます。

Aurora Serverless作成に関しては、以下のページをご確認ください。

Aurora ServerlessのData APIを有効化する方法については、以下のページをご確認ください。

データソース

Resources:
  DataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId
      Name: DataSource
      RelationalDatabaseConfig:
        RdsHttpEndpointConfig:
          AwsRegion: !Ref AWS::Region
          AwsSecretStoreArn: !Ref SecretArn
          DatabaseName: !Ref DBName
          DbClusterIdentifier: !Ref DBClusterArn
        RelationalDatabaseSourceType: RDS_HTTP_ENDPOINT
      ServiceRoleArn: !GetAtt DataSourceRole.Arn
      Type: RELATIONAL_DATABASE
Code language: YAML (yaml)

ポイントはTypeプロパティです。
Aurora Serverlessをデータソースとする場合は、「RDS_HTTP_ENDPOINT」を指定します。

またRelationalDatabaseConfigプロパティで詳細を設定します。
Aurora ServerlessのクラスターのARNや、クラスターに接続するために使用するSecrets Managerのシークレット等を指定します。

データソース用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:
                  - rds-data:DeleteItems
                  - rds-data:ExecuteSql
                  - rds-data:ExecuteStatement
                  - rds-data:GetItems
                  - rds-data:InsertItems
                  - rds-data:UpdateItems
                Resource:
                  - !Ref DBClusterArn
                  - !Sub "${DBClusterArn}:*"
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource:
                  - !Ref SecretArn
                  - !Sub "${SecretArn}:*"
Code language: YAML (yaml)

AppSyncがAurora Serverlessに接続して、SQL文を実行するためのIAMロールです。
AWS公式サイトで紹介されているポリシーを参考に作成しました。

スキーマ

Resources:
  GraphQLSchema:
    Type: AWS::AppSync::GraphQLSchema
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId
      Definition: |
        type Mutation {
          createPet(input: CreatePetInput!): Pet
          updatePet(input: UpdatePetInput!): Pet
          deletePet(input: DeletePetInput!): Pet
        }
        
        input CreatePetInput {
            type: PetType
            price: Float!
        }
        
        input UpdatePetInput {
        id: ID!
            type: PetType
            price: Float!
        }
        
        input DeletePetInput {
            id: ID!
        }
        
        type Pet {
            id: ID!
            type: PetType
            price: Float
        }
        
        enum PetType {
            dog
            cat
            fish
            bird
            gecko
        }
        
        type Query {
            getPet(id: ID!): Pet
            listPets: [Pet]
            listPetsByPriceRange(min: Float, max: Float): [Pet]
        }
        
        schema {
            query: Query
            mutation: Mutation
        }
Code language: YAML (yaml)

AWS公式ページを参考に、スキーマを定義します。

リゾルバ

リゾルバもAWS公式ページを参考に作成します。

参考としてcreatePetミューテーション用リゾルバを取り上げます。

Resources:
  CreatePetResolver:
    Type: AWS::AppSync::Resolver
    DependsOn:
      - GraphQLSchema
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId
      DataSourceName: !GetAtt DataSource.Name
      FieldName: createPet
      Kind: UNIT
      RequestMappingTemplate: |
        #set($id=$utils.autoId())
        {
            "version": "2018-05-29",
            "statements": [
                "insert into Pets VALUES ('$id', '$ctx.args.input.type', $ctx.args.input.price)",
                "select * from Pets WHERE id = '$id'"
            ]
        }
      ResponseMappingTemplate: |
        $utils.toJson($utils.rds.toJsonObject($ctx.result)[1][0])
      TypeName: Mutation
Code language: YAML (yaml)

RequestMappingTemplateおよびResponseMappingTemplateプロパティに、公式ページで紹介されているテンプレートマッピングを記述します。

(参考) GraphQLクライアントLambda関数

AppSyncによるGraphQL APIを実行するクライアントとして、Lambda関数を作成します。
この関数のFunction URLを有効化します。
Function URLに関する詳細は、以下のページをご確認ください。

以下に、関数で実行するPythonコードを取り上げます。

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)

CREATE_PET = 'createPet'
UPDATE_PET = 'updatePet'
DELETE_PET = 'deletePet'
GET_PET = 'getPet'
LIST_PETS = 'listPets'
LIST_PETS_BY_PRICE_RANGE = 'listPetsByPriceRange'


def lambda_handler(event, context):
  operation = ''
  document = None
  result = None
  
  if not 'queryStringParameters' in event or (
      not 'operation' in event['queryStringParameters']):
    operation = LIST_PETS
  else:
    operation = event['queryStringParameters']['operation']
    
  if operation == CREATE_PET:
    document = gql(
      """
      mutation add($type: PetType!, $price: Float!) {
        createPet(input: {
          type: $type,
          price: $price
        }){
          id
          type
          price
        }
      }
      """
      )
      
    params = {
      'type': event['queryStringParameters']['type'],
      'price': event['queryStringParameters']['price']
    }
    
    result = client.execute(document, variable_values=params)
    
  elif operation == UPDATE_PET:
    document = gql(
      """
      mutation update($id: ID!, $type: PetType!, $price: Float!) {
        updatePet(input: {
          id: $id,
          type: $type,
          price: $price
        }){
          id
          type
          price
        }
      }
      """
      )
      
    params = {
      'id': event['queryStringParameters']['id'],
      'type': event['queryStringParameters']['type'],
      'price': event['queryStringParameters']['price']
    }
    
    result = client.execute(document, variable_values=params)
    
  elif operation == DELETE_PET:
    document = gql(
      """
      mutation delete($id: ID!) {
        deletePet(input: {
          id: $id
        }){
          id
          type
          price
        }
      }
      """
      )
      
    params = {
      'id': event['queryStringParameters']['id']
    }
    
    result = client.execute(document, variable_values=params)
    
  elif operation == GET_PET:
    document = gql(
      """
      query get($id: ID!) {
        getPet(id: $id){
          id
          type
          price
        }
      }
      """
      )
      
    params = {
      'id': event['queryStringParameters']['id']
    }
    
    result = client.execute(document, variable_values=params)
    
  elif operation == LIST_PETS:
    document = gql(
      """
      query allpets {
        listPets {
          id
          type
          price
        }
      }
      """
      )
      
    result = client.execute(document)
    
  elif operation == LIST_PETS_BY_PRICE_RANGE:
    document = gql(
      """
      query list($min: Float!, $max: Float!) {
        listPetsByPriceRange(min: $min, max: $max) {
          id
          type
          price
        }
      }
      """
      )
      
    params = {
      'min': event['queryStringParameters']['min'],
      'max': event['queryStringParameters']['max']
    }
    
    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を使用します。

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.

GQLを使って、スキーマで定義したクエリ・ミューテーションを実行します。

Pythonの場合、event[‘queryStringParameters’]でURLクエリパラメータを取得することがきます。
URLクエリパラメータで必要なパラメータを渡します。
operationパラメータで実行するGraphQLクエリを指定する形となります。

(参考) CloudFormationカスタムリソース

CloudFormationカスタムリソースを使用して、Aurora Serverlessの初期化処理を実行します。
Aurora ServerlessをCloudFormationカスタムリソースを使って初期化する方法については、以下のページをご確認ください。

以下にカスタムリソースとして実行するLambda関数を取り上げます。

Resources:
  Function2:
    Type: AWS::Lambda::Function
    Properties:
      Environment:
        Variables:
          DBCLUSTER_ARN: !Ref DBClusterArn
          DBNAME: !Ref DBName
          DBTABLE: !Ref DBTableName
          REGION: !Ref AWS::Region
          SECRET_ARN: !Ref SecretArn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import json
          import os
          
          dbcluster_arn = os.environ['DBCLUSTER_ARN']
          dbname = os.environ['DBNAME']
          dbtable = os.environ['DBTABLE']
          region = os.environ['REGION']
          secret_arn = os.environ['SECRET_ARN']
          
          sql1 = 'create table {table}(id varchar(200), type varchar(200), price float)'.format(table=dbtable)
          client = boto3.client('rds-data', region_name=region)
          schema = 'mysql'
          
          CREATE = 'Create'
          response_data = {}
          
          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                response1 = client.execute_statement(
                  database=dbname,
                  resourceArn=dbcluster_arn,
                  schema=schema,
                  secretArn=secret_arn,
                  sql=sql1
                )
                print(response1)
                
              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
                
            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      FunctionName: !Sub "${Prefix}-function2"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole2.Arn
Code language: YAML (yaml)

環境構築

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

事前準備

以下の2つの準備を行います。

  • Lambda関数用のデプロイパッケージを用意してS3バケットにアップロードする
  • Lambdaレイヤー用のデプロイパッケージを用意してS3バケットにアップロードする

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

$ sudo pip3 install --pre gql[all] -t python

zip -r layer.zip python
Code language: Bash (bash)

Lambdaレイヤーに関する詳細については、以下のページをご確認ください。

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

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

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

  • AppSync API:fa-061-GraphQLApi
  • Aurora ServerlessのID:fa-061-dbcluster
  • GraphQLクライアントLambda関数のFunction URL:https://dzmbqhvhyhkzm4pyufkzmketsy0gdoxt.lambda-url.ap-northeast-1.on.aws/

AWS Management ConsoleからAppSyncを確認します。
まずデータソースです。

Detail of AppSync Data Source.

データソースを見ると、Typeが「RELATIONAL_DATABASE」とあります。
正常にAurora Serverlessがデータソースとして設定されています。

次にスキーマ・リゾルバを確認します。

Detail of AppSync Schema.
Detail of AppSync Resolver 1.
Detail of AppSync Resolver 2.
Detail of AppSync Resolver 3.
Detail of AppSync Resolver 4.
Detail of AppSync Resolver 5.
Detail of AppSync Resolver 6.

CloudFormationテンプレートファイルで定義した通りに作成されています。

続いてAurora Serverlessを確認します。

Detail of Aurora Serverless.

こちらも正常に作成されています。

動作確認

準備が整いましたので、GraphQLクライアントLambda関数のFunction URLにアクセスします。

createPet

まずデータを保存します。
URLクエリのoperationの値に「createPet」を指定し、typeとpriceに任意の値を指定します。
これで以下のGraphQLクエリを実行することになります。

mutation add($type: PetType!, $price: Float!) {
  createPet(input: {
    type: $type,
    price: $price
  }){
    id
    type
    price
  }
}
Code language: plaintext (plaintext)

以下が実行結果です。

Result of AppSync 1.

正常にデータが追加されました。

getPet

次にIDを指定してデータを取得します。
URLクエリのoperationの値に「getPet」を、idに先ほど確認したIDを指定します。
これで以下のGraphQLクエリを実行することになります。

query get($id: ID!) {
  getPet(id: $id){
    id
    type
    price
  }
}
Code language: plaintext (plaintext)

以下が実行結果です。

Result of AppSync 2.

先ほど保存されたデータが返ってきました。

updatePet

次にIDを指定してデータを更新します。
URLクエリのoperationの値に「updatePet」を、idに先ほど確認したIDを、typeとpriceに任意の値を設定します。
これで以下のGraphQLクエリを実行することになります。

mutation update($id: ID!, $type: PetType!, $price: Float!) {
  updatePet(input: {
    id: $id,
    type: $type,
    price: $price
  }){
    id
    type
    price
  }
}
Code language: plaintext (plaintext)

以下が実行結果です。

Result of AppSync 3.

typeの値が「fish」から「bird」、priceの値が「10.0」から「50.0」に更新されました。

listPets

listPetsを実行する前に、もう一度getPetを実行し、データを追加しておきます。

保存されているデータの一覧を取得します。
URLクエリのoperationの値に「listPets」を指定します。
これで以下のGraphQLクエリを実行することになります。

query allpets {
  listPets {
    id
    type
    price
  }
}
Code language: plaintext (plaintext)

以下が実行結果です。

Result of AppSync 4.

保存されている2つのデータが返ってきました。

listPetsByPriceRange

priceの値で条件付けした上で、保存されたデータを取得します。
URLクエリのoperationの値に「listPetsByPriceRange」を、minとmaxに任意の値を設定します。
これで以下のGraphQLクエリを実行することになります。

query list($min: Float!, $max: Float!) {
  listPetsByPriceRange(min: $min, max: $max) {
    id
    type
    price
  }
}
Code language: plaintext (plaintext)

以下が実行結果です。

Result of AppSync 5.

30<=price<=100の条件を満たす「bird」のデータが返ってきました。

deletePet

IDを指定してデータを削除します。
URLクエリのoperationの値に「deletePet」を、idに保存されているデータのIDを設定します。
これで以下のGraphQLクエリを実行することになります。

mutation delete($id: ID!) {
  deletePet(input: {
    id: $id
  }){
    id
    type
    price
  }
}
Code language: plaintext (plaintext)

以下が実行結果です。

Result of AppSync 6.

「bird」のデータが削除されました。

改めてlistPetsを実行します。

Result of AppSync 7.

「fish」のデータのみが返ってきました。
「bird」が削除されたことがわかります。

まとめ

AppSyncのデータソースにAurora Serverlessを設定する構成をご紹介しました。

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