Edited at

Nuxt.jsをAWS Lambda上で動かす.サーバレス・サーバサイドレンダリング


概要

vue.jsのサーバサイドレンダリングで行うNuxt.jsをAWS Lambda上で動かしてみます.あくまでも試験的に試すことが目的の記事です.もし,本番サービスでの導入する場合には慎重な議論をお願いします.


構成


  • AWS Lambdaでnuxt.jsを可動させる

  • API Gateway経由でWebブラウザ等からアクセスする.


前提


  • yarn, npx, nodeといったnuxt.jsを開発する環境がある.

  • AWSアカウントを取得しaws-cliがインストール済みで実行できる状態である.

  • node.jsとexpressでWebサーバを作ったことがある.

  • AWS LambdaやAPI Gateway, CloudFormation, S3などAWSのサービスを使ったことがある.


手順


1. nuxtプロジェクト作成


1.1 初期化

npxコマンドを使ってnuxtプロジェクトを立ち上げます.


  • カスタムサーバフレームワークにはexpressを選択してください.後ほどexpressを使ったLambdaサーバにするため.

  • UIフレームワークにVuetifyを指定していますが必須ではありません.

  • パッケージマネージャはyarnを選択していますがnpmでも動くと思います(未検証)

$ npx create-nuxt-app nuxt_lambda_sample

? Project name nuxt_lambda_sample
? Project description My remarkable Nuxt.js project
? Use a custom server framework express
? Choose features to install
? Use a custom UI framework vuetify
? Use a custom test framework none
? Choose rendering mode Universal
? Author name Hirokazu Yokoyama
? Choose a package manager yarn


1.2 nuxt.js 起動

初期化後,下記コマンドでローカル環境にnuxt.jsが起動します.

$ cd ./nuxt_lambda_sample

$ yarn install
$ yarn run dev

....
READY Server listening on http://localhost:3000


2. lambdaサービスに変更する


2.1 expressを確認

server/index.js にexpressを使ったnode.jsのプログラムがあることを確認しましょう.

package.jsonのscriptsにも定義されている通り,このファイルがプログラムのエントリポイントになっています. app.useでnuxtを登録していることがわかります.


server/index.js

const express = require('express')

const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')

async function start() {
// Init Nuxt.js
const nuxt = new Nuxt(config)

const { host, port } = nuxt.options.server

// Build only in dev mode
if (config.dev) {
const builder = new Builder(nuxt)
await builder.build()
} else {
await nuxt.ready()
}

// Give nuxt middleware to express
app.use(nuxt.render)

// Listen the server
app.listen(port, host)
consola.ready({
message: `Server listening on http://${host}:${port}`,
badge: true
})
}
start()



2.2 lambdaで動くようにする

このままでは通常のnode.jsで動くための状態であるので, AWS Lambda上で動くように改修します.

expressをAWS Lambdaで可動させるaws-serverless-expressを導入します.

$yarn add aws-lambda aws-serverless-express

server/index.jsを下記のように改修します. また,ミドルウェアを新規作成します(server/middleware.js). ポイントは2つです.


  • Lambdaのエントリポイントであるhandler関数を作成し, awsServerlessExpresを動かしている.

  • ミドルウェアとしてURLを置き換える処理(customDomainAdaptorMiddleware)を追加している.

なお, binaryMimeTypesでバイナリデータとして処理させるMimeTypeを追加しています.バイナリタイプとして処理させないとAPI Gatewayを経由したときにgzip圧縮等の関係でブラウザが正常に処理できない事象が発生するので指定しています.(ERR_CONTENT_DECODING_FAILEDというエラーになりました.)


server/index.js

const awsServerlessExpress = require('aws-serverless-express')

const express = require('express')
const { Nuxt, Builder } = require('nuxt')
const { customDomainAdaptorMiddleware } = require('./middleware');
const app = express()

// Import and Set Nuxt.js options
let config = require('../nuxt.config.js')
config.dev = false

async function initApp() {
// Init Nuxt.js
const nuxt = new Nuxt(config)

const { host, port } = nuxt.options.server

// Build only in dev mode
if (config.dev) {
const builder = new Builder(nuxt)
await builder.build()
} else {
await nuxt.ready()
}

// Give nuxt middleware to express
app.use(customDomainAdaptorMiddleware)
app.use(nuxt.render)

return app
}

