Nuxt + Sails + TypeScript + Fargateでタスク管理ツールを作ったら快適だった話

Repsona LLCの[@GussieTech](https://twitter.com/GussieTech)です。[無料ガントチャート、無料カンバン、無料ドキュメント管理が便利な「理想のプロジェクト管理ツール」](https://repsona.com/ja)を作っています。

タスク管理ツール、情報共有ツール、便利ですね！

これまでいろんな仕事で、いろんなツールを使ってきました。それぞれ、特に不自由もなく、乗り換えるほどのモチベーションもなく使い続けていたんですが、**不満が全くなかったわけではありません**でした。

```
・遅い
・ダサい
・わかりにく
・カンバンがない
・ガントチャートがない
・Wiki的なものがない
・なぜか仕事がうまく進まない
・SNSみたいな感じで、社員がもっと楽しくつながれたらおもしろそう
・スキルがレベルアップしてる様子とか、可視化されたらおもしろそう
・勝手に仕事してくれたりしないかな、AIとかで
・使ってたら無意識にPMBOKみたいになるように、レールが敷かれていると便利な気がする
・（ひどいコメントをしそうになったときに）「言葉にトゲがないですか？」とか、ボットがやんわり教えてくれるとか・・
```

みたいなことを妄想してたらワクワクが止まらなくなり、[自分で作ることにしました](https://note.mu/gussietech/n/n6e3ed03eec40)。

このサービスの開発に採用した技術や、ハマったポイントと解決策を書いていきたいと思います。

## 何を作ったのか

ガントチャート、カンバン、マークダウンでの情報共有ツールが揃ったタスク管理ツールです。詳しくは[プロジェクト管理ツールRepsonaのサイト](https://repsona.com/ja)で紹介しています。

- ガントチャート
![gantt.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/5c94decc-280b-726b-7d19-6867bda9cbd1.gif)

- カンバン
![ball.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/a2e7e48f-d3de-3b43-9624-7c70d1ef3c7d.gif)

- 情報共有
![edit.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/057437f4-3728-db76-aa07-217bd9bd28f9.gif)

## こだわったところ

わかりやすく、動作を軽くというところです。リリース後、多くのポジティブなフィードバックをいただきました。

はやさにこだわった結果、色々と悩んだ挙句、このような形を採用しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/fd9ed53f-9af6-d6cd-1b2f-ccae0b6e09b4.png)

- 初期表示の体感をよくするためのSSR
- jsリソースのキャッシュが効くようにPWA
- DOMのレンダリングコストを抑えるためのSPA
- API通信はWebSocketを利用しhttp接続オーバーヘッドを軽減
- WebSocketによりチャットのような体感でリロードなく画面に反映

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/f60f9973-719f-58b1-5e2f-06d865661e05.png)

Performance 98・・もうちょっと。しかしほとんど難しいチューニングをせずに、この結果が出ています。これからさらに改善していきたいです。

## アーキテクチャ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/819b141b-8cb2-afae-c644-05f03ae47a0c.png)

- フロント＆APIサーバー本体にFargateを利用
- Fargate内コンテナにALBでポート毎に振り分け
- ECSのService設定でタスクの必要数を変更すれば簡単スケール
- CloudFrontはパフォーマンス向上のため配置（後述）
- Lambda EdgeはIP制限のため（開発時のみ）
- Lambdaはエラーレスポンス用
- ElastiCacheでsessionとsocket.ioを管理
- SESでメール送信、Bounces通知をSNSで受け取ってwebhookで処理
- 添付ファイルなどのストレージはS3
- 決済はStripe

## 採用した技術とその理由

### なぜNuxtを選んだか

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/379b5500-792c-4f8b-2720-0c702a60a6ba.png)

「規模が多くなった時のjQueryの**オバケコード**には付き合えないな。どうしようか、フロントフレームワーク・・」と考えていた時に、たまたまプロトタイピングのような案件があったのでVue.jsを採用しました。

しかし、ルーティングやSSRを考えると結構作り込まなきゃいけなくて、ルールを決めたりするのも大変だなと思っていた時に、この記述を見ました。

