API Gateway + Lambda + Amazon Elasticserach ServiceによるオートコンプリートAPI構築

下條です。
この度、Elasticsearchを利用したオートコンプリート的なAPIの開発をする機会がありました。ElasticsearchとしてはAWSマネージドのAmazon Elasticsearch Serviceを基本とし、API GatewayおよびLambda経由でAmazon Elasticsearch Serviceにアクセスする構成としました。今回はAWSのインフラ的な部分に焦点を当てて構築時のポイントについてご紹介したいと思います。実際のオートコンプリートを実現するにあたってはElasticsearchのマッピング定義をいろいろと検討しなければなりませんが、今回はElasticsearch自体の話はいたしません。

構成について

Amazon Elasticsearch Serviceを直接インターネットに公開するとElasticsearchの中身が外部からいじられたい放題になってしまうため、一般的に何らかのアクセス制限をする必要があります。
実際、Amazon Elasticsearch Serviceの設定でグローバルに開放するポリシーにしようとすると、以下のような警告が表示されます。

それを解決するため、今回はElasticsearchの前段にAPI GatewayおよびLambdaを経由させ、必要なエンドポイントのみへのアクセスを実現することにしました。また、Elasticsearch自体のリクエスト・レスポンスのインタフェース形式は外から見て保持されるようにしました。
以下に各サービスの設定ポイントについて記載します。

Amazon Elasticsearch Service

Amazon Elasticsearch Serviceは非常に簡単にElasticsearchクラスタを構築できてしまうので特筆すべきポイントはあまりありません。
今回はLambdaにIAMロールを割り当てて、Lambdaからのアクセスのみ許可するため、AmazonESFullAccessポリシーをアタッチしたIAMロール作成した上で、アクセス制限ではそのIAM Roleを指定します。

API Gateway

  • 「Lambda プロキシ統合の使用」オプションを設定します。
  • API Gatewayで独自ドメインを設定する際にACM証明書を利用する際には、us-east-1リージョンで証明書をインポートする必要があることにも注意です。

Lambda

ポイントは大きく2つあります。

  • 前述したようにLambdaにはIAMロールを付与しますが、Amazon Elasticsearch Serviceへのアクセスには明示的に署名を付ける必要があります。

  • API GatewayでLambdaをプロキシ統合として利用する場合、Lambdaからのレスポンスの形式を以下のようにする必要があります。

1
{
    "statusCode": httpStatusCode,
    "headers": { "headerName": "headerValue", ... },
    "body": "..."
}

これは情報がいろいろあって、
http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html
http://www.awslessons.com/2017/lambda-api-gateway-internal-server-error/
などをご参照ください。

上記を踏まえ、LambdaのコードはPythonの場合以下のような感じとなります。

1
import json
from botocore.awsrequest import AWSRequest
from botocore.auth import SigV4Auth
from botocore.endpoint import BotocoreHTTPSession
from botocore.credentials import Credentials
 
ES_ENDPOINT = "https://xxxxxxxxxxxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com/some_index/some_mapping/_search?pretty"

def lambda_handler(event, context):
    credentials = Credentials(
        os.environ["AWS_ACCESS_KEY_ID"],
        os.environ["AWS_SECRET_ACCESS_KEY"],
        os.environ["AWS_SESSION_TOKEN"])

    response = request(ES_ENDPOINT, "POST", credentials, 'es', event['body'].encode("utf-8"))

    res= {
        'statusCode': 200,
        'headers': { 'Content-Type': 'application/json' },
        'body': response.text
    }
    return res


def request(url, method, credentials, service_name, data, headers=None):
    region = os.environ["AWS_REGION"]
    aws_request = AWSRequest(url=url, method=method, headers=headers, data=data)
    SigV4Auth(credentials, service_name, region).add_auth(aws_request)
    return BotocoreHTTPSession().send(aws_request.prepare())

まとめ

以上、簡単にAPI Gateway + Lambda + AWS Elasticserach Serviceの設定をまとめました。こう書いてみるとそこまでハマりどころはないのですが、当初Lambdaから署名の送信を忘れていたり、bodyをエンコードするのを忘れた故にAmazon Elasticsearch Serviceからアクセス拒否をされたり、実はいろいろとハマりました。
今回はElasticsearchのマッピング定義などの話は書きませんでしたが、またの機会に書ければと思います。

このエントリーをはてなブックマークに追加