データベース

Amazon DocumentDB

概要

Amazon DocumentDBは、AWSが提供するフルマネージドのMongoDB互換ドキュメントデータベースサービスです。MongoDB APIとの高い互換性を保ちながら、AWS独自の分散ストレージアーキテクチャにより、高い可用性、耐久性、拡張性を実現しています。

詳細

Amazon DocumentDBは2019年にAWSによってリリースされ、MongoDBワークロードをクラウドネイティブに実行するために設計されました。以下の特徴を持ちます。

主要特徴

  • MongoDB互換性: MongoDB 3.6、4.0、5.0 APIとの完全互換
  • 分散アーキテクチャ: 6個のレプリカに自動複製、3つのAZ間での分散
  • 自動スケーリング: ストレージは必要に応じて64TBまで自動拡張
  • 高可用性: 99.99%のSLA、自動フェイルオーバー
  • セキュリティ: VPC分離、暗号化(保存時・転送時)、IAM統合
  • 自動バックアップ: 継続的バックアップと35日間のポイントインタイムリカバリ
  • パフォーマンス: SSDストレージによる高速I/O、読み取りレプリカサポート
  • 運用簡素化: フルマネージドサービス、自動パッチ適用

アーキテクチャ

  • 分離されたコンピュートとストレージ: 独立してスケール可能
  • クラスター構成: 最大16個のインスタンス(1プライマリ、15リードレプリカ)
  • 分散ストレージ: 6-way複製、自動修復
  • 多AZ対応: 高可用性とディザスタリカバリ

対応インスタンスタイプ

  • t3.medium〜r5.24xlarge: 様々なワークロードに対応
  • メモリ最適化: r5, r6gファミリー
  • 汎用: t3, m5ファミリー

メリット・デメリット

メリット

  • MongoDB互換: 既存のMongoDBアプリケーションをそのまま移行可能
  • 運用負荷軽減: フルマネージドでインフラ管理不要
  • 高い可用性: 99.99%のSLA、自動フェイルオーバー
  • 自動スケーリング: ストレージとコンピュートの独立したスケーリング
  • 強固なセキュリティ: AWS IAM、VPC、暗号化機能の統合
  • 自動バックアップ: 継続的バックアップとポイントインタイムリカバリ
  • パフォーマンス: 高速SSDストレージ、読み取りレプリカ
  • AWSエコシステム: 他のAWSサービスとの緊密な統合

デメリット

  • ベンダーロックイン: AWS専用サービス、他クラウドに移行困難
  • コスト: セルフマネージドMongoDBより高額
  • 機能制限: MongoDBの一部機能(Map-Reduce等)未対応
  • リージョン制限: 利用可能リージョンが限定的
  • カスタマイズ制限: インフラ・設定の詳細な制御が困難
  • レイテンシ: MongoDBネイティブより若干のオーバーヘッド

主要リンク

書き方の例

インストール・セットアップ

# AWS CLIインストール・設定
aws configure

# DocumentDBクラスター作成(AWS CLI)
aws docdb create-db-cluster \
  --db-cluster-identifier sample-cluster \
  --engine docdb \
  --master-username username \
  --master-user-password password \
  --vpc-security-group-ids sg-12345678 \
  --db-subnet-group-name sample-subnet-group

# インスタンス作成
aws docdb create-db-instance \
  --db-instance-identifier sample-instance \
  --db-instance-class db.t3.medium \
  --engine docdb \
  --db-cluster-identifier sample-cluster

# 接続文字列の取得
aws docdb describe-db-clusters \
  --db-cluster-identifier sample-cluster \
  --query 'DBClusters[0].Endpoint'

基本操作(CRUD)

// Node.js MongoDB Driver使用
const MongoClient = require('mongodb').MongoClient;

// 接続設定(SSL必須)
const client = new MongoClient('mongodb://username:password@sample-cluster.cluster-123456789.us-east-1.docdb.amazonaws.com:27017/?tls=true&replicaSet=rs0&readPreference=secondaryPreferred', {
  tlsCAFile: 'rds-ca-2019-root.pem', // AWSから取得
  retryWrites: false
});

