Documentation Next.js

はじめに

この記事では、Next.jsとAWS Lambdaを組み合わせたサーバーレスアーキテクチャの構築方法を解説します。サーバーレスアーキテクチャを採用することで、インフラ管理の負担を軽減しながら、高いスケーラビリティとコスト効率を実現できます。

具体的には、以下の内容を学べます。

  • サーバーレスアーキテクチャの基本概念と仕組み
  • Next.jsでのサーバーレスAPIルートの作成方法
  • Serverless Frameworkを使ったAWSへのデプロイ手順
  • 実践的なユースケースと最適化テクニック
  • サーバーレス特有の課題と対策

サーバーレスアーキテクチャとは

基本概念

サーバーレスアーキテクチャとは、開発者がサーバーの管理を意識せずにアプリケーションを構築・実行できるクラウドコンピューティングモデルです。「サーバーレス」という名前ですが、実際にはサーバーは存在します。ただし、そのプロビジョニング、スケーリング、管理はすべてクラウドプロバイダーが担当します。

従来のアーキテクチャとの違い

項目従来のサーバーサーバーレス
サーバー管理自分で管理プロバイダーが管理
スケーリング手動設定が必要自動スケーリング
課金モデル常時稼働分を支払いリクエスト単位で支払い
コールドスタートなしあり(初回起動時の遅延)

Next.jsとサーバーレスの相性

Next.jsは、サーバーレスアーキテクチャとの統合に最適化されたフレームワークです。API Routesやサーバーサイドレンダリング(SSR)の機能を、AWS Lambdaなどのサーバーレス環境で効率的に動作させることができます。

サーバーレスAPIルートの作成

基本的なAPIルート

Next.jsでは、pages/apiディレクトリ(Pages Router)またはapp/apiディレクトリ(App Router)にAPIルートを定義できます。これらのAPIルートは、サーバーレス関数として自動的にデプロイされます。

// pages/api/hello.js
// シンプルなGETリクエストを処理するAPIルート

