Nuxt.js(v2.2.0)+TypeScriptなアプリをAWS Lambda+αにデプロイしてみた

Nuxt.jsのv2系でTypeScriptを利用しつつ、AWS Lambda+αにデプロイできるか確認してみました。

## 手順

GitHubにいい感じのコードをアップいただけている御方がおられたので、利用させてもらいます。

jeehyukwon/nuxt-serverless: Nuxt.js Serverless SSR Starter on AWS (Lambda + API Gateway + S3) with Serverless Framework
https://github.com/jeehyukwon/nuxt-serverless

静的ファイルはS3、AWS LambdaのNode.jsでExpressを立ち上げて、SSRをして、API Gatewayがエンドポイント、ドメインはカスタムドメインを利用する構成です。

カスタムドメインを利用する前提でしたので、それを外してデプロイするようにしてみます。

Githubに上記リポジトリをフォークしたものがありますので、よければご参考ください。
https://github.com/kai-kou/nuxt-serverless


### ソースの取得と環境構築

ローカル環境で構築します。

```node``` と```npm``` がインストールされている前提です。

```sh
> node -v
v10.11.0

> npm -v
6.4.1
```

```sh
> git clone https://github.com/jeehyukwon/nuxt-serverless.git
> cd nuxt-serverless
```

```sh:コンテナ内
> npm install
```

### 動作確認

ローカルで動作するか確認します。

```sh
> npm run dev

(略)
✔ success Builder initialized
✔ success Nuxt files generated

 READY  Listening on http://localhost:3000
```

ブラウザでアクセスします。
http://localhost:3000
<img width="878" alt="スクリーンショット 2018-11-13 11.07.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/410fffb7-067a-f08d-2c6c-756dbae9b1e2.png">

<img width="1025" alt="スクリーンショット 2018-11-13 11.07.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/d66e8c28-cd41-3bf2-a210-575dc0586f95.png">

はい。
ソースを確認するとSSRしてますね。


### デプロイ準備

#### API Gatewayのステージ対応

AWS Lambdaへデプロイする場合、HTTPアクセスにはAWS API Gatewayを利用することになります。
カスタムドメインを利用しない場合、API GatewayのURLは```https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/``` のように、ステージ名```dev``` が含まれます。

Nuxt.jsでこのステージ名を```BaseURL``` として設定してやる必要があります。
詳細は下記記事で詳しく解説されています。感謝！

Nuxt.js on AWS Lambda with Serverless Framework - mya-ake com
https://mya-ake.com/posts/nuxtjs-on-aws-lambda/


```nuxt.config.js``` に```router.base``` を追加します。

```js:nuxt.config.js(一部抜粋)
module.exports = {
  (略)
  router: {
    base: '/dev/'
  },
  (略)
}
```

Nuxt.jsの設定以外にExpressでも設定が必要となります。リクエストのURLにステージ名```dev``` が含まれないため、強制的に含めてやります。BaseURLはNuxtの設定```nuxtConfig.router.base``` から取得しています。

```js:handler.js
app.use((req, res) => (
  setTimeout(() => {
    req.url = `${nuxtConfig.router.base}${req.url}`.replace('//', '/')
    nuxt.render(req, res)}
  , 0)
))
```

#### カスタムドメインを利用しない設定

カスタムドメインを利用しないので、```package.json``` の```scripts``` に指定されている```sls create_domain``` と```sls delete_domain``` を削除します。

```json:package.json(一部抜粋)
  "scripts": {
    "dev": "nuxt",
    "build": "cross-env NODE_ENV=production nuxt build",
    "start": "nuxt start",
    "sls:local": "sls offline",
    "sls:create": "npm run build && sls deploy",
    "sls:delete": "sls remove",
    "sls:deploy": "npm run build && sls deploy"
  },
```

```serverless.yml``` でもカスタムドメイン作成の定義を削除します。(ここではコメントアウトしています。)```service``` や```region``` 、```BUCKET_NAMESPACE``` は任意で変更してください。


