ひげぶろぐ

開発とか組織とかの話

CloudFormation で RDS の ARN を動的に指定したい

Serverless Framework で Aurora Serverless の arn を指定したかったが、若干特殊だったのでメモ。

結論

(執筆時点で)CloudFormationでは RDS( AWS::RDS::DBCluster )のARNを動的に取得する手段を提供しておらず、
こちら をベースに手動で指定するのが代替手段のようです。

調査ログ

Serverless Step Functions

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 では RefFn::GetAtt によって参照を実現出来るが、リソースタイプに応じて異なるもの(ARN、ID、リソース名など)を返すため、このページ を参照して、Ref または Fn :: GetAtt のどちらを使用する必要があるかを確認してください。

というわけで以下を参照。

theburningmonk.com

RDSではARNを返してくれなさそうです。

同じ悩みを抱えた人を発見しました。 こちらで紹介されていたのが結論に示した手動指定(文字列連結)の手法です。

stackoverflow.com

TypeORM とか使ってるとRDSのARNが環境変数に欲しくなると思うので、早く RefFn :: GetAtt が対応してくれると嬉しいですね。

クリエイティブなチームの在り方 - プロダクトとチームの人間味 -

f:id:macha_tokyo:20200412035445j:plain

エンジニアも8年目になり、組織全体を考えるようになりました。

自分の頭の中の整理がてら、クリエイティブ組織(特にtoCアプリケーション開発組織)の在り方について、最近考えていることを書き残しておこうと思います。

主に、デザインシンキング、アジャイル開発、心理的安全性という最近よく聞く言葉が何故大事なのか、現在どういう組織が求められているのかという話です。
ありふれた話をしている部分もあるし、主観的な部分もあります。

 

続きを読む

LaravelのEloquent Modelのマジックメソッドが辛い

前置き

Laravelで中規模程度のサービスを運営してるんですが、スキーマ変更のタイミングでマジックメソッド辛い問題にぶち当たりました。
同じ気持ちの人を見つけて安心したかったけど、これくらいしか見つからなかったので自分で書き残しておこうと思いました。

www.freecodecamp.org

 

本題

LaravelのEloquent Model は、DBテーブルのカラム名でプロパティアクセス出来るように作られています。

こんな具合です。 

$createdDate = new Carbon($item->created_at);

プロパティ名がスネークケースになっていて既に気持ち悪さはありますが、便利といえば便利な機能なんだと思います。

 

ただこれ、多用するとかなりきつい事になります。
カニズム的には未定義のプロパティに対してマジックメソッド(__get())で半ば無理やりアクセスしているので、

  • IDEの補完が効かない
  • スキーマが変わったりした時には地道に使用箇所を検索して直していくしかない

というとても辛い未来が待ち受けています。

ご利用は計画的に。と言いたいですが、多分この問題に気づいたときにはあらゆる所でこの呼び出しが使われちゃってるんですよね。

 

Laravelはこういう暗黙的な実装が多い気がします。
PHPという言語のゆるふわさも相まって、コードリーディングやリファクタリングの難易度の高さはかなり高くなってしまう印象です。

皆どうしてるんだろう。
プロパティちゃんと定義しているのかな。
良いノウハウがあれば是非ご共有いただきたいところです。

Amazon Elasticsearch Service でインデクシングされなくなった時の調査・解決ログ

AWS Amplify の @searchable を使って Elasticsearch による検索を実現していたのですが、ある時から突然、新しくDynamoDBに登録したデータが検索に引っかからなくなりました。
そんな時のメモ。

状況

  • 検索機能を作って最初のうちは普通に検索が出来ていた
  • スキーマ変更、データの形式変更(日付データのフォーマット変更等)を行なってからうまくいかなくなった
  • スキーマ変更の直後にスクリプトで大量にデータを投入していたが、スキーマ変更したテーブルだけうまくいっていなかった

引っかからない理由の調査

検索に引っかからない理由はいくつか考えられた。
まずいくつかあたりをつけてみる。
今回なら以下のような感じ。

  • 1 : 検索のコードが良くない
  • 2 : インフラの構成がおかしい
  • 3 : cliからのデータ登録だとインデクシングされない
  • 4 : スキーマ変更が影響した
  • 5 : データのフォーマット変更が影響した

今回は 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を使って削除した。

▼ 参考

inside.dmm.com

そんなわけで調査と解決にやたら時間がかかったけど解決。
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という言語で記述するもの。 f:id:macha_tokyo:20190628223440p:plain

これにより、「データの所有者だけにデータ更新を許可する」「全データから、リクエストしたユーザのデータのみをフィルタリングして返す」等の処理が可能となる。
もちろんバリデーション等の処理もここで行える。

つまりは、一般的なサーバサイドの処理と言って良いと思う。
クライアントサイドでいくら頑張ってもデータは改ざんされるので、一定規模のサービスになれば必ずサーバサイドの処理が必要となる。
AWS Amplifyでは、VTLの記述のみで、Lambda等を個別に設定することなくサーバサイドの処理を実現出来る。

具体的なユースケースについてはこちらの資料に詳しく記載されている。 認証のユースケース - AWS AppSync

AWS Amplifyにおけるカスタムリゾルバーの追加方法

公式のドキュメントによると以下の通り。

  1. AWS AppSync APIを作成後、Amplify projectのAPIフォルダに resolvers という空フォルダが出来ている
  2. カスタムリゾルバーを作るには、 Query.getTodo.req.vtl のようなファイルを上述の resolvers フォルダに作る
  3. 次に amplify pushamplify 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.vtlres.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で orand を使うことで複数条件のフィルタリング指定が可能。
日本語は細かく分割されて保存される関係で 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 すると直る。