※ このお話は ( おそらく ) フィクションです。実在の人物や団体とは関係ありません。
前書き
中規模程度のサービスを Serverless 構成の SPA をパイロットリリースしました。具体的なサービス名等は紹介できません ( おそらくフィクションなので ) が、筆者が3ヶ月間でやってきたことを殴り書きして行きます。
基本的にはリリースまでに必要となった材料 ( 参考にしたドキュメントやサイト ) を重点的に紹介していくだけですが、同じような境遇の方々の手助けになれば幸いです。
筆者のプロジェクトイン時スペック
- 社歴3年程度
- 主にバックエンド・インフラを担当してきた
- と言いつつもフロントエンドもある程度は(jQuery ゴリゴリ)
- ナウい Web フロントエンド ( いわゆる ES6 や Flux、Webpack 等 ) については「聞いたことある。触ったことある。」程度
3ヶ月間のスケジュール ( ざっくりと )
※ 既に要件定義済み
- 1ヶ月目 第1週〜第2週
- 技術選定
- 技術習得
- 1ヶ月目 第3週〜第4週
- 設計
- 外部設計
- 内部設計
- インフラ設計
- DB 設計
- API 設計
- URL 設計
- コンポーネント設計
- スタイルガイド作成
- 開発環境構築
- 設計
- 2ヶ月目
- 開発
- 3ヶ月目 第1週〜第2週
- 開発
- 3ヶ月目 第3週〜第4週
- テスト
- リリース
構成
プロジェクトを構成するパッケージ
※ インストールしたパッケージ全てを掲載していないのでご注意ください。
SPA
global
devDependencies
dependencies
Serverless
global
devDependencies
dependencies
技術選定
インフラ
ユーザー認証基盤として Cognito を利用しました ( ユーザー認証基盤を開発してたらリリース間に合わない ) 。詳しくは説明できませんが、SPA + API をベースにすることが必須ということもあり、インフラコストやメンテコストの削減に期待できる Serverless Architecture を選択しました。
DB
パイロット運用ということもあり、DB に変更が加わることが予想されました。
そのため、RDBMS ではなく変更に対して柔軟に対応できる NoSQL を選択しました。時期、本リリース時には RDS にするか検討しています。
また、開発当初は DynamoDB を利用していましたが、今まで NoSQL として MongoDB を利用してきた身からするとクエリが貧弱だったりと使いづらく、
途中で MongoDB に変更しました。
※ DynamoDB は悪くありません。そもそも利用用途として間違っていました。
「EC2 上に MongoDB をインストールしているんだから Serverless Architecture じゃねーだろ!!」と言われるとぐうの音も出ないのでやめてください...。
API
GraphQL が気になってはいたのですが、
GraphQL の学習コストと採用実績を考慮し、利用例のある REST を選択しました。
View ライブラリ
View ライブラリは以下の理由から Riot.js を選択しました。
- 中規模なサービスに React や Angular を導入するのは少し大げさ
- 他のView ライブラリよりシンプルでわかりやすいため学習コストが低い
- デザイナーとの協業がしやすい
技術習得
基本的に公式ドキュメントとサンプルは一通り目を通しました ( 通したつもり )。
バックエンド
AWS
Serverless Architecture
- サーバーレスアーキテクチャとは何か? AWSの「Lambda」と「EC2」を比較して解説 CIOのためのAWS最新動向(1)|ビジネス+IT
- サーバーレスアーキテクチャという技術分野についての簡単な調査 - Qiita
Serverless Framework
- 公式 Examples
- Serverless Frameworkでつくる | シリーズ | Developers.IO
- Serverless Framework v1.0の使い方まとめ - Qiita
- とことんサーバーレス①:Serverless Framework入門編 - Qiita
フロントエンド
JavaScript ( ES6 )
- JavaScript本格入門
- ゼロから始めるJavaScript生活
- jQueryは必要ない(You Don't Need jQuery)
- レトロエンジニアのための近代Webフロントエンド事情 - Qiita
- 連載 | ECMAScript 2015(ECMAScript 6)特集 | HTML5Experts.jp
Web Components
連載 | 基礎からわかる Web Components 徹底解説 〜仕様から実装まで理解する〜 | HTML5Experts.jp
Riot.js
- 公式 Examples
- Riot.jsでフロントエンドの複雑さに反乱するときがやってきた - Qiita
- フロント界隈で一番イケてるのは AngularJS でも React でもなく Riot.js だという話 | phiary
- Riot.js と Atomic Design ではじめるテクニカルクリエイター|Technical Creator Hub
Flux
- 公式 Examples
- これから始めるReact.js 発展編 - Fluxという設計思想 | CodeGrid
- Fluxアーキテクチャのメモ - yukisovのメモ帳
- ReactとFluxのこと // Speaker Deck
- React.jsとFlux - Qiita
- React+Redux入門 - Qiita
RiotControl
Babel
webpack
gulp
設計
外部設計
省略
内部設計
省略
インフラ設計
省略
DB 設計
省略
API 設計
以下の記事を参考にしながら設計をしていきました。
- 翻訳: WebAPI 設計のベストプラクティス - Qiita
- Web API 設計のベストプラクティス集 "Web API Design - Crafting Interfaces that Developers Love" - フリーフォーム フリークアウト
- WebAPI でファイルをアップロードする方法アレコレ - Qiita
- API Design Guide | Cloud APIs | Google Cloeud Platform
手軽さから普段から使い慣れている Google スプレッドシートで設計書を作成しました。
しかし、Swagger のようなツールを利用すれば、もっとクールな設計書になったかなと思います。
SwaggerでRESTful APIの管理を楽にする - Qiita
URL 設計
コンポーネント設計
コンポーネント設計なんてしたことがなかったため、タグの粒度をどうするかで非常に悩みました。
デザイナーさんが Atomic Design ( 日本語訳 ) を取り入れたいという話をしていたので、こちらの記事を参考にして AtomicDesign のコンポーネント単位で設計していきました。
スタイルガイド作成
スタイルガイドを作る十分な時間を確保することが出来なかったため、以下のスタイルガイドに載っとるようにしました。
- Riot
voorhoede/riotjs-style-guide ( 日本語訳 )
- JavaScript
- CSS / Sass
airbnb/css ( 日本語訳 )
開発環境構築
Node.js
nvm
Node.js のバージョン管理ツールとして nvm
( Node Version Manager ) を利用し Node.js をインストールしました。 nvm
を選択した理由としては、バージョン番号を含む .nvmrc ファイルをプロジェクトごとに作成することで Node.js のバージョンをプロジェクトごとに固定出来るためです。
例えば、プロジェクト ( カレントディレクトリ ) の Node.js のバージョンを v4.3.2 に設定するには以下のようにします。
$ echo "4.3.2" > .nvmrc
次に、 nvm
を実行すると v4.3.2 の Node.js を利用することができます。
$ nvm use
Found '/path/to/project/.nvmrc' with version <4.3.2>
Now using node v4.3.2 (npm v2.14.12)
.nvmrc ファイルを SPA のプロジェクトと Serverless のプロジェクト ( Lambda で使われている Node のバージョン ( v4.3.2 ) に合わせている ) それぞれに作成しています。
Yarn
パッケージマネージャとして NPM よりも高速な Yarn を利用しています。
Yarn については以下の記事参照してください。
npm互換のJavaScriptパッケージマネージャーYarn入門 - ICS MEDIA
バックエンド
Serverless Architecture の環境を Serverless Framework を使って構築しています。
また、 serverless-offline プラグインを使用してローカルで動作するようにしています。また、DynamoDB Local インスタンスは serverless-dynamodb-local プラグインによって提供されます。
公式 Examples が用意されているため参考にしながら構築していきました。導入や設定方法は以下の記事を参照してください。
Serverless アプリケーションをローカルで開発する - Qiita
フロントエンド
今回構築した環境は以下がベースとなっています。
- Riot
- RiotControl
- Babel
- Sass
- Webpack
こちらの環境は 公式 Examples を参考に構築しました。
※ 余談ですが、 こちらのコミット以降、 Webpack のローダーは、 riotjs-loader ではなく公式の Webpack ローダーである tag-loader に変更されたようです。筆者も 公式サンプル を見つける前は、 Webpack のローダーとして riotjs-loader を利用していました...。riotjs-loader は Riot.js v3 以降に対応していないようですので、riotjs-loader を利用している方は、tag-loader に切り替えることをお勧めします。
また、以下の理由により Gulp は導入してあります。
Webpackはさまざまなことを行います。プロジェクトがクライアントサイドのものであれば、Gulpをすっかり置き換えてしまうこともできます。一方で、Gulpはもっと汎用的なツールで、linting、テスト、バックエンドタスクなどに向いています。
とはいえ、今回は linting やテストは行なっていません。今後導入していきたい...。
開発
基本的には、技術習得で上げた記事を参考にしながら開発を進めて行きました。
以下では、事前に知っておけばよかった知識や、どんなパッケージを利用して開発をしたかを簡単に記載しています。
バックエンド
開発着手前に読んでおいた方が良い記事
Serverless Architecture のコードパターン
Serverless Architecture のコードパターンは以下の4パターンです。
- Microservices Pattern
- Services Pattern
- Monolithic Pattern
- Graph Pattern
筆者は、以下のように 各 Lambda 関数 ( functions
) につき単一の HTTP エンドポイント ( handler
) を持つ Microservices Pattern で開発して行きました。
service: serverless-social-network
provider: aws
functions:
usersCreate:
handler: handlers.usersCreate
events:
- http: post users/create
commentsCreate:
handler: handlers.commentsCreate
events:
- http: post comments/create
しかし、開発を進めていくに伴いデプロイの際に CloudFormation のリソース上限に達してしまいました。
CloudFormation自体のリソース上限が200なので、デプロイの際に200を超えるリソースがあるとエラーになります。
そのため、以下のように 全てのCRUD操作をもつ各 Lambda 関数につき複数の HTTP エンドポイントを持つ Services Pattern に変更しリソース制限を回避しました。
service: serverless-social-network
provider: aws
functions:
users:
handler: handler.users
events:
- http: post users
- http: put users
- http: get users
- http: delete users
comments:
handler: handler.comments
events:
- http: post comments
- http: put comments
- http: get comments
- http: delete comments
Services Pattern を実現するにはリクエストメソッドまたはエンドポイントに基づいて適切なロジックを呼び出すためにルータを作成する必要があります。
ルータの作成においては以下を参考にしました。
Routing API Gateway Traffic Through One Lamda Function
CORS
Serverless Framework で CORS を有効にする際は、以下のように設定してください。
cors
を true
設定すると、次のようなデフォルト設定が仮定されます。
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
cors:
origins:
- '*'
headers:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Amz-Security-Token
allowCredentials: false
この cors
プロパティを設定すると、CORS preflight レスポンスに Access-Control-Allow-Origin
、 Access-Control-Allow-Headers
、 Access-Control-Allow-Methods
、 Access-Control-Allow-Credentials
ヘッダーが設定されます。
XMLHttpRequest を使う場合は X-Requested-With
ヘッダーと X-Requested-By
ヘッダーを付与してください。
headers:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Amz-Security-Token
+ - X-Requested-With
+ - X-Requested-By
Lambda プロキシとの統合で CORS を使用する場合 Access-Control-Allow-*
は、ヘッダーオブジェクトに次のようにヘッダーを含めるようにしてください。
'use strict';
module.exports.hello = function(event, context, callback) {
const response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin" : "*", // Required for CORS support to work
"Access-Control-Allow-Credentials" : true // Required for cookies, authorization headers with HTTPS
},
body: JSON.stringify({ "message": "Hello World!" })
};
callback(null, response);
};
認証と認可
- Amazon Cognito ユーザープールを使用 - Amazon API Gateway
- Amazon API Gateway カスタム認証を使用する - Amazon API Gateway
- AWSとシステムの認証認可を考える - プログラマでありたい
- Cognito UserPoolを使ってAPIを保護しよう | HIGHWAY for AWS
フロントエンド
開発着手前に読んでおいた方が良い記事
Amazon Cognito User Pools を使ったユーザ認証基盤
aws-sdk と amazon-cognito-identity-js を利用してユーザー認証を実現しています。
認証基盤を構築する上での Flux のフローは以下の記事を参考にしました。
Adding authentication to your React Flux app
ルーティング
Riot 公式の riot-route を利用しています。
Flux
コンポーネント間のデータの受け渡しをシンプルにするために、 Dispatcher として riot-control を利用して Flux を実現 しました。
構成としては、 Riot と RiotControl をベースとした以下のスターターキットに習った形となっています。
This starterkit is based on:
- Riot
- RiotControl
- PostCSS
- Webpack
コンポーネント設計 の記載にある通り、コンポーネントは AtomicDesign のコンポーネント単位で分割したため、最終的に src のディレクトリ構造は以下のようになりました。
.
├── assets/ # Image, Font, etc.
├── css/
├── components/ # Components by AtomicDesign
│ ├── atoms/
│ ├── my-example-1
│ │ ├── README.md
│ │ ├── my-example-1.scss
│ │ └── my-example-1.tag.html
│ ├── my-example-2
│ │ ├── README.md
│ │ ├── my-example-2.scss
│ │ └── my-example-2.tag.html
│ ├── molecules/
│ ├── organisms/
│ ├── templates/
│ └── pages/
├── constants/ # Action Types
│ └── myExampleActionTypes.js
├── actions/ # Action Creators
│ └── myExampleActions.js
├── stores/ # Store
│ └── myExampleStore.js
├── utils/ # Web API Utils, Mixin, etc.
│ └── MyExampleApi.js
├── config/ # config
│ ├── development.js
│ └── production.js
├── index.html
├── index.js
└── router.js
RiotControl を利用する上での注意点としては、ルーティング等でタグが mount、unmount を繰り返す場合、mount がされる度に dispatcher のイベントもその都度増えていくため、一回だけ実行されればいいイベントが複数回呼ばれページが重くなるケースに遭遇しました。
import dispatcher from 'riotcontrol';
<my-example>
<script>
dispatcher.on('EVENT', () => {
console.log('called event!');
});
</script>
</my-example>
このような場合は、タイミングは自由ですが dispatcher のイベントを破棄する必要があります。例えば、以下のようにすることで unmount 時にイベントを破棄できます。
import dispatcher from 'riotcontrol';
<my-example>
<script>
+ var tag = this;
+ tag.on('unmount', () => {
+ dispatcher.off('EVENT');
+ });
dispatcher.on('EVENT', () => {
console.log('called event!');
});
</script>
</my-example>
Riot.js
Atom を作成していく際、例えば四角いボタンを作成する場合はどのように作成するでしょうか?筆者は以下のように開発当初作成していました。
import './button-square.scss';
<square-button>
<button class={ opts._class }>
<yield/>
</button>
<square-button>
<square-button _class="green">四角ボタン</square-button>
これだと、厳密には Atom ではありません。
以下のように HTML エレメントをタグとして扱うことで Atom を作成することができます。
import './button-square.scss';
<button-square>
<yield/>
</button-square>
<button data-is="button-square" class="green">四角ボタン</button>
REST API リクエスト
Web リクエストを行うためのパッケージとして、superagent や axios がありますが、WHATWG によって策定が進んでいる次期標準 のFetch API を利用しています。過去バージョンのブラウザへの対応が必要となったため、whatwg-fetch を利用しています。
お疲れさまXMLHttpRequest、こんにちはfetch - Qiita
Flux において API リクエストは Action Creator で行い、取得したデータを Store に引き渡すのが正しいそうです。
バリデーション
フロント側のバリデーションとして、validatorjs を利用しています。
フロントサイドのバリデーションにはvalidatorjsが便利 - Qiita
validatorjs の Available Rules の日本語訳として、使用可能なバリデーションルール を参照するのがオススメです。
Flux においてバリデーションロジックは Store に書くべきだそうです。
Validationのロジック自体は役割としてはStoreにあるのが正しいのかなと思っています。ViewがActionを投げてStoreが受け取ったときに不正なデータの場合は、エラーのイベントをViewに投げてViewは必要あればエラーの表示をする流れがいいのかなと個人的には思っています。
------ ------------ ------------------- ------
|View| --------|Dispatcher|--------|StoreでValidation|--------|View|--- エラー表示
------ action ------------ action ------------------- error ------
筆者はバリデーションロジックを View に書きましたが...。
テスト
省略
終わりに
ざっくりとした内容のため、ここが伝わらない、ここどうしたの?という部分があればコメントください。
また、開発環境の構築や Riot.js + RiotControl を利用した Flux の実現については、詳しく記載した内容として別の機会に公開しようと思っています。