```yaml:serverless.yml
service: nuxt-serverless  # 1. Edit whole service name

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${opt:stage, 'dev'}
  region: ap-northeast-1 # 2. Edit AWS region name
  environment:
    NODE_ENV: production
    BUCKET_NAMESPACE: nuxt-serverless  # 3. Specify a new AWS S3 bucket namespace for bundled assets and static assets (should be unique)
    ASSETS_BUCKET_NAME: ${self:provider.environment.BUCKET_NAMESPACE}-assets-${opt:stage, 'dev'}
    STATIC_BUCKET_NAME: ${self:provider.environment.BUCKET_NAMESPACE}-static-${opt:stage, 'dev'}
    ASSETS_BUCKET_URL: https://s3.${self:provider.region}.amazonaws.com/${self:provider.environment.ASSETS_BUCKET_NAME}
    STATIC_BUCKET_URL: https://s3.${self:provider.region}.amazonaws.com/${self:provider.environment.STATIC_BUCKET_NAME}

custom:
  # customDomain:
  #   domainName: service.mydomain.io  # 4. Specify a new domain name to be created
  #   stage: ${opt:stage, 'dev'}
  #   certificateName: mydomain.io  # 5. Enter the certificate name in AWS Certificate Manager (us-east-1) for https connection
  #   createRoute53Record: true
  serverless-offline:
    port: 4000
  s3Sync:
    - bucketName: ${self:provider.environment.ASSETS_BUCKET_NAME}
      localDir: .nuxt/dist/client
    - bucketName: ${self:provider.environment.STATIC_BUCKET_NAME}
      localDir: static

package:
  exclude:
    - src/**
  include:
    - serverless.yml

functions:
  nuxt-renderer:
    handler: handler.render
    memorySize: 512
    timeout: 30
    events:
      - http:
          path: /
          method: ANY
          cors: true
      - http:
          path: /{proxy+}
          method: ANY
          cors: true

resources:
  Resources:
    AssetsBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:provider.environment.ASSETS_BUCKET_NAME}
    AssetsBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket:
          Ref: AssetsBucket
        PolicyDocument:
          Version: "2012-10-17"
          Statement: [
            {
              Action: ['s3:GetObject'],
              Effect: 'Allow',
              Resource: {
                Fn::Join: ['', ['arn:aws:s3:::', { Ref: 'AssetsBucket' }, '/*']],
              },
              Principal: '*'
            },
          ]
    StaticBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:provider.environment.STATIC_BUCKET_NAME}
    StaticBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket:
          Ref: StaticBucket
        PolicyDocument:
          Version: "2012-10-17"
          Statement: [
            {
              Action: ['s3:GetObject'],
              Effect: 'Allow',
              Resource: {
                Fn::Join: ['', ['arn:aws:s3:::', { Ref: 'StaticBucket' }, '/*']],
              },
              Principal: '*'
            },
          ]

plugins:
  - serverless-offline
  - serverless-s3-sync
  # - serverless-domain-manager
```

### デプロイする

```npm run sls:create``` または```npm run sls:deploy``` することで、Nuxt.jsのビルドと、AWSへのデプロイがされます。

```sh
> npm run sls:create
(略)
Entrypoint app = 55e8f003a83a598dd113.js 5e45cc8df25adbaac94b.js 1d1bc122ddb9c7b7f271.css 6a943e90e669a1de7258.js
[13:27:00] Compiling server
[13:27:04] Compiled server in 4s

Hash: 2c7c34f6c1ea4aefd63d
Version: webpack 4.25.1
Time: 3924ms
Built at: 2018-11-13 13:27:04
             Asset     Size  Chunks             Chunk Names
server-bundle.json  213 KiB          [emitted]
Entrypoint app = server-bundle.js
✨  Done in 114.07s.
(略)
Serverless: Stack update finished...
Service Information
service: nuxt-serverless
stage: dev
region: ap-northeast-1
stack: nuxt-serverless-dev
api keys:
  None
endpoints:
  ANY - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
  ANY - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/{proxy+}
functions:
  nuxt-renderer: nuxt-serverless-dev-nuxt-renderer
S3 Sync: Syncing directories and S3 prefixes...
....
S3 Sync: Synced.
```

