SCSKの畑です。8回目の投稿です。
今回は、初回のエントリで少しだけ触れた、AWS Amplify が生成した AWS AppSync リソースの手動移行に関する備忘録のような、Tips のような内容になります。如何せんこのような作業が必要となるような案件もといシチュエーション自体が珍しいと思いますので、ニッチな内容になるとは思いますが御覧頂ければ幸いです。
なお、タイトル通り Amplify と AppSync の話題に閉じているのでアーキテクチャ図は載せません。それも何気に今回が初めてですね・・
前提条件(初回エントリのおさらい含む)
- お客さんが主体的に管理している AWS 環境上で各種 AWS サービスやリソースの構築・実装を行った、
- CloudFormation や AWS CDK、terraform などの使用は原則 NG。AWS マネジメントコンソールの使用を使用した構築が基本。
- Amplify についてはお客さんの環境ポリシー上導入 NG だったため、弊社の AWS 環境上で Amplify により作成された各種 AppSync リソースを、お客さんの AWS 環境に手動移行する必要があった。
- 移行対象はスキーマ、リゾルバ、関数の3つ。
- データソースについては上記3つと比較して変更のタイミングが限定的であり、かつリソースとなる DynamoDB のテーブルや Lambda 関数についても申請の上お客さん側で作成頂く必要があるため、設定情報をお客さんに連携して作成頂く形式とした。
以上の内容を踏まえて、いくつかの観点から備忘録替わりに記載していきます。
余分な query や mutation を Amplify に生成させない
まずは Amplify スキーマ定義上の工夫から。
@model として定義すると、テーブルだけでなく対応した query なり mutation を自動生成してくれるのが Amplify の便利な点なのですが、アプリケーションから使用しないことが明確なものもあるため、そのような不要なものは生成しないようにすることで移行作業の対象を減らすのが目的です。具体的には、スキーマ定義において生成対象の type を指定することで実現できます。
排他制御の実装例(第一回)で例示した TableInfo において、query は list、mutation は create のみをそれぞれ生成するようにする場合は以下のように定義することで実現できます。
type TableInfo
@model (
queries: { list: "listTableInfos" },
mutations: { create : "createTableInfo" },
)
@auth(rules: [
{allow: public, provider: apiKey},
{allow: private, provider: iam},
])
{
name: String! @primaryKey
status: TableStatus!
editor: String
locked_by: String
}
手動移行時に設定変更が必要なマッピングテンプレートの整理
次に、実作業についてですが・・こちらは単純に AWS マネジメントコンソールを弊社の AWS 環境とお客さんの AWS 環境で2画面開いて、愚直に手動で移行(もといコピペ)していただけなので、特に内容として取り上げたいものはありません。もっと楽なやり方を採用したかったとは思いますが。。
ただ、AWS 環境が異なる以上、移行時に変更する必要がある項目も幾つかあったためその点には留意しました。特に、リゾルバのリクエストマッピングテンプレートについては VTL を書き換える必要があったため、備忘録として以下に記載しておきます。逆に言うと、注意深く変更する必要があったのはこの項目くらいだったため、移行自体は思っていたほど時間がかからなかったのは幸いでした。
- authRole, unAuthRole を使用しないため、マッピングテンプレートから削除
- adminRoles の AWS アカウント ID をお客さんの AWS 環境のものに変更
## [Start] Stash resolver specific context.. ** $util.qr($ctx.stash.put("typeName", "<Query or Mutation or Subscription>")) $util.qr($ctx.stash.put("fieldName", "<QueryName or MutationName or SubscriptionName>")) $util.qr($ctx.stash.put("adminRoles", ["arn:aws:sts::<AWS Account ID>:assumed-role"])) {} ## [End] Stash resolver specific context.. **
手動移行した AppSync リソースの設定チェックにおける工夫
最後に、手動移行後の AppSync 設定確認方法について。
手動で移行したこともあり、環境間で移行したリソース設定をどのように比較チェックするのかがちょっとした課題でした。スキーマは画面からコピペしたものを diff 比較すれば良いとしても、関数/リゾルバのマッピングテンプレートで都度それをやるのはさすがに面倒だし、マッピングテンプレート内の関数実行順などは画面上で比較するしかないということで、どうしたものかと。当初は目視確認もやむなしかと思っていたのですが、案の定設定ミスで動作しなかった mutation が出てしまい、ある程度はちゃんと機械的に比較できる方法を考える必要がありました。
と言っても、先述の通り IaC に該当する仕組みは使用できなかったので、大人しく Python/Boto3 で AppSync の設定を一覧化して S3 に出力するようなコードを書くことにしました。弊社/お客さんの AWS 環境で出力したものを diff 比較するようにすれば、AWS アカウント ID など差異が発生する項目は出るにせよ、画面の目視確認よりは大分マシだろうなと。
ということで内容は大したことないのですが、以下 Lambda による実装例です。幸い Boto3 にそのものズバリなメソッドが揃っていたため、それらを順次実行した結果を S3 に出力しているだけの簡単仕様ですが、少しだけ補足です。
- get_introspection_schema() と list_types() が返す情報はほぼ同義ですが(スキーマ設定により各 type が定義されるので)、一応両方出力するようにしています。
- 前者は JSON ではなく SDL で出力しているので、streamingBody を read() したものをそのまま出力しています。
- list_resolvers() の引数として、リゾルバを実装した type 名を指定する必要があります。Amplify を使用していることもあり、graphql のクエリ言語に対応するものをそのまま直指定しています。
また、出力結果はフォルダごと Winmerge などの diff ツールで比較するような使い方をしていますが、functionID 絡みで比較に支障があった部分については以下のように変換、ソート処理を入れています。
- list_functions() の結果がどうやら functionId でソートされて返ってくるようなのですが、ID は当然ながら環境間で異なるためこのままだと有意な比較ができません。よって、 name(関数名)でソートし直したものを出力しています。
- リゾルバ内の関数実行定義も functionId による表現となっているため、functionId から name を導出できるような dict を用意の上、定義内容を置換しています。
他にも、マッピングテンプレートや types における definition の出力結果を整形するなど、もうちょっと頑張りどころはありそうなんですが、それらの内容で差分が生じてもそこまで比較に支障が出なさそうだったので、一旦そのまま出力してしまっています。
import json import datetime import boto3 import botocore BUCKET_NAME = '<BUCKET_NAME>' API_ID = '<APPSYNC_API_ID>' appsync = boto3.client('appsync') s3 = boto3.resource('s3') bucket = s3.Bucket(BUCKET_NAME) def put_json_to_S3(obj_key, json_data): obj = bucket.Object(obj_key) obj.put(Body = json.dumps(json_data, indent=4, sort_keys=True, separators=(',', ': '))) def lambda_handler(event, context): obj_key_prefix = f'appsync_setting_dumps/{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}' # 情報取得(functions) response = appsync.list_functions( apiId=API_ID ) # functionの名前でソート sorted_func_json = sorted(response['functions'], key=lambda x: x['name']) put_json_to_S3(f'{obj_key_prefix}/functions.json', sorted_func_json) # 情報取得(resolvers) id_name_dict = {item['functionId']: item['name'] for item in sorted_func_json} for typename in ['Query', 'Mutation', 'Subscription']: response = appsync.list_resolvers( apiId=API_ID, typeName=typename ) # functions実行定義をIDから名前に変換 converted_resolvers_json = response['resolvers'] for item in converted_resolvers_json: pipeline_config = item["pipelineConfig"] if "functions" in pipeline_config: pipeline_config["functions"] = [id_name_dict[id] for id in pipeline_config["functions"]] put_json_to_S3(f'{obj_key_prefix}/resolvers_{typename}.json', converted_resolvers_json) # 情報取得(types) response = appsync.list_types( apiId=API_ID, format='JSON' ) put_json_to_S3(f'{obj_key_prefix}/types.json', response['types']) # 情報取得(schema) response = appsync.get_introspection_schema( apiId=API_ID, format='SDL' ) obj = bucket.Object(f'{obj_key_prefix}/introspection_schema.json') obj.put(Body = response['schema'].read())
余談
Amplify の自動生成が便利とはいえ手動移行に手間をかけるより、自分でリゾルバを書けば良いんじゃないの?と疑問を持たれた方もいるかと思うのですが、その考え自体は正しいと思います。何故それでも Amplify を採用したのかというと、単純に期間や工数との兼ね合いからでした。AppSync を本格的に扱うのが初めてだったということもあるのですが、リゾルバの作成を未経験の言語(VTL or AppSync JS)で頑張るのはちょっと現実的ではなかったです。特に VTL は正直構文とか見てもあまり好きになれず・・
データソースが全て Lambda になるようなアプリケーションであれば、AppSync をやめて API Gateway + Lambda という構成でもよかったのですが、その場合はアプリケーションで使用するDBをどのように用意するか、というのがネックでした。なるべくサーバレスのサービスで構成しようとすると結局一番筋が良さそうなのは DynamoDB になりますが、それなら AppSync 及び Amplify と組み合わせて使った方が筋が良いのではないかと。Aurora Serverless も選択肢ではありましたが、さすがに今回の用途だとオーバースペックでしたし。後は、別エントリで説明した通り AppSync の Subscription をアプリケーション側で活用することを念頭に置いていたという要因も大きいです。
まとめ
本来であればこのような対応を取らないに越したことはないのですが、手動移行したことによりリゾルバや関数の仕組みや実装について理解が深まったことも確かなので、トータルでは良かったのかもしれません。現在もアプリケーションの開発は継続しており、ちょくちょく弊社環境からお客さん環境に AppSync 更新分を移行する機会はあるのですが、これらの取り組みにより移行自体にかかる工数は大分削減できるようになりました。ただ、もし別の案件で同じような対応を迫られた場合は、もっとラクなやり方を検討したいところではあります。。
本記事がどなたかの役に立てば幸いです。