API側でCognito認証してアプリ側でCognito認可してamplify

AWS Amplify はすごく便利だけど、レールから外れると難しい。

普通にやると、Cognitoで認証・認可して Pinpoint でPUSH通知が簡単にできる。

ただ、Cognitoの認証部分だけAPI Gateway経由で呼び出すようなシステム構成だと簡単ではない。

Cognitoの認証と認可

Cognitoは、「User Pool」と「Identify Pool」がある。

User Poolはアプリに対して認証を提供してくれて、Identify Poolはユーザーに対して認可を行うことができる。

認可することで、AWSリソースに対してIAM Roleに応じてユーザーがアクセスできる。

ちなみに、認可は認証しなくても使える。(認証済み or 未認証で Role を分けれる)

認証する場合は、User Pool や Facebook認証などを Provider として設定できる。

アプリで認証しない

システムの都合上、API Gateway を介して Cognito認証する必要があって、その場合は普通には出来ない。

aws-amplify/amplify-js では、アプリ側で Cognito認証を行ってそのまま認可まで行くことを想定しているので、

API経由で認証する場合は、自分でamplifyを認証状態にする必要がある。

その前に、ConsoleLoggerのLOG_LEVELをDEBUGにしておくと調査が捗る。

結構ログを出してくれているので、エラーが出たら、Githubでソースを検索して問題の処理を見てく感じで。

import {ConsoleLogger} from '@aws-amplify/core'

ConsoleLogger.LOG_LEVEL = 'DEBUG'

前提になりますが、configureはこんな感じで。

今回は、インフラ構築にamplify cliは使ってないので、aws-exprots.jsは使ってない。

Amplify.configure({
  Auth: {
    identityPoolId: 'ap-northeast-1:XXXX-XXXX-XXXX',
    userPoolId: 'ap-northeast-1_XXXXXXX',
    userPoolWebClientId: 'XXXXXXXXXXXXXXXX',
    region: 'ap-northeast-1',
  },
  Analytics: {
    AWSPinpoint: {
      appId: 'XXXXXXXXXXXXXXXXXX',
      region: 'us-east-1',
    },
  },
})

APIからのレスポンスで、Credentialsを設定する(認証状態にする)

import {
  CognitoUserSession,
  CognitoIdToken,
  CognitoAccessToken,
  CognitoRefreshToken,
} from 'amazon-cognito-identity-js'
import {Credentials} from '@aws-amplify/core'

...
const {token} = result.payload

const session = new CognitoUserSession({
  IdToken: new CognitoIdToken({IdToken: token.idToken}),
  AccessToken: new CognitoAccessToken({AccessToken: token.accessToken}),
  RefreshToken: new CognitoRefreshToken({
    RefreshToken: token.refreshToken,
  }),
})

await Credentials.set(session, 'session')
...

これで、認証状態になるので、User Poolで認証してIdentify Poolで認証済みのRoleを持ったもので認可できる。

まあ、わかれば簡単なのだけど。

トラブルシューティング

Identify Poolの未認証と認証のRoleを逆に設定してしまって1日はまった :|

DynamoDBと喧嘩しないスキーマ設計

DynamoDBはフルマネージドな「Key-Value Store」だ。

まず、KVSであることを前提に考える必要がある。

サマリー

  • 1つのテーブルで全てのデータを持つ
  • スキーマレスだけど、スキーマ設計はRDBよりも考える必要ある
  • 注文履歴などの一覧をクエリする難易度が高い

1つのテーブルで全てのデータを持つ

AWS公式のベストプラクティスでもあるように、RDBみたいにテーブルを分けずに1つのテーブルで全てのデータを持つ。

例外的にアクセス頻度が高くて大容量が求められるものは分けた方がリソース面で効率がいいケースはあります( DynamoDB で時系列データを処理するベストプラクティス。 - Amazon DynamoDB )

そもそもJOIN出来ないから分けても意味ないっていう話ありますが、テーブルを分けないと一覧の取得が難しい、GSI(グローバルセカンダリーインデックス)も個数に制限があって、RDB脳だと分けた方が管理しやすい気持ちあるのですが、以下の問題があります。

  • リソースはテーブル単位で与えられるため、複数テーブルあると、リソース面で効率が悪い。キャパシティーユニットは読み込み・書き込みが発生してなくても、テーブルに割り当てられてる分だけ時間で請求される(オンデマンドキャパシティーもあって利用分だけ支払えますが割高)
  • オートスケールもテーブル単位になってしまい、スケールされてないテーブルにアクセスが集中するとスロットリングが発生する。

