TypeScript な サーバーレス Nuxt を API Gateway で動かす

2020/11/19 15:00 追記

これ全然ダメでした。 `nuxt.config.js` がパッケージに含まれていないため、`modules` が読み込めない。
`nuxt.config.js` を含めたら今度は `serverMiddleware` が `.ts` なので読み込めない。
このままだと使い物にならないため、修正します。

# TypeScript + Nuxt + API Gateway + AWS SAM

いちばん最後に構成図があります。（でかいので最後）

近頃では SSR よりも SSG という風潮ですが、公開速度が求められるケースでは SSG だと実現できないこともあるわけで。いまさらながら Nuxt.js を AWS Lambda + API Gateway で実行するまでのやり方を記録として残しておきます。

あと、この構成のメリットは、めちゃくちゃ安いことです。全体のコストとしては CloudFront の料金が支配的で、アクセス量によっては月額 1,000 円以内に収まります。

ぶっちゃけ、よほど Nuxt, TypeScript, AWS に自信がないかぎりやらない方がいいです。モジュール 1 つ追加するだけでも相当な Try & Error だし、切り分けがしんどいし、インフラコストに 1,000 円もかけられないんだ！という人以外には本当にオススメしません。自分は全部かなり自信がある人ですが、それでもめちゃくちゃしんどかったです。

序盤はおもにパッケージサイズとの戦いの記録です。中盤は Nuxt の API との戦い、終盤は AWS との戦いになります。

成果物はここ（記事と若干の乖離があるけど） => https://github.com/sonodar/nuxt-serverless-app

ちなみにまだ微妙に書き途中。

# プロジェクト作成

プロジェクトを 1 から作る場合は `create-nuxt-app` で作成するのが楽です。

```console
yarn create nuxt-app nuxt-serverless-app

create-nuxt-app v3.4.0
✨  Generating Nuxt.js project in nuxt-serverless-app
? Project name: nuxt-serverless-app
? Programming language: TypeScript               ・・・（1）
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules:
? Linting tools: ESLint, Prettier, Lint staged files
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)          ・・・（2）
? Deployment target: Server (Node.js hosting)    ・・・（3）
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git
```

ポイントは、`Programming language` で必ず `TypeScript` を選択すること。これを選択しないで後から `TypeScript` を追加するのは意外と面倒です。あと `Universal (SSR / SSG)` と `Server (Node.js hosting)` も必須ですが, 間違えても簡単に直せます。他の選択肢は任意です。

> 個人的には、既存のプロジェクトをマイグレーションする場合でも, 先に `create-nuxt-app` で雛形を作成してから components などのファイルをコピーして適宜修正しています。足回りを実装し直すよりも楽なことが多いので。特に ESLint とかはバージョンによって全然変わるし辛い。すでに大規模なシステムではできないと思いますけど。

# srcDir 変更

サーバーサイドのソースコードは nuxt とは完全に分離する必要があるため, `srcDir` を変更して nuxt 管理のソースを `nuxt-src`, Lambda で実行されるサーバーサイドを `server` とします。

```sh
mkdir nuxt-src
mv assets components layouts middleware pages plugins static store nuxt-src/
```

```diff:nuxt.config.js
 export default {
+  srcDir: './nuxt-src',
```

tsconfig.json の `paths` も忘れずに変更します。
`~` を `nuxt-src` に, `~~` をプロジェクトルートにマッピングします。
プロジェクトルートのマッピングは `serverMiddleware` にパスを追加する際に絶対に必要です。ないと nuxt が `serverMiddleware` を解決できません。

```diff:tsconfig.json
     "paths": {
       "~/*": [
-        "./*"
+        "./nuxt-src/*"
       ],
-      "@/*": [
+      "~~/*": [
         "./*"
       ]
     },
```

最初からある`@`は邪悪なので消します。scssでは `~` しか使えないので, 表記を統一するため `@` は毎回消しています。既存プロジェクトで、すでに利用している場合は `~` と同じ修正をします。

