Help us understand the problem. What is going on with this article?

Nuxt.js(SSR)をLambdaで配信する【個人開発】

概要

Qiitaのストックを整理するためのサービス「Mindexer(ミンデクサー)」のフロントエンドを
Vue.jsで構築したSPAのアプリケーションからNuxt.jsでSSRを行うように再構築しました。

LambdaでExpressサーバーを動かし、その上でNuxt.jsを動かしています。
この記事では、今回の開発で得られたNuxt.jsに関するTipsやインフラ構成などについて解説していきたいと思います。

なお、Vue.jsからの移行に関する解説は含みません。

サービスについて

「Mindexer(ミンデクサー)」については、こちらの記事で解説しています。
バックエンドで利用している技術についても解説していますので、合わせてご覧いただけると嬉しいです。

:link: AWS + Laravel + Vue.js でQiitaのストックを整理するサービスを作りました!【個人開発】
:link: 個人開発のインフラをEC2からFargateに置き換えました!

ソースコード

ソースコードは全てこちらで公開しています。

Nuxt.js : https://github.com/nekochans/qiita-stocker-frontend
Terraform : https://github.com/nekochans/qiita-stocker-terraform

Nuxt.jsへの移行理由

以下のような理由です。

  • SSRによってページの表示速度を向上させたかった為
  • BFF層を導入したかった為(認証部分はサーバーサイドで行ったほうが安全)
  • Nuxtのレイアウト機能を利用したかった為

アーキテクチャ選定理由

Nuxt.jsを実行させる為のアーキテクチャ選定に悩みました。

選択肢としては、AWS FargateやGAE(Google App Engine)等があります。
最初は、GAE(Google App Engine)が簡単そうだなと思ったのですが、ドメインレジストラとしてRoute53、証明書にACMを利用している関係上、このあたりの連携が複雑そうだったので止めました。

最終的にAWS Lambda上でNuxt.jsを動作させるという選択をしました。

理由としては下記の通りです。

  • アクセス数が相当増えない限り、ランニングコストが安い
  • AWS上のサービスだけで完結出来る

AWS Lambda上でNuxt.jsを動作させる設定が少々複雑になったので、次項でそれを説明します。

技術要素

  • Nuxt.js v2.10.0
  • Vue.js + Vuex
  • TypeScript
  • Serverless Framework

インフラ構成

AWS 構成図

mindexer-nuxt.png

フロントエンド

Lambda上でExpressサーバーを動かし、その上でNuxt.jsを動かしています。

まず、CloudFrontによって、S3とAPI Gatewayへのアクセスの振り分けを行います。
画像やJavaScriptファイルなどの静的リソースについてはS3から配信し、SSRやBFFへのアクセスはAPI Gateway経由でLambdaから配信しています。

バックエンド

バックエンドのAPIは、ECS Fargateで配信しています。
ECS Fargateについては、こちらの記事で解説していますので、よろしければこちらをご覧ください。
個人開発のインフラをEC2からFargateに置き換えました!

インフラ構築

AWSのインフラ構成図に登場するリソースの構築は、少しややこしいのですがServerless FrameworkTerraformの2つによって構築しています。

  • Serverless Framework
    • API Gateway
    • Lambda
  • Terraform
    • CloudFront
    • S3

まず、AWSのリソース説明の前に、Lambdaで動いているExpressサーバーについて解説し、その後にAWSのリソースについてみていきたいと思います。

Expressサーバー

Nuxt.jsのuniversalモードを選択し、SSRを行うためにExpressサーバーを用意しています。
Nuxt.jsは、Expressサーバーのミドルウェアとして使用しています。
これに加え、ExpressサーバーではBFFの役割を担っています。

Nuxt.jsのrender(SSR)

nuxt.render()を呼ぶことで、Express上でNuxt.jsを動かしています。

API: nuxt.render(req, res) - NuxtJS

BFF(Backends For Frontends)