1テーブルに全てを収めるときのコツは、汎用的なカラムをパーティションキー + ソートキーに設定して、汎用的なカラムにGSIを設定する(GSIの多重定義)ことで、GSIを節約します。

この様に定義しておくと、PK+SKで一意なレコードを保存できるようになるため、IDにリソース名のPrefixを付けることで、複数のリソースを持てるようになります。

ID(PK) ColumnName(SK) ColumnData
User-1 User-name hikouki
User-1 User-age 100
Order-1 Order-orderedBy User-1
Order-1 Order-amount 9999

ここで、前方一致でユーザー名を検索したい場合のGSIを考えます。

(※部分一致は scan でもサポートされてないはず)

ColumnNameをPK、ColumnDataをSKとするGSIを定義すれば、そのGSIを使って前方一致で一致するUserを取得できる。

ColumnName: (eq) User-name
ColumnData (begins_with) hik

=> |User-1|User-name|hikouki| がヒットする。

GSIを設定しなくても、scanを使えば検索できるのですが、scanはテーブルフルスキャンになるため、データ量が多くなると線形にパフォーマンスが落ちる。

スキーマレスだけど、スキーマ設計はRDBよりも考える必要ある

RDBの場合、アプリケーションの事を考えずに設計できるので、スキーマ定義を早い段階から着手できる。 ようは、DBに対してどうアクセスされるか(アクセスパターン)を気にする必要が少ない。(indexとか、最適化する意味では別はですけど)

DynamoDBの場合、アクセスパターンを意識してスキーマを定義する必要があるため、 まずは、「どの画面でどんな情報を表示する必要があるか」を決める必要がある。

アクセスパターンを網羅できるようにパーティションキーやソートキーを設定して、必要に応じてGSIを設定していく感じになる。

注文履歴などの一覧をクエリする難易度が高い

パーティションキーは必ず完全一致でないとqueryでは検索できない。

なので、それを回避するために、リソース名をパーティションキーに設定する方法もある。

Resource(PK) ID(SK) UserName OrderBy
users User-1 hikouki
users User-2 toby
orders Order-1 User-1
orders Order-2 User-2

ただ、これで高頻度でアクセスされるとスロットリングが発生するはず。

パーティションキーによって分割してデータが登録されるため、パーティションキーが同じデータが多くあると、そのパーティションにアクセスが集中して I/O キャパシティが効率的に使われずにホットパーティション化して、スロットリングが発生します。

ユーザーに紐付かないトランザクションデータを一覧したい場合は、大抵高機能な一覧画面になるので、ESを使えばいい。

ユーザーに紐づく場合はユーザーIDをPKにして検索項目をSKにしたGSIを作って検索する。

参考

辛くならないmarginの付け方を考える

WIP: 図は時間あるときに付けます。

なんでそんなマージンの付け方してるんだー。ってならないことがない。

CSSは簡単なのだけど、継続的に開発するのが非常に難しい。

maginの設計もアプリケーションで統一してないと、Viewが動的に変化する箇所で、

意図せず、隙間が空きすぎたり、隙間が狭すぎたりしちゃうことがある。

簡単にmarginを運用するために、僕は以下の方針で設計しています。

垂直方向のmarginは基本的に下にしか付けない

部品によって、上下にmarginが付いてたり、上についてたり、下についてたりすることがある。

marginが統一されてないと、部品を使い回す場合や、動的にViewが変更される場合に問題になることが多い。

// 図

上か下どっちかに統一されていればいいのだけど、 個人的には下についていた方がわかりやすい。

コンテナ要素内の最後の部品のmargin-bottomは消す

連続要素や、動的にViewが追加・削除される場合、最後のmargin-bottomが問題になることがある。

// 図

コンテナ要素のpaddingをmargin-bottomで作ってしまうコードもよく見るけど、 そうすると、後からその下に要素を追加する必要が出てきた時に問題になりやすい

// 図

なので、僕の場合はコンテナ要素で > *:last-child に対して margin-bottom: 0 として最後の部品のマージンを消すようにする。