デプロイができたら、ブラウザでアクセスしてみます。```xxxxxxxxxx``` はご自身の環境に合わせてください。

https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/

<img width="811" alt="Hello__Nuxt_Serverless_Template.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/9cdeef2c-68f9-f84b-3546-f351d34426d8.png">

<img width="815" alt="Hello__TypeScript.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/974618ab-89f0-60d8-49f5-85b8bcc7afdf.png">

<img width="812" alt="view-source_https___vvb2o02739_execute-api_ap-northeast-1_amazonaws_com_dev_.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/347deae3-a137-ed38-78f2-797f9dc821e8.png">

なんとか動きました^^



## はまりポイント

### ```nuxt dev``` でホスト名に```0.0.0.0``` が指定できない

当初Dockerコンテナ内で実行させようとしたのですが、ホスト名に```0.0.0.0``` が指定できなかったので、ローカル環境で構築しました。

Host and Port - Nuxt.js
https://nuxtjs.org/faq/host-port

```0.0.0.0``` を指定すると、ランダムっぽいIPが振られてしまいます。。。
環境変数```HOST``` も```NUXT_HOST``` も ```package.json``` で指定も ```--hostname``` もだめ。むむぅ。なんでしょう。。。
ドキュメントには
>Note: If port is assigned the string value of '0' (not 0, which is falsy), a random port will be assigned to your Nuxt application.

とありますので、ポートはわかるのですが、ホスト名でも同じような挙動です。。。ローカルでもDockerコンテナ内でも同じ挙動でした。

```sh:ローカル
INFO Building project

✔ success Builder initialized
✔ success Nuxt files generated

READY Listening on http://192.168.34.25:3001
```

```sh:Dockerコンテナ
INFO Building project

✔ success Builder initialized
✔ success Nuxt files generated

READY Listening on http://172.28.0.2:3001
```

### API Gatewayのパスが面倒

Nuxt.jsとExpressと別で```BaseURL``` の設定が必要だったのが非常にわかりにくかったです。今回Serverless-httpを利用していたのですが、いくつかIssueが上がっていました。どうにかならないものでしょうか^^

Correct path for non-custom endpoints by bsdkurt · Pull Request #42 · dougmoscrop/serverless-http
https://github.com/dougmoscrop/serverless-http/pull/42

Include baseUrl in request object · Issue #35 · dougmoscrop/serverless-http
https://github.com/dougmoscrop/serverless-http/issues/35


## まとめ

ローカル環境の構築がさくっと終わったのでAWS Lambdaにもさくっと構築できるだろうと思って取り組んだら、思いの外ハマって時間がかかりました。Nuxt.jsもv2がリリースされたこともあり、かつTypeScriptを利用だったので、正しい情報・設定を見極めるのに試行錯誤することになりました。

デプロイできることは確認できましたので、ひとまずは

やったぜ^^

## 参考

jeehyukwon/nuxt-serverless: Nuxt.js Serverless SSR Starter on AWS (Lambda + API Gateway + S3) with Serverless Framework
https://github.com/jeehyukwon/nuxt-serverless

Nuxt.js on AWS Lambda with Serverless Framework - mya-ake com
https://mya-ake.com/posts/nuxtjs-on-aws-lambda/

Host and Port - Nuxt.js
https://nuxtjs.org/faq/host-port

Correct path for non-custom endpoints by bsdkurt · Pull Request #42 · dougmoscrop/serverless-http
https://github.com/dougmoscrop/serverless-http/pull/42

Include baseUrl in request object · Issue #35 · dougmoscrop/serverless-http
https://github.com/dougmoscrop/serverless-http/issues/35