OpenSearch Serverless半年運用で学んだ本当の落とし穴——コスト最適化と実装のコツ

オンプレEsから移行したOpenSearch Serverlessで実際にハマった地雷とその対策。OCU設定ミスからインデックス設計の失敗まで、運用経験から得た教訓を共有します。

Serverlessに移行した理由——自分たちのOpenSearch地獄

去年の秋、うちのチームは約2年間オンプレのElasticsearchを運用していたんです。それが段々キツくなってきた。ノード管理、バージョンアップ、ディスク逼迫への対応……定期的に深夜対応が発生するようになっていました。

特に辛かったのが、スパイク時のメモリリーク。ある日突然、ログが流れ込まなくなって、チーム全体で1時間近く原因特定に追われたことがあります。その時点で「これはもう自分たちで持つのは無理だ」と判断して、OpenSearch Serverlessの導入を決めました。

正直、最初は懐疑的だったんですよ。Serverlessって本当に本番環境で安定するのか、コストは跳ね上がらないのか……いろんな不安がありました。でも実際に6ヶ月運用してみると、思いの外うまく回っています。もちろん地雷もありますけど。

最初の設定で失敗した話——容量単位とインデックス戦略

OpenSearch Serverlessの価格モデルは「OCU(OpenSearch Compute Units)」という容量単位で課金されます。インデックスの書き込み、クエリの実行、スナップショットの保存……それぞれに最小OCUが必要なんです。ここで僕たちは大失敗しました。

最初は最小値で構成してみたんですよ:

項目設定値理由
インデックス書き込み OCU2最小値だから
クエリ実行 OCU2最小値だから
スナップショット OCU1最小値だから
月額コスト約4万5000円

これで「シンプルだし安くできた」と思ってました。でも運用を始まったら、ダッシュボード更新がしょっちゅう失敗するようになった。リクエストが集中した時間帯では、クエリがタイムアウトしまくったわけです。

実は、OCUの最小値は「あくまで最小」。実際の負荷に応じて自動スケーリングするんですが、そのスケーリング幅が実負荷に追いつかないんです。スパイク対応には最小値では足りていなかったんだなって、ここで初めて理解しました。

3週間後に設定を見直して、こう修正しました:

項目旧設定新設定変更理由
インデックス書き込み OCU24スパイク時の書き込み対応
クエリ実行 OCU26ダッシュボード同時アクセス対応
スナップショット OCU12バックアップ処理の安定化
月額コスト約4万5000円約9万8000円

コストは倍近くになりましたが、本番の安定性には代え難いということを学びました。正直、この失敗がなかったら、今ごろまた深夜対応に追われてたと思いますよ。

インデックス設計の現実的なアプローチ

ログを効率的に検索するには、インデックス戦略が超重要です。最初は「すべてのログを1つのインデックスに詰め込もう」なんて甘いことを考えていました。が、これはスケーリングの観点で地獄です。

OpenSearch Serverlessは1インデックスあたり300GBの制限があります。うちは1日のログ量が約50GBなので、6日でいっぱいになります。手作業で古いインデックスを削除するわけにはいきませんし、ILM(Index Lifecycle Management)を使ってインデックスを自動管理することにしました。

from opensearchpy import OpenSearch
from datetime import datetime, timedelta
import os

host = 'xxxxx.aoss.amazonaws.com'
client = OpenSearch(
    hosts=[{'host': host, 'port': 443}],
    http_auth=('admin', os.getenv('OPENSEARCH_PASSWORD')),
    use_ssl=True,
    verify_certs=True,
    connection_class=RequestsHttpConnection
)

# ILMポリシーの設定
ilm_policy = {
    "policy": "logs-ilm-policy",
    "phases": {
        "hot": {
            "min_age": "0d",
            "actions": {
                "rollover": {
                    "max_primary_shard_size": "50GB",
                    "max_age": "1d"
                }
            }
        },
        "warm": {
            "min_age": "7d",
            "actions": {
                "set_priority": {"priority": 50}
            }
        },
        "cold": {
            "min_age": "30d",
            "actions": {
                "set_priority": {"priority": 0}
            }
        },
        "delete": {
            "min_age": "90d",
            "actions": {
                "delete": {}
            }
        }
    }
}

client.transport.perform_request(
    'PUT',
    '/_plugins/_ism/policies/logs-ilm-policy',
    body=ilm_policy
)

print("ILMポリシーを適用しました")