主に以下のような役割を担います。

  • 複数のWebAPIへのリクエスト処理を1つにまとめる
  • SSR(サーバーサイドレンダリング)を行う(これはNuxtの仕組みでやってくれる)

普通にサーバーサイドのコードが書ける(Express等のコードが普通に動く)ので、認証周りの処理はここで書いたほうがシンプルに書けます。

また外部APIにアクセスする際に利用するクレデンシャル(APISecret等)もサーバーサイドの環境変数として隠蔽出来るので、セキュリティ的にも安全度が向上します。

今回、Qiitaアカウントを利用してログインを行う部分をこのBFF上で行うように改修しました。

補足となりますが、serverMiddlewareを使用することで、 Nuxt.js内にExpressサーバーの処理を持たせることもできます。
API: serverMiddleware プロパティ - NuxtJS

Serverless

ここまで、Express上のサーバーでNuxt.jsを動かしているという解説をしてきましたが、どのようにLambda上でExpressを動かしているのかを見ていきたいと思います。

Serverless Framework

Serverless Frameworkは、はServerless Applicationを構成管理デプロイするためのCLIツールです。 Serverless Frameworkを使うことで簡単にAWS Lmabdaへのデプロイを行うことができます。

Serverless Frameworkに関する解説は下記の記事が参考になりましたので、掲載させて頂きます。
Serverless Frameworkの使い方まとめ

今回のケースでは、以下のことを行います。

  • LambdaとAPI Gatewayの作成
  • Lamndaへのデプロイ

LambdaでNuxt.jsを配信するためには、Lambdaを呼び出すための入り口となるAPI Gatewayが必要となります。
API Gateway用意して、HTTPリクエストをLambdaに送ります。

API Gatewayの設定については、下記をご確認ください。
Serverless Framework - AWS Lambda Events - API Gateway

また、LambdaでNuxt.jsを配信するために、下記のライブラリを使用しています。

  • aws-serverless-express
  • serverless-plugin-warmup

aws-serverless-express

AWS Labsによって提供されているaws-serverless-expressを使用することで、Lambda上でExpressサーバーを動かすことができます。

aws-serverless-expressは、API GatewayからのリクエストをNode.jsのHTTPリクエストに変換してくれるので、簡単にLambda上でExpressサーバーを動かすことができます。

serverless-plugin-warmup

Lambdaのコールドスタート対策のために利用しています。
詳細については、下記の記事に詳しく記載されていますので、掲載させて頂きます。
Serverless FrameworkでLambdaのコールドスタート対策を行う

S3、CloudFront

Serverless Frameworkで作成されるAWSリソース以外については、Terraformで管理します。
Vue.jsでSPA配信していた際も、インフラはバックエンド含めて全てTerraformで管理しています。
ここではTerraformには触れずに、S3、CloudFrontの設定内容について解説します。

Serverless Frameworkによって、LambdaとAPI Gatewayが作成されていることを前提とします。

S3

画像やJavaScriptファイルなどの静的リソースを配信するためのS3バケットを用意します。

アクセスポリシーを下記の通り設定し、CloudFrontからのアクセス(GetObject)のみを許可しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <CloudFrontのID>"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<S3バケット>/*"
        }
    ]
}

また、S3バケットのオブジェクトは下記のディレクトリ構成とします。(S3へのデプロイ方法の解説については省略します。)

├── _nuxt  # Nuxt.jsのクライアントのリソース
└── assets # 画像などの静的リソース

nuxt buildを実行すると、.nuxt/dist/clientにクライアントのコードが生成されます。
このクライアントのソースコードは、CDNにアップロード可能なJavaScriptファイルになります。
CDNの設定はnuxt.config.jspublicPathで定義されます。デフォルトは_nuxtとなっており、今回はそのまま利用するので、S3のディレクトリも上記のような構成としています。

API: build プロパティ - NuxtJS

CloudFront