async function basicOperations() {
  try {
    await client.connect();
    
    const db = client.db('sampledb');
    const collection = db.collection('employees');

    // ドキュメント挿入
    const insertResult = await collection.insertOne({
      name: '田中太郎',
      email: '[email protected]',
      department: 'Engineering',
      salary: 75000,
      joinDate: new Date(),
      skills: ['JavaScript', 'Python', 'AWS']
    });
    console.log('Inserted document:', insertResult.insertedId);

    // 複数ドキュメント挿入
    await collection.insertMany([
      {
        name: '佐藤花子',
        email: '[email protected]',
        department: 'Marketing',
        salary: 65000,
        skills: ['Marketing', 'Analytics']
      },
      {
        name: '鈴木次郎',
        email: '[email protected]',
        department: 'Engineering',
        salary: 80000,
        skills: ['Java', 'Kubernetes']
      }
    ]);

    // ドキュメント検索
    const employees = await collection.find({
      department: 'Engineering'
    }).toArray();
    console.log('Engineering employees:', employees);

    // ドキュメント更新
    await collection.updateOne(
      { email: '[email protected]' },
      { 
        $set: { salary: 82000 },
        $push: { skills: 'Docker' }
      }
    );

    // ドキュメント削除
    await collection.deleteOne({ email: '[email protected]' });

  } finally {
    await client.close();
  }
}

basicOperations().catch(console.error);

データモデリング

// 複雑なドキュメント構造の例
const customerSchema = {
  _id: ObjectId(),
  customerId: 'CUST-12345',
  profile: {
    firstName: '田中',
    lastName: '太郎',
    email: '[email protected]',
    phone: '+81-90-1234-5678',
    address: {
      zipCode: '100-0001',
      prefecture: '東京都',
      city: '千代田区',
      street: '千代田1-1-1'
    }
  },
  orders: [
    {
      orderId: 'ORD-001',
      date: new Date('2024-01-15'),
      items: [
        {
          productId: 'PROD-A',
          name: 'Laptop',
          quantity: 1,
          price: 150000
        }
      ],
      total: 150000,
      status: 'shipped'
    }
  ],
  preferences: {
    language: 'ja',
    currency: 'JPY',
    notifications: {
      email: true,
      sms: false
    }
  },
  metadata: {
    createdAt: new Date(),
    updatedAt: new Date(),
    source: 'web'
  }
};

// インデックス作成例
async function createIndexes() {
  const collection = db.collection('customers');
  
  // 単一フィールドインデックス
  await collection.createIndex({ 'profile.email': 1 });
  
  // 複合インデックス
  await collection.createIndex({ 
    'customerId': 1, 
    'orders.date': -1 
  });
  
  // テキストインデックス
  await collection.createIndex({
    'profile.firstName': 'text',
    'profile.lastName': 'text'
  });
  
  // 部分インデックス
  await collection.createIndex(
    { 'orders.status': 1 },
    { partialFilterExpression: { 'orders.status': { $ne: 'cancelled' } } }
  );
}

インデックス・最適化

// インデックス戦略
async function indexStrategy() {
  const collection = db.collection('products');
  
  // 効率的なクエリのためのインデックス
  await collection.createIndex({ category: 1, price: -1 });
  await collection.createIndex({ 'tags': 1 });
  await collection.createIndex({ 'location.coordinates': '2dsphere' });
  
  // インデックス使用状況の確認
  const stats = await collection.aggregate([
    { $indexStats: {} }
  ]).toArray();
  console.log('Index usage stats:', stats);
  
  // クエリプランの確認
  const explainResult = await collection.find({
    category: 'electronics',
    price: { $gte: 100 }
  }).explain('executionStats');
  console.log('Query execution plan:', explainResult);
}

// 集約パイプライン(Analytics)
async function analyticsQueries() {
  const orders = db.collection('orders');
  
  // 売上レポート
  const salesReport = await orders.aggregate([
    {
      $match: {
        orderDate: {
          $gte: new Date('2024-01-01'),
          $lt: new Date('2024-02-01')
        }
      }
    },
    {
      $group: {
        _id: '$customerId',
        totalAmount: { $sum: '$total' },
        orderCount: { $sum: 1 },
        avgOrderValue: { $avg: '$total' }
      }
    },
    {
      $sort: { totalAmount: -1 }
    },
    {
      $limit: 100
    }
  ]).toArray();
  
  return salesReport;
}