var server = undefined;
const binaryMimeTypes = [
'application/javascript',
'application/json',
'application/octet-stream',
'application/xml',
'font/eot',
'font/opentype',
'font/otf',
'image/jpeg',
'image/png',
'image/svg+xml',
'text/comma-separated-values',
'text/css',
'text/html',
'text/javascript',
'text/plain',
'text/text',
'text/xml'
]

exports.handler = (event, context) => {
initApp().then((app) => {
if (server === undefined) {
server = awsServerlessExpress.createServer(app, null, binaryMimeTypes)
}
awsServerlessExpress.proxy(server, event, context)
})
}


下記のミドルウェアを新規に作成します. API Gatewayでは/dev/といったステージ名を示すパスが入るのでそれに対応させるためにURLを置き換えています.


server/middleware.js

let config = require('../nuxt.config.js')

const customDomainAdaptorMiddleware = (req, res, next) => {
const apigatewayHeader = req.headers['x-apigateway-event'];

if (apigatewayHeader === undefined) {
next()
return
}

req.url = req.originalUrl = `${config.router.base}${req.url}`.replace('//', '/')

next()
};

module.exports = {customDomainAdaptorMiddleware};


API Gatewayのステージ名となる既定パスをnuxt.config.jsに追記して設定します.今回は/dev/が既定パスとして設定します. (URLのhttps://xxxx.amazonaws.com/devのこと)


nuxt.config.js

  router: {

base: '/dev/',
},


3. デプロイ


3.1 デプロイ定義を作成

CloudFormationのデプロイ定義を作成します. LambdaとAPI Gateway及びそれらに付随する権限設定を書いています.


cloudformation.yaml

AWSTemplateFormatVersion: '2010-09-09'

Transform: 'AWS::Serverless-2016-10-31'

Resources:
NuxtServerLambda:
Type: AWS::Lambda::Function
Properties:
Code: .
Timeout: 5
MemorySize: 512
FunctionName: nuxt_sample_server
Role: !GetAtt [ "NuxtServerLambdaRole", "Arn" ]
Runtime: nodejs8.10
Handler: server/index.handler

NuxtServerLambdaRole:
Type: AWS::IAM::Role
Properties:
ManagedPolicyArns:
- !Ref NuxtServerLambdaPolicy
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: "sts:AssumeRole"

NuxtServerLambdaPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "logs:CreateLogStream"
- "logs:PutLogEvents"
- "logs:CreateLogGroup"
Resource: "arn:aws:logs:*:*:*"

LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${NuxtServerLambda}
RetentionInDays: 3

ApiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: dev
DefinitionBody:
swagger: "2.0"
info:
version: "1.0.0"
title: "nuxt_lambda_sample"
basePath: dev
x-amazon-apigateway-binary-media-types:
- '*/*'
paths:
/:
get:
produces:
- "application/json"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
x-amazon-apigateway-integration:
uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${NuxtServerLambda.Arn}/invocations"
responses:
default:
statusCode: "200"
passthroughBehavior: "when_no_match"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws_proxy"
/{proxy+}:
get:
produces:
- "application/json"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
x-amazon-apigateway-integration:
uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${NuxtServerLambda.Arn}/invocations"
responses:
default:
statusCode: "200"
passthroughBehavior: "when_no_match"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws_proxy"

ApiPermission:
Type: "AWS::Lambda::Permission"
DependsOn:
- ApiGateway
- NuxtServerLambda
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !Ref NuxtServerLambda
Principal: apigateway.amazonaws.com



3.2 ビルド & デプロイ

yarnでビルド後, awsコマンドでデプロイしています.

STACK_NAME, S3_BUCKETは適切に変更してください.

$ STACK_NAME=nuxt-lambda-sampl

$ S3_BUCKET=<デプロイするパッケージを置くS3バケット名>

$ yarn run build
$ aws cloudformation package --template-file cloudformation.yaml --s3-bucket $S3_BUCKET --output-template-file cloudformation_dist.yaml
$ aws cloudformation deploy --template-file cloudformation_dist.yaml --stack-name $STACK_NAME --capabilities CAPABILITY_IAM


4. 動いた


まとめ

Nuxt.jsのWebページをAWS Lambdaで動くようにしました.


参考にした文献