Amazon Elasticsearch Serviceの手動スナップショットを管理するLambda関数を作成してみた

Amazon Elasticsearch Serviceの自動スナップショットは、保持期間が14日間だったり、別ドメインにリストアできなかったりと制約があります。このような制約を回避したい場合は手動スナップショットを作成しよう。というお話
2021.08.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Amazon Elasticsearch Serviceの手動スナップショットとは

こんにちは、のんピ です。

皆さんはAmazon Elasticsearch Service(以降Amazon ES)のスナップショットに2つ種類があることをご存知ですか? 私は知りませんでした。

まず、Elasticsearchのスナップショットは、ElasticsearchクラスターのインデックスとStateのバックアップです。Stateとは、クラスタの設定、ノード情報、インデックスの設定、シャードの割り当てなどのことです。

そして、Amazon ESには、自動スナップショット手動スナップショットと2種類のスナップショットがあります。2つのスナップショットの違いとして、以下が挙げられます。

  • 自動スナップショット
    • クラスターのリカバリー用途で使われる
    • クラスターのステータスが赤くなったり、データが失われたりした場合に、ドメインを復元するために使用する
    • 事前設定された Amazon S3 バケットに追加料金なしで保存される
  • 手動スナップショット
    • クラスターのリカバリーや、あるクラスターから別のクラスターへのデータ移動のために使用される
    • スナップショットはAmazon S3バケットに保存され、標準のS3料金が適用される
    • 手動スナップショットがあれば、そのスナップショットを使ってAmazon ESドメインに移行することができる(詳細: Amazon Elasticsearch Service への移行)

また、AWS公式ドキュメントに記載がある通り、自動スナップショットの作成間隔と保持期間は以下の通りです。

  • Elasticsearch 5.3以降のドメイン
    • 1時間ごとに自動でスナップショットを作成し、最大 336 個のスナップショットを 14 日間保持
  • Elasticsearch 5.1以前のドメイン
    • 指定した時刻に毎日自動的にスナップショットを作成し、14 個のスナップショットを保持する。30 日以上はスナップショットデータを保持しない

そのため、Amazon ESを運用する上で、以下のような場面に遭遇することが考えられる場合は、手動スナップショットを作成する必要があります。

  • 既存のAmazon ESドメインから、新しいAmazon ESドメインにデータをリストアしたいとき
  • 14日以上前の状態にリストアしたいとき

例えば、Amazon ESドメインで障害が発生し、復旧できないまま14日以上経過する場合、正常なスナップショットが全て削除されてしまい、リストアができない(=構築し直し)といったことも起こり得ます。

そこで、今回は手動スナップショットをLambda関数を使って楽に管理する仕組みを実装したいと思います。

いきなりまとめ

  • 手動スナップショットを作成するためには、スナップショットのリポジトリを登録する必要がある
  • 手動スナップショット作成などのリクエストを投げるために、Amazon ESドメインで操作元のIAMユーザーもしくは、IAMロールを指定する必要がある
  • Amazon ESのスナップショットに対して_searchで詳細な検索をすることはできない
  • Elasticsearchの運用支援ツールであるCuratorを使用して、指定した期間よりも以前に作成された手動スナップショットを削除する
  • Elasticsearchのクライアントと、ドメインのバージョンが一致しない場合は、エラーが出力されて正常に動作しない
  • スナップショットの削除は1つあたり15-30秒かかるため、Lambda関数で複数のスナップショットをまとめて削除をする場合は、タイムアウト値を長くする

検証の構成

一からAmazon ESを構築してログを可視化するのも手間がかかるため、SIEM on Amazon ESを使用します。

検証する内容としては、以下の4つです。

  1. CloudTrailのログをSIEM on Amazon ESで取り込み、可視化する
  2. Amazon ESのスナップショットを定期的(30分ごと)に作成する
  3. 古いAmazon ESのスナップショット(2日前よりも前)を定期的(1日ごと)に削除する
  4. 作成したスナップショットからリストアする

全体の構成は以下の通りです。

作成するLambda関数は以下の3つです。

  1. 手動スナップショット用リポジトリの登録
  2. 手動スナップショットの作成
  3. 指定した期間よりも以前に作成された手動スナップショットの削除

1. 手動スナップショット用リポジトリの登録について補足します。

Open Distro for Elasticsearchの公式ドキュメントを確認すると、以下のような記載があります。

Register repository

Before you can take a snapshot, you have to “register” a snapshot repository. A snapshot repository is just a storage location: a shared file system, Amazon S3, Hadoop Distributed File System (HDFS), Azure Storage, etc.

スナップショットを作成する前に、スナップショットリポジトリの登録が必要」と記載されています。そのため、手動スナップショット用のリポジトリの登録のLambda関数も作成します。

SIEM on Amazon ESのデプロイとCloudTrailの設定

まず、SIEM on Amazon ESをデプロイします。

SIEM on Amazon ESのデプロイは以下の記事と同様にCloudFormationを使って行いました。

しばらく経った後、CloudFormationのコンソールを確認すると、SIEM on Amazon ESのスタックであるaes-siemのステータスがCREATE_COMPLETEになっていることが確認できます。

