AppSyncのデータソースにRDS(Aurora Serverless)を設定する
AppSyncは以下のサービスの中からデータソースを選択することができます。
- Lambda
 - DynamoDB
 - OpenSearch
 - None
 - HTTPエンドポイント
 - RDS
 
今回はRDSをデータソースにする構成を確認します。
RDSとして指定できるリソースはAurora Serverlessです。
なおAppSyncの基本的な解説と、DynamoDBをデータソースとする構成については、以下のページをご確認ください。

構築する環境

データソースとして動作するAurora Serverlessを作成します。
AppSyncからAurora Serverlessを操作するために、スキーマ・リゾルバを定義します。
本ページは、以下のAWS公式ページを参考に進めます。
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/tutorial-rds-resolvers.html
2つのLambda関数を作成します。
関数のランタイム環境はPython3.8とします。
1つ目の関数はCloudFormationカスタムリソースに関連付けて、スタック作成時に実行されるように設定します。
この関数の働きは、Aurora Serverless DBを初期化することです。
2つ目の関数はGraphQL APIを実行するクライアントとして設定します。
Function URLを有効化し、URLクエリパラメータで実行する操作を指定できるようにします。
CloudFormationテンプレートファイル
上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。
https://github.com/awstut-an-r/awstut-fa/tree/main/061
テンプレートファイルのポイント解説
本ページは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を使用します。
https://github.com/graphql-python/gql
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を確認します。
まずデータソースです。

データソースを見ると、Typeが「RELATIONAL_DATABASE」とあります。
正常にAurora Serverlessがデータソースとして設定されています。
次にスキーマ・リゾルバを確認します。







CloudFormationテンプレートファイルで定義した通りに作成されています。
続いて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)
以下が実行結果です。

正常にデータが追加されました。
getPet
次にIDを指定してデータを取得します。
URLクエリのoperationの値に「getPet」を、idに先ほど確認したIDを指定します。
これで以下のGraphQLクエリを実行することになります。
query get($id: ID!) {
  getPet(id: $id){
    id
    type
    price
  }
}
Code language: plaintext (plaintext)
以下が実行結果です。

先ほど保存されたデータが返ってきました。
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)
以下が実行結果です。

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)
以下が実行結果です。

保存されている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)
以下が実行結果です。

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)
以下が実行結果です。

「bird」のデータが削除されました。
改めてlistPetsを実行します。

「fish」のデータのみが返ってきました。
「bird」が削除されたことがわかります。
まとめ
AppSyncのデータソースにAurora Serverlessを設定する構成をご紹介しました。