Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
33
Help us understand the problem. What is going on with this article?
@hiroyky

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

概要

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

TypeScriptでも試してみました。Nuxt.jsをAWS Lambdaで動かす。【TypeScript編】

構成

image.png

  • 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. 動いた

image.png

まとめ

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

参考にした文献

33
Help us understand the problem. What is going on with this article?
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
33
Help us understand the problem. What is going on with this article?