Documentation Next.js

はじめに

Next.jsアプリケーションを本番環境で運用する際、バックアップと復旧戦略は最も重要な要素の一つです。データの紛失やシステム障害は、ビジネスに深刻な影響を与える可能性があります。

この記事では、Next.jsアプリケーションのバックアップと復旧について、実践的なコード例を交えながら解説します。

バックアップ対象の分類

Next.jsアプリケーションでバックアップすべきデータは、主に以下の3つに分類されます。

分類具体例バックアップ頻度
データベースMongoDB、PostgreSQL、MySQLのデータ日次〜リアルタイム
静的ファイル画像、PDF、アップロードファイル変更時
設定・環境.env、設定ファイル、シークレット変更時

MongoDBのバックアップ

基本的なバックアップコマンド

MongoDBではmongodumpコマンドを使用してバックアップを取得します。

# 基本的なバックアップ
mongodump --uri="mongodb+srv://<username>:<password>@cluster0.mongodb.net/mydb" --out=/backup/$(date +%Y%m%d)

# 特定のコレクションのみバックアップ
mongodump --uri="mongodb+srv://<username>:<password>@cluster0.mongodb.net/mydb" \
  --collection=users \
  --out=/backup/users_$(date +%Y%m%d)

# 圧縮してバックアップ(推奨)
mongodump --uri="mongodb+srv://<username>:<password>@cluster0.mongodb.net/mydb" \
  --gzip \
  --archive=/backup/mydb_$(date +%Y%m%d).gz

Node.jsでのバックアップ自動化

Next.jsのAPIルートやスクリプトからバックアップを自動化する例です。

// scripts/backup-mongodb.ts
import { exec } from 'child_process';
import { promisify } from 'util';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { readFileSync } from 'fs';

const execAsync = promisify(exec);