実用例

// 本番環境での設定例
const productionConfig = {
  // 接続プール設定
  maxPoolSize: 50,
  minPoolSize: 5,
  connectTimeoutMS: 30000,
  socketTimeoutMS: 30000,
  
  // 読み取り設定
  readPreference: 'secondaryPreferred',
  readConcern: { level: 'majority' },
  
  // 書き込み設定
  writeConcern: { w: 'majority', j: true },
  
  // SSL/TLS設定
  tls: true,
  tlsCAFile: './rds-ca-2019-root.pem',
  tlsAllowInvalidHostnames: false,
  
  // 再試行設定
  retryWrites: false, // DocumentDBでは無効
  retryReads: true
};

// エラーハンドリングとリトライ
async function robustOperation(collection, operation) {
  const maxRetries = 3;
  let retries = 0;
  
  while (retries < maxRetries) {
    try {
      return await operation(collection);
    } catch (error) {
      retries++;
      
      if (retries >= maxRetries) {
        throw error;
      }
      
      // 一時的なエラーの場合のみリトライ
      if (error.code === 11000 || // 重複キーエラー
          error.code === 16500 || // シャーディングエラー
          error.message.includes('connection')) {
        await new Promise(resolve => setTimeout(resolve, 1000 * retries));
        continue;
      }
      
      throw error; // リトライしない
    }
  }
}

// CloudWatchメトリクス監視用の接続状態チェック
async function healthCheck() {
  try {
    await client.db('admin').command({ ismaster: 1 });
    return { status: 'healthy', timestamp: new Date() };
  } catch (error) {
    return { 
      status: 'unhealthy', 
      error: error.message, 
      timestamp: new Date() 
    };
  }
}

ベストプラクティス

// セキュリティのベストプラクティス
const securityConfig = {
  // IAM認証使用(推奨)
  authMechanism: 'MONGODB-AWS',
  authSource: '$external',
  
  // TLS設定
  tls: true,
  tlsCAFile: 'rds-ca-2019-root.pem',
  tlsCertificateKeyFile: 'client.pem'
};

// パフォーマンス最適化
class DocumentDBOptimizer {
  constructor(client) {
    this.client = client;
  }
  
  // バッチ処理
  async batchInsert(collection, documents, batchSize = 1000) {
    const batches = [];
    for (let i = 0; i < documents.length; i += batchSize) {
      batches.push(documents.slice(i, i + batchSize));
    }
    
    const results = [];
    for (const batch of batches) {
      const result = await collection.insertMany(batch, { ordered: false });
      results.push(result);
    }
    
    return results;
  }
  
  // 効率的なページネーション
  async paginateWithCursor(collection, query, limit = 20, lastId = null) {
    const pipeline = [
      { $match: query }
    ];
    
    if (lastId) {
      pipeline[0].$match._id = { $gt: ObjectId(lastId) };
    }
    
    pipeline.push(
      { $sort: { _id: 1 } },
      { $limit: limit + 1 }
    );
    
    const results = await collection.aggregate(pipeline).toArray();
    const hasMore = results.length > limit;
    
    if (hasMore) {
      results.pop();
    }
    
    return {
      data: results,
      hasMore,
      nextCursor: hasMore ? results[results.length - 1]._id : null
    };
  }
}

// 監視とログ設定
const logging = {
  // 接続イベント
  client.on('serverOpening', () => {
    console.log('Connected to DocumentDB');
  });
  
  client.on('serverClosed', () => {
    console.log('Disconnected from DocumentDB');
  });
  
  // パフォーマンス監視
  client.on('commandStarted', (event) => {
    console.log('Command started:', event.commandName);
  });
  
  client.on('commandSucceeded', (event) => {
    console.log(`Command ${event.commandName} succeeded in ${event.duration}ms`);
  });
  
  client.on('commandFailed', (event) => {
    console.error(`Command ${event.commandName} failed:`, event.failure);
  });
};