> jest.config.js がある場合は, `moduleNameMapper` や `collectCoverageFrom` も忘れずに修正します。

# Lambda ハンドラーの作成

Lambda 関数も TypeScript で実装したいので `webpack` をインストールします。webpack や rollup のようなバンドラーを利用しないと Lambda 関数のデプロイパッケージがあっという間に上限の 50 MB を超えるためバンドラーは必須です。

nuxt に webpack は含まれているので, cli と loader のみを追加します。

```
yarn add -D webpack-cli ts-loader
```

aws-serverless-express を利用した Lambda 関数を実装するために必要なファイルを揃えます。`express` などは `webpack` でバンドルするので, 依存はすべて `devDependencies` に入れます。

```sh
yarn add -D express @types/express cors @types/cors
yarn add -D aws-serverless-express @types/aws-serverless-express @types/aws-lambda
# 以下は必要に応じて
yarn add -D cookie-parser @types/cookie-parser
```

また、`nuxt` パッケージそのものは babel などに依存があり, パッケージサイズが肥大化するため, `nuxt-start` を別途インストールして node_modules の容量を削減します。

まず、`package.json` の `dependencies` をすべて `devDependencies` に移動します。

```diff:package.json
-  "dependencies": {
-    "@nuxt/typescript-runtime": "^2.0.0",
-    "core-js": "^3.6.5",
-    "nuxt": "^2.14.6"
-  },
+  "dependencies": {},
   "devDependencies": {
     "@nuxt/types": "^2.14.6",
     "@nuxt/typescript-build": "^2.0.3",
+    "@nuxt/typescript-runtime": "^2.0.0",
     "@nuxtjs/eslint-config": "^3.1.0",
     "@nuxtjs/eslint-config-typescript": "^3.0.0",
     "@nuxtjs/eslint-module": "^2.0.0",
@@ -35,6 +31,7 @@
     "@types/express": "^4.17.8",
     "aws-serverless-express": "^3.3.8",
     "babel-eslint": "^10.1.0",
+    "core-js": "^3.6.5",
     "eslint": "^7.10.0",
     "eslint-config-prettier": "^6.12.0",
     "eslint-plugin-nuxt": "^1.0.0",
@@ -42,6 +39,7 @@
     "express": "^4.17.1",
     "husky": "^4.3.0",
     "lint-staged": "^10.4.0",
+    "nuxt": "^2.14.6",
```

```sh
yarn add nuxt-start@2.14.7 # バージョンは nuxt に合わせる
```

> `@nuxtjs/axios` など, nuxt の module や plugin も dependencies に含める必要があります。

Lambda やサーバーサイドのソース用に `server` ディレクトリを作成します。

```sh
mkdir server
```