.is-packed > *:last-child {
  margin-bottom: 0;
}

.item {
  margin: 0;
  margin-bottom: 8px;
}
<div class="is-packed">
  <p class="item">item1</p>
  <p class="item">item2</p>
</div>

デフォルトマージンは付けても良いかなー

部品を使い回すときに、マージンが付いていると使いにくい場合があるので、 部品自体にmargin-bottomを定義しない方法もあると思う。 p要素などの標準のmarginを消してしまうリセットCSSもあるぐらいなので。

/* topSearchBarContainerでsearchBarをラップしてトップページで使うみたいな感じ */

.topSearchBarContainer {
  marginBottom: 8px;
}

.searchBar {
  backgroundColor: '#fff';
  border: 1px solid black;
  borderRadius: 3px;
  padding: 4px 6px;
}

部品とそれを配置(レイアウト)してページを作る部分のスタイルを完全に分けてやる場合は、うまくいくのだけど、 結構堅めの設計になるので、僕の場合は結構デフォルトマージンつけちゃうかも。

多くの部品は、他の画面で使う場合も同じマージンをあける事があるので、デフォルトでmargin-bottomを定義しておき、 特定のページだけ、marginを調整したい場合はスタイルを上書きする形で定義して、classに追加する。 単純にmargin-bottomを消したい場合は、汎用的なmargin-bottomを0にするスタイルを定義しておけばいい。

コンテナと部品によってマークアップする

部品の集まりをコンテナでラップしてpaddingや、margin-bottomを付けて隙間を作るようにすると結構うまくいく。 特定の部品の集まりの後に、次の要素を配置する場合のmargin-bottomは、前の要素の部品よりも大きめの隙間を空けたいことが多い。

// 図

次のまとまりの間の隙間を作るために、コンテナ内の最後の要素でマージンを作るコードをよく見る。 sectionとかは集合がわかりやすいので、sectionで説明すると、コンテナで部品集合間のmargin-bottomを管理して、 is-packedすることで、コンテナ内の最後のmargin-bottomを削除してpaddingでコンテナ内の隙間を管理しやすくする。

.profile-section {
  padding: 8px;
  margin-bottom: 12px;
}

/* h1/p要素のmargin-topは0としている前提 */
<section class="profile-section is-packed">
  <h1>Your name.</h1>
  <p>hikouki</p>
</section>
<section class="profile-section is-packed">
  <h1>Your job</h1>
  <p>Programmer</p>
</section>

// 図

sectionで説明しましたが、普通は、section内にもコンテナがあって、コンテナ間で隙間を作り合うことは結構ある。 部品集合をコンテナでラップして隙間を管理すると、コンテナに新しい部品を追加した時でも、コンテナ内のpaddingやコンテナ間のmarginが変わらないので保守性が高まる。

コンテナ要素でネガティブマージンを使う

flexで、水平方向に連続要素を並べたいことは良くあると思う。

1列の場合はそんなに問題にならないが、2列以上に flex-wrap: wrap する場合は上下のマージンで問題になることがある。

// 図

連続要素自体にmargin-bottomを付けてしまうとコンテナのpaddingにmargin-bottom分が加算されて管理が難しくなる。 なので、親の要素でネガティブマージンを付けて連続要素の隙間を相殺する形で定義すると良い。

.container {
  /* overflowしないと画面端からはみ出て水平方向にスクロールバーが出たりする */
  overflow: hidden;
  margin-bottom: 12px;
}

.row {
  margin: -4px -8px;
}

.col {
  padding: 4px 8px; 
}
<div class="container">
  <div class="row">
    <div class="col">
      <article class="news-card">
        <h1>Today's Weather</h1>
         .....
      </article>
    </div>
  </div>
</div>

1KBを超える文字列をAsyncStorageに保存してはいけない。

1KBを超える文字列は、読み出しに時間が掛かりすぎるので避けたほうが良い。

AsyncStorageにダウンロードした画像をbase64で保存して、次回からキャッシュから取り出すみたいな事をやってる人も多いんじゃないかな。

ただ、iOSの場合は、1KB以下の文字列はオンメモリで持つようになっているので高速に読み取れますが、それを超える文字列は別ファイルに保存され、毎回読み取り(File Read)が発生する。

