0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS Lambda + OpenSearch で古いデータを自動削除する仕組みを作ってみた 🚀

Last updated at Posted at 2025-11-28

はじめに

OpenSearchに溜まっていくデータを、定期的に古いものを自動削除する仕組みが必要になってきました。毎回手動で削除するのは大変なので、LambdaとEventBridgeを使って自動化することにしました。

この記事では、実装の手順とともに、OpenSearchの権限周りでハマったポイントについても詳しく解説します。

概要 📋

OpenSearchの古いデータを自動削除するシステムを構築します。Lambda + EventBridgeというシンプルな構成で、毎日定時に180日前のデータを削除します。

EventBridge (cron: 毎日深夜2時)
    ↓
Lambda Function
    ↓
OpenSearch (_delete_by_query API)

やりたいこと 🎯

  • OpenSearchに保存されているデータのうち、180日以上経過したものを自動削除
  • 毎日深夜2時に自動実行
  • 削除された件数をCloudWatch Logsに記録

180日にこだわらず、データ保存要件や法務要件に合わせて調整可能です 👍

アーキテクチャ設計 🏗️

非常にシンプルな構成です。

コンポーネント

  1. EventBridge: スケジュール実行(cron式)
  2. Lambda: 削除処理を実行
  3. OpenSearch: データ削除(_delete_by_query API使用)

なぜ_delete_by_queryを使うのか? 🤔

  • 日付条件で柔軟に削除対象を指定できる
  • 大量データにも対応
  • トランザクション処理で整合性が保たれる

P.S.
実行するのがめちゃくちゃ怖かったので、クエリ本文は AWS サポートに確認してもらいました・・・w

EventBridgeの作成方法・設定方法 ⏰

CloudFormationテンプレートでEventBridgeルールを作成します。

OpenSearchCleanupRule:
  Type: AWS::Events::Rule
  Properties:
    Name: !Sub ${ProjectName}-${Environment}-opensearch-cleanup-rule
    Description: "OpenSearch 180日経過データ削除(毎日午前2時実行)"
    ScheduleExpression: "cron(0 17 * * ? *)"
    State: ENABLED
    Targets:
      - Arn: !GetAtt LambdaFunctionForOpenSearchCleanup.Arn
        Id: OpenSearchCleanupLambda

ポイント 💡

  • ScheduleExpression: cron(0 17 * * ? *)は毎日17時(UTC)に実行
  • 日本時間の午前2時はUTCの前日17時なので、この設定でOK
  • Lambda関数と連携してタスクIDを設定

cron式の解説 📖

cron(分 時 日 月 曜日 年)

例:cron(0 17 * * ? *)

  • 毎日17時0分(UTC)
  • ?は曜日指定なし

Lambdaの作成方法・設定方法 🔧

基本的な設定

LambdaFunctionForOpenSearchCleanup:
  Type: AWS::Lambda::Function
  Properties:
    FunctionName: !Sub ${ProjectName}-${Environment}-opensearch-cleanup
    Runtime: python3.12
    Handler: lambda_function.lambda_handler
    MemorySize: 128
    Timeout: 300  # 5分(大量データ削除に対応)
    Role: !GetAtt LambdaForOpenSearchCleanupRole.Arn
    Environment:
      Variables:
        ENV: !Ref Environment
        OPENSEARCH_HOST: !Sub /${ProjectName}/${Environment}/back/OPENSEARCH_HOST

IAMロールの権限設定 🔐

✨重要なポイント: OpenSearchへのes:ESHttp*権限は不要です

必要な権限は以下だけ

