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

# 概要

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


TypeScriptでも試してみました。[Nuxt.jsをAWS Lambdaで動かす。【TypeScript編】](https://qiita.com/hiroyky/items/298e8a475dcd6dc70727)

# 構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73358/db485ca9-f538-85aa-09c9-119db83f8e0c.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でも動くと思います(未検証)

```shell
$ 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を登録していることがわかります．

```javascript: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](https://github.com/awslabs/aws-serverless-express)を導入します．

```shell
$yarn add aws-lambda aws-serverless-express
```

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

- Lambdaのエントリポイントである`handler関数`を作成し， awsServerlessExpresを動かしている．
- ミドルウェアとしてURLを置き換える処理(`customDomainAdaptorMiddleware`)を追加している．

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

```javascript: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を置き換えています．

```javascript: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`のこと）

```javascript:nuxt.config.js
  router: {
    base: '/dev/',
  },
```

## 3. デプロイ

### 3.1 デプロイ定義を作成

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

```yaml: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は適切に変更してください．

```shell
$ 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73358/335bca3d-199b-16f0-84ee-0390f1a3cfac.png)

# まとめ

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


# 参考にした文献

- https://qiita.com/kai_kou/items/5b1ebb9abc5d62545e04
- https://github.com/awslabs/aws-serverless-express/tree/master/examples/basic-starter