CloudFormation で RDS の ARN を動的に指定したい
Serverless Framework で Aurora Serverless の arn を指定したかったが、若干特殊だったのでメモ。
結論
(執筆時点で)CloudFormationでは RDS( AWS::RDS::DBCluster
)のARNを動的に取得する手段を提供しておらず、
こちら をベースに手動で指定するのが代替手段のようです。
調査ログ
You can use CloudFormation intrinsic functions such as Ref and Fn::GetAtt to reference Lambda functions, SNS topics, SQS queues and DynamoDB tables declared in the same serverless.yml. Since Ref returns different things (ARN, ID, resource name, etc.) depending on the type of CloudFormation resource, please refer to this page to see whether you need to use Ref or Fn::GetAtt.
日本語訳
serverless.yml では
Ref
やFn::GetAtt
によって参照を実現出来るが、リソースタイプに応じて異なるもの(ARN、ID、リソース名など)を返すため、このページ を参照して、Ref
またはFn :: GetAtt
のどちらを使用する必要があるかを確認してください。
というわけで以下を参照。
RDSではARNを返してくれなさそうです。
同じ悩みを抱えた人を発見しました。 こちらで紹介されていたのが結論に示した手動指定(文字列連結)の手法です。
TypeORM とか使ってるとRDSのARNが環境変数に欲しくなると思うので、早く Ref
か Fn :: GetAtt
が対応してくれると嬉しいですね。
LaravelのEloquent Modelのマジックメソッドが辛い
前置き
Laravelで中規模程度のサービスを運営してるんですが、スキーマ変更のタイミングでマジックメソッド辛い問題にぶち当たりました。
同じ気持ちの人を見つけて安心したかったけど、これくらいしか見つからなかったので自分で書き残しておこうと思いました。
本題
LaravelのEloquent Model は、DBテーブルのカラム名でプロパティアクセス出来るように作られています。
こんな具合です。
$createdDate = new Carbon($item->created_at);
プロパティ名がスネークケースになっていて既に気持ち悪さはありますが、便利といえば便利な機能なんだと思います。
ただこれ、多用するとかなりきつい事になります。
メカニズム的には未定義のプロパティに対してマジックメソッド(__get())で半ば無理やりアクセスしているので、
というとても辛い未来が待ち受けています。
ご利用は計画的に。と言いたいですが、多分この問題に気づいたときにはあらゆる所でこの呼び出しが使われちゃってるんですよね。
Laravelはこういう暗黙的な実装が多い気がします。
PHPという言語のゆるふわさも相まって、コードリーディングやリファクタリングの難易度の高さはかなり高くなってしまう印象です。
皆どうしてるんだろう。
プロパティちゃんと定義しているのかな。
良いノウハウがあれば是非ご共有いただきたいところです。
Amazon Elasticsearch Service でインデクシングされなくなった時の調査・解決ログ
AWS Amplify の @searchable
を使って Elasticsearch による検索を実現していたのですが、ある時から突然、新しくDynamoDBに登録したデータが検索に引っかからなくなりました。
そんな時のメモ。
状況
- 検索機能を作って最初のうちは普通に検索が出来ていた
- スキーマ変更、データの形式変更(日付データのフォーマット変更等)を行なってからうまくいかなくなった
- スキーマ変更の直後にスクリプトで大量にデータを投入していたが、スキーマ変更したテーブルだけうまくいっていなかった
引っかからない理由の調査
検索に引っかからない理由はいくつか考えられた。
まずいくつかあたりをつけてみる。
今回なら以下のような感じ。
今回は amplify env add
で同じ構成の別環境を作って色々と試し、結果として 1,2,3,4 は原因でないことが分かった。
調査の中で Elastic Search の index 状況を見る場面があったが、その際、検索がうまくいかないテーブルのインデックスが明らかにデータ数に対して少ないことが分かった。
検索に引っかからないのはそもそもインデクシングされていないからだと判明した。
インデクシングされない原因の調査
インデクシングの処理の流れとしては、DynamoDBにデータが登録された時にLambdaが走り、Elasticsearch Serviceに届く形である。
したがってLambdaが怪しい。
AWS ConsoleでLambdaを見ると、DdbToEsFn-hogehoge-env
というFunctionが出来ている。
これが DynamoDBからElasticsearchServiceへデータを送る関数。
サービスにアクセスして(或いはcli等で)DynamoDBにデータを流し込み、モニタリング > CloudWatch Logs Insights でログを監視してみる。
すると見事にエラーが出ていた。
{ "took":9, "errors":true, "items": [ {"index": {"_index":"user", "_type":"doc", "_id":"XXX", "status":400, "error":{ "type":"mapper_parsing_exception", "reason":"failed to parse [birthday]", "caused_by":{ "type":"illegal_argument_exception", "reason":"Invalid format: \\"01/01\\" is too short" } } } } ] }
調べてみると、Elasticsearch のスキーマ自動マッピング機能が問題だった。
yyyy/mm/dd
形式の日付のデータを数件入れた後で、それをmm/dd
に変更した為、自動生成されたフォーマットに合わずに弾かれていたようだ。
これで原因が分かった。
インデクシングしない問題の解決
既存のマッピングが悪いので、これを書き換えるか削除して作り直すかすれば良い。
今回はデータ量も少なかったので作り直す方針をとる。(というかAWSでマッピング更新している事例が見つからなかった)
作り直しといっても、既存のindexを削除してデータを再度流し込むだけ。
だが、indexの削除をconsoleやcliから実行する手段は見当たらなかった。
面倒だったがLambdaからCuratorを使って削除した。
▼ 参考
そんなわけで調査と解決にやたら時間がかかったけど解決。
Dynamoがスキーマ変更に強いと言っても、Elasticsearchを使うなら出来る限りスキーマは変えたくないなあと思った次第。
AWS AmplifyプロジェクトにおけるCustom Resolverを用いた細かな権限管理
この記事は?
AWS Amplifyにおける、カスタムリゾルバーを用いたDynamoDBの細かいパーミッション管理について記載している。
具体的には、Cognitoのユーザプールをベースに、他人が自分のIDでデータを作ることを防ぐ仕組みを作る。
DynamoDBにおける一意性(Unique)の担保の仕方についても軽く触れている。
経緯
AWS Amplify は2019年2月にカスタムリゾルバーのサポートを発表している。
これにより、AppSyncコンソールからの作業やCloudFormation Templateの編集は不要となり、Amplifyユーザは非常に簡単な手順でカスタムリゾルバーを組み込むことが可能となった。
が、まだWeb上では利用事例が少なく、不明点も多かったので、ログとしてここに残しておく。
前提知識
カスタムリゾルバーとは
リゾルバーに関しては以下に詳しく記載されている。 docs.aws.amazon.com
ざっくり言うと、DynamoDB等へのアクセスが走る前と後の処理をVTLという言語で記述するもの。
これにより、「データの所有者だけにデータ更新を許可する」「全データから、リクエストしたユーザのデータのみをフィルタリングして返す」等の処理が可能となる。
もちろんバリデーション等の処理もここで行える。
つまりは、一般的なサーバサイドの処理と言って良いと思う。
クライアントサイドでいくら頑張ってもデータは改ざんされるので、一定規模のサービスになれば必ずサーバサイドの処理が必要となる。
AWS Amplifyでは、VTLの記述のみで、Lambda等を個別に設定することなくサーバサイドの処理を実現出来る。
具体的なユースケースについてはこちらの資料に詳しく記載されている。 認証のユースケース - AWS AppSync
AWS Amplifyにおけるカスタムリゾルバーの追加方法
公式のドキュメントによると以下の通り。
- AWS AppSync APIを作成後、Amplify projectのAPIフォルダに
resolvers
という空フォルダが出来ている - カスタムリゾルバーを作るには、
Query.getTodo.req.vtl
のようなファイルを上述のresolvers
フォルダに作る - 次に
amplify push
かamplify api gql-compile
を実行したタイミングで、自動生成されたテンプレートの代わりにカスタムのテンプレートが利用される。
カスタムリゾルバを試す
実際に試してみる。
今回は以下のようなシステムを組む。
amplify add api
によりGraphQLベースのAPIを構築- Cognitoユーザプールと、そこに登録されたユーザに紐づくユーザ属性テーブル
User
がある(DynamoDB) User
テーブルのIDはCognitoユーザプールのusernameと一致させ、ユーザ本人しか create できない
schema.graphql はこんなイメージ。
type User @model @auth( rules: [{ allow: owner, ownerField: "id", operations: [create, update] }] ) { id: ID! name: String! gender: String birthday: String image: String }
1. resolversフォルダの確認
amplify add api
した後のプロジェクトでは、自動で以下のようにフォルダ/ファイルが生成されている。
-> % tree -L 2 amplify/backend/api amplify/backend/api └── amplifysample ├── build ├── parameters.json ├── resolvers ├── schema.graphql └── stacks
この中の resolvers
フォルダが今回ファイルを生成していくフォルダになる。
ちなみに build
フォルダの中にも resolvers
というフォルダがあるが、そちらは最終的にサーバへpushされるファイル群。
2. resolvers フォルダ以下にVTLを置く
VTLについては初めて触る人も多いかもしれない。
公式のドキュメントを一度読むとちょっとだけ分かる。
リゾルバーのマッピングテンプレートプログラミングガイド - AWS AppSync
おすすめの勉強法は build
フォルダ以下に吐き出された自動生成されたVTLファイルを読む方法。
特に @auth
をつけたテーブルのmutationは勉強になる。
今回はまず build/resolvers
フォルダか resolvers
フォルダに Mutation.createUser.req.vtl
をコピーする。
build/resolvers
には自動生成済みの VTL ファイルが設置されているため、それをベースに細かな権限の処理を加えていく。
req.vtl
と res.vtl
があるが、それぞれリクエストとレスポンスに対する処理で、今回は req.vtl
でバリデーションを行なう。
ファイルの内容は以下のようにする。
## [Start] Owner Authorization Checks ** #set( $isOwnerAuthorized = false ) ## Authorization rule: { allow: owner, ownerField: "id", identityField: "cognito:username" } ** #set( $allowedOwners0 = $util.defaultIfNull($ctx.args.input.id, null) ) #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) #if( $util.isString($allowedOwners0) ) #if( $allowedOwners0 == $identityValue ) #set( $isOwnerAuthorized = true ) #end #end #if( $util.isNull($allowedOwners0) && (! $ctx.args.input.containsKey("id")) ) $util.qr($ctx.args.input.put("id", $identityValue)) #set( $isOwnerAuthorized = true ) #end ## [End] Owner Authorization Checks ** ## [Start] Throw if unauthorized ** #if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized == true) ) $util.unauthorized() #end ## [End] Throw if unauthorized ** ## [Start] Prepare DynamoDB PutItem Request. ** $util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $util.time.nowISO8601()))) $util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601()))) $util.qr($context.args.input.put("__typename", "User")) { "version": "2017-02-28", "operation": "PutItem", "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else { "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.id, $util.autoId())) } #end, "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input), "condition": { "expression": "attribute_not_exists(#id)", "expressionNames": { "#id": "id" } } } ## [End] Prepare DynamoDB PutItem Request. **
処理の内容としては以下の通り。
- リクエスト送信者の cognito username を取得
id
が指定されていたら、cognito username と比較。一致すればisOwnerAuthorized
フラグを立てるid
が指定されていなかったら、cognito username を id をして採用。isOwnerAuthorized
フラグを立てるisOwnerAuthorized
が立っていない場合、unauthorized()
を実行して処理を修了するattribute_not_exists(#id)
で、まだテーブルに該当idのレコードが存在しないかチェック(一意性の担保)
3. amplify push を実行
amplify push
を実行する。実行後に build/resolvers/Mutation.createUser.req.vtl
を見ると、カスタムした内容に置き換わっているのが分かるはず。
同様に、AppSyncのConsoleでも確認可能。
4. 実行
ログインした状態でリクエストパラメータの id
を自身の username
以外で指定して createUser
を実行すると、失敗するはず。
逆にレコード未作成の状態で id
を指定せずに createUser
を実行すれば、id
に自動で username
が入ったレコードが生成される。
終わりに
最初こそVTLの記法や仕組みに戸惑いはするが、作ってみれば非常に低コストにパーミッション管理が行えた印象。
まだAmplifyやVTLに関しては記事が少なく、今回の手順の中にも間違いがあるかもしれない。ので、何かあればご指摘いただけると幸いです。
AWS AmplifyでElasticSearchを使う
公式ドキュメントで言うとこの辺の話。
https://aws-amplify.github.io/docs/cli/graphql#searchable
めちゃくちゃ簡単に出来る。
今回は日本語名と英語名を持つUserテーブル@DynamoDB に対し、「田中」でも「tanaka」でも検索出来るOR検索を作ってみる。
schema.graphql
@searchable
をつけるだけ。
type User @model @searchable { id: ID! nameJp: String! nameEn: String! }
保存したら amplify push
する。
queries.js に searchUsers
が追加されているのを確認できる。
バックエンドの構成としては DyanamoDBのデータ更新をトリガーにLambdaが起動してElasticsearchにデータが入る形。
AWS ConsoleでもLambda Functionが作られているのが確認出来るはず。
インデックスが更新されない等の問題が発生した場合はLambdaのエラーログを追うと原因が分かることがあるので、この辺の構成も頭に入れておくと良い。
javascript
filterで or
や and
を使うことで複数条件のフィルタリング指定が可能。
日本語は細かく分割されて保存される関係で wildcard
だと上手く一致してくれないので、 match
も指定。
import * as Query from "./graphql/queries"; searchUsers = async (searchText) => { try { const ret = await API.graphql( graphqlOperation(Query.searchUsers, { filter: { or: [ { nameJp: { wildcard: "*" + searchText + "*" } }, { nameJp: { match: searchText} }, { nameEn: { wildcard: "*" + searchText + "*" } } ] } }) ); console.log(ret.data.searchUsers.items); return; } catch (e) { console.error(e); } };
これで上手くいくと思います。
amplify push時の Cannot update GSI's properties other than Provisioned Throughput エラー
GraphQLのスキーマを書き換えて amplify push した時のエラー
Cannot update GSI's properties other than Provisioned Throughput. You can create a new GSI with a different name.
@connection
のカラム名を変えたら出た。
Reason: Resource update cancelled
このエラーもいっぱい出たけど、これは上のエラーの影響で発生したもの。
一回該当の @connection
の記述を削除して amplify push
し、その後元に戻して amplify push
すると直る。