3ヶ月で Serverless Framework を導入し、SPA ( Riot.js + RiotControl で Flux 実装 ) をリリースした話

  • 11
    いいね
  • 0
    コメント

※ このお話は ( おそらく ) フィクションです。実在の人物や団体とは関係ありません。

前書き

中規模程度のサービスを 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 が気になってはいたのですが、

RESTの次のパラダイムはGraphQLか - Qiita

GraphQL の学習コストと採用実績を考慮し、利用例のある REST を選択しました。

View ライブラリ

View ライブラリは以下の理由から Riot.js を選択しました。

  • 中規模なサービスに React や Angular を導入するのは少し大げさ
  • 他のView ライブラリよりシンプルでわかりやすいため学習コストが低い
  • デザイナーとの協業がしやすい

技術習得

基本的に公式ドキュメントとサンプルは一通り目を通しました ( 通したつもり )。

バックエンド

AWS

Serverless Architecture

Serverless Framework

フロントエンド

JavaScript ( ES6 )

Web Components

連載 | 基礎からわかる Web Components 徹底解説 〜仕様から実装まで理解する〜 | HTML5Experts.jp

Riot.js

Flux

RiotControl

Babel

webpack

gulp

設計

外部設計

省略

内部設計

省略

インフラ設計

省略

DB 設計

省略

API 設計

以下の記事を参考にしながら設計をしていきました。

手軽さから普段から使い慣れている Google スプレッドシートで設計書を作成しました。
しかし、Swagger のようなツールを利用すれば、もっとクールな設計書になったかなと思います。

SwaggerでRESTful APIの管理を楽にする - Qiita

URL 設計

コンポーネント設計

コンポーネント設計なんてしたことがなかったため、タグの粒度をどうするかで非常に悩みました。
デザイナーさんが Atomic Design ( 日本語訳 ) を取り入れたいという話をしていたので、こちらの記事を参考にして AtomicDesign のコンポーネント単位で設計していきました。

スタイルガイド作成

スタイルガイドを作る十分な時間を確保することが出来なかったため、以下のスタイルガイドに載っとるようにしました。

  • Riot

voorhoede/riotjs-style-guide ( 日本語訳 )

  • JavaScript

airbnb/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に統合する

Webpackはさまざまなことを行います。プロジェクトがクライアントサイドのものであれば、Gulpをすっかり置き換えてしまうこともできます。一方で、Gulpはもっと汎用的なツールで、linting、テスト、バックエンドタスクなどに向いています。

とはいえ、今回は linting やテストは行なっていません。今後導入していきたい...。

開発

基本的には、技術習得で上げた記事を参考にしながら開発を進めて行きました。
以下では、事前に知っておけばよかった知識や、どんなパッケージを利用して開発をしたかを簡単に記載しています。

バックエンド

開発着手前に読んでおいた方が良い記事

Serverless Architecture のコードパターン

Serverless Architecture のコードパターンは以下の4パターンです。

Serverless Code Patterns

  1. Microservices Pattern
  2. Services Pattern
  3. Monolithic Pattern
  4. Graph Pattern

筆者は、以下のように 各 Lambda 関数 ( functions ) につき単一の HTTP エンドポイント ( handler ) を持つ Microservices Pattern で開発して行きました。

serverless.yml
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 に変更しリソース制限を回避しました。

serverless.yml
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

CORSまとめ - Qiita

Serverless Framework で CORS を有効にする際は、以下のように設定してください。

corstrue 設定すると、次のようなデフォルト設定が仮定されます。

serverless.yml
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-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-MethodsAccess-Control-Allow-Credentials ヘッダーが設定されます。

XMLHttpRequest を使う場合は X-Requested-With ヘッダーと X-Requested-By ヘッダーを付与してください。

serverless.yml
 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-* は、ヘッダーオブジェクトに次のようにヘッダーを含めるようにしてください。

handler.js
'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 User Pools を使ったユーザ認証基盤

aws-sdkamazon-cognito-identity-js を利用してユーザー認証を実現しています。

Babel webpack example

認証基盤を構築する上での Flux のフローは以下の記事を参考にしました。

Adding authentication to your React Flux app

ルーティング

Riot 公式の riot-route を利用しています。

Flux

コンポーネント間のデータの受け渡しをシンプルにするために、 Dispatcher として riot-control を利用して Flux を実現 しました。

flux-diagram-white-background

構成としては、 Riot と RiotControl をベースとした以下のスターターキットに習った形となっています。

riotjs-startkit

This starterkit is based on:

  • Riot
  • RiotControl
  • PostCSS
  • Webpack

コンポーネント設計 の記載にある通り、コンポーネントは AtomicDesign のコンポーネント単位で分割したため、最終的に src のディレクトリ構造は以下のようになりました。

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 のイベントもその都度増えていくため、一回だけ実行されればいいイベントが複数回呼ばれページが重くなるケースに遭遇しました。

my-example.tag.html
import dispatcher from 'riotcontrol';

<my-example>
  <script>
  dispatcher.on('EVENT', () => {
    console.log('called event!');
  });
  </script>
</my-example>

このような場合は、タイミングは自由ですが dispatcher のイベントを破棄する必要があります。例えば、以下のようにすることで unmount 時にイベントを破棄できます。

my-example.tag.html
 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 を作成していく際、例えば四角いボタンを作成する場合はどのように作成するでしょうか?筆者は以下のように開発当初作成していました。

square-button.tag.html
import './button-square.scss';

<square-button>
  <button class={ opts._class }>
    <yield/>
  </button>
<square-button>
タグ呼び出し
<square-button _class="green">四角ボタン</square-button>

これだと、厳密には Atom ではありません。
以下のように HTML エレメントをタグとして扱うことで Atom を作成することができます。

square-button.tag.html
import './button-square.scss';

<button-square>
  <yield/>
</button-square>
タグ呼び出し
<button data-is="button-square" class="green">四角ボタン</button>

REST API リクエスト

Web リクエストを行うためのパッケージとして、superagentaxios がありますが、WHATWG によって策定が進んでいる次期標準 のFetch API を利用しています。過去バージョンのブラウザへの対応が必要となったため、whatwg-fetch を利用しています。

お疲れさまXMLHttpRequest、こんにちはfetch - Qiita

Flux において API リクエストは Action Creator で行い、取得したデータを Store に引き渡すのが正しいそうです。

バリデーション

フロント側のバリデーションとして、validatorjs を利用しています。

フロントサイドのバリデーションにはvalidatorjsが便利 - Qiita

validatorjs の Available Rules の日本語訳として、使用可能なバリデーションルール を参照するのがオススメです。

Flux においてバリデーションロジックは Store に書くべきだそうです。

データのValidationについて

Validationのロジック自体は役割としてはStoreにあるのが正しいのかなと思っています。ViewがActionを投げてStoreが受け取ったときに不正なデータの場合は、エラーのイベントをViewに投げてViewは必要あればエラーの表示をする流れがいいのかなと個人的には思っています。

------         ------------        -------------------        ------
|View| --------|Dispatcher|--------|StoreでValidation|--------|View|--- エラー表示
------  action ------------ action ------------------- error  ------

筆者はバリデーションロジックを View に書きましたが...。

テスト

省略

終わりに

ざっくりとした内容のため、ここが伝わらない、ここどうしたの?という部分があればコメントください。
また、開発環境の構築や Riot.js + RiotControl を利用した Flux の実現については、詳しく記載した内容として別の機会に公開しようと思っています。