Amazon OpenSearch Serverless を別アカウントの Lambda から操作してみた

## はじめに
本記事では、Amazon OpenSearch Serverless に対して、別アカウントの AWS Lambda からアクセスする方法を解説します。あまり一般的なユースケースではないかもしれませんが、特定の要件で必要になる場合があります。

OpenSearch Serverless は、[データアクセスポリシー](https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-data-access.html)によってアクセスできる対象 (プリンシパル) を制限します。プリンシパルに指定できるのは、IAM ユーザーや IAM ロールの ARN、もしくは SAML アイデンティティです。
しかし、[ドキュメント](https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-data-access.html#serverless-data-access-cross)に記載の通り、OpenSearch Serverless のデータアクセスポリシーは他 AWS アカウントのプリンシパルを直接追加できません。そのため、クロスアカウントでのアクセスには工夫が必要です。本記事では、IAM ロールの引き受け（AssumeRole）を利用した方法を紹介します。
> クロスアカウントデータアクセス
> クロスアカウント ID またはクロスアカウントコレクションを使用してデータアクセスポリシーを作成することはできませんが、ロールの引き受けオプションを使用してクロスアカウントアクセスを設定することもできます。例えば、account-b がアクセスする必要があるコレクションを account-a が所有している場合、account-b のユーザーは account-a でロールを引き受けることができます。ロールには IAM アクセス許可、aoss:APIAccessAll および aoss:DashboardsAccessAll があり、account-a のデータアクセスポリシーに含まれている必要があります。

## 本記事で作成するアーキテクチャと処理の流れ
以下のアーキテクチャを構築していきます。

![アーキテクチャ図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/310deb8a-401a-b916-e743-c3fc9048b2d7.png)


1. **アカウント A** にある Lambda が、**アカウント B** で作成された IAM ロールを Assume します
1. **アカウント B** の IAM ロールは OpenSearch Serverless のデータアクセスポリシーにプリンシパルとして指定されています。これにより **アカウント A** の Lambda が OpenSearch Serverless にアクセスできます


**注意 :** クロスアカウントでのアクセスを推奨するという訳ではなく、あくまで方法の一つとして紹介します。例えば、Lambda は OpenSearch Serverless と同じアカウントに配置して、Lambda の呼び出しを別アカウントから行う方が構成としてシンプルになる場合も考えられるため、要件に応じて採用するアーキテクチャを検討してください。

## やってみる
[re:Post の記事](https://repost.aws/knowledge-center/lambda-function-assume-iam-role)を参考に、クロスアカウントでの Lambda の AssumeRole を実装していきます。

2 つのアカウントを行き来するため、混乱しないように OpenSearch Serverless をデプロイするアカウントを **[AOSS アカウント]**、Lambda をデプロイするアカウントを **[Lambda アカウント]** として、各手順がどちらのアカウントでの操作であるかを示します。


### 1. [AOSS アカウント] OpenSearch Serverless コレクションの作成
1. OpenSearch Service コンソールから以下の設定でコレクションを作成します
   - コレクション名: `lambda-assume-test-collection`
   - コレクションタイプ: 検索
   - デプロイタイプ: アクティブレプリカを無効化

    ![AOSSの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/7fd2f476-4ce6-df77-808a-4827ff76e484.png)

1. 作成後に払い出されるエンドポイントは後ほど利用するのでコピーしておきます

    ![スクリーンショット 2024-12-23 18.09.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/dfc6b691-4a7e-ba37-faaf-06cf1278a6e7.png)


### 2. [AOSS アカウント] OpenSearch Serverless 用の IAM ポリシーと、IAM ロールを作成する
1. 以下のポリシーを持つ IAM ポリシーを作成します。ポリシー名は `aoss-full-access-policy` とします

    ```
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "AOSSFullAccessPolicy",
                "Effect": "Allow",
                "Action": "aoss:*",
                "Resource": "*"
            }
        ]
    }
    ```

1. 作成した許可ポリシー (`aoss-full-access-policy`) を持つ IAM ロールを作成します。信頼関係などは後ほど編集するため、ひとまず以下のように設定します
    1. 信頼されたエンティティタイプ：AWS アカウント
    1. AWSアカウント：このアカウント
        ![スクリーンショット 2024-12-23 10.59.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/851049cc-b9bf-4d1f-6d27-478ee6d5a495.png)

    1. 許可ポリシーには 先ほど作成した IAM ポリシー (`aoss-full-access-policy`) を選択し、IAM ロールを作成します。ロール名は `lambda-assume-test-role` とします。このロールの ARN は後ほど利用するので、コピーしておきます

### 3. [AOSS アカウント] 作成した IAM ロールをOpenSearch Serverless のデータアクセスポリシーに追加する
1. 作成した OpenSearch Serverless (`lambda-assume-test-collection`) のデータアクセスポリシーの編集画面から、先ほど作成した `lambda-assume-test-role` を追加します

![スクリーンショット 2024-12-23 12.31.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/d9feb22b-2590-3ee7-571f-4245d1787e80.png)

### 4. [Lambda アカウント] Lambda 関数を作成する
1. 以下の設定で Lambda を作成します
    - 関数名: `lambda-assume-test-function`
    - ランタイム: Python 3.10
    - アーキテクチャ: x86_64
    ![image (30).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/034a4df9-ec6b-c743-4b73-ff5eefca2ad0.png)

1. aws-sdk-pandas をレイヤーに追加する
    - 今回は [opensearchpy](https://opensearch.org/docs/latest/clients/python-low-level/) を利用するので、レイヤーを追加する必要があります。[aws-sdk-pandas](https://github.com/aws/aws-sdk-pandas?tab=readme-ov-file) を利用すると簡単なので、今回はこちらを使います
    - [ここ](https://aws-sdk-pandas.readthedocs.io/en/latest/layers.html)から、Python のバージョン (Python 3.10)、アーキテクチャ (x86_64)、リージョン (Lambda を作成したリージョン) に合致するレイヤーの ARN を Lambda に追加します。 ARN を使用する Lambda レイヤーの追加方法の詳細は[こちらのドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/adding-layers.html)を参照してください

1. 環境変数を以下の通り設定します
    - `AOSS_HOST` : [手順 1](#1-aoss-アカウント-opensearch-serverless-コレクションの作成) でコピーした OpenSearch Serverless のエンドポイント（先頭の `https://` を除いたもの）
    - `AOSS_REGION` : OpenSearch Serverless がデプロイされているリージョン
    - `AOSS_ACCESS_ROLE_ARN` : [手順 2](#2-aoss-アカウント-opensearch-serverless-用の-iam-ポリシーとiam-ロールを作成する) でコピーした `lambda-assume-test-role` の ARN

    ![スクリーンショット 2024-12-20 13.23.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/245fb946-4c84-87b4-c9f2-472065b41b40.png)

1. 以下のコードを `lambda_function.py` に貼り付けて Lambda をデプロイします
    ```
    from opensearchpy import OpenSearch, RequestsHttpConnection, AWSV4SignerAuth
    import os
    import boto3
    
    # OpenSearchのホスト情報
    host = os.environ["AOSS_HOST"]
    region = os.environ["AOSS_REGION"]
    service = 'aoss'
    
    # AWS STSを使用して一時的な認証情報を取得
    sts = boto3.client("sts", region_name=region)
    res = sts.assume_role(
        RoleArn=os.environ["AOSS_ACCESS_ROLE_ARN"],
        RoleSessionName="example-session",
    )
    print(res)
    
    # セッションを作成
    session = boto3.Session(
        aws_access_key_id=res["Credentials"]["AccessKeyId"],
        aws_secret_access_key=res["Credentials"]["SecretAccessKey"],
        aws_session_token=res["Credentials"]["SessionToken"],
        region_name=region
    )
    
    # OpenSearchクライアントの設定
    auth = AWSV4SignerAuth(session.get_credentials(), region, service)
    client = OpenSearch(
        hosts=[{'host': host, 'port': 443}],
        http_auth=auth,
        use_ssl=True,
        verify_certs=True,
        connection_class=RequestsHttpConnection
    )
    
    def lambda_handler(event, context):
        index_name = 'lambda-access-index'
        
        # インデックスが存在しない場合は作成
        if not client.indices.exists(index_name):
            client.indices.create(index_name,
                body={
                    "mappings": {
                        "properties": {
                            "title": {
                                "type": "text"
                            }
                        }
                    }
                }
            )
    
        # ドキュメントの投入
        document = {
            "title": "Sample Document Title"
        }
        response = client.index(
            index=index_name,
            body=document,
        )
        print("put item")
        print(response)
    
        # インデックスの検索
        query = {
            'size': 5,
            'query': {
                'match_all': {}
            }
        }
        response = client.search(
            body=query,
            index=index_name
        )
        print("search")
        print(response)
    
        return {
            'statusCode': 200,
            'body': response
        }
    ```

### 5. [Lambda アカウント] Lambda の実行ロールを編集し、AOSS アカウントで作成した IAM ロールを Assume できるようにする
1. Lambda の実行ロールを編集します。アクセス権限のページから Lambda に設定された IAM ロールを選択します

    ![スクリーンショット 2024-12-23 11.57.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/98bde1f0-342e-a8f8-0f3b-e233641f55c5.png)

1. Lambda の許可ポリシーの末尾に以下の記述を追加します。Resource は、[手順 2](#2-aoss-アカウント-opensearch-serverless-用の-iam-ポリシーとiam-ロールを作成する) でコピーした `lambda-assume-test-role` の ARN に置き換えてください

    ```
    {
        "Effect": "Allow",
        "Action": "sts:AssumeRole",
        "Resource": "arn:aws:iam::{AOSS_ACCOUNT}:role/{ROLE_NAME}"
    }
    ```

1. 編集後のポリシーは以下のようなイメージになります

    ![スクリーンショット 2024-12-23 18.19.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/b8e188e1-f53e-20e8-4b0a-a9efbffb8729.png)


1. Lambda 実行ロールの ARN は次の手順で利用するので、コピーしておきます

### 6. [AOSS アカウント] IAM ロールの信頼関係に Lambda 実行ロールを追加する
1. `lambda-assume-test-role` の信頼関係は以下のようになっているはずです。これを編集していきます

    ![スクリーンショット 2024-12-23 12.23.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/1db3918d-35ec-c980-db86-534ed492895f.png)

1. 「信頼ポリシーを編集」から、信頼ポリシーを以下の通り変更します。Principal は、[手順 5](#5-lambda-アカウント-lambda-の実行ロールを編集しaoss-アカウントで作成した-iam-ロールを-assume-できるようにする) でコピーした Lambda 実行ロールの ARN に置き換えてください

    ```
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {
                    "AWS": "arn:aws:iam::{LAMBDA_ACCOUNT}:role/service-role/{LAMBDA_ROLE_NAME}"
                },
                "Action": "sts:AssumeRole"
            }
        ]
    }
    ```

### 7. [Lambda アカウント] Lambda を実行して動作確認する
1. Lambda コンソールから関数をテストします。この関数は、作成した OpenSearch Serverless に対して以下の操作を順番に実施します
    1. `lambda-access-index` という名前のインデックスを作成
    2. サンプルのドキュメントを 1 つ書き込む
    3. 書き込んだドキュメントを検索する

1. 上手く設定出来ていれば以下のように別アカウントの OpenSearch Serverless に対するクエリ結果が返ってきます！タイムアウトエラーが発生する場合はタイムアウトを 30 秒ほどに伸ばして再度試してみてください

![スクリーンショット 2024-12-23 12.51.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484583/a060bdf2-41ec-ba56-3ce2-f2b6cefd1cef.png)

## まとめ
今回は別アカウントの Lambda から OpenSearch Serverless を操作してみました。レアなケースではありますが、参考になれば幸いです。

## 参考文献
- https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-data-access.html
- https://repost.aws/knowledge-center/lambda-function-assume-iam-role
- https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-clients.html#serverless-python