// S3クライアントの設定
const s3Client = new S3Client({
  region: process.env.AWS_REGION || 'ap-northeast-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

// バックアップ関数
async function backupMongoDB(): Promise<string> {
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const backupPath = `/tmp/backup_${timestamp}.gz`;

  // mongodumpでバックアップを作成
  const mongoUri = process.env.MONGODB_URI!;
  await execAsync(
    `mongodump --uri="${mongoUri}" --gzip --archive=${backupPath}`
  );

  return backupPath;
}

// S3にアップロード
async function uploadToS3(filePath: string): Promise<void> {
  const fileName = filePath.split('/').pop()!;
  const fileContent = readFileSync(filePath);

  await s3Client.send(
    new PutObjectCommand({
      Bucket: process.env.S3_BACKUP_BUCKET!,
      Key: `mongodb-backups/${fileName}`,
      Body: fileContent,
      ContentType: 'application/gzip',
      // 暗号化を有効にする
      ServerSideEncryption: 'AES256',
    })
  );

  console.log(`Backup uploaded: ${fileName}`);
}

// メイン処理
async function main(): Promise<void> {
  try {
    console.log('Starting MongoDB backup...');
    const backupPath = await backupMongoDB();

    console.log('Uploading to S3...');
    await uploadToS3(backupPath);

    console.log('Backup completed successfully!');
  } catch (error) {
    console.error('Backup failed:', error);
    process.exit(1);
  }
}

main();

PostgreSQLのバックアップ

Prismaを使用している場合のPostgreSQLバックアップ方法です。

// scripts/backup-postgres.ts
import { exec } from 'child_process';
import { promisify } from 'util';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { createReadStream } from 'fs';

const execAsync = promisify(exec);

async function backupPostgres(): Promise<string> {
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const backupPath = `/tmp/postgres_backup_${timestamp}.sql.gz`;

  // pg_dumpでバックアップを作成
  const dbUrl = new URL(process.env.DATABASE_URL!);
  const { hostname, port, pathname, username, password } = dbUrl;
  const database = pathname.slice(1); // 先頭の / を削除

  await execAsync(
    `PGPASSWORD="${password}" pg_dump \
      -h ${hostname} \
      -p ${port || 5432} \
      -U ${username} \
      -d ${database} \
      --format=custom \
      --compress=9 \
      -f ${backupPath}`
  );

  return backupPath;
}

// 日次バックアップのスケジュール例(Cronジョブ用)
// 0 3 * * * node scripts/backup-postgres.js

export { backupPostgres };

AWS S3を使ったファイルバックアップ

静的ファイルのバックアップ

// lib/backup/s3-sync.ts
import { S3Client, PutObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
import { readdirSync, statSync, readFileSync } from 'fs';
import { join, relative } from 'path';
import { lookup } from 'mime-types';

const s3Client = new S3Client({
  region: process.env.AWS_REGION!,
});

// 再帰的にファイルを取得
function getAllFiles(dir: string, files: string[] = []): string[] {
  const entries = readdirSync(dir);

  for (const entry of entries) {
    const fullPath = join(dir, entry);
    if (statSync(fullPath).isDirectory()) {
      getAllFiles(fullPath, files);
    } else {
      files.push(fullPath);
    }
  }

  return files;
}

// ディレクトリをS3に同期
async function syncToS3(
  localDir: string,
  bucketName: string,
  s3Prefix: string = ''
): Promise<void> {
  const files = getAllFiles(localDir);

  console.log(`Syncing ${files.length} files to S3...`);

  const uploadPromises = files.map(async (filePath) => {
    const relativePath = relative(localDir, filePath);
    const s3Key = s3Prefix ? `${s3Prefix}/${relativePath}` : relativePath;
    const contentType = lookup(filePath) || 'application/octet-stream';

    await s3Client.send(
      new PutObjectCommand({
        Bucket: bucketName,
        Key: s3Key,
        Body: readFileSync(filePath),
        ContentType: contentType,
      })
    );

    console.log(`Uploaded: ${s3Key}`);
  });

  await Promise.all(uploadPromises);
  console.log('Sync completed!');
}

// 使用例: ビルド成果物のバックアップ
async function backupBuild(): Promise<void> {
  const timestamp = new Date().toISOString().split('T')[0];
  await syncToS3(
    './out',
    process.env.S3_BACKUP_BUCKET!,
    `builds/${timestamp}`
  );
}

export { syncToS3, backupBuild };

AWS Lambdaでの自動バックアップ

定期的なバックアップを自動化するLambda関数の例です。

// lambda/scheduled-backup.ts
import { Handler, ScheduledEvent } from 'aws-lambda';
import { MongoClient } from 'mongodb';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const s3Client = new S3Client({ region: process.env.AWS_REGION });

export const handler: Handler<ScheduledEvent> = async (event) => {
  const timestamp = new Date().toISOString();
  console.log(`Starting scheduled backup at ${timestamp}`);

  try {
    // MongoDBに接続してデータを取得
    const client = await MongoClient.connect(process.env.MONGODB_URI!);
    const db = client.db();

    // 各コレクションのデータをバックアップ
    const collections = await db.listCollections().toArray();

    for (const collection of collections) {
      const data = await db.collection(collection.name).find({}).toArray();
      const backupData = JSON.stringify(data, null, 2);

      await s3Client.send(
        new PutObjectCommand({
          Bucket: process.env.BACKUP_BUCKET!,
          Key: `backups/${timestamp}/${collection.name}.json`,
          Body: backupData,
          ContentType: 'application/json',
          ServerSideEncryption: 'AES256',
        })
      );

      console.log(`Backed up collection: ${collection.name}`);
    }

    await client.close();

    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'Backup completed successfully',
        timestamp,
        collectionsCount: collections.length,
      }),
    };
  } catch (error) {
    console.error('Backup failed:', error);
    throw error;
  }
};

CloudFormationでのスケジュール設定

# serverless.yml または SAM template
Resources:
  BackupFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: nextjs-scheduled-backup
      Runtime: nodejs18.x
      Handler: scheduled-backup.handler
      Timeout: 300
      Environment:
        Variables:
          MONGODB_URI: !Ref MongoDBUri
          BACKUP_BUCKET: !Ref BackupBucket

  BackupSchedule:
    Type: AWS::Events::Rule
    Properties:
      Name: daily-backup-schedule
      Description: "毎日午前3時にバックアップを実行"
      ScheduleExpression: "cron(0 3 * * ? *)"
      State: ENABLED
      Targets:
        - Id: backup-function
          Arn: !GetAtt BackupFunction.Arn

バックアップからの復旧

MongoDBの復旧

# ローカルバックアップからの復旧
mongorestore --uri="mongodb+srv://<username>:<password>@cluster0.mongodb.net/mydb" \
  --gzip \
  --archive=/backup/mydb_20240101.gz

# 特定のコレクションのみ復旧
mongorestore --uri="mongodb+srv://<username>:<password>@cluster0.mongodb.net/mydb" \
  --gzip \
  --archive=/backup/mydb_20240101.gz \
  --nsInclude="mydb.users"

# ドロップしてから復旧(データを完全に置き換える)
mongorestore --uri="mongodb+srv://<username>:<password>@cluster0.mongodb.net/mydb" \
  --gzip \
  --archive=/backup/mydb_20240101.gz \
  --drop

Node.jsでの復旧スクリプト

