概要
vue.jsのサーバサイドレンダリングで行うNuxt.jsをAWS Lambda上で動かしてみます。あくまでも試験的に試すことが目的の記事です。もし,本番サービスでの導入する場合には慎重な議論をお願いします。
なお、この記事は以前私が書いたNuxt.jsをAWS Lambda上で動かす.サーバレス・サーバサイドレンダリングをTypeScriptでも実現できるようにしたものです。JavaScriptで書きたい人はリンク先を参照して頂きたいと思います。なお、内容自体はこの記事単体で完結します。
構成
- AWS Lambdaでnuxt.jsを可動させる
- API Gateway経由でWebブラウザ等からアクセスする.
前提
- yarn, npx, nodeといったnuxt.jsを開発する環境がある。
- TypeScript, JavaScriptを知っている。
- AWSアカウントを取得しaws-cliがインストール済みで実行できる状態である。
- node.jsとexpressでWebサーバを作ったことがある。
- AWS LambdaやAPI Gateway, CloudFormation, S3などAWSのサービスを使ったことがある。
手順
1. nuxtプロジェクト作成
1.1 初期化
npxコマンドを使ってnuxtプロジェクトを立ち上げます.
- UIフレームワークにVuetifyを指定していますが必須ではありません.
- パッケージマネージャはyarnを選択していますがnpmでも動くと思います(未検証)
手順
1. nuxtプロジェクト作成
1.1 初期化
npxコマンドを使ってnuxtプロジェクトを立ち上げます。2020年8月8日現在、言語選択でTypeScriptを選択すればTypeScriptで実装できるように設定してくれるようです。便利になりましたね。また、この時点でexpressの導入はできなくなったようです。
- 言語選択ではTypeScriptを選択します。
- UIフレームワークにVuetifyを指定していますが必須ではありません。
- パッケージマネージャはyarnを選択していますがnpmでも動くと思います(未検証)。
- 必要に応じて適宜変更してください。
$ yarn create nuxt-app nuxt_lambda_ts_sample
? Project name: nuxt_lambda_ts_sample
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: Vuetify.js
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
1.2 nuxt.jsを起動
初期化後,下記コマンドでローカル環境にnuxt.jsが起動します。
$ cd nuxt_lambda_ts_sample
$ yarn run dev
ℹ Listening on: http://localhost:3000/
1.3 expressを導入し、Lambdaで動くようにする。
必要なパッケージをインストールします。
$ yarn add -D aws-lambda aws-serverless-express express @types/aws-lambda @types/aws-serverless-express
AWS Lambdaのエンドポイントとなるプログラム実装します。ここでExpressを使います。./server/lambda.ts
を作成します。
ポイントは以下2つです。
- Lambdaのエントリポイントであるhandler関数を作成し, awsServerlessExpresを動かしている.
- ミドルウェアとしてURLを置き換える処理(customDomainAdaptorMiddleware)を追加している.
import http from 'http'
import { APIGatewayEvent, Context } from 'aws-lambda'
import awsServerlessExpress from 'aws-serverless-express'
import express from 'express'
import { Nuxt, Builder } from 'nuxt'
import config from '../nuxt.config'
import { customDomainAdaptorMiddleware } from './middleware'
const app = express()
async function initApp () {
const nuxt = new Nuxt(config)
if (config.dev) {
const builder = new Builder(nuxt)
await builder.build()
} else {
await nuxt.ready()
}
app.use(customDomainAdaptorMiddleware)
app.use(nuxt.render)
return app
}
let server: http.Server
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'
]
export function handler (event: APIGatewayEvent, context: Context) {
initApp().then((app) => {
if (server === undefined) {
server = awsServerlessExpress.createServer(app, undefined, binaryMimeTypes)
}
awsServerlessExpress.proxy(server, event, context)
})
}
上記のコードで導入したミドルウェアを記述します。これはAPI GatewaydでURL解決を良い感じにやるためです。
import express from 'express'
export const customDomainAdaptorMiddleware = (req: express.Request, _: express.Response, next: express.NextFunction) => {
const apigatewayHeader = req.headers['x-apigateway-event']
if (apigatewayHeader === undefined) {
next()
return
}
req.url = req.originalUrl = `/${req.url}`.replace('//', '/')
next()
}
またNuxt
, Builder
の型定義が内容なので一時的な型定義を作成します。
declare module 'nuxt' {
const Nuxt: any
const Builder: any
export { Nuxt, Builder }
}
1.4 設定変更
1.4.1 nuxt.config.jsをnuxt.config.tsに
nuxt.config.jsをnuxt.config.tsに名前変更します。また、必須ではありませんが、下記のように型指定するとエディタの支援を受けられやすくなります。また、本番環境かそれ以外で分岐処理を先ほど記述したため、devという項目で環境を定義します。
+ import { NuxtConfig } from '@nuxt/types'
- export default {
+ const config: NuxtConfig {
+ dev: process.env.NODE_ENV !== 'production',
+ export default config
1.4.2 ビルド設定
Nuxt部分は標準で設定されたnuxt-ts
でビルドし、その後、Express, Lambdaの部分をwebpackでビルドします。
必要なパッケージを追加します。
yarn add -D webpack webpack-cli ts-loader
webpackの設定ファイルを下記のように記述します。
const path = require('path')
module.exports = {
mode: 'production',
entry: {
lambda: path.resolve(__dirname, './server/lambda.ts')
},
output: {
path: path.resolve(__dirname, './.nuxt/dist'),
filename: '[name].js',
libraryTarget: 'commonjs'
},
target: 'node',
externals: [
'nuxt'
],
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
}
}
1.4.3 package.jsonのscriptsにpostbuildを追加
Nuxtの通常ビルド完了後にExpressをWebpackでビルドするようにpackage.json
のscripts追記します。
"scripts": {
・・・・
"postbuild": "webpack", // 追記
・・・・
}
上記の設定により、Nuxtビルド後にwebpackがLambdaのエンドポイントであるserver/lambda.ts
を./nuxt/dist/lambda.js
に出力します。これがAWS Lambdaのエンドポイントとなります。従ってこれがエンドポイントとなるようにインフラを設定します。
tsconfig.jsonを改修
ビルド時にwebpackで
Error: TypeScript emitted no output
というエラーが出たので、tsconfig.json
のnoEmit
をfalse
にします。
- "noEmit": true,
+ "noEmit": false,
1.4.4 vuetifyで型が無いためビルドエラー
UIフレームワークにvuetifyを使用していた場合、nuxt.config.tsでインポートしているvuetify/es5/util/colors
が見つけられずにエラーになりました。
通常のvuetifyをインストールした上でtsconfig.json
のtypes
に追記することで解決できました。
$ yarn add -D vuetify
"types": [
"vuetify",
・・・
]
1.4.5 ESLintの実行設定
package.json
で定義されたESLintの実行コマンドが.tsファイルを対象外にしていたため対象にするように修正しました。
"scripts": [
"lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .",
- "lint": "yarn lint:js",
+ "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
]
1.5 デプロイ定義
CloudFormationでデプロイ定義を作成します。LambdaとAPI Gateway及びそれらに付随する権限設定を書いています。
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Resources:
NuxtServerLambda:
Type: AWS::Lambda::Function
Properties:
Code: .
Timeout: 10
MemorySize: 512
FunctionName: nuxt-lambda-ts-sampl
Role: !GetAtt [ "NuxtServerLambdaRole", "Arn" ]
Runtime: nodejs12.x
Handler: .nuxt/dist/lambda.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: default
DefinitionBody:
swagger: "2.0"
info:
version: "1.0.0"
title: "nuxt-lambda-ts-sampl-gateway"
basePath: default
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
1.6 デプロイ
yarnでビルド後, awsコマンドでデプロイしています。
STACK_NAME, S3_BUCKETは適切に変更してください。
AWS API GatewayにエンドポイントとなるURLが作成されていると思います。
その後、カスタムドメイン設定やRoute53などでドメインを割り当てればウェブサイトとして機能するかと思います。
$ STACK_NAME=nuxt-lambda-ts-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
全体
個人開発でこれと同様の変更を加えました。ここには記載していない細かい変更も含まれていますので、ぜひご参考にしてください。