一応、その対策として2MBのキャッシュもあって、1回目のファイル読み取り後はキャッシュに乗るようになってますが、それも超えると、まったくキャッシュにヒットしない。

探索順: オンメモリ(Dictionary) -> 2MBキャッシュ -> File Read

コード: https://github.com/react-native-community/async-storage/blob/b04ffbf1b80bdf5295cb36c21fc5ca62a7333e3c/ios/RNCAsyncStorage.m#L384

AsyncStorageをキャッシュとして使う場合、低速だと意味がないので、1KB以下の文字列にした方が良いかと思います。

バリデーションを再考する

僕は以前、 「DDDで考えるマイクロサービスのバリデーション」という大それた記事を書いた。

qiita.com

マイクロサービスっていうのが、ちょっと良くわかんない。

整理されてなくて、何が言いたいのかも良く分からないので、

再考した結果を、リライトしてみます。

リクエストパラメタの入力値検証

結論

値オブジェクトを作って、検証しよう。

詳細

プレゼンテーション層では、アプリケーションサービスの引数に渡す値だと考える。

ただ、値が妥当かどうかを判断するには、プリミティブな型だと情報が足りない。

それが、「メールアドレスとして欲しい」のか、「7桁の文字列が欲しい」のかStringじゃ分からない。

値オブジェクトを作って、validかどうか検証した結果、validならアプリケーションサービスに渡せばいいし、 駄目だったら、ユーザーにバリデーションエラーとして返す。

業務処理でのバリデーション

結論

  • 引数の値オブジェクトは検証済みが来る想定ですが、念の為DbC的にチェックして、invalidな場合は、例外で落としてもいい気がする
  • 例外を通知して、プレゼンテーション層側で、ユーザーに返せる形にする

詳細

入力値としては正しいが、業務上処理できない場合がある。

例えば、「登録済みのメールアドレスでユーザーを登録できない」がある。

この検証は、アプリケーションサービス中で行わないと、業務上、同じメールアドレスを持つユーザーを登録できても良いことになる。

検証に失敗したら、例外をスローして、プレゼンテーション側で、422として返すのか、500として返すのか振り分ける。

入力値がそもそも正しいかは、契約による設計(DbC)的にチェックして弾いちゃっていいかも。

インフラ層での検証

結論

テーブルの制約で頑張る

詳細

結論通り、テーブルの制約で頑張る。

正直、アプリケーションコードで頑張ってバリデーションしても、通り抜ける事はよくある。

チーム開発している大きなアプリケーションは、気付いた時にはトランザクション中で、重たい処理をしていて、メールアドレスが重複してユーザーが登録されたりしている。

メールアドレスにユニーク制約つけて、退会したらメールアドレスをランダムにマスクしたりして、強制的に変更しておく方が誤送信の防止とかにもなるし、安全なはず。

非常駐フリープログラマーになるためにしたこと

こんにちは、hikoukiです。

ex-crowdworks Advent Calendar 2018の18日目の記事になります。

このアドベントカレンダーは、元クラウドワークスのエンジニア達によるものです。

僕も、今年の8月末で退職して、個人事業主ジョブチェンジしました。

2017年12月に入社したので、1年半ぐらいお世話になったことになります。

engineer.crowdworks.jp

「こうやればフリーになれるよ!」みたいなHOWTO記事ではないですが、

僕は、こんな感じでフリーになって働いてるよ!っていうのを紹介できればと思います。

プロフィール

※ 興味がない方は飛ばしてもらってOKです!