// scripts/restore-mongodb.ts
import { exec } from 'child_process';
import { promisify } from 'util';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { writeFileSync } from 'fs';
import { Readable } from 'stream';

const execAsync = promisify(exec);

const s3Client = new S3Client({
  region: process.env.AWS_REGION!,
});

// S3からバックアップをダウンロード
async function downloadFromS3(s3Key: string): Promise<string> {
  const localPath = `/tmp/${s3Key.split('/').pop()}`;

  const response = await s3Client.send(
    new GetObjectCommand({
      Bucket: process.env.S3_BACKUP_BUCKET!,
      Key: s3Key,
    })
  );

  const stream = response.Body as Readable;
  const chunks: Buffer[] = [];

  for await (const chunk of stream) {
    chunks.push(chunk as Buffer);
  }

  writeFileSync(localPath, Buffer.concat(chunks));
  return localPath;
}

// MongoDBを復旧
async function restoreMongoDB(backupPath: string): Promise<void> {
  const mongoUri = process.env.MONGODB_URI!;

  console.log('Restoring MongoDB from backup...');

  await execAsync(
    `mongorestore --uri="${mongoUri}" --gzip --archive=${backupPath} --drop`
  );

  console.log('Restore completed!');
}

// メイン処理
async function main(): Promise<void> {
  const s3Key = process.argv[2];

  if (!s3Key) {
    console.error('Usage: ts-node restore-mongodb.ts <s3-key>');
    process.exit(1);
  }

  try {
    console.log(`Downloading backup: ${s3Key}`);
    const localPath = await downloadFromS3(s3Key);

    await restoreMongoDB(localPath);
  } catch (error) {
    console.error('Restore failed:', error);
    process.exit(1);
  }
}

main();

S3ライフサイクルポリシー

古いバックアップを自動的に削除するためのライフサイクルポリシーです。

{
  "Rules": [
    {
      "ID": "DeleteOldBackups",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "mongodb-backups/"
      },
      "Expiration": {
        "Days": 30
      }
    },
    {
      "ID": "MoveToGlacier",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "mongodb-backups/"
      },
      "Transitions": [
        {
          "Days": 7,
          "StorageClass": "GLACIER"
        }
      ]
    }
  ]
}

バックアップのセキュリティ対策

暗号化の設定

// バックアップ時の暗号化設定
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const uploadWithEncryption = async (
  bucket: string,
  key: string,
  body: Buffer
): Promise<void> => {
  await s3Client.send(
    new PutObjectCommand({
      Bucket: bucket,
      Key: key,
      Body: body,
      // SSE-S3による暗号化
      ServerSideEncryption: 'AES256',
      // または KMS による暗号化
      // ServerSideEncryption: 'aws:kms',
      // SSEKMSKeyId: 'alias/backup-key',
    })
  );
};

アクセス制御ポリシー

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowBackupOperations",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-backup-bucket",
        "arn:aws:s3:::my-backup-bucket/*"
      ],
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": ["203.0.113.0/24"]
        }
      }
    }
  ]
}

バックアップ検証の自動化

バックアップが正常に復旧できるか定期的に検証することが重要です。

// scripts/verify-backup.ts
import { MongoClient } from 'mongodb';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

async function verifyBackup(backupPath: string): Promise<boolean> {
  const testDbUri = process.env.TEST_MONGODB_URI!;

  try {
    // テスト用DBに復旧
    await execAsync(
      `mongorestore --uri="${testDbUri}" --gzip --archive=${backupPath} --drop`
    );

    // 接続テスト
    const client = await MongoClient.connect(testDbUri);
    const db = client.db();

    // コレクション数の確認
    const collections = await db.listCollections().toArray();
    console.log(`Verified ${collections.length} collections`);

    // サンプルデータの確認
    for (const collection of collections) {
      const count = await db.collection(collection.name).countDocuments();
      console.log(`  ${collection.name}: ${count} documents`);
    }

    await client.close();
    return true;
  } catch (error) {
    console.error('Backup verification failed:', error);
    return false;
  }
}

export { verifyBackup };

まとめ

Next.jsアプリケーションのバックアップと復旧戦略のポイントをまとめます。

  • 定期的なバックアップ: データベースは日次、重要なデータはリアルタイムでバックアップ
  • 自動化: AWS LambdaやCronジョブを使って手動作業を排除
  • 暗号化: S3の暗号化機能を活用してデータを保護
  • 検証: バックアップが正常に復旧できるか定期的にテスト
  • ライフサイクル管理: 古いバックアップを自動的に削除してコストを最適化

これらの戦略を実装することで、万が一の障害時にも迅速にシステムを復旧できる体制を構築できます。

参考文献

円