収益物件.com Web版がリリースされるまで ~ Nuxt.jsアプリをデプロイ ~

Posted by SpaceAgent Tech Blog スペテク on Wednesday, December 5, 2018

はじめに

フロントエンドエンジニアの松田です。
最近はジョジョ5部のアニメを毎週待ち遠しく思っています。

今回は、収益物件.com Web版の開発話をします。
収益物件.com Webの開発では、Nuxt.jsアプリケーションの開発に初チャレンジしています。

TL;DR

  • Serverless + Nuxt.js + aws-serverless-express
  • slsコマンドでシンプルにデプロイが可能です
  • Lambdaのコールドスタートには要注意

開発の背景と技術選定

収益物件.com iOSアプリはすでにあったのですが、Androidユーザーに対してどのようにサービスを提供するか、というところで悩んでいました。

残念ながらAndroidアプリを開発できるエンジニアが社内にいなかったため、ネイティブアプリではなく、PWAで提供できるかどうかを検討することにしました。

PWAを提供するにあたって考えた要素についてご紹介します。

SSR

Server Side Rendering(SSR)の有無でアプリケーションの構成は大きく変わります。SSRが不要であればS3でホスティングして配信もできるのですが、収益物件.comはSEOが重要である点と、そもそもAndroidアプリとして提供したいという面があるためSSRで配信することにしました。

Next.js vs Nuxt.js

フロントエンドでSSRをサポートしているフレームワークにNext.jsとNuxt.jsがあります。それぞれReact.js、Vue.jsのフレームワークです。

弊社では、収益物件.comに掲載する物件を管理するサービス「スペースクラウド」を不動産会社向けに提供しており、こちらのサービスではフロントエンドにReact.jsを採用しています。

すでに知見のあるReact.jsを利用するのであればNext.jsを採用するのですが、新しい技術へのチャレンジおよび国内のコミュニティの盛り上がりを考慮して、Nuxt.jsを採用することにしました。

Serverless Framework

Nuxt.jsアプリケーションのデプロイにはServerless Frameworkを利用しました。Serverlss Frameworkは、Nuxt.jsアプリを配信するために必要な、AWSの各種サービスの設定を簡単に行ってくれます。

AWSにデプロイする場合は、以下を行ってくれるCloud FormationのStackを作成してくれます。

  1. ビルドしたNuxt.jsアプリのソースコードをS3配置
  2. アクセスされたURLのHTMLをレンダリングしてくれるLambda Functionの作成
  3. LambdaのエンドポイントとなるAPI GatewayのAPI作成

煩雑な環境構築を自動で行ってくれるので使わない手はありませんでした。

デプロイ

ここからは実装した実際のコードをお見せしながら、Nuxt.jsアプリのデプロイ方法を紹介します。

ディレクトリ構成

/clientにNuxt.jsのアプリケーション、/serverにServerless Frameworkと連携するためのコードを置いています。

Serverless, Nuxt.jsアプリケーションのディレクトリ構成

フロントエンド側

nuxt.config.js(一部抜粋)

modeはuniversalでないと動作しません。明示的に書いていますが、デフォルト値なので書かなくても良いです。
https://ja.nuxtjs.org/api/configuration-mode/

srcDirにはNuxt.jsのコードが配置されているディレクトリを設定します。ルートに配置するとサーバー側のコードと混在して見通しが悪くなるため、フロントエンド側のコードを配置するディレクトリを作成しました。

module.exports = {
  mode: 'universal',
  srcDir: 'client/',
}

サーバー側

server/handler.js

Lambda Functionで実行されるエンドポイントです。 ここでは、aws-serverlss-expressを利用し、Serverlessで利用できる形のexpressサーバーを作成しています。

ソースコードはこちらを参考にしています。(favicon.icoを使うため、image/x-iconを追加しています)
https://github.com/awslabs/aws-serverless-express/blob/master/examples/basic-starter/lambda.js

'use strict';
const awsServerlessExpress = require('aws-serverless-express');
const app = require('./index');

const binaryMimeTypes = [
  'application/javascript',
  'application/json',
  'application/octet-stream',
  'application/xml',
  'font/eot',
  'font/opentype',
  'font/otf',
  'image/jpeg',
  'image/png',
  'image/svg+xml',
  'image/x-icon',
  'text/comma-separated-values',
  'text/css',
  'text/html',
  'text/javascript',
  'text/plain',
  'text/text',
  'text/xml'
];
const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes);

module.exports.handler = (event, context) =>
  awsServerlessExpress.proxy(server, event, context);

server/index.js

リクエストに対して、Nuxt.jsでレンダリングされたHTMLを返却するexpressサーバーを作成し、 それをさらにaws-serverless-expressと連携させるコードです。

上記のhandler.js内でaws-serverless-expressのサーバーを作成する際に参照しています。

const express = require('express');
const { Nuxt } = require('nuxt');
const path = require('path');
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware');

// Create App
const app = express();

// Set API Gateway Middleware
app.use(awsServerlessExpressMiddleware.eventContext());

// Provide Assets
app.use('/_nuxt', express.static(path.join(__dirname, '.nuxt', 'dist')));