中学生ぐらいからHTML触り始めて、高校ぐらいからEmacsに出会い、Emacsのすばらs...(ry

まだまだ、社会人5年目か6年目ぐらいですが、IT業界はそこそこ経験値が溜まってる気がします。

新卒で入った会社では、SESを2年ぐらいやって、その間、数社に常駐して色んなシステムを作りました。

多分、その会社の中でも僕ほど、常駐先を転々とした人は居なかったのではなかろうか。

SES先の会社から、さらにSES先の会社に派遣されるとかよくあった。SES業界はそれが許されているらしい。

SIP/RTPのリピーターをNettyで書いたり、XamarinとかAndroidとか触ったり、HTMLのゲームアプリ作ったりとか、色んな事やった。

cocos2dxで、自社のゲームアプリ作って、1年ぐらいでサービス終了したとかあったなー。

最初はSESが主軸だったけど、会社の中に残るものがなくて、ただただ外に人を派遣するビジネスなので、

これじゃあ駄目だと、気づいて、受託メインの会社になった。(多分)

その後、クラウドワークスに転職して、サービス開発しつつ、チームマネージメントとかもちょっとやらせて貰いました。

なんでフリーになったのか

学校も会社もそうですが、月曜日から金曜日まで、普通に出勤して退勤するみたいな、

当たり前なことが毎日続いてることに、気持ち悪さを感じてました。

自分の時間生きてないなー。みたいな。

正直、そんな強い意味はなく、20代後半になったので少し振り返る時間が欲しくなったとかもです。

もっと、小さいチームで、なんか作ってみたいなーという気もしてるので、来年やるかも。

仕事を貰えるようになるまで

自分でサービス立ち上げて、運営してくみたいな事も考えたけど、あんまり現実味がなかったので、

まずは、受託で仕事を貰えるようになるのが早いかなと思ってました。

この辺りは、結構運が良くて、新卒で入った会社が受託開発が主軸だった事もあって、

クラウドワークスに転職した後も、副業で、前職から仕事を貰ってました。

あとは、リモートワーク系のコミュニティに参加したら、そこで知り合った人から仕事を貰うことができたり、

サービス開発に関わったりして、そんな感じで副業している内に人を紹介してもらったりして仕事が増えていきました。

この辺りから色んな人と合って、考え方が広がった感じがします。

フリーになる前に、クラウドワークスを選んだ理由

仕事を貰えるって言っても、結婚してるし、受託だけだと怖いっていう思いがありました。

転職したいと思った一番の理由は、前職が受託・SESだったので、

「自社でサービス開発している人達と、最新の技術使いながらサービスを育ててみたい」と思ってたのが大きいです。

ただ、満員電車で通勤したくなかったので、最低でもリモートワークと、

今後、フリーになることも視野に入れたかったので、副業有りを条件に転職先を探してました。

探してるといっても、ほぼ1択だった気がします。

自分が実現したい世界と、クラウドワークスのビジョンが重なってた事と、上記の条件を満たしてました。

入社した後も、退職する前も、特にギャップはなかったです。

皆、ユーザー中心で考えるし、組織的にも強くなろうと改善を進めていて、

働いてて楽しかったです。開発体験(DX)もかなりいいと思います。

改善する過程で、「あー、そっちに行っちゃうか。」って時もあったけど、

最終的に良い方向に進んでそうなので、嬉しいです。

退職ブログっぽくなるので、これ以上書くの辞めておきますが、転職を考えてる人にはオススメです!

どんな感じで仕事してるの?

もちろん、コアタイムもないし、明日休んだっていい。

月によっては、1日に2~4時間ぐらいしか働かない時もあります。

でも、年末年始は大きい仕事か被ってるので、慌ただしく年を明けそうです :|

基本的にリモートワークで、1~2ヶ月に1回ぐらい対面でMTGがあるぐらいです。

最近はReactでWebフロントとか、ReactNativeでアプリつくったりしてます。

実はクラウドワークスにも、月40時間ぐらいお世話になってます。

あとは、数社のサイトやサービスを運用しているぐらいです。

でも実は、8月で退職する時には、2018年分ぐらいは既に稼いでたので、

最悪、全く仕事が来なくても、生活に困ることはないかー。ぐらいの気持ちでフリーになりました。

なので、また状況が変わったり、やりたいことが変わったら、サラリーマンに戻るかもしれません。

あとがき

やりたい事が決まってれば、簡単。

その目的が一番早く達成されそうな方法で、取り組めば良い。

でも、何がやりたいかって言われてもわかんない時は、「何をやりたくないか」で考えるといいかも。

そんなにやりたくない事なのに、YESと言ってしまって、本当にやりたいことに集中できる時間が減ってるかもしれない。

毎日同じ時間に、満員電車で出社。

色んなMTGや諸事情により、作業が中断してしまい、中々集中できないし、気持ちも作れない。

査定のために、作りたくない目標を作り、別にしたくない成長を促される。

ちょっと過剰に書きましたが、そんな、自分の人生を生きていない気がする人は、

一度フリーになることを考えてみてもいいかもしれません。

コードレビューって効率良いって話

こんにちは、hikoukiです。

この記事は ex-crowdworks Advent Calendar 2018 6日目の記事です。

クラウドワークスの人達によるアドベントカレンダーなので、僕もクラウドワークスを退職済みです。

フリーになるために、辞めさせて頂きましたが、良い会社なので転職を考えてる方はオススメです!

18日も書きますが、そっちがメインになってます。

空きがあったので、主旨と違いそうですが、クラウドワークスで学んだ系の記事です。

本題

コードレビューやってますか?

この記事は、「コードレビューやると、質が上がったり、知見が溜まるよ!」って話じゃないです。

それを前提に、既にレビューやってるけど、「億劫でレビューしたくない」って人向け。

貴方がコードを書くよりも、他の人のコードをレビューした方が効率が良いっていう

ちょっとトゲがある話です。

(要は、僕のコードをレビューしてほしい。)

並列化しよう

貴方がレビューしないと、誰かの作業を止めてるかもしれない。

誰かの作業を止めてまで、やらないといけない仕事をやってるなら仕方ないけど、ほとんどの場合、そうでないと思う。

30分ぐらいかければ、レビュー出来るのに、何時間も誰かの時間を止めてしまうのは勿体無い。

この話は、レビューに限らず、教育的な話も同じで、コードを書けないメンバーがチームにいた場合、

そのチームの最優先事項は、全員がコードを書ける状態にすることだと、僕は思う。

常に、並列で動ける状態にしたほうが、全体として効率がいいはず。

伝達する

貴方がチームに閉じた状態でコードだけを書いていたとする。

多くの場合、他のチームの人からすると、貴方が何をやってるのかよくわかってない。

基盤周りを整理する人達は、他のチームにも影響があるので感謝されやすい。

でも、アプリケーションのコードを書いてる人たちは、会社の人から感謝されたり、仕事をしている事を知ってもらえてない可能性が高い。

チームリーダとかPOとかマネージャーが優秀で、うまく伝達できてる場合もあるかもだけど、その人に依存しちゃう。

コードレビューは、そんな問題を解決できる1つの方法かもしれない。

レビューされなくて作業が止まってしまう人からすると、レビューしてくれる人への好感度はかなり上がる。

slackで、「僕やります!」とログを残すとより良いかも。

多くの人に大変な作業を自分の手を止めてまでやってくれる人だと周知できる。

自分の能力に自信があって、自分でやった仕事だけで評価されたいと思う人は、別の方法で伝達する方法があると思うけど、

僕みたいな普通の人は、こんな方法で、仕事やってるぜ!感を伝達する必要がある。(給与を上げるために)

伝達する2(番外編)

ちなみに、コードレビュー以外にも伝達する方法はあって、

ユーザーサポートだったり、営業だったりの仕事を率先してやる方法もある。

サービスを運営してる会社だったら、サポートから、「ユーザーさんから、こんな問い合わせ来たんですが、状況確認できますか?」みたいな事は結構あると思う。

そんなの時は、伝達するチャンス!今すぐ手を止めて、リアクションするだけで、エンジニアの外にも伝達することができる。

コードレビュー然り、「困ってる人を助ける」行為は、表現が悪いが、かなりコスパが良い事が多い。

なので、困ってる人がいたら、いち早く反応しよう!

レビューしない人を怒らない(後付)

僕は、別にレビューしない人に怒ってるわけではない。

(むしろ、レビューしない人が増えた方が、僕の仕事が増えるので、業務委託的には嬉しい)

彼・彼女は、仕事にかなり集中するタイプで、メンションに気づかないかもしれない。

そもそも、slackを確認しない人かも。

でも、多様性は大事なので、そんな人も会社には非常に必要だと思う。違う所で力を発揮してるはず。

僕は、あくまで、レビューすることの良さに気づけてない人もいるかもなので、記事にしてみました。

あとがき

「こんな感じで考えてるんだコイツ」みたいな事を思われたらヤダなーと思ったので、正直、書くか迷いました ;(

でも、ここに書いてるのは自分が行動する「裏付け」であって、本当にこんな事思いながら仕事してないです。

単純に目の前で、困ってる人を助けたいって気持ちでやってるので、誤解しないでね!