SIEM on Amazon ESデプロイ後の以下の作業については、上述した記事の通り行います。

  • Kibanaへのログイン
  • Dashboard等設定のインポート
  • CloudTrailのログのSIEM on Amazon ESのログ収集用S3バケットへの出力設定

最終的には以下のようにCloudTrailのログが可視化できればOKです。

IAMの設定

スナップショット管理用IAMロールの確認

SIEM on Amazon ESのデプロイ後、IAMのコンソールからaes-siem-snapshot-roleがあることを確認します。

このIAMロールを使って、Amazon ESは手動スナップショットの作成、リスト、削除を行います。

aes-siem-snapshot-roleのIAMポリシーは以下のように設定されてます。SIEM on Amazon ESではなく、自分でAmazon ESを構築される方はご参考ください。

{
  "Version": "2012-10-17",
  "Statement": [{
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::<Amazon ESスナップショット保存用S3バケットの名前>"
      ]
    },
    {
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::<Amazon ESスナップショット保存用S3バケットの名前>/*"
      ]
    }
  ]
}

また、Amazon ESがこのaes-siem-snapshot-roleを受け取り、S3の操作をするために、信頼されたエンティティとして、以下のようにes.amazonaws.comを設定します。(SIEM on Amazon ESの場合は既に設定されています)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "es.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Lambda関数用のIAMポリシーとIAMロールの作成

Lambda関数からElasticsearchのクライアントを使ってAmazon ESにクエリを投げたり、Amazon ESドメインにaes-siem-snapshot-roleを渡すために、IAMポリシーとIAMロールを作成します。 

まず、IAMポリシーです。

cm-aes-siem-snapshot-policyというIAMポリシーを作成しました。ポリシーの内容は以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "arn:aws:iam::<AWSアカウントID>:role/aes-siem-snapshot-role"
        },
        {
            "Effect": "Allow",
            "Action": [
                "es:ESHttpGet",
                "es:ESHttpPost",
                "es:ESHttpPut",
                "es:ESHttpDelete"
            ],
            "Resource": "arn:aws:es:us-east-1:<AWSアカウントID>:domain/aes-siem/*"
        }
    ]
}

続いて、IAMロールの作成です。

Lambda関数にアタッチするIAMロールとしてcm-aes-siem-snapshot-roleというIAMロールを作成しました。IAMロールには以下のIAMポリシーをアタッチしています。

  • cm-aes-siem-snapshot-policy
  • AWSLambdaBasicExecutionRole (CloudWatch Logsへのログ出力をするためにアタッチ)

また、Lambda関数がこのcm-aes-siem-snapshot-roleを受け取り、Amazon ESの操作をするために、信頼されたエンティティとして以下のようにlambda.amazonaws.comを設定します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Amazon ESのロール設定

Amazon ESが手動スナップショットを管理するために使用するmanage_snapshotsロールとcm-aes-siem-snapshot-roleのマッピングと、不足している権限を追加します。

まず、manage_snapshotsロールとcm-aes-siem-snapshot-roleのマッピングです。

KibanaにログインしてSecurity - Rolesからmanage_snapshotsをクリックし、Mapped usersタブのManage mappingをクリックします。

Backend rolesに作成したcm-aes-siem-snapshot-roleのARNを入力して、Mapをクリックします。

続いて、不足している権限の追加です。

後述するElasticsearchの運用支援ツールであるCuratorを使ってスナップショットを削除する際に、cluster:monitor/tasks/listsへの許可が必要になります。

manage_snapshotsPermissionsタブのEdit roleをクリックします。

Cluster permissionsのテキストエリアでcluster_monitorを入力し、Updateをクリックします。

最終的に、ロールの一覧でmanage_snapshotsを表示したときに、以下のようになっていればOKです。

手動スナップショット用リポジトリの登録

Lambda関数を作成するための準備

作成するLambda関数にはrequests_aws4authなどパッケージをインポートする必要があります。

そのため、必要なパッケージをzipでまとめたデプロイパッケージを作成する必要があります。

デプロイパッケージ作成のため、以下の作業を実施しました。

# Pythonのバージョン確認
> python3 --version
Python 3.9.3

# 2021/8/8現在、LambdaがサポートしているPythonは3.8まで
# そのため、pyenvで、Python3.8系をインストールする
> pyenv local 3.8.9
> python3 --version
Python 3.8.9

# 手動スナップショット用リポジトリの登録をするLambda関数の作業用ディレクトリの作成
> mkdir register-aes-manual-snapshot-repository
> cd register-aes-manual-snapshot-repository 

# venvで仮想環境を作成
> python -m venv venv

# 仮想環境をアクティベート
# 私の環境ではfishを使用しているので、. ./venv/bin/activate.fish を実行
# 以降の操作は全て仮想環境上で行う
> . ./venv/bin/acrivate.fish

# pipのアップグレード
> pip install --upgrade pip

# 仮想環境に必要なパッケージをインストール
> pip install boto3 requests requests_aws4auth

# インストール済みパッケージ一覧を確認
> pip list
Package            Version
------------------ ---------
boto3              1.18.15
botocore           1.21.15
certifi            2021.5.30
charset-normalizer 2.0.4
idna               3.2
jmespath           0.10.0
pip                21.2.2
python-dateutil    2.8.2
requests           2.26.0
requests-aws4auth  1.1.1
s3transfer         0.5.0
setuptools         49.2.1
six                1.16.0
urllib3            1.26.6

# デプロイパッケージ用のディレクトリの作成
> mkdir dist
> mkdir package

# デプロイパッケージ作成用のシェルスクリプトの作成
> vi build.sh
> cat build.sh
#!/bin/bash

rm -rf package/*
cp *.py package/
pip install -r requirements.txt -t ./package/

cd ./package/
zip -r ../dist/lambda.zip .

# デプロイパッケージ作成用のシェルスクリプトに実行権限を追加
> chmod +x build.sh

コードの説明

こちらのAWS公式ドキュメントに記載されているサンプルのコードを参考に作成しました。

S3バケットをスナップショットのリポジトリとして登録する際には、以下いくつかの設定項目があります。

項目 説明
base_path スナップショットを保存するS3バケット内のパス (例:my/snapshot/directory)
指定しない場合、スナップショットはS3バケットのルートに保存される
(オプション)
bucket S3バケットの名前
(必須)
buffer_size chunk_sizeで指定したチャンクをbuffer_sizeで指定した単位に分割し、別のAPIを使用してS3に送信する際の閾値
デフォルトは、100MBまたはJavaヒープの5%の2つの値のうち小さい方
有効な値は、5mbから5gbの間
(オプション変更は非推奨)
canned_acl repository-s3プラグインがS3にオブジェクトを作成する際に、オブジェクトにACLを追加することができる
デフォルトはprivate
(オプション)
chunk_size スナップショット操作時にファイルをチャンクに分割する (例:64mb、1gb)
デフォルトは1gb
(オプション)
client クライアントの設定(s3.client.default.access_keyなど)を指定する際に、デフォルト以外の文字列(s3.client.backup-role.access_keyなど)を使用することができる
別の名前を使用した場合は、この値を一致するように変更するように指定する
既定値および推奨値はdefault
(オプション)
compress メタデータファイルを圧縮するかどうかを指定する
この設定は、データファイルには影響しない
データファイルはインデックスの設定によってはすでに圧縮されている場合がある
デフォルトはfalse
(オプション)
max_restore_bytes_per_sec スナップショットを復元する際の最大速度
デフォルトは40MB/秒(40m)
(オプション)
max_snapshot_bytes_per_sec スナップショットを取得する際の最大レート
既定値は40MB/秒(40m)
(オプション)
readonly リポジトリを読み取り専用にするかどうか
あるクラスター(登録時に "readonly":false)から別のクラスター(登録時に "readonly":true)に移行する際に利用する
(オプション)
server_side_encryption S3バケット内のスナップショットファイルを暗号化するかどうかを指定する
この設定では、S3で管理された鍵でAES-256を使用します
デフォルトはfalse
(オプション)
storage_class スナップショットファイルを保存するS3のストレージクラスを指定する
デフォルトはstandard
glacierおよびdeep_archiveの使用は非推奨
(オプションで指定)

(参考: Elasticsearch - Take and Restore Snapshots - Amazon S3)

また、Amazon ESのエンドポイントやIAMロールを変更する度にデプロイパッケージを作成し直すのが手間なので、環境変数で各種パラメーターを指定するようにしています。

実際のコードは以下の通りです。

./register-aes-manual-snapshot-repository/lambda_function.py

import os
import boto3
import requests
from logging import getLogger, StreamHandler, DEBUG
from requests_aws4auth import AWS4Auth

logger = getLogger("urllib3")
handler = StreamHandler()
handler.setLevel(DEBUG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False

# host is include https:// and trailing /
host = os.environ["AES_HOST"]
region = os.environ["REGION"]
repository_path = os.environ["SNAPSHOT_REPOSITORY_PATH"]
bucket_name = os.environ["BUCKET_NAME"]
role_arn = os.environ["ROLE_ARN"]
service = "es"

# the Elasticsearch API endpoint
path = repository_path
url = host + path

payload = {
    "type": "s3",
    "settings": {
        "bucket": bucket_name,
        "base_path": repository_path,
        "region": region,
        "role_arn": role_arn,
    },
}

headers = {"Content-Type": "application/json"}

credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(
    credentials.access_key,
    credentials.secret_key,
    region,
    service,
    session_token=credentials.token,
)

# Register repository
def lambda_handler(event, context):
    r = requests.put(url, auth=awsauth, json=payload, headers=headers)

    logger.info(r.text)
    
    return {"statusCode": r.status_code, "body": r.text}

Lambda関数の作成

それでは、Lambda関数を作成します。

まず、以下コマンドでデプロイパッケージを作成します。

# インストール済みパッケージ一覧を requirements.txt に出力
> pip freeze > requirements.txt

# デプロイパッケージの作成
> ./build.sh

./build.sh実行後のディレクトリツリーは以下のようになり、dist配下にlambda.zipが作成されます。

./register-aes-manual-snapshot-repository/
├── .gitignore
├── .vscode/
│   └── settings.json
├── build.sh
├── dist/
│   └── lambda.zip
├── lambda_function.py
├── package/
├── requirements.txt
└── venv/

また、requirements.txtは以下の通りです。blackはPythonのフォーマッターとして使用しています。

./register-aes-manual-snapshot-repository/requirements.txt

appdirs==1.4.4
black==21.7b0
boto3==1.18.15
botocore==1.21.15
certifi==2021.5.30
charset-normalizer==2.0.4
click==8.0.1
idna==3.2
jmespath==0.10.0
mypy-extensions==0.4.3
pathspec==0.9.0
python-dateutil==2.8.2
regex==2021.8.3
requests==2.26.0
requests-aws4auth==1.1.1
s3transfer==0.5.0
six==1.16.0
tomli==1.2.1
urllib3==1.26.6

続いて、Lambda関数の設定をします。

Lambdaのコンソールを開き、関数の作成をクリックします。

関数名、ランタイム、Lambda関数に割り当てるIAMロールを指定して関数の作成をクリックします。

次に、デプロイパッケージをアップロードします。

コードタブからアップロード元-.zipファイルをクリックします。

アップロードをクリックして、作成したデプロイパッケージを指定します。その後保存をクリックします。

これでコードのアップロードは完了です。

次に環境変数の設定をします。

設定タブの環境変数から編集をクリックします。

以下のように環境変数を設定します。なお、AES_HOSTは、Amazon ESのエンドポイントの先頭のhttps://と末尾の/を含めて記載します。

これで環境変数の設定も完了です。

実行してみた

それでは、Lambda関数を実行して、手動スナップショットを作成します。

テストタブからテストをクリックします。

正常に実行できれば、以下のように{"acknowledged":true}が返ってきます。

正しくリポジトリが作成できたか確認します。

Kibanaにログインして、Dev Toolsを開きます。コンソールにGET _snapshot/を入力してをクリックします。すると、以下のように_snapshot配下にmanualというリポジトリが作成されたことが確認できます。

なお、cs-automated-encは自動スナップショット用のリポジトリです。

手動スナップショットの作成

Lambda関数を作成するための準備

手動スナップショットリポジトリ登録用のLambda関数と同様に、デプロイパッケージを作成するための作業を実施します。

# 手動スナップショットを作成するLambda関数の作業用ディレクトリの作成
> mkdir create-aes-manual-snapshot 
> cd create-aes-manual-snapshot

# venvで仮想環境を作成
> python -m venv venv

# 仮想環境をアクティベート
# 私の環境ではfishを使用しているので、. ./venv/bin/activate.fish を実行
# 以降の操作は全て仮想環境上で行う
> . ./venv/bin/acrivate.fish

# pipのアップグレード
> pip install --upgrade pip

# 仮想環境に必要なパッケージをインストール
> pip install boto3 requests requests_aws4auth

# インストール済みパッケージ一覧を確認
> pip list
Package            Version
------------------ ---------
boto3              1.18.15
botocore           1.21.15
certifi            2021.5.30
charset-normalizer 2.0.4
idna               3.2
jmespath           0.10.0
pip                21.2.2
python-dateutil    2.8.2
requests           2.26.0
requests-aws4auth  1.1.1
s3transfer         0.5.0
setuptools         49.2.1
six                1.16.0
urllib3            1.26.6

# デプロイパッケージ用のディレクトリの作成
> mkdir dist
> mkdir package

# デプロイパッケージ作成用のシェルスクリプトの作成
> vi build.sh
> cat build.sh
#!/bin/bash

rm -rf package/*
cp *.py package/
pip install -r requirements.txt -t ./package/

cd ./package/
zip -r ../dist/lambda.zip .

# デプロイパッケージ作成用のシェルスクリプトに実行権限を追加
> chmod +x build.sh

コードの説明

コードは手動スナップショットリポジトリ登録用のLambda関数のコードとほぼほぼ同じです。

作成するスナップショットは、「指定したプレフィックス-YYYY-MM-DDtHH-MM-SS」という名前で作成します。

実際のコードは以下の通りです。

./create-aes-manual-snapshot/lambda_function.py

import os
import boto3
import requests
from logging import getLogger, StreamHandler, DEBUG
from requests_aws4auth import AWS4Auth
from datetime import datetime

logger = getLogger("urllib3")
handler = StreamHandler()
handler.setLevel(DEBUG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False

# host is include https:// and trailing /
host = os.environ["AES_HOST"]
region = os.environ["REGION"]
repository_path = os.environ["SNAPSHOT_REPOSITORY_PATH"]
snapshot_prefix = os.environ["SNAPSHOT_PREFIX"]
service = "es"

credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(
    credentials.access_key,
    credentials.secret_key,
    region,
    service,
    session_token=credentials.token,
)

# Create Elasticsearch Snapshot
def lambda_handler(event, context):
    # the Elasticsearch API endpoint
    path = (
        repository_path
        + snapshot_prefix
        + "-"
        + datetime.now().strftime("%Y-%m-%dt%H-%M-%S")
    )
    url = host + path

    r = requests.put(url, auth=awsauth)

    logger.info(r.text)

    return {"statusCode": r.status_code, "body": r.text}

Lambda関数の作成

手動スナップショットリポジトリ登録用のLambda関数と同様の手順でLambda関数を作成します。

以下コマンドでデプロイパッケージを作成します。

# インストール済みパッケージ一覧を requirements.txt に出力
> pip freeze > requirements.txt

# デプロイパッケージの作成
> ./build.sh

./build.sh実行後のディレクトリツリーは以下のようになり、dist配下にlambda.zipが作成されます。

./create-aes-manual-snapshot/
├── .gitignore
├── .vscode/
│   └── settings.json
├── build.sh
├── dist/
│   └── lambda.zip
├── lambda_function.py
├── package/
├── requirements.txt
└── venv/

また、requirements.txtは以下の通りです。手動スナップショットリポジトリ登録用のLambda関数と同じですね。

./create-aes-manual-snapshot/requirements.txt

appdirs==1.4.4
black==21.7b0
boto3==1.18.15
botocore==1.21.15
certifi==2021.5.30
charset-normalizer==2.0.4
click==8.0.1
idna==3.2
jmespath==0.10.0
mypy-extensions==0.4.3
pathspec==0.9.0
python-dateutil==2.8.2
regex==2021.8.3
requests==2.26.0
requests-aws4auth==1.1.1
s3transfer==0.5.0
six==1.16.0
tomli==1.2.1
urllib3==1.26.6

次にLambda関数の作成です。手動スナップショットリポジトリ登録用のLambda関数と同様の手順でLambda関数を作成します。

環境変数は以下のように設定します。SNAPSHOT_PREFIXは手動スナップショットのプレフィックスです。今回はmanual-snapshot-testとしました。

定期実行の設定

手動スナップショットを30分間隔で作成するように、EventBridgeを使って設定をします。

create-aes-manual-snapshotトリガーを追加をクリックします。その後、トリガーとしてEventBridgeを選択し、ルール名や説明、スケジュールを入力して、追加をクリックします。

実行してみた

それでは、Lambda関数を実行して、手動スナップショット用リポジトリの登録をします。

テストタブからテストをクリックします。

正常に実行できれば、以下のように{"accepted":true}が返ってきます。

正しくスナップショットが作成できたか確認します。

Kibanaにログインして、Dev Toolsを開きます。コンソールにGET _snapshot/manual/*を入力してをクリックします。すると、以下のように_snapshot/manual配下にスナップショットが作成されていることが確認できます。

また、手動スナップショットのリポジトリとして設定しているS3バケットを確認すると、以下のように_snapshot/manual/ディレクトリの配下にスナップショットが作成されていることが確認できます。

もう一度、手動スナップショット作成のLambda関数を実行してみます。実行後、再度Dev Toolsと手動スナップショットのリポジトリとして設定していS3バケットを確認すると、手動スナップショットが追加されていることが確認できます。

指定した期間よりも以前に作成された手動スナップショットの削除

Lambda関数を作成するための準備

手動スナップショットリポジトリ登録用のLambda関数と同様に、デプロイパッケージを作成するための作業を実施します。

# 指定した期間よりも以前に作成された手動スナップショットの削除をするLambda関数の作業用ディレクトリの作成
> mkdir delete-aes-manual-snapshot 
> cd delete-aes-manual-snapshot

# venvで仮想環境を作成
> python -m venv venv

# 仮想環境をアクティベート
# 私の環境ではfishを使用しているので、. ./venv/bin/activate.fish を実行
# 以降の操作は全て仮想環境上で行う
> . ./venv/bin/acrivate.fish

# pipのアップグレード
> pip install --upgrade pip

# 仮想環境に必要なパッケージをインストール
> pip install boto3 requests requests_aws4auth elasticsearch-curator==5.7.0 elasticsearch==7.9.1

# インストール済みパッケージ一覧を確認
> pip list
Package               Version
--------------------- ---------
boto3                 1.18.16
botocore              1.21.16
certifi               2021.5.30
charset-normalizer    2.0.4
click                 6.7
elasticsearch         7.9.1
elasticsearch-curator 5.7.0
idna                  3.2
jmespath              0.10.0
pip                   21.2.3
python-dateutil       2.8.2
PyYAML                5.4.1
requests              2.26.0
requests-aws4auth     1.1.1
s3transfer            0.5.0
setuptools            49.2.1
six                   1.16.0
urllib3               1.26.6
voluptuous            0.12.1

# デプロイパッケージ用のディレクトリの作成
> mkdir dist
> mkdir package

# デプロイパッケージ作成用のシェルスクリプトの作成
> vi build.sh
> cat build.sh
#!/bin/bash

rm -rf package/*
cp *.py package/
pip install -r requirements.txt -t ./package/

cd ./package/
zip -r ../dist/lambda.zip .

# デプロイパッケージ作成用のシェルスクリプトに実行権限を追加
> chmod +x build.sh

Elasticsearch(elasticsearch)とElasticsearchの運用支援ツールであるCurator(elasticsearch-curator)をインストールしています。それぞれのパッケージでバージョンを指定している理由は、Amazon ESドメインとバージョンを合わせるためです。

2021/8/8時点のSIEM on Amazon ESのAmazon ESドメインのデフォルトのバージョンは7.9です。

この2つのバージョン指定をしない場合は現時点での最新バージョンである7.14.0がインストールされます。

> pip install boto3 requests requests_aws4auth elasticsearch-curator elasticsearch

> pip list
Package               Version
--------------------- ---------
boto3                 1.18.16
botocore              1.21.16
certifi               2021.5.30
charset-normalizer    2.0.4
click                 7.1.2
elasticsearch         7.14.0
elasticsearch-curator 5.8.4
idna                  3.2
jmespath              0.10.0
pip                   21.2.3
python-dateutil       2.8.2
PyYAML                5.4.1
requests              2.26.0
requests-aws4auth     1.1.1
s3transfer            0.5.0
setuptools            49.2.1
six                   1.16.0
urllib3               1.26.4
voluptuous            0.12.1

SIEM on Amazon ESドメインとクライアントのElasticsearchのバージョンが一致しない場合は、Unable to find repository "manual": Error: The client noticed that the server is not a supported distribution of Elasticsearchと表示され、スナップショットのリポジトリであるmanualにアクセスすることができません。

パッケージの依存関係を確認する際はpipdeptreeを使います。

実行すると、Curatorの最新バージョンである5.8.4は、Elasticsearchの7.12.0以上、8.0.0未満のバージョンをインストールする必要があることがわかります。

# pipdeptree のインストール
> pip install pipdeptree

# パッケージの依存関係の確認
> pipdeptree                                                                                                                                               
elasticsearch-curator==5.8.4
  - boto3 [required: >=1.17.57, installed: 1.18.16]
    - botocore [required: >=1.21.16,<1.22.0, installed: 1.21.16]
      - jmespath [required: >=0.7.1,<1.0.0, installed: 0.10.0]
      - python-dateutil [required: >=2.1,<3.0.0, installed: 2.8.2]
        - six [required: >=1.5, installed: 1.16.0]
      - urllib3 [required: >=1.25.4,<1.27, installed: 1.26.4]
    - jmespath [required: >=0.7.1,<1.0.0, installed: 0.10.0]
    - s3transfer [required: >=0.5.0,<0.6.0, installed: 0.5.0]
      - botocore [required: >=1.12.36,<2.0a.0, installed: 1.21.16]
        - jmespath [required: >=0.7.1,<1.0.0, installed: 0.10.0]
        - python-dateutil [required: >=2.1,<3.0.0, installed: 2.8.2]
          - six [required: >=1.5, installed: 1.16.0]
        - urllib3 [required: >=1.25.4,<1.27, installed: 1.26.4]
  - certifi [required: >=2020.12.5, installed: 2021.5.30]
  - click [required: >=7.0,<8.0, installed: 7.1.2]
  - elasticsearch [required: >=7.12.0,<8.0.0, installed: 7.14.0]
    - certifi [required: Any, installed: 2021.5.30]
    - urllib3 [required: >=1.21.1,<2, installed: 1.26.4]
  - pyyaml [required: ==5.4.1, installed: 5.4.1]
  - requests [required: >=2.25.1, installed: 2.26.0]
    - certifi [required: >=2017.4.17, installed: 2021.5.30]
    - charset-normalizer [required: ~=2.0.0, installed: 2.0.4]
    - idna [required: >=2.5,<4, installed: 3.2]
    - urllib3 [required: >=1.21.1,<1.27, installed: 1.26.4]
  - requests-aws4auth [required: >=1.0.1, installed: 1.1.1]
    - requests [required: Any, installed: 2.26.0]
      - certifi [required: >=2017.4.17, installed: 2021.5.30]
      - charset-normalizer [required: ~=2.0.0, installed: 2.0.4]
      - idna [required: >=2.5,<4, installed: 3.2]
      - urllib3 [required: >=1.21.1,<1.27, installed: 1.26.4]
    - six [required: Any, installed: 1.16.0]
  - six [required: >=1.15.0, installed: 1.16.0]
  - urllib3 [required: ==1.26.4, installed: 1.26.4]
  - voluptuous [required: >=0.12.1, installed: 0.12.1]
pipdeptree==2.1.0
  - pip [required: >=6.0.0, installed: 21.2.3]
setuptools==49.2.1

コードの説明

こちらのAWS公式ドキュメントに記載されているサンプルコードを参考に作成しました。

スナップショットは、インデックスのGET /index-name/_searchのように、作成日時などの条件を指定して検索を行うことができません。

そのため、GET _snapshot/リポジトリ名/*で、全てのスナップショットを取得してから、条件に合うスナップショットをフィルタリングするコードを自分で実装する必要があります。

このような場面で活躍するのが、Elasticsearchの運用支援ツールであるCuratorです。Curatorを使用することで、90日以上前や、1年以上前に作成されたスナップショットのフィルターや削除を簡単に実装することができます。

コードは上述のサンプルをベースに、手動スナップショットリポジトリ登録用のLambda関数と同じような書き方にしています。

実際のコードは以下の通りです。

./delete-aes-manual-snapshot/lambda_function.py

import os
import boto3
import curator
from logging import getLogger, StreamHandler, DEBUG
from requests_aws4auth import AWS4Auth
from elasticsearch import Elasticsearch, RequestsHttpConnection

logger = getLogger("curator")
handler = StreamHandler()
handler.setLevel(DEBUG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False

host = os.environ["AES_HOST"]
region = os.environ["REGION"]
repository_name = os.environ["SNAPSHOT_REPOSITORY_NAME"]
snapshot_prefix = os.environ["SNAPSHOT_PREFIX"]
unit = os.environ["UNIT"]
unit_count = int(os.environ["UNIT_COUNT"])
service = "es"

credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(
    credentials.access_key,
    credentials.secret_key,
    region,
    service,
    session_token=credentials.token,
)


def lambda_handler(event, context):
    # Build the Elasticsearch client.
    es = Elasticsearch(
        hosts=[{"host": host, "port": 443}],
        http_auth=awsauth,
        use_ssl=True,
        verify_certs=True,
        connection_class=RequestsHttpConnection,
        timeout=120,  # Deleting snapshots can take a while, so keep the connection open for long enough to get a response.
    )

    try:
        # Get all snapshots in the repository
        snapshot_list = curator.SnapshotList(es, repository=repository_name)

        # Filter by prefix
        snapshot_list.filter_by_regex(kind="prefix", value=snapshot_prefix)

        # Filter by creation_date
        snapshot_list.filter_by_age(
            source="creation_date",
            direction="older",
            unit=unit,
            unit_count=unit_count,
        )

        # Delete the old snapshots.
        curator.DeleteSnapshots(
            snapshot_list, retry_interval=30, retry_count=3
        ).do_action()
    except (
        curator.exceptions.SnapshotInProgress,
        curator.exceptions.NoSnapshots,
        curator.exceptions.FailedExecution,
    ) as e:
        print(e)

curator.SnapshotList()で現在のスナップショットの一覧を取得します。その後、snapshot_list.filter_by_regex()snapshot_list.filter_by_age()で削除対象のスナップショットのフィルターを行います。

まず、snapshot_list.filter_by_regex()で、スナップショット名のプレフィックスを環境変数で指定した文字列でフィルターします。

次に、snapshot_list.filter_by_age()で、スナップショットの作成日を確認し、指定した期間よりも以前に作成されたスナップショットをフィルターします。

指定した期間は、Curatorの公式ドキュメントにある通り、unitunit_countを使って指定します。

unitは時間や週、年など期間の単位を指定します。unitに指定可能な値は以下の通りです。

  • seconds
  • minutes
  • hours
  • days
  • weeks
  • months
  • years

unit_countunitで指定した期間の数を示します。例えば{unit:weeks, unit_count:2}の場合は2週間ということになります。

スナップショットの削除処理はcurator.DeleteSnapshots().do_action()で行っています。

curator.DeleteSnapshots(). do_dry_run()とすることで、実際の削除は行わずに削除対象を確認することができます。
DeleteSnapshots()のその他のプロパティ、メソッドは、Curatorの公式ドキュメントをご確認ください。

Lambda関数の作成

手動スナップショットリポジトリ登録用のLambda関数と同様の手順でLambda関数を作成します。

以下コマンドでデプロイパッケージを作成します。

# インストール済みパッケージ一覧を requirements.txt に出力
> pip freeze > requirements.txt

# デプロイパッケージの作成
> ./build.sh

./build.sh実行後のディレクトリツリーは以下のようになり、dist配下にlambda.zipが作成されます。

./delete-aes-manual-snapshot/
├── .gitignore
├── .vscode/
│   └── settings.json
├── build.sh
├── dist/
│   └── lambda.zip
├── lambda_function.py
├── package/
├── requirements.txt
└── venv/

また、requirements.txtは以下の通りです。

./delete-aes-manual-snapshot/requirements.txt

appdirs==1.4.4
attrs==21.2.0
black==20.8b0
boto3==1.18.15
botocore==1.21.15
certifi==2021.5.30
charset-normalizer==2.0.4
click==6.7
elasticsearch==7.9.1
elasticsearch-curator==5.7.0
idna==3.2
jmespath==0.10.0
mypy-extensions==0.4.3
pathspec==0.9.0
python-dateutil==2.8.2
PyYAML==5.4.1
regex==2021.8.3
requests==2.26.0
requests-aws4auth==1.1.1
s3transfer==0.5.0
six==1.16.0
toml==0.10.2
tomli==1.2.1
typed-ast==1.4.3
typing-extensions==3.10.0.0
urllib3==1.26.6
voluptuous==0.12.1

次にLambda関数の作成です。手動スナップショットリポジトリ登録用のLambda関数と同様の手順でLambda関数を作成します。

環境変数はmanualリポジトリにある、プレフィックスがmanual-snapshot-testの2日以上前に作成されたスナップショットを削除するため、以下のように設定します。なお、AES_HOSTはAmazon ESのエンドポイントの先頭のhttps://と末尾の/を含めずに記載します。

また、スナップショットの削除にはしばらく時間がかかります。そのため、タイムアウトをデフォルトの3秒から15分に変更します。実際に運用する際は、削除対象のスナップショットが多すぎて、Lambdaのタイムアウト値の上限である15分を超えないように注意する必要があります。

定期実行の設定

スナップショットの削除を日次で実行するように、EventBridgeを使って設定をします。

delete-aes-manual-snapshotトリガーを追加をクリックします。その後、トリガーとしてEventBridgeを選択し、ルール名や説明、スケジュールを入力して、追加をクリックします。

実行してみた

それでは、Lambda関数を実行して、手動スナップショットを作成します。

テストタブからテストをクリックします。実行後、delete-aes-manual-snapshotのCloudWatch Logsを確認すると、以下のように36個のスナップショットが削除されていることが確認できます。

実行時間は約11分でした。スナップショットを1つ削除するにあたり15〜30秒ほどかかるため、Lambda関数を使って大量のスナップショットを削除する際は、タイムアウトに注意しなければならないということが分かります。

なお、スナップショットが150個ほどある状態で、再度delete-aes-manual-snapshotを実行した場合のCloudWatch Logsは以下の通りです。

snapshot_list object is empty.と表示され、削除対象のスナップショットが存在しないことが分かります。また、実行時間は約2秒でした。

手動スナップショットを使ったリストア

リストア前のインデックスの確認

それでは、作成した手動スナップショットを使ってリストアをしていきます。

その前に、リストア前のインデックスを確認します。

KibanaにてIndex Management - Indicesより、リストア対象であるCloudTrailのインデックスを確認します。

log-aws-cloudtrail-2021-08log-aws-cloudtrail-000001の2つのインデックスが存在していることが確認できます。SIEM on Amazon ESのデフォルトでは、インデックスを月単位で作成するため、実データを保存しているインデックスはlog-aws-cloudtrail-2021-08の方になります。

また、リストア前のデータ件数も確認します。Discoverより、直近3日間のCloudTrailのデータ件数は、46,683件であることが分かります。

インデックスのクローズ

続いて、リストアの前準備として、リストア対象のインデックスをクローズします。

インデックスをクローズすることで、読み取り/書き込み操作をブロックします。結果として、DashboardやDiscoverなどで検索したときに検索対象から外れます。

Amazon ES 7.1よりも古いバージョンでは、インデックスのクローズ(/index-name/_close)がサポートされていませんでした。

しかし、AWS公式ドキュメントを確認すると、Amazon ES 7.1以降から/index-name/_closeをサポートしています。2021/8/8時点のSIEM on Amazon ESのAmazon ESドメインのバージョンは7.9であるため、インデックスのクローズを行うことができます。

なお、インデックスが存在(オープン)している状態でリストアをしようとすると、以下のように「同じ名前のインデックスがクラスターに存在するためリストアできない」とのメッセージを出力して失敗します。

インデックスをクローズする際は、Dev ToolsからPOST log-aws-cloudtrail-2021-08/_closeと入力してをクリックします。すると、以下のように"acknowledged" : trueと表示され、インデックスのクローズが行えます。

インデックスのクローズ後の状態を確認します。

Discoverより、log-aws-cloudtrail-2021-08クローズ後のデータを確認すると、「検索条件に一致する結果がありません」とのメッセージが出力されました。確かにCloudTrailのインデックスがクローズされていおり、検索対象範囲外になっていることが分かります。

手動スナップショットを使ったリストア

それでは、手動スナップショットを使ってリストアします。

リストアで使用するスナップショットは、作業時点で最も新しいmanual-snapshot-test-2021-08-09t00-00-43を使います。

スナップショットの作成開始時間(start_time)が2021-08-09T00:00:43.496Zであることから、日本時間の2021/8/9 9時ごろの状態にリストアできるはずです。

スナップショットmanual-snapshot-test-2021-08-09t00-00-43を使って、インデックスlog-aws-cloudtrail-2021-08をリストアをする際のコマンドは以下の通りです。

POST _snapshot/manual/manual-snapshot-test-2021-08-09t00-00-43/_restore
{
  "indices": "log-aws-cloudtrail-2021-08"
}

コマンドの実行結果は以下の通りです。

レスポンスとして{"accepted" : true}が返ってきており、正常にリストアできていそうです。また、実行時間は約10秒でした。

Discoverより、log-aws-cloudtrail-2021-08リストア後のデータを確認すると、44,882件のデータがあることが確認できます。

リストア前のデータ件数は46,683件でしたが、これはリストア前のデータ確認時の時間と、スナップショットの作成時間が異なるためです。

表示するデータの範囲を直近3時間に変更すると、意図した通り、2021/8/9 9時ごろの状態にリストアできていることが確認できます。

手動スナップショットが必要なのであれば作成しておこう

Amazon ESのスナップショットの管理をLambda関数を使って行ってみました。

今まで、Amazon ESの手動スナップショットを作成したことはなかったですが、スナップショットはトラブル時の頼み綱でもあるので、トラブル時にスムーズに対応できるよう、このような事前検証は非常に大事だなと実感しました。

なお、この記事を書き終えた直後(2021/8/10)に、SIEM on Amazon ESのAmazon ESドメインのデフォルトのバージョンが7.10になりました。

Curatorを使う際は、本記事で紹介した通り、依存関係に気をつけてクライアントのElasticsearchのバージョンを合わせるようにしてください。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!