// Add Nuxt
let config = require('../nuxt.config.js');
const nuxt = new Nuxt(config);
app.use((req, res, next) => {
  nuxt.render(req, res, next);
});
module.exports = app;

Serverless.yml

Serverless Frameworkの設定ファイルです。

functionsに設定した内容が、Lambda Functionとしてデプロイされます。handlerにはserver/handler.jsでexportしたhandlerを参照するよう設定します。eventsには対象のURLにアクセスされた時の設定を書くことができます。

service:
  name: syueki-bukken-web

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${opt:stage}
  region: ap-northeast-1

functions:
  nuxt:
    handler: server/handler.handler
    events:
      - http:
          path: '/'
          method: get
          private: false
      - http:
          path: '{proxy+}'
          method: get
          private: false

plugins:
  - serverless-apigw-binary

custom:
  apigwBinary:
    types:
      - '*/*'

デプロイの実行

AWSの権限設定

こちらを参考にAWS CLIのインストールと設定を行います。当然といえば当然ですが、API Gateway、Lambda、S3、Cloud Formationの全てにアクセスできる権限が必要です。
https://docs.aws.amazon.com/ja_jp/streams/latest/dev/kinesis-tutorial-cli-installation.html

Serverless CLIツールのインストール

公式サイトの案内にしたがってインストールします。
https://serverless.com/framework/docs/providers/aws/guide/installation/

npm install -g serverless

いざ、デプロイ

npm build && sls deploy --stage production 

デプロイするとコンソールにログが流れるためしばらく待ちましょう。完了するとURLがログに出力されるため、アクセスしてみましょう。Nuxt.jsで作成したアプリケーションが表示されていれば成功です。

cURLでURLをGETしてみて、コンテンツが全て表示されている状態のHTMLが返ってきていれば、適切にSSRも実行されていることがわかります。

プロダクションで使うために

カスタムドメイン

上記の流れでデプロイすると xxx.amazonaws.com/production/のようなURLにデプロイされます。当然プロダクションではドメインを設定しないといけません。

serverlessのプラグインとしてserverless-domain-managerというものがあります。これを利用することで簡単にカスタムドメインでのデプロイができます。(Route53の設定を自動で行います)

serverless.ymlの設定

serverless.ymlを編集し、ドメインを利用するための設定を行いましょう。ドメイン名はデプロイコマンド実行時の引数、ARNは別ファイルから読み込んで使うように設定しています。

plugins:
  - serverless-apigw-binary
+  - serverless-domain-manager

custom:
  apigwBinary:
    types:
      - '*/*'
+  customDomain:
+    domainName: ${env:DOMAIN_NAME}
+    basePath: ''
+    stage: ${self:provider.stage}
+    certificateArn: ${file(./serverless.config.yml):certificateArn}
+    createRoute53Record: true

デプロイ

再びデプロイしてみましょう。package.jsonを編集し、プロダクション用のデプロイコマンドを作っておきましょう。cross-env、npm-run-allを別途インストールしています。

{
  "scripts": {
    "sls:deploy:production": "npx sls deploy --stage production",
    "sls:create-domain": "cross-env SLS_DEBUG=* sls create_domain",
    "app:deploy:production": "cross-env NODE_ENV=production BASE_URL=https://syueki-bukken.com DOMAIN_NAME=syueki-bukken.com run-s build sls:deploy:production"
  },
}

初めてデプロイする場合は、ドメインの作成コマンドをまず実行しておく必要があります。ドメインの設定と反映が終わるまでにアプリケーションをデプロイしてしまうと、アクセスできなくなってしまうので注意が必要です。

npm run sls:create-domain
npm run cross-env DOMAIN_NAME=syueki-bukken.com run-s build sls:deploy:production

Lambdaのコールドスタート問題

Lambdaはしばらくアクセスがないと起動するところからスタートするため、ページが表示されるまでにかなり時間がかかってしまいます。

こちらもプラグインがあるため、利用していきましょう。
serverless-plugin-warmup

serverless.ymlの設定

以下の設定を追加してデプロイすると、先ほどまではデプロイ直後はアクセスしてからページが表示するまでに時間がありましたが、かなり短くなっているはずです。

plugins:
  - serverless-apigw-binary
  - serverless-domain-manager
+  - serverless-plugin-warmup

custom:
  apigwBinary:
    types:
      - '*/*'
  customDomain:
    domainName: ${env:DOMAIN_NAME}
    basePath: ''
    stage: ${self:provider.stage}
    certificateArn: ${file(./serverless.config.yml):certificateArn}
    createRoute53Record: true
+  warmup:
+    default: production
+    prewarm: true

終わりに

Nuxt.jsアプリをServerlessを使ってデプロイする方法を実際のコードと合わせてご紹介しました。収益物件.com Web版はまだまだ開発がスタートしたばかりですが、これからアプリと同じようにコンテンツの拡充を目指していきます。おもしろかった、参考になった、という方はぜひSNSでシェアしてください!

次回は未定ですが、React.jsとVue.jsの比較記事を書く予定です。またみてください〜。

参考記事