OpenSearch Serverless半年運用で学んだ本当の落とし穴——コスト最適化と実装のコツ
オンプレEsから移行したOpenSearch Serverlessで実際にハマった地雷とその対策。OCU設定ミスからインデックス設計の失敗まで、運用経験から得た教訓を共有します。
Serverlessに移行した理由——自分たちのOpenSearch地獄
去年の秋、うちのチームは約2年間オンプレのElasticsearchを運用していたんです。それが段々キツくなってきた。ノード管理、バージョンアップ、ディスク逼迫への対応……定期的に深夜対応が発生するようになっていました。
特に辛かったのが、スパイク時のメモリリーク。ある日突然、ログが流れ込まなくなって、チーム全体で1時間近く原因特定に追われたことがあります。その時点で「これはもう自分たちで持つのは無理だ」と判断して、OpenSearch Serverlessの導入を決めました。
正直、最初は懐疑的だったんですよ。Serverlessって本当に本番環境で安定するのか、コストは跳ね上がらないのか……いろんな不安がありました。でも実際に6ヶ月運用してみると、思いの外うまく回っています。もちろん地雷もありますけど。
最初の設定で失敗した話——容量単位とインデックス戦略
OpenSearch Serverlessの価格モデルは「OCU(OpenSearch Compute Units)」という容量単位で課金されます。インデックスの書き込み、クエリの実行、スナップショットの保存……それぞれに最小OCUが必要なんです。ここで僕たちは大失敗しました。
最初は最小値で構成してみたんですよ:
| 項目 | 設定値 | 理由 |
|---|---|---|
| インデックス書き込み OCU | 2 | 最小値だから |
| クエリ実行 OCU | 2 | 最小値だから |
| スナップショット OCU | 1 | 最小値だから |
| 月額コスト | 約4万5000円 | — |
これで「シンプルだし安くできた」と思ってました。でも運用を始まったら、ダッシュボード更新がしょっちゅう失敗するようになった。リクエストが集中した時間帯では、クエリがタイムアウトしまくったわけです。
実は、OCUの最小値は「あくまで最小」。実際の負荷に応じて自動スケーリングするんですが、そのスケーリング幅が実負荷に追いつかないんです。スパイク対応には最小値では足りていなかったんだなって、ここで初めて理解しました。
3週間後に設定を見直して、こう修正しました:
| 項目 | 旧設定 | 新設定 | 変更理由 |
|---|---|---|---|
| インデックス書き込み OCU | 2 | 4 | スパイク時の書き込み対応 |
| クエリ実行 OCU | 2 | 6 | ダッシュボード同時アクセス対応 |
| スナップショット OCU | 1 | 2 | バックアップ処理の安定化 |
| 月額コスト | 約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秒かかっていました。問題点は、
_sourceで全フィールド取得: 実は必要なのはlevel,message,timestampの3つだけsize: 10000: 実際の使用は最初の100件のみ- インデックスが複数: 日付ごとに分かれているため、複数スキャン
修正したクエリがこれなんです:
# 最適化されたクエリ
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ヶ月運用してみて、ここが大事だなってポイントをまとめるとこんな感じです:
-
OCU設定は最小値ではなく、実負荷に合わせて決める:スパイク対応が考慮されていない最小値設定では、本番でタイムアウトする
-
インデックス戦略(ILM)が超重要:自動ロールオーバーと古いインデックス削除を設定しないと、容量管理が地獄になる
-
クエリパフォーマンスチューニングはコスト最適化に直結:遅いクエリを放置するとOCU消費が増え、請求額が跳ね上がる
-
ログレベルのフィルタリングと事前集約で月2〜3万円削減可能:Serverlessだからといって、無制限に流すのは愚策
-
スキーマ管理とセキュリティ設定は後付けするな:初期段階で明示的なマッピング定義と認証設定を入れることで、後の運用が圧倒的に楽になる
Serverlessはインフラ管理の負担を大幅に減らしてくれますが、アプリケーション側の最適化を怠ると、結果的に費用と複雑性が増してしまいます。うちのチームでも、今後は入社時点で「Serverlessの正しい使い方」をドキュメント化して、後続に引き継ぎたいと考えています。
正直、次のチームがOpenSearch Serverlessを導入する際は、この記事の失敗パターンを避けるだけで、運用がかなりスムーズになると思いますよ。深夜対応の地獄からは脱出できます。