このポリシーで運用すると、こんな流れになるんです:

  • Hot フェーズ(0日目): 書き込み最適化。1日経つか50GBに達したら自動ロールオーバー
  • Warm フェーズ(7日目): キャッシュプライオリティを下げて、検索がまだあれば対応
  • Cold フェーズ(30日目): さらにプライオリティを下げて、S3連携を検討
  • Delete フェーズ(90日目): 自動削除

これで容量管理が完全に自動化されるんですよね。人間が「あ、容量やばい」って気づく前に、古いログが削除されていく。実際、この設定を入れてから、容量超過による障害は0になっています。

クエリパフォーマンスチューニングの実装例

Serverlessだからといって、クエリが遅いままでいい理由はありません。むしろ、きちんと設計しないと、OCUがオーバーで課金が増えます。

実装チームから「ダッシュボードが遅い」という報告を受けて、実際に計測してみたんです。

from opensearchpy import OpenSearch
import time

# 遅いクエリの例
slow_query = {
    "query": {
        "bool": {
            "must": [
                {"match": {"message": "error"}},
                {"range": {"timestamp": {"gte": "now-24h"}}}
            ]
        }
    },
    "_source": ["*"],  # すべてのフィールドを取得(無駄)
    "size": 10000  # 大量のドキュメント
}

start = time.time()
response = client.search(index="logs-*", body=slow_query)
elapsed = time.time() - start

print(f"実行時間: {elapsed:.2f}秒")
print(f"ヒット数: {response['hits']['total']['value']}")

これが約8秒かかっていました。問題点は、

  1. _sourceで全フィールド取得: 実は必要なのはlevel, message, timestampの3つだけ
  2. size: 10000: 実際の使用は最初の100件のみ
  3. インデックスが複数: 日付ごとに分かれているため、複数スキャン

修正したクエリがこれなんです:

# 最適化されたクエリ
optimized_query = {
    "query": {
        "bool": {
            "must": [
                {"match": {"message": "error"}},
                {"range": {"timestamp": {"gte": "now-24h"}}}
            ]
        }
    },
    "_source": ["level", "message", "timestamp"],  # 必要なフィールドだけ
    "size": 100,  # ページネーション対応
    "sort": [{"timestamp": {"order": "desc"}}]  # インデックスを活用した並べ替え
}

start = time.time()
response = client.search(index="logs-*", body=optimized_query)
elapsed = time.time() - start

print(f"実行時間: {elapsed:.2f}秒")  # → 0.3秒に短縮

実行時間が8秒から0.3秒に短縮されました。これはただ単に体験が良くなるだけじゃなく、OCU使用量も激減したので、月の請求額も若干下がった。正直、こういうチューニングって軽く見られがちですけど、Serverlessだと直結するんですよね。

AWS構成図——実際に運用している構成

graph TB
    subgraph VPC["VPC (ap-northeast-1)"]
        subgraph AZ1["AZ-1a"]
            APP1["Application Server 1"]
            FLUENTBIT1["Fluent Bit Container"]
        end
        subgraph AZ2["AZ-1c"]
            APP2["Application Server 2"]
            FLUENTBIT2["Fluent Bit Container"]
        end
        LB["Network Load Balancer"]
    end

    subgraph AOSS["OpenSearch Serverless"]
        COLLECTION["Collection<br/>4 Indexing OCU<br/>6 Query OCU"]
        DASHBOARD["OpenSearch Dashboards"]
    end

    subgraph S3COLD["Cold Storage"]
        BACKUP["S3 Snapshot Repo"]
        ARCHIVE["S3 Archive Bucket"]
    end

    subgraph MONITORING["CloudWatch"]
        METRICS["Metrics & Alarms"]
        LOGS["Log Insights"]
    end

    FLUENTBIT1 -->|logs via HTTP| COLLECTION
    FLUENTBIT2 -->|logs via HTTP| COLLECTION
    COLLECTION --> DASHBOARD
    COLLECTION -->|ILM Auto Rollover| BACKUP
    BACKUP -->|30days later| ARCHIVE
    COLLECTION --> METRICS
    COLLECTION --> LOGS
    APP1 --> FLUENTBIT1
    APP2 --> FLUENTBIT2

    style AOSS fill:#FF9900
    style COLLECTION fill:#FFB84D
    style VPC fill:#E8F4F8

実際にはFluentBitで各アプリケーションのログを集約して、OpenSearch Serverlessに投下しています。ILMポリシーで自動ロールオーバーして、古いインデックスはS3にバックアップ。CloudWatchでメトリクス監視も入れて、異常検知も自動化しています。

コスト削減で工夫したこと

Serverlessだからコストが自動最適化されるわけではありません。むしろ意識しないとすぐ跳ね上がります。

1. ログレベルの事前フィルタリング

