CloudFormationを使用してAppSync環境を構築
AppSyncはAWSが提供するマネージドサービスの1つで、容易にGraphQL APIを構築することができます。
本ページはAppSync入門ということで、CloudFormationを使用して基本的なAppSync環境を構築します。
構築する環境
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に関する詳細は、以下のページをご確認ください。
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レイヤーに関する詳細は、以下のページをご確認ください。
今回の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スタックを作成し、スタック内のリソースを確認する
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を確認します。
APIが作成され、APIのURLが作成されていることがわかります。
またAPIキーも作成されていることがわかります。
データソースを確認します。
DynamoDBテーブルがデータソースとして登録されていることがわかります。
スキーマを確認します。
CloudFormationテンプレートファイルで記載した通りにスキーマが作成されています。
作成された3つのリゾルバーを確認します。
こちらもCloudFormationテンプレートファイルで設定した通りの内容です。
動作確認
準備が整いましたので、各Function URLにアクセスします。
まず関数1です。
関数1は日時情報を保存する働きをします。
正常にミューテーションが実行されました。
日時情報が保存され、保存された値が返ってきました。
このようにAppSyncのGraphQL APIを通じて、データを書き込むことができました。
次に関数2です。
関数2は保存されている日時情報を全て取得する働きをします。
正常にクエリが実行されました。
保存されているデータの内、エポック秒を除いたIDと日時データのみが返ってきました。
このようにAppSyncのGraphQL APIを通じて、ユーザーが必要な形式でデータを取得することができました。
最後に関数3です。
関数3はIDを指定して日時情報を取得する働きをします。
URLクエリパラメータでIDを指定し、正常にクエリが実行されました。
今回のクエリでは、3種類の全てのデータを取得するクエリを実行していますので、それに応じた結果となります。
まとめ
AppSyncの入門ということで、CloudFormationを使用して、AppSync環境を構築しました。
環境構築を通じて、AppSynを構成するいくつかのリソースと、AppSyncで構築したGraphQL APIをLambda関数(Python)で実行する方法を確認しました。