フロントエンドのデプロイ

Adventarを支える技術 Advent Calendar 2019 の11日目です。

今日はフロントエンドのデプロイについて書きます。フロントエンドの構成は昨日の記事で書いたとおり、一部を Lambda で SSR していて、静的コンテンツは S3 で配信しています。

なので、S3 と Lambda の両方にデプロイする必要があってやや面倒です。

S3 へのデプロイ

まず、静的コンテンツの S3 へのデプロイ自体は簡単で、nuxt build で生成された成果物(デフォルトではdistディレクトリ)を S3 にアップロードするだけです。

$ nuxt build
$ aws s3 sync ./dist/ s3://your-bucket/prefix

SSR しなければ、これだけで Nuxt.js のアプリケーションは配信できます。注意する点としては、SPA の場合クライアント側でルーティングするので、すべてのパスに対して index.html を返す必要があるという点です。

CloudFront と S3 でやる場合は、ファイルが存在しない場合 CloudFront には S3 から 403 が返るので、これをハンドリングして index.html を返します。terraform で書くとこんな感じです。

custom_error_response {
  error_code         = "403"
  response_code      = "200"
  response_page_path = "/index.html"
}

https://github.com/adventar/adventar/blob/b491c3be64b8d35b3484b69e4f9ce8f6ecfb93eb/terraform/cloudfront.tf#L38-L42

SSR のためのビルド

今回、一部だけ SSR するという構成にしたのでめんどくさかった点がこれで、Nuxt.js は SPA の場合は spa mode でビルド、SSR の場合は universal mode でビルドする必要があります。

https://nuxtjs.org/api/configuration-mode/

なので、ビルドプロセスも両方でやる必要があるのですが、spa mode と universal mode でビルドした JS ファイルのダイジェスト値が微妙に食い違ったりしたということがありました。そうするとどうなるかという、spa mode でビルドして S3 にアップロードした中に、universal mode からで生成した HTML に含まれる JS のファイル名がなくて JS が 404 になって動かない、ということが発生しました。たぶんこんな使い方想定してないのでエッジケースだと思います。

Nuxt.js を直すほどの元気はなかったので、universal mode でビルドした結果も S3 にアップすることでしのぎました。なんとかこれで動いてます。

以下が苦肉の策のコードです。

https://github.com/adventar/adventar/blob/fa0714e49ea3aa60888532b60b924af0c12bbc80/frontend/bin/deploy#L11

Serverless フレームワークを使った API Gateway と Lambda のデプロイ

次に、SSR するための Lambda のデプロイですが、これには今回 Serverless Framework を使いました。API Gateway や Lambda のデプロイは、Serverless や AWS SAM などを使うと便利です。SAM でもよかったのですが、今回は知見のあった Serverless を採用しました。

Serverless でのデプロイはそんなに難しいことはしていなくて、以下のような設定ファイルを書くだけです。

https://github.com/adventar/adventar/blob/fa0714e49ea3aa60888532b60b924af0c12bbc80/frontend/serverless.yml

これで Nuxt.js のビルドを実行後に

$ serverless deploy

API Gateway と Lambda にデプロイできます。

また、サーバーのコードも量が少ないとはいえ TypeScript で管理したいのと、一部クライアントの TypeScript のコードに依存があるので webpack で変換して bundle することにしました。

https://github.com/adventar/adventar/blob/fa0714e49ea3aa60888532b60b924af0c12bbc80/frontend/webpack.config.server.js

本当は Lambda にアップロードする容量を削減のため、必要なコードだけ bundle して一枚の JS にてアップロードしたかったのですが、色々とうまくいかずに bundle するのは諦め、webpakc で node_modules の解決は無視して、node_moduels をアップロードすることにして対応しました。Lambda の容量の上限に引っかかるほど大きくはないので一旦これで妥協しています。

キャッシュのパージ

今回 S3 にアップするアセットは CloudFront でキャッシュしています。JS や CSS はダイジェスト値がついていてファイル名がユニークになるのでキャッシュのパージは必要ありませんが、index.htmlsw.js などはキャッシュのパージが必要です。また、本来は SSR した結果もキャッシュしたいと思っていたので、上記に書いたデプロイの処理をおこなったあとにキャッシュをパージする必要があります。CloudFront のキャッシュのパージは以下のようにします。

aws cloudfront create-invalidation --distribution-id xxx --paths '/*'

最終的な、デプロイスクリプトは以下ような感じです。

https://github.com/adventar/adventar/blob/fa0714e49ea3aa60888532b60b924af0c12bbc80/frontend/bin/deploy

まとめ

フロントエンドのデプロイについて書きました。Nuxt.js と Serverless のおかげで、そんなに複雑には見えませんが、ここまでたどり着くにはけっこうな時間を消費しました...。

明日はこの流れで API サーバーのデプロイについて書きます。