https://jp.vuejs.org/v2/guide/ssr.html#Nuxt-js

> これまでに議論されたすべての側面を適切に構成するプロダクション向けのサーバーレンダリングに対応したアプリケーションの開発は難しい作業です。幸いにも、これをもっと簡単にすることを目指す優れたコミュニティプロジェクト **Nuxt.js** があります。

「なんか変な名前だけど、公式が言及してるってことは、きっといいものなんだろうな。」くらいの気持ちで触ってみたところ、起動から開発開始までとってもスムーズにいき、ドキュメントも充実していて、Nuxt本体のことでハマることもほとんどなく開発を進めていけました。

[SFC](https://jp.vuejs.org/v2/guide/single-file-components.html)は見通しがよく、`<style scoped>`を使えば、多少無茶してもcssが破綻しないのも気に入っています。

こんな感じのコードです。

```vue
<i18n>
  en:
    "ノート": "Notes"
</i18n>

<template>
  <section class="hello">
    Hello, {{ $t('ノート') }}!!
  </section>
</template>

<style scoped>
  .hello {
    width: 300px;
  }
</style>

<script lang="ts">
  import NuxtBase from '@/modules/NuxtBase'
  import { Component } from 'nuxt-property-decorator'

  @Component({
    layout: 'app',
    components: {}
  })
  export default class Note extends NuxtBase {

    isLoaded = false

    head () {
      return {
        title: this.$i18n.t('ノート') + this.projectTitle
      }
    }
  }
</script>
```

### なぜSailsを選んだか

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/b98db82c-c6f7-9aa8-29e2-3e428acf025b.png)   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/61ef6f05-032e-80cd-6414-da21d89b9576.png)

サーバーのフレームワークはかなり迷いました。もともと機械学習的な要素も入れていきたかったので、バッチ処理と合わせてPythonにしようかなと思っていました。

しかし、言語違いで脳を切り替えるコストや、実質**二重実装**になることが目に見えている、フロントとAPIのバリデーションなどを考えると、同じ言語で開発した方が早いのではないかと考え、Node.jsを選択しました。

Node.jsのフレームワークといえばExpressしか知りませんでした。ただこれはほんとに軽量で、DB関連とかMVC的なこととか全部自分でやらなきゃいけない。もともと**Symfony野郎**だったので、Model（Entity）にプロパティを記述するとイミュータブルになってくれる感じがいいなーなんて探していると、Railsライクなフレームワーク[Sails](https://sailsjs.com/)にたどり着きました。

さーっとドキュメントを読むと、MVCになってて、APIとフロントという考え方になってて、WebSocketが標準で組み込まれてて、あまり迷ってる時間もないので、採用！となりました。

初期プロトタイプ時はBlueprint APIが大活躍で、爆速開発できました。

こんな感じのコードです。

```ts
import BaseController from './BaseController'

declare const sails: any

class TestController extends BaseController {

  test = (req, res) => {
    return res.ok()
  }
}
```

### なぜFargateを選んだか

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/25e2f420-701c-117b-5ec3-fd7b1ea7c08c.png)

最初からAWSだと割高だし、どうしようかというところから考え始めましたが、後々の移行を考えると、始めからAWSにしようと決めました。Free Tierも活用できるし。結果$300クレジットももらえて（この件は今度書きます）、今のところ無料です。AWSにしてよかった！

開始時はEC2でレガシーにいったほうが変なハマり方しなくていいかもなーと漠然と考えていたのですが、EC2にデプロイするというのが環境構築やらスペック不足やらで全然順調にいかなくて、あーもうめんどくさい！**いっそこの際覚えちゃえ！**コンテナデプロイ！という感じでFargateを選びました。

ちょうど値下げされたタイミングで、徐々にWeb上にも情報が出てきていたので、わりとすんなり動きました。

### なぜTypeScriptを選んだか

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/fa09d774-b3a9-d689-fb02-048b4c5dd37c.png)

JavaScriptが書き慣れていたわけでもないので、いずれにしても学習なのですが、

- クラスを使いたい気持ち
- なんでTypeScript使わないの？って空気

