Amazon Elasticsearch Serviceでインデックスを作成するまで

16

10月

Amazon Elasticsearch Serviceでインデックスを作成するまで

2019年10月16日 AWS, Elasticsearch

実現したいこと

WEBアプリケーション内で全文検索をセキュアに行いたいという案件がありました。DBとして使用しているDynamoDBはクエリが弱いためAlgliaを使うかElasticsearchを使うか検討し、結果Amazon Elasticsearch Serviceの導入に決定しました。

この記事のゴール

Amazon Elasticsearch Service(AES)にインデックスを作成するところまでです。開発メモの側面もありますので読みづらい記事になっているかと思いますがご容赦いただければと思います。

ざっくりとした構成イメージ

file

この記事に書いてあること

  • AWSドメインの登録
  • Lambda関数の作成
  • IAMロール、ポリシーの作成
  • セキュリティグループの作成

書いていないこと

  • 数々のデバッグ...

流れ

以降Amazon Elasticsearch ServiceをAESと略します。

AESドメインを登録してみる

基本的には流れに沿えばドメインは作成できると思います。

  • インスタンスは必要最小限に、ミニマムスタート。
  • アベイラビリティーゾーンは2-AZ。
  • マスターインスタンスはデフォルトのまま作成。

どこでインデックスの登録テストを行うか?

テスト登録のためにどこでHTTPリクエストを叩いて検証するか迷いました。
AESはVPC内Lambda関数から呼ぶ形になりますので、外部からアクセスするのは少々面倒です。

最初はPostmanを使って試そうとしましたが、SignerV4の認証設定が面倒だったためLambda関数としました。
ではLambdaで実装するにあたり、まずロールの検討が必要です。
今回最低限必要なのはAESへのアクセス権限とCloudWatchへの権限です。
つまり、以下のようなポリシーを作成しそのポリシーを包含したロールを作成することになります。
下記例ではAESの各種権限を与えていますが、本番環境では必要な権限のみ付与するようにしましょう。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "es:ESHttpPost",
                "es:ESHttpGet",
                "es:ESHttpPut",
                "es:ESHttpDelete"
            ],
            "Resource": "arn:aws:es:ap-northeast-1:{ID}:domain/{DOMAIN}/*"
        }
    ]
}

このロールをLambda関数に与え、関数の初期設定を終えます。

Lambda関数は以下URLを参考にLambda関数として手直しします(機会があればLambda関数をGithubで公開します)。

ここでインデックスを実際に登録してみますが中々うまくいきません。AWSサポートにも問い合わせを行い、以下のアドバイスをいただきました。

  • AESのエンドポイント設定を再確認(VPC内にAESドメインを配置した場合、vpcから始まる)。
  • AESに登録したVPCにHTTPS(443)のインバウンドが設定されていない。
  • Lambda関数はVPC内に配置すること。
  • それでも駄目ならAESドメインのアクセスポリシーを一時的にフルアクセスにして問題の切り分け。

AESのエンドポイント

ドキュメントを見ると、以下のように書いてあります。
まずここで引っかかりました。
file

正解は以下のようにAESのVPC エンドポイント名が正しいです。search-も不要です。ただ、どこにも記載がなくハマりました。

{VPCドメイン名}.ap-northeast-1.es.amazonaws.com

AESドメインのアクセスポリシー

アクセスポリシー(フルアクセス)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "es:*",
      "Resource": "arn:aws:es:ap-northeast-1:{ID}:domain/{DOMAIN}/*"
    }
  ]
}

最終形態はこちら
指定ロール以外からのアクセスを遮断するというポリシーです。即ち、このロールを当てているLambda関数からのみAESドメインへアクセスができるということになります。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::{ID}:role/{Lambdaに当てたロール名}"
      },
      "Action": "es:*",
      "Resource": "arn:aws:es:ap-northeast-1:{ID}:domain/{DOMAIN}/*"
    }
  ]
}

VPC

結論、以下のようにLambda関数で設定しました。

セキュリティグループは、HTTPSのみインバウンドを有効にしたものを作成します。
この設定内容については、AES側の設定も合わせておく必要があると思います。

ここまでで、何とかインデックス登録に成功しましたのでデバッグ用の処理を戻していきましょう。

  • ポリシーを厳格に(""から"ロール"に。(ただ、EC2インスタンスからデバッグ用のcurlを叩きたかったので、開発時は""としていました。VPC内からしかアクセスできないようにしているので、EC2インスタンスにSSH接続して弄るのは検証フェーズにおいては楽ですね。)

本番稼働に向けて

実際のデータの投入テストを行います。
とりあえず以下のようなデータを入れてみましょう。

{
  "user": {
    "first_name": "テスト"
  }
}

しかしここでエラーが発生します。エラー内容が認証のことを言っています。

The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.

動的マッピングではなく手動マッピングを試してみるなど、その他考えられる範囲でデバッグしましたが結果は変わらずです。ちなみに、一度INDEXを作ると動的にマッピングを作るため、データ形式を変えるとデータが登録できなくなります。そのため、以下のコマンドでインデックスを削除しながら作業を行うのが確実です。

curl -XDELETE https://{ドメイン名}.ap-northeast-1.es.amazonaws.com/orders/?pretty=true

ちなみにマッピングを確認する方法は以下の通りです。
?pretty=trueは返却されるjsonを整形してくれます。

curl --XGET https://{ドメイン名}.ap-northeast-1.es.amazonaws.com/orders/_mapping?pretty=true

ここで、試しに以下のようにすると登録をパスすることが判明しました。全角だと通らないのでしょうか?最初に全角でトライしたのは間違いでした。

{
  "user": {
    "first_name": "aaa"
  }
}

結論としては、全角文字を含むリクエストの中にContent-Lengthがあるとエラーになります。原因はLambdaのaws-sdk v2のSignerV4に問題があるためでした。英語以外の言語をリクエストに含む場合に署名バージョン 4 の計算に問題が発生します。

回避策としてはAWS SDK 3.0(開発者プレビュー)にするか、サードパーティライブラリを使用するかのいずれかとなります。

終わりに

ここまでで、テストデータをインデックスに登録するところまでは成功しました。
本番環境に向けては、Lambda関数にAppSyncを通してデータを受け渡すようにするなど、各アプリに合わせて組み込みを進めていきましょう。