OpenSearch Serverless を CDK でデプロイしてダッシュボードを操作してみた

[OpenSearch](https://opensearch.org/) はオープンソースのデータ検索および分析スイートであり、[Amazon OpenSearch Service](https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/what-is.html) は AWS 上での OpenSearch のデプロイや運用を容易にしてくれるサービスだ。

そして、[Amazon OpenSearch Serverless](https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless.html) はスケーリングやインフラ管理を自動化し、従量課金モデルでコストを最適化できるサーバーレスサービスだ。

# 背景
OpenSearch Serverless は、Amazon Bedrock ナレッジベースのベクトルストアとして利用していた。コンソールで選択するだけで自動作成されるため、特に意識したことはなかった。

そこで、CDK を使うことで、どのように作成・管理できるのか確かめたかった。

<br>
今回は

* CDK プロジェクトの作成
* OpenSearch Serverless のデプロイ
* ダッシュボードの操作

をやってみた。

# CDK プロジェクトの作成
作業ディレクトリを作成し、移動する。

```bash
mkdir opensearch-serverless-sample && cd opensearch-serverless-sample
```

CDK プロジェクトを作成する。
```bash
cdk init app --language=typescript
```


```bash
opensearch-serverless-sample/
├── ...
├── cdk.json
├── lib
│   └── opensearch-serverless-sample-stack.ts
├── ...
```

`cdk init` で作成されるファイルの内、`lib/opensearch-serverless-sample-stack.ts` と `cdk.json` を以下のように変更する。

```typescript:lib/opensearch-serverless-sample-stack.ts
import { CfnOutput, Stack, StackProps } from "aws-cdk-lib";
import {
  CfnAccessPolicy,
  CfnCollection,
  CfnSecurityPolicy,
} from "aws-cdk-lib/aws-opensearchserverless";
import { Construct } from "constructs";

export class OpensearchServerlessSampleStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const collectionName = "collection";

    const encryptionSecurityPolicy = new CfnSecurityPolicy(
      this,
      "EncryptionSecurityPolicy",
      {
        name: "encryption-policy",
        type: "encryption",
        policy: JSON.stringify({
          Rules: [
            {
              ResourceType: "collection",
              Resource: [`collection/${collectionName}`],
            },
          ],
          AWSOwnedKey: true,
        }),
      }
    );

    const networkSecurityPolicy = new CfnSecurityPolicy(
      this,
      "NetworkSecurityPolicy",
      {
        name: "network-policy",
        type: "network",
        policy: JSON.stringify([
          {
            Rules: [
              {
                ResourceType: "dashboard",
                Resource: [`collection/${collectionName}`],
              },
            ],
            AllowFromPublic: true,
          },
        ]),
      }
    );

    const dashboardUserOrRoleArn = this.node.tryGetContext('dashboardUserOrRoleArn');

    if (!dashboardUserOrRoleArn) {
      throw new Error("dashboardUserOrRoleArn required!");
    }

    const accessPolicy = new CfnAccessPolicy(this, "AccessPolicy", {
      name: "access-policy",
      type: "data",
      policy: JSON.stringify([
        {
          Rules: [
            {
              ResourceType: "collection",
              Resource: [`collection/${collectionName}`],
              Permission: [
                "aoss:*",
              ],
            },
            {
              ResourceType: "index",
              Resource: [`index/${collectionName}/*`],
              Permission: [
                "aoss:*",
              ],
            },
          ],
          Principal: [dashboardUserOrRoleArn],
        },
      ]),
    });

    const collection = new CfnCollection(this, "Collection", {
      name: collectionName,
      type: "SEARCH",
      standbyReplicas: "DISABLED",
    });
    collection.addDependency(encryptionSecurityPolicy);
    collection.addDependency(networkSecurityPolicy);
    collection.addDependency(accessPolicy);

    new CfnOutput(this, "OpenSearchDashboardEndpoint", {
      value: collection.attrDashboardEndpoint,
    });
  }
}
```

:::note warn
コレクションが作成される前に、暗号化ポリシーが作成されている必要がある。これは `collection.addDependency(encryptionSecurityPolicy)` を用いて実現している。

参考：[class CfnCollection (construct)](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchserverless.CfnCollection.html)

> You must create a matching encryption policy in order for a collection to be created successfully. You can specify the policy resource within the same CloudFormation template as the collection resource if you use the DependsOn attribute. ... Otherwise the encryption policy must already exist before you create the collection.
:::


```JSON:cdk.json
{
  ...,
  "context": {
    ...,
    "dashboardUserOrRoleArn": "<your-user-or-role-arn>"
  }
}
```

`dashboardUserOrRoleArn` は OpenSearch ダッシュボードを使用するユーザーまたはロールの ARN だ。

# OpenSearch Serverless のデプロイ
```bash
cdk bootstrap
```

```
cdk deploy
```

> ✨  Total time: 270.19s

デプロイ完了後、CloudFormation コンソールに **OpensearchServerlessSampleStack** が作成されている。**出力** タブでダッシュボードの URL を確認できる。

![2025018_0_cfn_output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/2584700c-307e-6b82-7c0c-ac45602c013e.png)

URL をクリックして OpenSearch ダッシュボードにアクセスする。



# ダッシュボードを操作してみる

今回は [チュートリアル: Amazon OpenSearch Serverless の開始方法](https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-getting-started.html#serverless-gsg-index) を参考にダッシュボードを操作してみた。

## Dev Tools を開く
**Explore on my own** をクリックする。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/a9560f05-355f-2d7b-1457-2b05d7850435.png" width="400" >

メニューを開き、**Dev Tools** をクリックする。

![20250118_2_humburger_devtools.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/8cb08b13-02ab-b03f-822f-fc4723941d36.png)

**Console** が表示される。左側が入力欄、右側が出力結果だ。

![20250118_3_dev_tools_console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/1c8462e2-abde-8017-593a-e523825e14d6.png)

## インデックスを作成

以下のリクエストを送信すると、「movies-index」というインデックスが作成される。
```
PUT movies-index
```

![20250118_4_create_index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/748a7784-ed85-8611-23c4-721e2e6e621d.png)

`GET` で確認してみる。

```
GET movies-index
```

![20250118_5_get_index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/c5e49be8-f475-07b8-43e6-82a59662677c.png)


## ドキュメントをインデックス
以下のリクエストを送信すると、「movies-index」にドキュメントがインデックスされる。

```
PUT movies-index/_doc/1
{ 
  "title": "Shawshank Redemption",
  "genre": "Drama",
  "year": 1994
}
```

![20250118_6_document_indexing.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/5b92c5e7-ed69-9bc8-25fb-637ff91e5162.png)

もう一度 `GET movies-index` する。

![20250118_7_get_index_after_document_indexed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/c841db2a-02df-0395-6c9f-d14fc82b04c3.png)

「movies-index」の **mappings** に

* properties
  * title
  * genre
  * year

が追加されているのがわかる。

## データ検索
データ検索を行うには、まずインデックスパターンを設定する必要がある。

### インデックスパターンの設定
メニューの **Stack Management** をクリックする。

![20250118_8_select_stack_management.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/a5474040-8791-87af-69e4-ca19c359a4dc.png)

**Index Patterns**、**Create index pattern** の順にクリックする。

![20250118_9_index_pattern_create_index_pattern.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/7157b4b1-05fc-7aae-c2bb-0cc1cd9d965a.png)

「movies」と入力し、**Next step** をクリックする。

![20250118_10_define_index_pattern.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/f72a246f-77ad-44f9-d775-ed0ca251dc29.png)

**Create index pattern** をクリックする。

![20250118_11_create_index_pattern.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/8517a03e-dbf4-7f17-8e38-bc17c9c21917.png)

ドキュメントの項目を確認できるようになる。

![20250118_12_document_fields.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/04bf34fe-fd18-4c59-6499-21442ce49f70.png)

### Discover

ダッシュボードの **Discover** ページでは、GUI 操作で検索できる。

![20250118_13_discover.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/efd81e2a-4773-5757-a543-365eb63162c6.png)

### Search API

コンソールで、[Search API](https://opensearch.org/docs/latest/api-reference/search/) を使用した検索を試すことも可能だ。

```
GET /movies-index/_search
{
  "query": {
    "match": {
      "genre": "Drama"
    }
  }
}
```

![20250118_14_console_search_api.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/4ca89032-9c00-8a7b-1948-7fb517ad18d1.png)

## インデックスの削除
```
DELETE movies-index
```

![20250118_15_delete_index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/a85bbad2-a41a-b094-3bff-cef39064e55b.png)

`GET movies-index` すると「404 - Not Found」が返ってくる。

![20250118_16_movies-index_not_found.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/0411f98b-1e8c-d873-472e-be5b2123fafa.png)

## クリーンアップ
```bash
cdk destroy
```

> ✅  OpensearchServerlessSampleStack: destroyed

# 感想

CDK による作成を通して、OpenSearch Serverless を構成するリソースを知ることができた。これまで Bedrock ナレッジベース作成時に自動作成されていた各種のポリシーや設定を確認する機会となり、興味深かった。CDK の使用を通して、インフラ構築をコードベースで学べる良さを感じた。

## 気になったこと
Infrastructure Composer を見ても、リソース間の関連性が良くわからない。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3816889/c31dd6c8-dd9d-241a-e537-bde6ba4aaccf.png" width="400" >

Infrastructure Composer 上で関連性が見えにくい理由は、リソース間の依存関係を [CfnResource.addDependency](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.CfnResource.html#addwbrdependencytarget) で定義していることにある。
\
Infrastructure Composer を見る機会がどれほどあるかはわからないが、CDK や CloudFormation テンプレートが長大になると、依存関係を追いにくくなるのかなと感じた。
\
こうした場合は、CloudFormation テンプレートの `DependsOn` を活用して依存関係を追う必要がありそうだ。

<br><br>
それにしても、映画が見たくなった。OpenSearch Serverless のチュートリアルで「ショーシャンクの空に」がサンプルデータとして登場したせいかもしれない。