に後押しされました。ただ、現状、敗北者のTypeScript[^1]です。それでもクラスを使えるだけでも気持ちよくコーディングができるので、意味はありました。今後は恩恵にあずかれるよう、改善していきたいと思っています。

[^1]: [敗北者のTypeScript](https://qiita.com/uhyo/items/aae57ba0734e36ee846a)

JavaScriptだいぶ慣れてきて、いまではスイスイこういう記述をしています。

```js
const ids = projects.map(project => project.id)
```

ちなみに、サーバーもフロントもTypeScriptで書けるのは、想像以上によい体感です。

### なぜStripeを選んだか

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/970144eb-ad94-9794-1299-8232ce805014.png)

StripeがSailsマニュアルで言及されてる
https://sailsjs.com/documentation/reference/configuration/sails-config-custom#?what-is-this

> The custom configuration for your app. This is useful for one-off settings specific to your application, like the domain to use when sending emails, or third-party API keys for **Stripe**, Mailgun, Twitter, Facebook, etc.

日本ではpay.jpがよく使われてるのかな？

実際に実装して比較検討しているような時間はないので、さーっとググって、評判大丈夫そうなら、どっちかかな。世界での実績を踏まえてStripeかな。という感覚で決めました。適当だなあ。

実際やってみて、Stripeはドキュメントがかなり充実していてわかりやすいのと、そもそもAPIが直感的でわかりやすいので、ほとんど迷うことなく一週間程度で課金の実装ができました。ダッシュボードもとても素敵です。

### なぜバージニアリージョンを選んだか

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/64632581-09ba-650a-0f0a-eb64dd1743b7.png)

**いきなり世界リリース**して、逆輸入的に日本で宣伝するつもりでした。というのもあり、世界で使うには東京リージョンにおいておくことはないかなと思っていました（逆輸入作戦は全然うまくいきませんでした）。

それから、多少コストが**安い**こと、新しいサービスが導入されるのが**早い**こともバージニアを選んだ理由です。

### なぜFirebaseじゃないのか

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17680/b3b5ad0e-0ee5-c9bd-45c3-ef8de119b80f.png)

もともとは、 Nuxt.js + FirebaseでSPA×SSR×PWA×サーバーレスというのを考えていました。Firestoreとつなげて簡単に動いたんで、これだーとなっていたんですが、今回作ろうとしているものとスキーマレスが相性が悪かった。

具体的には、カンバン表示をステータスごと、担当者ごと、ボール持ってる人ごと、と切り替えようと思うと、それって全部中身持ってなきゃ実現できないじゃん！そして、ステータス変更するごとに、子階層のステータスも全件更新・・ないないない。となり、RDBを選択しました。Cloud SQLという選択肢もあったんですが、Functionsと相性が悪いので、だったらいっそコンテナデプロイか・・となると、使い慣れたAWSにしようかな、という感じです。

## チャレンジしたこと

### 全部WebSocket

うまくいくか心配でしたが、やってみました。結果、それほどハマるところはありませんでした。

リロードなしの体感、他者の更新を検知させるような作りは、場所によっては、WebSocketで更新イベントを受け取って、内部に保持している変数の配列の構造をいじって・・と複雑なことをやる必要があり、ここの可読性が課題です。あとは完璧にできていない。改善の余地ありです。

### バージニアリージョン

初期アクセス時（SSR時）のレイテンシーは気になりますが、APIのアクセスは気にならないくらい速度が出ていて快適です。あとはかけるコスト次第。

### Fargate無停止デプロイ

デプロイ時にコンテナを倍の数にして、新しいコンテナをデプロイして、起動完了したら元々居たものをALBから外して、タスクを停止する、というのを自動化しました。こんなことがあっさりできるんだからFargate採用してよかった。負荷がかかった時のスケールアウトも本当に簡単です。

### バッチ処理なし

バッチサーバーを配置できないので（コスト的にしたくないので）全部オンラインで処理しています。Stripeからの通知や、SESのBounces通知も、Webhook用のAPIを用意して通知を受け取った時点で処理します。ちょっと強引なところもあるので必要に応じていつかバッチ化していきたい。機械学習的な処理を入れようと思うと、オンラインだけでは無理ですよね。