# Fluent Bitの設定
[FILTER]
    Name grep
    Match app.*
    Regex message ^(?!(DEBUG|TRACE))
    # DEBUG・TRACEログはフィルタアウト

DEBUGログをServerlessに流さないことで、月額で約2万円の削減になりました。本番環境では不要なので、これは有効な最適化ですね。

2. ログ保持期間の削減

最初は「念のため180日保持」としていましたが、実際に検索されるのは過去30日のみ。ILMで90日以降は削除するようにしたら、約1万5000円削減されました。

3. クエリの事前集約

# ダッシュボードで使うアグリゲーション結果を事前計算
from opensearchpy import OpenSearch
from datetime import datetime

agg_query = {
    "aggs": {
        "errors_by_service": {
            "terms": {
                "field": "service",
                "size": 10
            },
            "aggs": {
                "error_count": {
                    "filter": {"term": {"level": "ERROR"}}
                }
            }
        }
    },
    "size": 0  # ドキュメント本体は不要
}

response = client.search(index="logs-*", body=agg_query)

# 結果をS3に保存(1時間ごと)
import json
import boto3

s3 = boto3.client('s3')
s3.put_object(
    Bucket='analytics-bucket',
    Key=f"logs-aggregates/{datetime.now().isoformat()}.json",
    Body=json.dumps(response['aggregations'])
)

頻繁に実行されるダッシュボード集計を事前計算して、S3に保存しておくことで、Serverlessへのクエリ負荷を大幅に削減できました。これで月約5000円削減。地味に便利ですよ。

運用で気づいた細かい落とし穴

1. データセットのスキーマが統一されていない問題

異なるサービスから送られてくるログが、フィールド定義の規約を守っていないと、OpenSearchのマッピングが自動生成されます。すると予期しないデータ型になることが多い。

# マッピングを明示的に定義
mapping = {
    "mappings": {
        "properties": {
            "timestamp": {"type": "date"},
            "level": {"type": "keyword"},  # keywordにすることで高速フィルタリング
            "service": {"type": "keyword"},
            "message": {"type": "text", "analyzer": "standard"},
            "duration_ms": {"type": "long"},
            "user_id": {"type": "keyword"}
        }
    }
}

client.indices.put_mapping(index="logs-*", body=mapping)

2. 認証・認可の落とし穴

OpenSearch Dashboardsにアクセスする際、IAM認証とFine-grained Access Controlを組み合わせるんですが、設定を間違えると管理画面に入れなくなります。実際、デプロイ直後は「Dashboardsが開けない地獄」に陥りました。

AWS公式のベストプラクティスに従って、IAMロール+OpenSearchユーザーの二重認証を設定することで、セキュリティと利便性のバランスを取っています。

Elasticsearchからの移行——実装的な工夫

既存のElasticsearchから移行する場合、インデックスとダッシュボードの両方を持ち運ぶ必要があります。Elasticsearchで「Kibana」を使っていたら、OpenSearch Dashboardsに移行できるんです。ただ、完全互換ではないので、いくつかの修正が必要でした。

特にアグリゲーション周りのクエリDSLが微妙に異なるので、テスト環境で十分に検証することをお勧めします。本当に細かいところで引っかかりますから。

まとめ

OpenSearch Serverlessを実際に6ヶ月運用してみて、ここが大事だなってポイントをまとめるとこんな感じです:

  1. OCU設定は最小値ではなく、実負荷に合わせて決める:スパイク対応が考慮されていない最小値設定では、本番でタイムアウトする

  2. インデックス戦略(ILM)が超重要:自動ロールオーバーと古いインデックス削除を設定しないと、容量管理が地獄になる

  3. クエリパフォーマンスチューニングはコスト最適化に直結:遅いクエリを放置するとOCU消費が増え、請求額が跳ね上がる

  4. ログレベルのフィルタリングと事前集約で月2〜3万円削減可能:Serverlessだからといって、無制限に流すのは愚策

  5. スキーマ管理とセキュリティ設定は後付けするな:初期段階で明示的なマッピング定義と認証設定を入れることで、後の運用が圧倒的に楽になる

Serverlessはインフラ管理の負担を大幅に減らしてくれますが、アプリケーション側の最適化を怠ると、結果的に費用と複雑性が増してしまいます。うちのチームでも、今後は入社時点で「Serverlessの正しい使い方」をドキュメント化して、後続に引き継ぎたいと考えています。

正直、次のチームがOpenSearch Serverlessを導入する際は、この記事の失敗パターンを避けるだけで、運用がかなりスムーズになると思いますよ。深夜対応の地獄からは脱出できます。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事