Nuxt & JAMstack & AWS

やりたいこと

Nuxtで作ったサイトを、JAMstackと呼ばれる方法を利用して、AWSにデプロイする。

JAMstackについて下記の記事がわかりやすく大変参考になりました!

https://qiita.com/miyaoka/items/9774f0250953da419a58

調べた感じのメリット

  • SPA modeと比較して、ユーザーはhtmlを見にいくだけ(APIレスポンスを待つ必要がない)ので早い。
  • SSR modeと比較して、Node Serverなどの管理が必要ない。

やったこと

1. NuxtでGenerateコマンドを実行し、静的なHTMLを生成する

https://ja.nuxtjs.org/api/configuration-generate

Nuxt には、静的なHTMLを生成するコマンドがあるので、それを利用する。

/pages/hoge/_id.vue を前もって準備する。

そして、下記のように配列をreturn すると、/hoge/1/index.html と /hoge/2/index.html というファイルが生成される。

module.exports = {
  generate: {
    routes() {
      return [{route: '/hoge/1'}, {route: '/hoge/2'}]
    }
  },

問題

ファイル生成の時に、一回一回APIを叩くため時間がかかる。

/pages/hoge/_id.vue 内部で、APIを叩いていた場合、毎回ページ閲覧時と同じようにAPIが叩かれる。

なので、hoge/1 ~ hoge/1000 まであった場合、1000回APIが叩かれる。

これの対処として、payload による動的ルーティング生成の高速化がある。

まず、1000回分のAPIのレスポンスをまとめたAPIを作成し、そのAPIでまとめてデータを取得する。

module.exports = {
  generate: {
    routes() {
      return axios
        .get(envSet.API_URL + '/api/hoges.json')
        .then(res => {
            return {
              route: '/hoge/' + res.data.id,
              payload: {
                data: res.data
              }
            };
        });
    }
  },

次に、/pages/hoge/_id.vue のasyncDataを下記みたいに変更する。

async asyncData ({ params, error, payload }) {
  if (payload) return { data: payload.data }
  else return { data: await axios.get(`/api/hoges/${params.id}.json`)... }
}

そうすると、1000件のページ生成に必要なAPIアクセスを一つにまとめれる。

静的HTMLのgenerateに時間がかかる。

1000件ものファイル生成は結構時間がかかる。

そこで、https://github.com/nuxt-community/nuxt-generate-cluster を利用すると、簡単に並列で静的HTMLの生成が達成できる。

下記のようにscriptを設定して、実行するだけでOK!!

"scripts": {
    "generate:local": "cross-env NODE_ENV=local nuxt-generate -b -qq",
    "generate:development": "cross-env NODE_ENV=development nuxt-generate -b -qq",
    "generate:production": "cross-env NODE_ENV=production nuxt-generate -b -qq",
}

2. CodePipline & CodeBuild設定

CodePipeline、CodeBuildを設定する。

CodeBuildで行うことは以下の通り。

# Docker Buildして、nuxt generateを行う。
docker build -t jamstack .
docker run jamstack yarn run generate:${ENV}

CONTAINER_ID=$(docker ps -qa --filter ancestor=jamstack)

# Docker内部で生成したものを、コピーする
docker cp ${CONTAINER_ID}:app/dist ../dist

# S3へ生成物を同期する。--deleteをつけると、古いファイルを削除できる。
aws s3 sync ../dist/ s3://${BACKET_NAME} --cache-control "max-age=31536000" --delete

# CloudFrontのキャッシュクリア
aws cloudfront create-invalidation --distribution-id ${CLOUD_FRONT_ID} --paths '/*'

Build時間について

今回、生成するHTMLのファイルは、約1万件。

Code Build の vCPUは8、メモリは15GBを利用。

なので、8個のgenerateWorkerが立ち上がる。

Docker Buildした場合

  • Docker Buildから、yarn install まで:1分
  • HTMLファイル生成:約5分(274sec ~ 300sec ぐらい)
  • S3へのアップロード:約2 ~ 3分
  • 合計:8分 ~ 9分

しない場合

Docker使わない方が早かったりしないかなと思ったので、CodeBuildのNode環境で試してみた。

  • npm install : 27秒
  • HTMLファイル生成:約4分ちょい(2回試したところ266secだった。)
  • S3へのアップロード:約2 ~ 3分
  • 合計:7 ~ 8分

vCPUを下げてみた

vCPUを2にして、HTMLの生成時間をみてみた。 当たり前だが、worker数が2つになって、生成に934secかかった...

結論

1分程度しか変わらなかったので、Localとの環境を一緒にするために、一旦Docker使った方法を利用しようと思います。

3. CloudFrontを経由して、S3にアクセスする

こんな感じに、 AWS CloudFrontとS3、ALB(ELB)でSPAを構築する /api 以下のアクセスは、APIにアクセスするようにして、それ以外は、S3を見るようにする。

S3のstatic website hosting を利用する。(index.htmlを常に見に行ってもらう必要があるので) https://qiita.com/dogwood008/items/a92abae789f4b0466f38

Route53経由で、CludFrontにアクセスするようにする。

証明書については、Amazon Certificate Managerを利用した。

4. バッチ生成タイミングで、CodePipelineをKickする。

今回はECサイトっぽいサイトを作るので、商品ページが1日1回更新される。

https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/pipelines-rerun-manually.html

aws codepipeline start-pipeline-execution --name MyFirstPipeline

CLIからなら上を叩くだけ。

運用上の懸念点

  • APIサーバデプロイ => Nuxtのデプロイと順番を守る必要がある。
    • そうしないと、古いAPIのデータでファイル生成されてしまう。
  • Landingしたページは静的ファイル、リンク遷移した場合は、SPA的な動きになることを注意する必要がある。
    • 内部のリンクに、nuxt-link使わなければいい話ではあるし、むしろそっちの方が、レスポンス速度的には早い。
    • ただ、下にあるようにPagingとか検索やりたい場合は、クライアントサイドのレンダリングが必要になりそう。
    • https://qiita.com/miyaoka/items/9774f0250953da419a58
    • 上記のリンクのように、APIを静的化したり、CDNでcacheしたりするのも良さそう。
    • ただ今回は、APIの処理にUAが必要だったり、ログイン機能追加がありそうだったりしたのでできなかった...
  • Pagingとか検索とかをやりたい時
    • Paging結果とかも静的ファイルで生成したら量が半端ないことになる...
    • 例えば、Pagingにアクセスした時だけ、クライアントサイドでレンダリングすれば良さそう。
    • 大量にアクセスが見込まれ、SEO的にも重視されるのは、あくまで1ページ目とかのはず。
  • 開発環境と差異がある
    • 開発環境で動いていても、動かないことがある。JSエラーとか、パラメータによるエラーとか。コツが必要そう。
    • 逆に言えば、テストの代わりをしてくれてるってことになるので、むしろいいのかもしれない。
    • Error情報があれば、Slack通知して、Deployを止めるみたいなことができそうなので、試してみる。
  • ページが増えるたびに、デプロイに時間がかかりそう。
    • 10万ページとかになった時に、単純計算でデプロイに1時間かかる想定になる...
    • Code Build はあくまでDocker Imageの生成のみ行わせて、ECS Taskを複数台立ち上げて、1万件づつとか同時平行で、Build行わせるってこともできそう。
    • 諦めてSSR使っちゃえばいいのかもしれないけど、ここについては、他にも方法ありそうなので、もっと調査したい。

まとめ

正直いうと、今の所Nodeサーバの運用にあまり困ってなかったり、悲しいことにレスポンス速度に対して明確な目標値がない状態なので、社内の他のアプリケーションに強く押すメリットがあまりなかった。

社内用の管理画面とか、新規サービスのユーザーテスト用サイトは、SPAで問題ないので。

それよりも、ページ数が増えた時に、デザイン変更のたびにビルドに時間めっちゃかかるかもという恐怖が大きい...

ただ、どこかで小規模なECサイトとかブログを作って、保守コストとか、サーバ代金を減らしたいってなった時に、すごく便利な選択肢だと思う。

あと、途中でSSRとかSPAにしようって思った時に、すぐ変更できちゃうので、Nuxtは素晴らしいなと思った。

もし、何かこうしたらもっと良くなるよとか、その辺のデメリット解決簡単だよとかありましたらお教えいただけるとありがたいです!