## ハマったポイントと解決法

### NuxtとSailsを同じプロセスで起動するか否か

（今となってはいずれにしてもALBでできたじゃんという話なのですが）開発時はホスト（ポート）が別れることが問題でした。CORSを考えなきゃいけないからです。

なので、SailsのミドルウェアにNuxtを組み込んで同時に立ち上げる方法を取っていました。ただ、開発がしにくい。ホットリロードが効かないので、いちいちNuxtを再起動しなきゃいけない。Nuxtの起動ってまあまあ時間かかるんですよね。

最終的に、フロントとAPIは別々に。ALBにがんばってもらって、問題なく動作しています。

### 認証

[Nuxt公式の認証ルート](https://ja.nuxtjs.org/examples/auth-routes/)が結構クセのある実装で、「SSRの時のあなたはサーバーなんだからセッション知ってますよね！」っていうのが前提になっていて、APIを別に立てようとした時に**これは違うな**となりました。それで、もともとSNS認証なんかもいれたかったのでPassport.jsを採用しました。

[Username & Password](http://www.passportjs.org/docs/username-password/)が用意されていたので、これを使えばいける！と思ったのですが、メールアドレスとパスワード以外にも認証に使いたい情報がある場合に、これでは足りなくてハマりました。

ずいぶん時間をかけて、[passport-custom](http://www.passportjs.org/packages/passport-custom/)というのを見つけて、解決することができました。

### Fargateが起動してもすぐ止まる

なんとかデプロイまではできたのですが、タスク起動したと思ったらすぐに勝手に止まってしまう。どのログをどう見ればいいのか理解するのにとても時間がかかりました。

どうやらALBがヘルスチェックでNG出して、切り離されたunhealthyのものはタスクも閉じられちゃう模様。サービスが必要数分タスクを起動して、また切り離して停止して、を繰り返して見た目上は何が起きているのやらという状態で・・。ヘルスチェックの猶予設定にたどり着くのにだいぶ時間がかかりました。

### バージニアリージョンが遅い

いざデプロイして動かしてみると、おっそー！これでは「はやさにこだわった」なんて言えないと青ざめました。[キャッシュなしでCloudFrontかますだけではやくなる](https://www.slideshare.net/AmazonWebServices/secured-api-acceleration-with-engineers-from-amazon-cloudfront-and-slack)らしいので、やってみるもイマイチ。

大引越しして東京リージョンに変えたけど・・あれ？それでも遅い。

遅い理由はFargateに割り当てたスペックでした。低すぎた。こんなのに気づくのにかなり時間をかけました。

ちなみに、「キャッシュなしでCloudFrontかますだけではやくなる」は、確かに効いています。

### 長くなりそうなので別の記事で

他にもハマったことがいろいろあるんですが、別の記事で・・。上記も端的すぎるので、もう少し詳しく書きたいです。

- タスクやノートの無限階層
- DBのマイグレーション
- Operational transformation
- cssでレイアウト調整
- pm2とかforeverとか
- コンテナサイズが1GB超え

## 反響

- はやい
- 直感的でわかりやすい
- 必要な機能がそろってる

ポジティブなフィードバックをいただいています。ただ、不足している点もいろいろツッコミいただいており、対応がんばってます。

## まとめ

- Nuxt、気持ちよく開発ができるのでとてもいいです
- Sails、ちょうどよくいいかげんで、はやく開発できていいです
- TypeScript、anyでクラス使うだけでも導入の価値ありです
- Fargate、最初ハマりましたが、動き出すと簡単さに助けられています
- 全体的に、とっても快適に、開発、運用しています
- 他にもいっぱいある、これまでハマったことと解決策など、共有していきたいです

やりたいようにやって、ハマっては解決して、なんとかたどり着いたリリースで、ポジティブなフィードバックをもらえており、とても幸せです。作ってよかった！

おもしろそうだなと思っていただけた方、ぜひ試してください。フリープランでも全機能、ずっと無料で使えます。
[無料ガントチャート無料カンバン - プロジェクト管理ツール Repsona](https://repsona.com/ja)