Lambda 関数のハンドラーを実装します。プログラマティックに Nuxt を扱う方法は[公式サイト](https://ja.nuxtjs.org/docs/2.x/internals-glossary/nuxt/)にありますが, 現時点では TypeScript の[型定義はない](https://github.com/nuxt/nuxt.js/issues/7651)ので自前で用意します。
また, 公式サイトにあるような `nuxt` ではなく `nuxt-start` を利用します。

```ts:server/nuxt-start.d.ts
declare module 'nuxt-start' {
  const loadNuxt: (command: string) => Promise<any>
}
```

express のインスタンス生成は別ファイルでやります。このファイルは、後に `serverMiddleware` としても利用可能なように, express のインスタンスを default export します。（必ず default で export します）

```ts:server/index.ts
import express from 'express'
import cors from 'cors'

const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

// express の動作確認用に API のエンドポイントを追加しておきます
const apiRouter = express.Router()
apiRouter.use(cors())
apiRouter.post('/echo', (req, res) => res.json(req.body))
app.use('/api', apiRouter)

export default app
```

> これにより、ローカル開発では通常の express アプリケーションとして `serverMiddleware` で扱うことができ, 開発効率が大幅に向上します。

```ts:server/lambda.ts
import http from 'http'
import { APIGatewayProxyHandler } from 'aws-lambda'
import awsServerlessExpress from 'aws-serverless-express'
import awsServerlessExpressMiddleware from 'aws-serverless-express/middleware'
import { loadNuxt } from 'nuxt-start'
import app from './index'

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',
]

let server: http.Server

async function createServer(): Promise<http.Server> {
  const nuxt = await loadNuxt('start')
  app.use(awsServerlessExpressMiddleware.eventContext())
  app.use(nuxt.render)
  server = awsServerlessExpress.createServer(app, undefined, binaryMimeTypes)
  return server
}

// ここでは async function 不可
export const handler: APIGatewayProxyHandler = (event, context) => {
  createServer().then((server) =>
    awsServerlessExpress.proxy(server, event, context)
  )
}
```

Lambda 関数の TypeScript トランスパイル用に webpack.config.js を作成します。
ポイントは, 出力先を `.nuxt/dist` ディレクトリにすることと, `externals` に `nuxt-start` を含めることです。

```js:webpack.config.js
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-start'],
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.ts', '.js'],
  },
}
```

nuxt build 後に webpack が走るように postbuild を package.json に追加します。

```diff:package.json
   "scripts": {
     "dev": "nuxt-ts",
     "build": "nuxt-ts build",
+    "postbuild": "webpack",
     "start": "nuxt-ts start",
```

試しに build してみます。

```sh
yarn build
```

このままだと以下のように `Error: TypeScript emitted no output` というエラーが出ます。

```console
ERROR in ./server/lambda.ts
Module build failed (from ./node_modules/ts-loader/index.js):
Error: TypeScript emitted no output for /Users/ryohei-sonoda/git/nuxt-serverless-app/server/lambda.ts.
    at makeSourceMapAndFinish (/Users/ryohei-sonoda/git/nuxt-serverless-app/node_modules/ts-loader/dist/index.js:53:18)
    at successLoader (/Users/ryohei-sonoda/git/nuxt-serverless-app/node_modules/ts-loader/dist/index.js:40:5)
    at Object.loader (/Users/ryohei-sonoda/git/nuxt-serverless-app/node_modules/ts-loader/dist/index.js:23:5)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
```

tsconfig.json で `noEmit` を `false` にします。

```diff:tsconfig.json
     "strict": true,
-    "noEmit": true,
+    "noEmit": false,
     "experimentalDecorators": true,
```

これでビルドが通ります。

最後に, nuxt.config.js で `serverMiddleware` を追加します。

```diff:nuxt.config.js
   // Build Configuration (https://go.nuxtjs.dev/config-build)
   build: {},
+
+  serverMiddleware: ['~~/server/index.ts'],
 }
```

まず、ローカルで express が動作するか確認します。

```sh
yarn dev
```

127.0.0.1:3000 で LISTEN されるので, 先程生やした動作確認用のエンドポイントに POST してみます。

```sh
curl -v -H "Content-Type: application/json" localhost:3000/api/echo -d '{"message":"hoge"}'
```

```console
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
> POST /api/echo HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 18
>
* upload completely sent off: 18 out of 18 bytes
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 18
< ETag: W/"12-HG8TY3NadWW3zeWB3QbSQHxbAxc"
< Date: Thu, 05 Nov 2020 14:16:51 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
{"message":"hoge"}
```

ローカルではちゃんと nuxt の `serverMiddleware` として機能しています。

# Lambda アップロードパッケージ作成

続いて, パッケージ作成処理です。
ビルドしたファイルのうち、含めるのは以下のみです。

* .nuxt/dist/server/
* .nuxt/dist/client/
* .nuxt/dist/lambda.js
* node_modules/

> assets を CloudFront で配信するので, `.nuxt/dist/client` は実質不要になる。手順の便宜上ここでは含める。

```sh
# devDependencies のパッケージを node_modules から削除
yarn install --production
zip -rq upload.zip .nuxt/dist node_modules
```

だいたい 8 MB くらいのサイズになります。なお, `nuxt-start` ではなく `nuxt` をそのまま使うと 31 MB にもなります。

> なお, 自分の場合, 毎回 devDependencies を削除していたら開発スピードが落ちるので, パッケージの作成は docker コンテナ上で実施しています。利用している docker イメージは `lambci/lambda:build-nodejs12.x` です。

# sam テンプレート作成

sam のテンプレートを作成します。ここでは 2 ファイルのみなので `sam init` コマンドは使いません。

```yaml:template.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Serverless Nuxt App

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

Resources:
  NuxtApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: serverless-nuxt-app
      StageName: v1
  NuxtFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: serverless-nuxt-app
      CodeUri: ./upload.zip
      Handler: .nuxt/dist/lambda.handler
      Runtime: nodejs12.x
      MemorySize: 512
      Timeout: 10
      Description: Serverless Nuxt App
      Environment:
        Variables:
          NODE_ENV: production
      Events:
        Root:
          Type: Api
          Properties:
            Path: "/"
            Method: any
            RestApiId: !Ref NuxtApi
        Nuxt:
          Type: Api
          Properties:
            Path: "/{proxy+}"
            Method: any
            RestApiId: !Ref NuxtApi
  # Lambda 関数用の CloudWatch LogGroup
  # あらかじめ作成してログ保持日数が指定しておく
  NuxtFunctionLog:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /aws/lambda/serverless-nuxt-app
      RetentionInDays: 7

Outputs:
  ApiEndpoint:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${NuxtApi}.execute-api.${AWS::Region}.amazonaws.com/v1/"
```

この状態でローカル実行してみます。

```sh
sam local start-api
```

127.0.0.1:3000 で起動したというメッセージが出てきます。

```console
Mounting NuxtFunction at http://127.0.0.1:3000/{proxy+} [DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT]
Mounting NuxtFunction at http://127.0.0.1:3000/ [DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2020-11-05 23:27:53  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
```

で、ブラウザで http://127.0.0.1:3000 にアクセスすると、しばらくたって以下の様な 200 OK のログが出ます。

```console
Invoking .nuxt/dist/lambda.handler (nodejs12.x)
Decompressing /Users/ryohei-sonoda/git/nuxt-serverless-app/upload.zip
Failed to download a new amazon/aws-sam-cli-emulation-image-nodejs12.x:rapid-1.1.0 image. Invoking with the already downloaded image.
Mounting /private/var/folders/x2/_5zc52s57jv5vsjrg3hxnwqr0000gp/T/tmp597u_xzy as /var/task:ro,delegated inside runtime container
START RequestId: 674bfa66-7dee-1b63-3f58-afbc633de810 Version: $LATEST
END RequestId: 674bfa66-7dee-1b63-3f58-afbc633de810
REPORT RequestId: 674bfa66-7dee-1b63-3f58-afbc633de810  Init Duration: 937.57 ms        Duration: 3220.72 ms    Billed Duration: 3300 ms        Memory Size: 512 MB     Max Memory Used: 98 MB  
2020-11-05 23:28:15 127.0.0.1 - - [05/Nov/2020 23:28:15] "GET / HTTP/1.1" 200 -
```

でもブラウザは真っ白もしくはエラー画面です。
curl で確認してみると Base64 にエンコードされた HTML が返り, ブラウザではデコードできずにエラーになっています。

```console
* Rebuilt URL to: localhost:3000/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< x-powered-by: Express
< etag: "e93-aSp1uNPhobCrmddhNg5VzLYfihY"
< content-type: text/html; charset=utf-8
< accept-ranges: none
< content-length: 3731
< vary: Accept-Encoding
< date: Thu, 05 Nov 2020 14:46:43 GMT
< connection: close
< Server: Werkzeug/1.0.1 Python/3.7.8
<
* Closing connection 0
PCFkb2N0eXBlIGh0bWw+CjxodG1sIGRhdGEtbi1oZWFkLXNz...
```

現状はおとなしく `sam local start-api` でのブラウザ確認を諦めるしかありません。

> 最初から deflate 圧縮しちゃえばバイナリ扱いになってイケるかな？と思って `compression` パッケージを試しましたが, compression された後に Base64 エンコードされるので結局ダメでした。

デプロイの設定を samconfig.toml に書きます。

```toml:samconfig.toml
version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "serverless-nuxt-app"
s3_bucket = "your-bucket-name"
s3_prefix = "sam-src/serverless-nuxt-app"
region = "ap-northeast-1"
capabilities = "CAPABILITY_IAM"
```

デプロイします。

```sh
cd sam
export AWS_ACCESS_KEY_ID=your-aws-access-key-id
export AWS_SECRET_ACCESS_KEY=your-aws-secret-access-key
sam deploy
```

```console
CloudFormation outputs from deployed stack
----------------------------------------------------------------------------------------------------------------------------------------------
Outputs
----------------------------------------------------------------------------------------------------------------------------------------------
Key                 ApiEndpoint
Description         API Gateway endpoint URL
Value               https://xxxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1/
----------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - serverless-nuxt-app in ap-northeast-1
```

出力された ApiEndpoint にブラウザでアクセスすると, 無事に nuxt の画面が表示されました。

![スクリーンショット 2020-11-06 13.20.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17735/5d866fdc-255c-4ce1-ed45-8281ba07635d.png)

# axios モジュールを試す

せっかく動作確認用の API があるので, axios モジュールをインストールして `asyncData` で実行してみます。

```sh
yarn add @nuxtjs/axios
```

```diff:nuxt.config.js
   // Modules (https://go.nuxtjs.dev/config-modules)
-  modules: [],
+  modules: ['@nuxtjs/axios'],
```

これだけだと `$axios` が認識されないので, tsconfig.json に追記。

```diff:tsconfig.json
     },
     "types": [
       "@types/node",
-      "@nuxt/types"
+      "@nuxt/types",
+      "@nuxtjs/axios"
     ]
   },
```

トップページで echo API を実行してレスポンスを画面表示する処理を追記。

```diff:nuxt-src/pages/index.vue
     <div>
       <Logo />
       <h1 class="title">nuxt-serverless-app</h1>
+      <pre>{{ data }}</pre>
       <div class="links">
         <a
           href="https://nuxtjs.org/"
@@ -27,8 +28,14 @@
 
 <script lang="ts">
 import Vue from 'vue'
+import { Context } from '@nuxt/types'
 
-export default Vue.extend({})
+export default Vue.extend({
+  async asyncData(ctx: Context) {
+    const { data } = await ctx.$axios.post('/api/echo', { message: 'hoge' })
+    return { data: JSON.stringify(data) }
+  },
+})
 </script>
 
 <style>
```

ローカルで実行（`yarn dev`）してみます。ちゃんと `{"message":"hoge"}` が表示されます。

![スクリーンショット 2020-11-06 13.29.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17735/94051967-a281-a514-188a-8fb6c8253037.png)

デプロイしてみる。

```sh
rm -rf .nuxt upload.zip
yarn build
yarn install --production
zip -rq upload.zip .nuxt/dist node_modules
sam deploy
```

ダメでした。Internal Server Error になります。Lambda のログを確認。

```console
{
    "errorType": "Runtime.UnhandledPromiseRejection",
    "errorMessage": "Error: connect ECONNREFUSED 127.0.0.1:3000",
    "reason": {
        "message": "connect ECONNREFUSED 127.0.0.1:3000",
        "name": "Error",
        "stack": "Error: connect ECONNREFUSED 127.0.0.1:3000\n    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16)",
        "config": {
...
```

axios で `127.0.0.1:3000` に繋ぎにいってる。Lambda では LISTEN プロセスがいるわけではないので, 当然これだとエラーになる。

なので `baseURL` を指定してあげればいい。環境変数から渡すようにしよう。

```diff:nuxt.config.js
   // Modules (https://go.nuxtjs.dev/config-modules)
   modules: ['@nuxtjs/axios'],
+ 
+  axios: {
+    baseURL: process.env.BASE_URL || 'http://localhost:3000',
+  },
+
   // Build Configuration (https://go.nuxtjs.dev/config-build)
-  build: {},
+  build: {
+    publicPath: (process.env.BASE_URL || '') + '/_nuxt/',
+  },
+  
+  router: {
+    base: (process.env.BASE_PATH || '') + '/',
+  },
```

ついでに忘れてた `publicPath` と `router.base` も修正。API Gateway の場合, 必ずステージ名が URL の末尾にパスとして追加されるので, これをしないと相対パスが解決できずに画像や js が読み込めなくなる。

> 例えば `/_nuxt/assets/hoge.img` の場合
> `https://xxxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1/_nuxt/assets/hoge.img`
> が正解なのに
> `https://xxxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/_nuxt/assets/hoge.img`
> になっちゃう。API Gateway みたいに baseURL がサブディレクトリで終わらなければ無問題。

```diff:template.yml
      Environment:
        Variables:
          NODE_ENV: production
+         BASE_PATH: /v1/
+         BASE_URL: !Sub "https://${NuxtApi}.execute-api.${AWS::Region}.amazonaws.com/v1/"
```

```sh
rm -rf .nuxt upload.zip
yarn build
yarn install --production
zip -rq upload.zip .nuxt/dist node_modules
sam deploy
```

sam のエラー。循環参照になってるからダメだよって。

```console
Error: Failed to create changeset for the stack: sonoda-nuxt-api-test,
  ex: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state Status: FAILED.
  Reason: Circular dependency between resources: [NuxtFunctionRootPermissionv1, NuxtFunction, NuxtApiDeploymentb5e0e7a20a, NuxtFunctionNuxtPermissionv1, NuxtApi, NuxtApiv1Stage]
```

仕方ないので, ベタ書き。どうせ後でドメインを割り当てるんだから, いったんはこれで行く。

```diff:template.yml
      Environment:
        Variables:
          NODE_ENV: production
          BASE_PATH: /v1/
-         BASE_URL: !Sub "https://${NuxtApi}.execute-api.${AWS::Region}.amazonaws.com/v1/"
+         BASE_URL: https://xxxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1
```

で, デプロイして動作確認すると・・・, 同じエラー `connect ECONNREFUSED 127.0.0.1:3000`

どうやら `nuxt.config.js` での `process.env` はビルド時の環境変数を参照して, 出力されたファイルに埋め込まれるようだ。なので、ビルド時に指定する。

template.yml は戻しておく。

```diff:template.yml
      Environment:
        Variables:
          NODE_ENV: production
-         BASE_PATH: /v1/
-         BASE_URL: https://xxxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1
```

```sh
rm -rf .nuxt upload.zip
# ビルドの前に指定
export BASE_PATH=/v1/
export BASE_URL=https://xxxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1
yarn build
yarn install --production
zip -rq upload.zip .nuxt/dist node_modules
sam deploy
```

今度はうまくいきました。

## TODO 以下、書きかけ

### CloudFront 経由にする場合、axios モジュールは via ヘッダを削除する

https://qiita.com/ykunimoto/items/9509aad5f024cb547fb1
https://qiita.com/kubotak/items/fc1a877f99a569fc54bb

### yarn add axios する

`@nuxtjs/axios` だけだと dependencies に含まれているはずなのに `axios` が認識されない。（未調査）

# 参考リンク

- https://qiita.com/hiroyky/items/298e8a475dcd6dc70727
- https://github.com/sketchnotes/nuxt-express-ts
- https://github.com/tonyfromundefined/nuxt-serverless

# 実際の構成図

* Directus という OSS の Headless CMS を利用しています。ここは Contentful や microCMS でも問題ないです。
* 実際の構成では CMS が VPC にいるので、Lambda も VPC に入れています。
* 1 つの CloudFront でパスを分けて 3 つのオリジンを構成しています。
* キャッシュクリアにキューを使っている主な目的はバッファリングのためです。
 
![スクリーンショット 2020-11-17 18.34.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17735/b2ab46f9-2c47-a2b6-ea7b-13fb70e0fa56.png)