インフラ構成の部分で、CloudFrontによって、S3とAPI Gatewayへのアクセスの振り分けを行います。と記載しましたが、どのような設定にしているか詳しく解説したいと思います。

Origins

S3とApiGatewayの両方を登録しています。

S3

項目  設定内容
Origin Domain Name 静的リソースを置いているS3バケットを指定
Origin Path 設定なし
Origin ID 任意のID
Restrict Bucket Access No
Origin Custom Headers 設定なし

Restrict Bucket AccessをNoとしているのは、S3のバケットポリシーを登録しているからです。

ApiGateway

項目 設定内容
Origin Domain Name Severless Frameworkによって作成されたAPI GatewayのAPIエンドポイントを指定
Origin Path /stage(例:/dev)
Origin ID 任意のID
Minimum Origin SSL Protocol TLSv1.2
Origin Protocol Policy HTTPS Only
Origin Response Timeout 任意の値
Origin Keep-alive Timeout 任意の値
HTTP Port 80
HTTPS Port 443
Origin Custom Headers 設定なし

Origin PathはServerlessFrameworkのserverless.ymlで設置しているstageの値になります。
stageを設定しなかった場合、デフォルトはdevです。

Behaviors

パスによるオリジンの振り分けの設定をします。

precedence Path Pattern Origin
0 _nuxt/* 静的リソースを置いているS3バケットを指定
1 assets/* 静的リソースを置いているS3バケットを指定
2 Default (*) API Gatewayを指定

設定の解説をすると、Nuxt.jsのクライアントのコードは/_nuxt/*にアクセスされます。クライアントのコードをS3にデプロイすることで、S3から配信を行います。
また、staticなファイル(faviconなど)もS3バケットに振り分けられるように設定を行っています。
それ以外のリクエストについては、API Gatewayへ振り分けています。

該当するソースコードはこちらです。
https://github.com/nekochans/qiita-stocker-terraform/blob/master/modules/aws/frontend/cloudfront.tf

Nuxt.js Tips

ディレクトリ構成

├── app    # Nuxt.jsのコード
├── server # Expressのコード
├── nuxt.config.ts
└── serverless.yml
  • appにNuxt.jsのコード、serverにExpressのコードを置き、ビルドすると下記のような結果になるように設定しています。

    • /.nuxt:Nuxt.jsのコード
    • /dist:Expressのコード
  • 画像などの静的リソースは、app/static/assetsに置いています。
    CloudFrontで、S3とAPI Gatewayへリクエストの振り分けを行っていますが、このように設定しておくことでCloudFrontの設定が簡単になります。

Nuxt.js TypeScript対応

Nuxt.js v2.10では、TypeScriptが公式によりサポートされています。
設定は下記をご確認ください。
Nuxt TypeScript

上記のドキュメント通りの設定を行いましたが、一部エラーとなってしまった箇所があったので記載したいと思います。

  • @nuxt/typescript-builddependenciesに移動

CodeBuild上でビルドする際に、@nuxt/typescript-builddevDependenciesに追加されているとエラーとなってしまいました。公式では、 devDependencies に追加するように記載されていますが、エラーを回避するためにdependenciesに移動しています。

  • developmentモードでのビルドエラーを回避

devモードでは、@nuxt/builderによってBuildしています。その際に、Vuexのモジュールでエラーとなってしまったので、下記の対応をしています。

devモードの場合、config.extensions[ts]を追加しています。

server/core/nuxt.ts
import config from '../../nuxt.config'
const { Nuxt } = require('nuxt')

config.dev = !(process.env.NODE_ENV === 'production')

if (config.dev) {
  config.extensions = ['ts'] // 追加
}

export const nuxt = new Nuxt(config)

export default config

これを追加しなかった場合、.nuxt/store.jsが下記の通りになり、tsファイルで定義したVuexのモジュールが認識されずにエラーとなりました。

.nuxt/store.js
function resolveStoreModules (moduleData, filename) {
  moduleData = moduleData.default || moduleData
  // Remove store src + extension (./foo/index.js -> foo/index)
  const namespace = filename.replace(/\.(js|mjs)$/, '')   //tsが含まれない 
  • nuxt.ready()を追加 LambdaでNuxt.jsを動かした際に、下記の設定を追加していなかったためエラーとなっていました。 原因は、Nuxt.js 2.5.x から、new Nuxt()をした後に、nuxt.ready()を呼ばないとサーバーがレンダリングしないようになっていたためでした。下記の対応をすることで、解消しました。
server/app.ts
// 変更前
app.use(nuxt.render)

// 変更後
app.use(async (req, res, next) => {
  await nuxt.ready()
  nuxt.render(req, res, next)
})

エラーページ

エラーページの表示には2つの方法をとっています。

Nuxt.jsのLayoutプロパティを利用

ビュー - NuxtJS (エラーページ)

APIからエラーが返ってきた場合に表示しています。
エラーが発生した場合に、 error()関数を呼び出すことで、Layoutプロパティを利用したエラーページを表示しています。
API: コンテキスト - NuxtJS (error (Function))

ページコンポーネント

app/pages/stocks/all.vue
  async fetch({ store, error }: Context) {
    try {
      await store.dispatch('qiita/fetchUncategorizedStocks')
      await store.dispatch('qiita/saveDisplayCategoryId', 0)
    } catch (e) {
      error({
        statusCode: e.code,
        message: e.message
      })
    }
  }

ページコンポーネント以外

app/components/pages/stocks/All.vue
  async onClickDestroyCategory(categoryId: number) {
    try {
      await this.destroyCategory(categoryId)
    } catch (error) {
      return this.$nuxt.error({
        statusCode: error.code,
        message: error.message
      })
    }
  }

Nuxt.js によって作成されるルーティングを拡張しエラーページに遷移

API: router プロパティ - NuxtJS (extendRoutes)

BFFでエラーが発生した場合、/errorに遷移し、エラーページを表示しています。

Nuxt.js によって作成されるルーティングを拡張し、エラーページを作成。
BFFでエラーが発生した場合は、/errorにリダイレクトし、app/pages/error.vueで定義したエラーページを表示しています。

nuxt.config.tsで定義している、routerプロパティにextendRoutesを追加しルーティングを拡張しています。

nuxt.config.ts
  router: {
    middleware: ['authCookieMiddleware', 'redirectMiddleware'],
    extendRoutes(routes: any, resolve) {
      routes.push({
        name: 'original_error',
        path: '/error',
        props: true,
        component: resolve(__dirname, 'app/pages/error.vue')
      })
    }
  },

Middllwere

認証に関連する処理を行うために、Middllwereを利用しています。
API: middleware プロパティ - NuxtJS

  • authCookieMiddleware.ts:Cookieに保持している認証情報をVuexのStoreに保存する
  • redirectMiddleware.ts:ユーザの認証状態から、必要に応じてリダイレクトする

Middllwereは、nuxt.config.tsに下記の通り追加するだけで動作します。
ここで記載した順番で、Middllwereは動作します。(authCookieMiddleware -> redirectMiddleware)

nuxt.config.ts
  router: {
    middleware: ['authCookieMiddleware', 'redirectMiddleware'],
  }

あとがき

インフラ構築からNuxt.jsのことまで解説したので、内容が盛りだくさんな記事になってしまいました。
最後まで読んで頂きありがとうございます。少しでもお役に立てれば幸いです。

これからも、技術的に参考になるような情報を発信していきたいと思っていますので、よろしくお願いします。

Mindexerは無料で利用できますので、多くの人に使っていただけると嬉しいです😊
:link: Mindexer | Qiitaのストックを整理するためのサービスです

ソースコードもこちらで公開しています!
ここにはMindexerのソースコード以外にもサービスのコードを公開しています!
:link: https://github.com/nekochans

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away