たったこれだけで!?Nuxt3をLambda@Edgeへデプロイ
NuxtをLambda@Edgeにデプロイするのはかなり手間な印象がありましたが、Nuxt3からはとても簡単です。サーバーレスなSSR環境として今後流行るのではないでしょうか。
はじめに
タイトルにあるNuxt3とはVue.jsの有名なフレームワークのNuxt.jsのバージョン3のことを指します。このNuxt3にはNitro[1]と呼ばれるサーバーエンジンが搭載されており、下図にイメージを示すようにNitroを利用することでNuxt3はVercel、Netlify、AWS Lambda等様々な環境用にビルドしてシームレスにデプロイすることができます。
以前のバージョンであるNuxt2でもserverless-expressを使用する事でLambdaを利用したサーバーレス構成を取ることができました。
しかし、このserverless-expressを使用した構成は自身でカスタマイズする部分もあり、少々複雑な構成になるため運用コストもかかってしまうデメリットがありました。
そこで登場するのがNitroです。
Nitroを利用すると下記のような構成がほとんどカスタマイズすることなく簡単に実施することができます。
今回はこのNitroの仕組みを利用してNuxt3をLambda@Edgeで配信する手順についてご紹介していきます。
Nitroについて
Nitroについてもう少しだけ説明しておきましょう。Nitroを利用すると任意環境向けにビルドできるように記述しましたが、Nitroは単に任意環境向けにビルドするためだけのツールではありません。NitroはNuxt3の中核となるサーバーエンジンで、Nuxt3が提供する次の機能はNitroの仕組みによって実現されています。
- 任意環境向けのビルド
- 自動インポート
- ルート処理
- ストレージレイヤー
- キャッシュ
- アセットハンドリング
- TypeScriptサポート
デプロイ先には事前定義されたプリセットを指定することで簡単に任意環境を選択することができます。現時点(2022/5/16)で次のプリセットが存在します。
- aws-lambda
- azure
- azure-functions
- cloudflare
- digital-ocean
- firebase
- heroku
- layer0
- netlify
- netlify-edge
- netlify-builder
- render-com
- stormkit
- vercel
ここで注意してほしいのですが、プリセットのリストにaws-lambda
というプリセットが存在しますが、これはAPI Gateway向けのプリセットとなっておりLambda@Edgeでは利用することができません。
それを解決するべく現在私がAWS Lambda@Edge向けのプリセットaws-lambda-edge
に関するPRを作成しています。
ただし、この記事を書いている段階ではマージされていないため、この記事ではaws-lambda-edge
プリセットを利用しない手順をご紹介します。
手順
次の環境で実施しました。
- Node.js v18.0.0
- Nuxt.js v3.0.0-rc.3
- AWS CDK v2.24.1
devcontainer定義を確認する
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/javascript-node
{
"name": "Node.js",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local arm64/Apple Silicon.
"args": { "VARIANT": "18-bullseye" }
},
// Set *default* container specific settings.json values on container create.
"settings": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"Vue.volar"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"features": {
"aws-cli": "latest"
}
}
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/javascript-node/.devcontainer/base.Dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT="18-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node modules
RUN su node -c "npm install -g aws-cdk"
Nuxt3アプリケーションを作成
以下の手順に沿って実施します。
次のコマンドを実行します。
npx nuxi init nuxt-app
このように最低限の構成がすぐに出来上がります。
まずは実行してみましょう
yarn install # 初回のみ実行します
yarn dev
http://localhost:3000/ にアクセスすることで次のような画面が確認できるはずです。
ここからSSRを確認できるよう簡単なアプリケーションにしていきます。
CSSフレームワーク導入
少しだけ見栄えを整えます。
こうした検証タイミングで簡単に扱えてオススメなのがpicocssです。
軽量かつほとんどクラスを書かずにスタイリングでき、HTMLを見るだけで直感的に分かるためオススメです。
picocssをインストールしてプラグインフォルダを作成します。
yarn add -D @picocss/pico
mkdir plugins
picocssを読み込むプラグインを作成します。
import "@picocss/pico/css/pico.min.css";
export default () => {};
アプリケーションページ作成
SSRを確認するためにトップページと動的ページを作成します。
まず、最初から用意されているapp.vue
を削除してpages
ディレクトリを作成します。
rm app.vue
mkdir pages
静的なTOPページを作成します。
<template>
<h1>This is example</h1>
<div>Please access https://your-domain.com/{anime-title}</div>
</template>
動的ページを作成します。
今回はURLパスパラメータに応じてアニメを検索できるアプリケーションにしてみます。
<script setup lang="ts">
interface Anime {
anime: string;
character: string;
quote: string;
}
const title = ref(useRoute().params.title);
const fetchAnimes = async () =>
$fetch<Anime[]>("https://animechan.vercel.app/api/quotes/anime", {
params: {
title: title.value,
},
});
const { data: animes } = useAsyncData("animes", () => fetchAnimes());
const search = async () => (animes.value = await fetchAnimes());
</script>
<template>
<main class="container">
<h1>search anime</h1>
<div class="grid">
<input v-model="title" />
<button @click="search">search</button>
</div>
<table>
<tr v-for="anime in animes">
<td>{{ anime.anime }}</td>
<td>{{ anime.character }}</td>
<td>{{ anime.quote }}</td>
</tr>
</table>
</main>
</template>
実行してみましょう。
yarn dev
NARUTOを検索してみます。
http://localhost:3000/naruto へアクセスすると次のような画面になるはずです。
これだけでこのアプリが出来上がるのですから、Nuxt3もPicocssもパワフルなのが良く分かります。
以上でNuxtアプリケーションの作成は完了です。
ここからはLambda@Edgeへのデプロイを設定していきます。
Lambda用にNuxtを設定
nuxt.config.ts
のnitro設定を以下のように変更します。
import { defineNuxtConfig } from "nuxt";
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
+ nitro: {
+ preset: "aws-lambda",
+ },
});
以上です。
ただし、これではLambda@Edgeに対応していません。
そのため後で薄いラッパーを作成してLambda@Edgeに対応させます。
AWS CDKアプリケーションを作成
AWSリソースを定義するためにAWS CDKアプリケーションを初期化します。
mkdir cdk
cd cdk
cdk init app --language=typescript
前項で薄いラッパーを作成すると説明しました。
これはCloudFrontイベントをaws-lambda
プレセットのインターフェースに合うように変換するラッパーです。今後不要になる予定ですが今は作成して保存してください。
import { URLSearchParams } from "url";
import * as nitro from "./index.mjs";
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const queryStringParameters = Object.fromEntries(
new URLSearchParams(request.querystring).entries()
);
const response = await nitro.handler({
path: request.uri,
queryStringParameters,
httpMethod: request.method,
headers: normalizeIncomingHeaders(request.headers),
body: request.body,
});
return {
status: response.statusCode,
headers: normalizeOutgoingHeaders(response.headers),
body: response.body,
};
};
function normalizeIncomingHeaders(headers) {
return Object.fromEntries(
Object.entries(headers).map(([key, keyValues]) => [
key,
keyValues.map((kv) => kv.value).join(","),
])
);
}
function normalizeOutgoingHeaders(headers) {
return Object.fromEntries(
Object.entries(headers).map(([key, values]) => [
key,
values.split(",").map((value) => ({ value })),
])
);
}
次にスタックを定義します。
個々の宣言についてはインラインコメントを参照ください。
import { spawnSync } from "child_process";
import { copyFileSync } from "fs";
import { CfnOutput, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deployment from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
export class CdkStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Nuxtプロジェクトをビルド
spawnSync("yarn", ["build"], {
stdio: "inherit",
cwd: "..",
});
// 先ほど作成したwrapperをNuxtのビルド後のserverフォルダへコピー
copyFileSync("./wrapper.mjs", "../.output/server/wrapper.mjs");
// wrapperをハンドラに設定したLambda@Edge関数を作成
const edgeFunction = new cloudfront.experimental.EdgeFunction(
this,
"EdgeFunction",
{
runtime: lambda.Runtime.NODEJS_16_X,
handler: "wrapper.handler",
code: lambda.Code.fromAsset("../.output/server"),
}
);
// S3とCloudFront Distributionを作成
const bucket = new s3.Bucket(this, "Bucket", {
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
const s3Origin = new origins.S3Origin(bucket);
const distribution = new cloudfront.Distribution(this, "Distribution", {
// デフォルトのパスはLambda@Edgeで処理
defaultBehavior: {
origin: s3Origin,
edgeLambdas: [
{
functionVersion: edgeFunction.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
},
],
},
// _nuxt/以下のパスにアクセスした場合はS3オリジンへアクセス
additionalBehaviors: {
"_nuxt/*": {
origin: s3Origin,
},
// 必要に応じてここにオリジンを追加 (e.g images/*)
},
});
// S3へ静的コンテンツをデプロイ
new s3deployment.BucketDeployment(this, "Deployment", {
sources: [s3deployment.Source.asset("../.output/public")],
destinationBucket: bucket,
distribution,
});
// アクセスできるCloudFrontのURLを出力
new CfnOutput(this, "URL", {
value: `https://${distribution.distributionDomainName}`,
});
}
}
AWS CDKのEdgeFunctionコンストラクトを使用すると自動的にLambda関数をus-east-1
リージョンへデプロイしてくれます。その代わり、オリジナルのスタック自体にはデプロイ先を明示する必要があります。
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { CdkStack } from "../lib/cdk-stack";
const app = new cdk.App();
new CdkStack(app, "CdkStack", {
+ env: {
+ region: "ap-northeast-1",
+ },
/* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work,
* but a single synthesized template can be deployed anywhere. */
/* Uncomment the next line to specialize this stack for the AWS Account
* and Region that are implied by the current CLI configuration. */
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
/* Uncomment the next line if you know exactly what Account and Region you
* want to deploy the stack to. */
// env: { account: '123456789012', region: 'us-east-1' },
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});
デプロイ
CDKのコマンドでデプロイします。
yarn cdk deploy --all
CloudFrontのデプロイは時間がかかります。
初回は15分程度かかるかもしれません。
デプロイ完了後 https://xxxxxx.cloudfront.net/naruto へアクセスしてみてください。
ページコンテンツとjsコンテンツが問題なくダウンロードできています。
また、ページコンテンツもSSRされています。
最後に
以上となります。
今後なくなるであろうwrapperを除くと100行にも満たないコードの変更でLambda@Edgeにデプロイできました。
Lambda@Edgeのプリセットが用意されればwrapperを作成する必要もなくなり、よりスマートなデプロイ方法にできるはずです。
Lambda@EdgeにデプロイすることでエッジロケーションでのSSRによって低レイテンシーなアクセスができてサイトのパフォーマンスも良くなるのではないでしょうか。
Nuxt3最高です🥰
今回紹介したソースコードはこちらに公開しています。
-
ナイトロと読みます、ニトロではありません ↩︎
Discussion