export default function handler(req, res) {
  // HTTPメソッドに応じて処理を分岐
  if (req.method === 'GET') {
    // 成功レスポンスを返す
    res.status(200).json({
      message: 'Hello, World!',
      timestamp: new Date().toISOString()
    });
  } else {
    // GETメソッド以外は405エラーを返す
    res.setHeader('Allow', ['GET']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

この例では、/api/helloにアクセスするとサーバーレスAPIルートが実行されます。

CRUD操作を含む実践的なAPIルート

実際のアプリケーションでは、データベースと連携したCRUD操作が必要になります。以下は、ユーザー情報を管理するAPIルートの例です。

// pages/api/users/[id].js
// 動的ルーティングを使用したユーザーAPI

// データベースクライアント(例:Prisma)
import { prisma } from '../../../lib/prisma';

export default async function handler(req, res) {
  // URLパラメータからユーザーIDを取得
  const { id } = req.query;

  try {
    switch (req.method) {
      case 'GET':
        // ユーザー情報の取得
        const user = await prisma.user.findUnique({
          where: { id: parseInt(id) },
          select: {
            id: true,
            name: true,
            email: true,
            createdAt: true
          }
        });

        if (!user) {
          return res.status(404).json({ error: 'User not found' });
        }

        return res.status(200).json(user);

      case 'PUT':
        // ユーザー情報の更新
        const updatedUser = await prisma.user.update({
          where: { id: parseInt(id) },
          data: req.body
        });

        return res.status(200).json(updatedUser);

      case 'DELETE':
        // ユーザーの削除
        await prisma.user.delete({
          where: { id: parseInt(id) }
        });

        return res.status(204).end();

      default:
        // サポートされていないメソッド
        res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
        return res.status(405).end(`Method ${req.method} Not Allowed`);
    }
  } catch (error) {
    console.error('API Error:', error);
    return res.status(500).json({ error: 'Internal Server Error' });
  }
}

App Routerでのルートハンドラー

Next.js 13以降のApp Routerを使用する場合は、以下のようにルートハンドラーを定義します。

// app/api/users/route.ts
// App Routerを使用したAPIルート

import { NextRequest, NextResponse } from 'next/server';

// ユーザー一覧の取得
export async function GET(request: NextRequest) {
  // クエリパラメータの取得
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '10');

  // ページネーション付きでユーザーを取得
  const users = await fetchUsers({ page, limit });

  return NextResponse.json({
    users,
    pagination: {
      page,
      limit,
      total: users.length
    }
  });
}

// 新規ユーザーの作成
export async function POST(request: NextRequest) {
  try {
    // リクエストボディを取得
    const body = await request.json();

    // バリデーション
    if (!body.name || !body.email) {
      return NextResponse.json(
        { error: 'Name and email are required' },
        { status: 400 }
      );
    }

    // ユーザーを作成
    const newUser = await createUser(body);

    return NextResponse.json(newUser, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create user' },
      { status: 500 }
    );
  }
}

AWS Lambdaを使ったデプロイ

Serverless Frameworkの導入

AWS LambdaにNext.jsアプリケーションをデプロイするには、Serverless Frameworkと@sls-next/serverless-componentを使用します。このコンポーネントは、AWS Lambda@EdgeとCloudFrontを組み合わせて、静的ファイルと動的リクエストの両方をサーバーレスで処理します。

まず、Serverless Frameworkをインストールします。

# Serverless Frameworkのグローバルインストール
npm install -g serverless

# プロジェクトでの設定ファイル作成
touch serverless.yml

serverless.ymlの設定

serverless.ymlファイルでデプロイ設定を定義します。

# serverless.yml
# Next.jsアプリケーションのAWSデプロイ設定

myNextApplication:
  component: "@sls-next/serverless-component@3.7.0"
  inputs:
    # ビルド設定
    build:
      # 本番環境用の最適化を有効化
      env:
        NODE_ENV: production
      # ビルドコマンドのカスタマイズ
      cmd: npm run build

    # Lambda@Edge設定
    cloudfront:
      # カスタムドメインの設定(オプション)
      aliases:
        - "example.com"
        - "www.example.com"
      # SSL証明書のARN
      certificate:
        arn: "arn:aws:acm:us-east-1:xxx:certificate/xxx"
      # キャッシュ設定
      defaults:
        ttl: 0
        maxTtl: 86400
        minTtl: 0

    # S3バケット設定
    bucketName: "my-nextjs-app-bucket"
    bucketRegion: "ap-northeast-1"

    # Lambda設定
    memory: 1024  # メモリサイズ(MB)
    timeout: 30   # タイムアウト(秒)

デプロイの実行

設定が完了したら、以下のコマンドでデプロイを実行します。

# AWSにデプロイ
serverless deploy

# 特定のステージにデプロイ
serverless deploy --stage production

# デプロイ状態の確認
serverless info

デプロイが完了すると、CloudFrontのURLが表示されます。このURLを使用してアプリケーションにアクセスできます。

静的ファイルの管理とCDN配信

S3とCloudFrontの連携

Next.jsのサーバーレスアーキテクチャでは、静的ファイル(画像、CSS、JSなど)はS3に保存され、CloudFrontを通じてキャッシュ・配信されます。この構成により、以下のメリットが得られます。

  • 高速なコンテンツ配信: CloudFrontのエッジロケーションからユーザーに最も近い場所で配信
  • オリジンサーバーの負荷軽減: キャッシュにより、S3やLambdaへのリクエストを削減
  • コスト最適化: データ転送量の削減による料金削減

画像最適化の設定

Next.jsのnext/imageコンポーネントをサーバーレス環境で使用する場合、画像最適化の設定が必要です。

// next.config.js
// サーバーレス環境での画像最適化設定

/** @type {import('next').NextConfig} */
const nextConfig = {
  // 画像最適化の設定
  images: {
    // 外部画像のドメインを許可
    domains: ['example.com', 's3.amazonaws.com'],
    // 画像フォーマットの指定
    formats: ['image/avif', 'image/webp'],
    // サーバーレス環境での最適化を有効化
    unoptimized: false,
    // ローダーの設定(カスタムローダーを使用する場合)
    loader: 'default',
  },
  // サーバーレス対応の設定
  output: 'standalone',
};

module.exports = nextConfig;

サーバーレスアーキテクチャのメリット

自動スケーリング

サーバーレスアーキテクチャの最大の利点は、自動スケーリングです。AWS Lambdaはリクエスト数に応じて自動的にインスタンスを増減させます。

// 大量の同時リクエストを処理する例
// pages/api/process-data.js

export default async function handler(req, res) {
  // Lambda関数は同時に数千のリクエストを処理可能
  // 各リクエストは独立したインスタンスで実行される

  const data = req.body;

  // 重い処理も並列で実行される
  const result = await processHeavyComputation(data);

  res.status(200).json({ result });
}

コスト効率

サーバーレスは使用した分だけ課金されるため、トラフィックが少ない時期のコストを大幅に削減できます。

トラフィック従来のサーバー(月額)サーバーレス(月額)
低(1万リクエスト/月)$50〜100$1未満
中(100万リクエスト/月)$100〜200$10〜20
高(1000万リクエスト/月)$500〜1000$100〜200

運用負担の軽減

サーバーの管理、パッチ適用、セキュリティアップデートなどの作業が不要になり、開発に集中できます。

課題と対策

コールドスタート問題

コールドスタートとは、Lambda関数が一定時間アイドル状態になった後、最初のリクエストで起動に時間がかかる現象です。

// コールドスタート対策の例
// pages/api/warmup.js

// ウォームアップ用のエンドポイント
export default function handler(req, res) {
  // このエンドポイントを定期的に呼び出すことで
  // Lambda関数を「ウォーム」な状態に保つ
  res.status(200).json({ status: 'warm' });
}

CloudWatch Eventsを使用して、定期的にウォームアップリクエストを送信できます。

# serverless.yml にウォームアップ設定を追加

myNextApplication:
  component: "@sls-next/serverless-component@3.7.0"
  inputs:
    # ウォームアップ設定
    warmup:
      enabled: true
      # 5分ごとにウォームアップリクエストを送信
      schedule: "rate(5 minutes)"

実行時間の制限

AWS Lambdaには最大実行時間の制限(15分)があります。長時間実行するタスクには、以下の対策が有効です。

// 長時間タスクを分割して処理する例
// pages/api/batch-process.js

import { SQS } from 'aws-sdk';

const sqs = new SQS();

export default async function handler(req, res) {
  const { items } = req.body;

  // 大量のアイテムをSQSキューに分割して投入
  // 各アイテムは別のLambda関数で非同期処理される
  const promises = items.map(item =>
    sqs.sendMessage({
      QueueUrl: process.env.SQS_QUEUE_URL,
      MessageBody: JSON.stringify(item)
    }).promise()
  );

  await Promise.all(promises);

  res.status(202).json({
    message: 'Processing started',
    itemCount: items.length
  });
}

ベンダーロックイン

特定のクラウドプロバイダーに依存しすぎると、将来的な移行が困難になります。以下のような抽象化レイヤーを設けることで、ベンダーロックインのリスクを軽減できます。

// lib/storage.ts
// ストレージ操作の抽象化レイヤー

interface StorageProvider {
  upload(key: string, data: Buffer): Promise<string>;
  download(key: string): Promise<Buffer>;
  delete(key: string): Promise<void>;
}

// AWS S3の実装
class S3StorageProvider implements StorageProvider {
  async upload(key: string, data: Buffer): Promise<string> {
    // S3にアップロード
    // ...
  }
  // 他のメソッドも同様に実装
}

// Google Cloud Storageの実装
class GCSStorageProvider implements StorageProvider {
  async upload(key: string, data: Buffer): Promise<string> {
    // GCSにアップロード
    // ...
  }
  // 他のメソッドも同様に実装
}

// 環境変数に基づいてプロバイダーを選択
export const storage: StorageProvider =
  process.env.STORAGE_PROVIDER === 'gcs'
    ? new GCSStorageProvider()
    : new S3StorageProvider();

本番環境でのベストプラクティス

環境変数の管理

# .env.production
# 本番環境の環境変数(実際の値はAWS Secrets Managerで管理)

DATABASE_URL=your-database-url
API_KEY=your-api-key
# serverless.yml での環境変数設定

myNextApplication:
  component: "@sls-next/serverless-component@3.7.0"
  inputs:
    build:
      env:
        DATABASE_URL: ${ssm:/myapp/database-url}
        API_KEY: ${ssm:/myapp/api-key}

モニタリングとログ

// lib/logger.js
// 構造化ログの実装

export const logger = {
  info: (message, meta = {}) => {
    console.log(JSON.stringify({
      level: 'info',
      message,
      ...meta,
      timestamp: new Date().toISOString()
    }));
  },
  error: (message, error, meta = {}) => {
    console.error(JSON.stringify({
      level: 'error',
      message,
      error: error.message,
      stack: error.stack,
      ...meta,
      timestamp: new Date().toISOString()
    }));
  }
};

まとめ

Next.jsとサーバーレスアーキテクチャを組み合わせることで、スケーラブルかつコスト効率の高いWebアプリケーションを構築できます。AWS Lambda、S3、CloudFrontを使用し、Serverless Frameworkを活用することで、複雑なインフラ設定を自動化し、開発に集中できる環境を実現できます。

主なポイントをまとめると以下の通りです。

  • 自動スケーリング: トラフィックの増減に自動で対応
  • コスト最適化: 使用した分だけの課金で、低トラフィック時のコストを削減
  • 運用効率: サーバー管理が不要になり、開発に集中可能
  • 課題への対策: コールドスタートやベンダーロックインには適切な対策が必要

サーバーレスアーキテクチャは、すべてのケースに最適というわけではありませんが、適切なユースケースでは大きな効果を発揮します。

参考文献

円