Nuxt3をLambdaで超高速にSSR!サーバーサイドレンダリングをサーバーレスでコスパよく行う方法😎

Nuxt3をLambdaで超高速にSSR!サーバーサイドレンダリングをサーバーレスでコスパよく行う方法😎

こんにちは!

NuxtJS や NextJS での開発事例がここ最近では急激に増えてきました。ここ 1 年でフロントエンド界隈のフレームワーク宗教論が少し落ち着いてきたかなと感じます。(私は個人的には HTML ライクに書ける NuxtJS 派です)

本記事では、AWS サーバーレスで実現できる NuxtJS のサーバーサイドレンダリングテクニックを紹介します。

想定する読者

  • NuxtJS や NextJS で開発しているフロントエンドエンジニア
  • AWS と WEB フロントエンドのフルスタックエンジニア
  • コスパ良くサーバーサイドレンダリングする方法を探しているヒト

はじめに

本記事のテクニックは、NuxtJS でも NextJS でも使用可能です。

お好きなフレームワークでデプロイして SSR 動作をお試しください!

APIGateway と Lambda で SSR 環境を構築

SSR 環境の構築には、APIGateway と Lambda を使用します。APIGateway で WEB ブラウザの表示要求を受信し、Lambda で NuxtJS をレンダリング → DOM をレスポンスするような流れとなります。

それでは必要なパッケージのインストールから、 serverlessFramework の実装方法を解説します。

Lambda Layer のパッケージのインストール

SSR を高速に完了させるためには、Lambda Layer の導入は必要不可欠です。node_modules は Layer に格納しましょう。 serverless-layers をプロジェクトにインストールします。

$ yarn add serverless-layers -D
# or
$ serverless plugin install --name serverless-layers

後述で serverless-layers プラグインの設定を行います。

API Gateway へバイナリサポートを設定

APIGateway で画像等のファイルを扱う ( バイナリーサポート ) 為に、以下のプラグインをインストールしてください。

yarn add serverless-apigw-binary -D

Serverless Framework の実装 & プラグイン の設定

Serverless Framework を使用して、関連する AWS リソースを構築していきます。本記事では、 Serverless Framework の提供する API Gateway や Lambda を数行で構築できる便利な機能を使用します。(他にも、 Serverless Framework ではサーバーレス系のリソースを簡単に立ち上げる機能が用意されていますので気になるヒトは公式サイトをご覧ください)

provider:
  runtime: nodejs18.x
  deploymentBucket:
    name: "${self:custom.deploymentBucketName}"

functions:
  nuxt:
    # NuxtJSのビルド結果のパスを指定
    handler: .output/server/index.handler
    # eventsには全てのリクエストをNuxtJSへ流すように設定(場合によってはGETに限定してもOK)
    events:
      - http: ANY /
      - http: ANY /{proxy+}

plugins:
  - serverless-apigw-binary
  - serverless-layers

custom:
  deploymentBucketName: "layer-bucket-name"
  apigwBinary:
    types:
      # 全てのバイナリーをサポート
      - "*/*"
  webpack:
    packager: "yarn"
  serverless-layers:
    packageManager: "yarn"

package:
  # NuxtJSのServerモジュールをパッケージに含める
  patterns:
    - "!**/**"
    - ".output/server/**"
  excludeDevDependencies: true
  individually: true

※ 関連しない部分のソースコードを省略しています

Lambda Layer 用の S3 バケットのデプロイ

serverless-layers プラグインで使用するバケットを生成します。バケットは CloudFormation で作成し GetAtt 等で ARN や名称を参照したいところですが、技術検証したところ自動生成される CloudFormation が原因で参照できませんでした。そのため、事前に AWS CLI でバケットを作成しておきます。

$ aws s3 mb "s3://layer-bucket-name"

NuxtJSの設定