- Effect: Allow
  Action:
    - ssm:GetParameter
    - ssm:GetParameters
  Resource: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ProjectName}/${Environment}/*
  
- Effect: Allow
  Action:
    - logs:CreateLogGroup
    - logs:CreateLogStream
    - logs:PutLogEvents
  Resource: "*"

OpenSearchへのアクセス権限は、次のセクションで説明する「Backend rolesへのマッピング」で解決します。

Lambdaレイヤー(Lambda Extension) 📦

SSMパラメータストアから値を取得するために、AWS Parameters and Secrets Lambda Extensionをレイヤーとして追加する必要があります。

Lambdaコンソールから以下のARNを指定

arn:aws:lambda:ap-northeast-1:123456789012:layer:AWS-Parameters-and-Secrets-Lambda-Extension:5

※地域に応じてARNが異なります。各リージョンのARNはAWS公式ドキュメントを参照。

OpenSearchへの権限設定 🔑

ここが一番重要なポイントです ⚠️
OpenSearchは権限管理が複雑で、IAMロールに権限を付けても動作しないことがあります。

必要な作業:Backend rolesへのマッピング

OpenSearchのSecurityロールのBackend rolesに、LambdaのIAMロールARNを追加する必要があります。
※ この概念を知らなくてかなり苦戦しました・・・

実施手順 📝

  1. OpenSearch Dashboardsにログイン

    • OpenSearch ドメインのダッシュボードURLにアクセス
    • マスターユーザーでログイン
  2. Security 画面を開く

    • 左メニューの "Management" → "Security" をクリック
  3. Roles を選択

    • "Roles" メニューをクリック
    • all_access ロールを選択(または適切なロールを選択)
  4. Backend roles を追加

    • "Mapped users" タブをクリック
    • "Manage mapping" ボタンをクリック
    • "Backend roles" 欄にLambdaのIAMロールARNを入力
      • 例:arn:aws:iam::123456789012:role/LambdaForOpenSearchCleanupRole-project-env
    • "Map" ボタンをクリックして保存

コマンドラインでの設定方法 💻

DashboardsのGUIを使わず、curlコマンドで設定することも可能です。

curl -X PUT "https://{domain-endpoint}/_plugins/_security/api/rolesmapping/all_access" \
  --aws-sigv4 "aws:amz:ap-northeast-1:es" \
  --user "{access_key}:{secret_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "backend_roles": ["arn:aws:iam::123456789012:role/LambdaForOpenSearchCleanupRole"]
  }'

権限が正しく設定されているかの確認 ✅

バージョンによっては、Reserved状態のロールでもダッシュボードからマッピング可能です。

マッピング後、Lambda関数をテスト実行して403エラーが出ないか確認しましょう。

実装の流れとコード例 💻

1. 日付の計算 📅

from datetime import datetime, timedelta

# 180日前の日付を計算(時分秒を0に設定)
cutoff_date = (datetime.now() - timedelta(days=180)).replace(
    hour=0, minute=0, second=0, microsecond=0
)

時分秒を0に設定する理由:日付単位での比較を明確にするためです。意外と大事

2. OpenSearchへの削除クエリ実行 🗑️

def delete_old_documents(cutoff_date):
    """180日経過したドキュメントを削除"""
    host = getParameterStoreValue(os.environ.get("OPENSEARCH_HOST"))
    
    # _delete_by_query APIを使用
    delete_query = {
        "query": {
            "range": {
                "created_at": {
                    "lt": cutoff_date.strftime('%Y/%m/%d %H:%M:%S')
                }
            }
        }
    }
    
    url = f"{host}/{index}/_delete_by_query"
    headers = {"Content-Type": "application/json"}
    
    r = requests.post(url, auth=awsauth, headers=headers, json=delete_query)
    r.raise_for_status()
    
    result = r.json()
    return result.get('deleted', 0)

⚠️重要なポイント:日付フォーマットは%Y/%m/%d %H:%M:%S(スラッシュ区切り)です。データ保存時と同じ形式にする必要があります。

注意事項 ⚠️

1. 日付フォーマットの統一 📅

データ保存時の日付形式と、削除クエリの日付形式を完全に一致させる必要があります。

不一致の場合、400 Bad Request エラーが発生します 💥

# 正しい例 ✅
cutoff_date.strftime('%Y/%m/%d %H:%M:%S')  # 2025/10/28 15:30:45

# 間違った例 ❌
cutoff_date.strftime('%Y-%m-%d %H:%M:%S')  # 2025-10-28 15:30:45  ← フォーマットが違う

2. Lambda関数のタイムアウト設定 ⏱️

大量データを削除する場合、処理時間がかかることがあります。タイムアウト時間は長めに設定しましょう。

Timeout: 300  # 5分(デフォルト60秒では短すぎる可能性がある)

3. 大量データ削除時のパフォーマンス 🚀

OpenSearchは削除処理中にレスポンスが返ってこないことがあります。必要に応じて以下の設定を検討:

{
  "query": {...},
  "wait_for_completion": true,  // 完了を待つ
  "refresh": true,              // 削除後にインデックスを更新
  "timeout": "10m"              // タイムアウト設定
}

4. テスト時の注意 🧪

本番環境でテストする前に、必ず以下を確認しましょう

  • ✅ 削除対象データの件数を事前に確認
  • ✅ 削除件数の妥当性を検証
  • ✅ CloudWatch Logsで削除結果を確認

誤って重要なデータを削除しないよう、十分注意してテストしてください 🚨
2名確認必須です!!!

5. Dev Toolsの制限事項 🛠️

OpenSearch DashboardsのDev Toolsでは、PATCHメソッドが使えません。Backend rolesの追加はダッシュボードのGUIかcurlコマンドを使用する必要があります。

6. エラーハンドリング 🔧

OpenSearchからのエラーレスポンスは適切にハンドリングしましょう。

try:
    r = requests.post(url, auth=awsauth, headers=headers, json=delete_query)
    r.raise_for_status()
    result = r.json()
    deleted_count = result.get('deleted', 0)
except requests.exceptions.HTTPError as e:
    print(f"HTTP Error: {e.response.status_code} - {e.response.text}")
    raise
except Exception as e:
    print(f"Delete error: {str(e)}")
    raise

7. CloudWatchアラームの設定 📊

削除処理が失敗した場合にアラートを出す設定も追加しました。

LambdaErrorAlarm:
  Type: AWS::CloudWatch::Alarm
  Properties:
    AlarmName: !Sub ${ProjectName}-${Environment}-opensearch-cleanup-errors
    AlarmDescription: "OpenSearch cleanup Lambda function errors"
    MetricName: Errors
    Namespace: AWS/Lambda
    Statistic: Sum
    Period: 300
    EvaluationPeriods: 1
    Threshold: 1
    ComparisonOperator: GreaterThanOrEqualToThreshold
    Dimensions:
      - Name: FunctionName
        Value: !Ref LambdaFunctionForOpenSearchCleanup
    AlarmActions:
      - !Ref AlertTopicArn  # SNSトピックのARN

動作確認 ✅

実装後、以下のように動作することを確認しました

OpenSearch cleanup started for {index}
Cutoff date: 2025-05-01T00:00:00
Cleanup completed. Deleted 8 documents

180日前を基準に正しく削除できていることを確認できました 🎉

まとめ 🎯

OpenSearchの古いデータを自動削除する仕組みを実装しました。主なポイントは以下の通りです

実装のポイント 📌

  1. OpenSearchの権限設定が重要 🔑

    • IAMロールにes:ESHttp*権限は不要
    • Backend rolesへのマッピングが必要
    • Dashboardsまたはcurlコマンドで設定
  2. 日付フォーマットに注意 📅

    • データ保存時の形式と削除クエリの形式を一致させる
    • %Y/%m/%d %H:%M:%Sのようなスラッシュ区切りが使われることもある
  3. Lambda Extensionの活用 📦

    • SSMパラメータストアから値を取得
  4. EventBridgeのcron式

    • UTC基準なので、JSTとの時差を考慮
    • 日本時間の午前2時はcron(0 17 * * ? *)(UTCの17時)

終わりに

OpenSearch っていろんな制約があったりして、複雑ですよね💦
学習コストが高い印象です。
皆様のお役に少しでもお役にたてられればと思います!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?