Lambda デプロイの為に、NuxtJSへ以下設定を行います。以下は nuxt.config.ts の例です。

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,
  nitro: {
    preset: "aws-lambda",
    serveStatic: false, // 画像はCloudFrontによるCDN提供を見据えてfalse
  },
});

NuxtJS では Lambda での SSR を見越して上記のような設定が提供されています。

デプロイ

必要な設定さえ終われば、デプロイは至ってシンプルです。

$ yarn install
$ nuxt build
$ npx sls deploy

デプロイが完了したらAPIGatewayのエンドポイントを表示してみてください。SSR結果のDOMが表示されるはずです。

TIPS

画像データはLambdaで管理しないことを推奨

通常のSSR構成のままでは、Lambda が画像データを全て扱う構成となってしまい、以下のような懸念があります。

  • Lambda の実行コスト
  • Lambda のスロットリングリスク
  • Lambda のファイルサイズ増加によるコールドスタート

画像やその他アセットの参照のたびに Lambda を参照するのはコスパが悪いです。また、せっかく node_modules を Layer 化したのに、バイナリーデータが Lambda に含まれると Layer 化した効果を最大限に発揮できません。

そのため、画像データ等々の静的アセットは、CloudFrontとS3等の構成へ逃しましょう。

以下は、画像データ等々の CloudFront と S3 のアセットを扱う例です。

Resources:
  AssetsHostingCloudFront:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        # General
        Comment: "AssetsHostingCloudFront"
        Enabled: true
        HttpVersion: http2
        PriceClass: PriceClass_All
        Aliases:
          - "{your custom domain}"
        ViewerCertificate:
          MinimumProtocolVersion: TLSv1.2_2019
          AcmCertificateArn: "{your ACM arn}"
          SslSupportMethod: sni-only
        Origins:
          - DomainName: '{your bucket name}.s3-${self:provider.region}.amazonaws.com'
            Id: 'S3-xxxx-xxxx'
            S3OriginConfig:
              OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${AssetsHostingOriginAccessIdentity}
            ConnectionAttempts: 3
            ConnectionTimeout: 10
        # Behaviors
        DefaultCacheBehavior:
          TargetOriginId: 'S3-xxxx-xxxx'
          ViewerProtocolPolicy: allow-all
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
          Compress: true
        # Error Pages
        CustomErrorResponses:
          - ErrorCode: 403
            ErrorCachingMinTTL: 10
            ResponsePagePath: "/"
            ResponseCode: 200
        # Restrictions
        Restrictions:
          GeoRestriction:
            RestrictionType: none

  AssetsHostingOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: "AssetsHostingOriginAccessIdentity"

  AssetsHostingBucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: 'your bucket name'
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  AssetsHostingBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AssetsHostingBucket
      PolicyDocument:
        Id: PolicyForCloudFrontPrivateContent
        Version: "2012-10-17"
        Statement:
          - Sid: "1"
            Effect: Allow
            Action: s3:GetObject
            Resource: 'your bucket arn'
            Principal:
              CanonicalUser: !GetAtt AssetsHostingOriginAccessIdentity.S3CanonicalUserId

画像を上記のS3へ同期する方法については、以下のいずれかの案を検討してみてください。

  • CodeCommit で静的アセットを別途保持しCodePipeLine で CI.CD 管理
  • CodePipeLine を使用し buildspec.yml の中に S3 同期処理を設定
  • 開発後にその都度 AWS CLI を使用し S3 へファイル同期

画像データをCloudFrontとS3構成へ逃した後は、フロントエンドの画像参照パスの変更も忘れずに行ってください。

まとめ

本記事では、お客様からも良く要望のある NuxtJS の SSR 方法について解説しました。Vercel 等々を使用してもほぼほぼ同じことが実現できますが、コスト面及び性能面で本記事のほうが圧倒的に良いです。開発環境を AWS 内に閉じれるのも魅力ですね。

AWSサーバーレスフロントエンドの開発はお気軽にお問い合わせください。