FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

Terraformを使ったAmazon OpenSearch Serverlessの構成管理

こんにちは、永倉です。
この記事はFLINTERS設立10周年ブログリレーの128日目の記事になります。

Amazon OpenSearch Serverlessが一般利用可能になってから、1年ぐらい経ちました。

今回はAmazon OpenSearch Serverlessに関連して、Terraformを使い2つのことをやってみようと思います。

  • Amazon OpenSearch Serverless コレクションと関連するリソースを作成
  • 作成したコレクションに対して、インデックス(OpenSearch index)を作成

OpenSearch Serverless コレクションやそれに関連するリソースをTerraformを使って作成する話は、AWSの公式のブログを含めていくつかあります。 しかし、その作成したコレクションに対して、Terraformを使いインデックスを作成する話はほぼ見たことがなかったため書いてみることにしました。

なお、ここではAmazon OpenSearch ServerlessやOpenSearchがどういったものなのかについては、ほとんど書いていません。Amazon OpenSearch Serverlessについては、公式ドキュメントや AWS Black Belt オンラインセミナーのAmazon OpenSearch Serverlessの資料を参照してください。 また、OpenSearchについては、公式サイトを参照してください。

使用するTerraformやTerraform Providerのバージョン

Amazon OpenSearch Serverless コレクションと関連するリソースの作成

Amazon OpenSearch Serverless コレクションと関連するリソースを作成します。

作成するのは以下の4つのリソースです。

  • コレクション
  • 暗号化ポリシー
    • コレクションにデータを保管する際のデータの暗号化についてのポリシーです
    • 今回は作成するコレクションのデータの暗号化について、AWS所有キーを使用するポリシーを設定します
  • ネットワークポリシー
    • コレクションのcollection endpoint(コレクションのOpenSearch APIにアクセスする際のエンドポイント)やdashboard endpoint(コレクションのダッシュボードにアクセスする際のエンドポイント)へのアクセス経路のポリシーです
    • 今回は作成するコレクションのcollection endpointに対して、パブリックネットワークからインターネット経由でアクセス可能とするポリシーを設定します(Access typeとしてはpublicのことです)
  • データアクセスポリシー
    • IAM UserやIAM Roleなどに対して、コレクションやインデックスに対する操作を許可するポリシーです
    • 今回は作成するコレクションに対して、Terraformを実行しているIAM Userがインデックスの作成や更新する許可を与えるポリシーを設定します

Terraformのコード

terraform-provider-aws を使い対象のリソースを作成します。

terraform-provider-aws では、AWSのリソースを操作するために認証情報が必要です。
今回の例では、AWS IAM Userを作成し、そのAWS IAM Userのアクセスキーとシークレットアクセスキーをそれぞれ環境変数 AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEYとして設定しているものとします。

terraform {
  required_providers {
    opensearch = {
      source  = "opensearch-project/opensearch"
      version = "2.2.0"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "5.31.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

variable "collection_name" {
  type        = string
  default     = "example-collection"
  description = "作成するOpenSearch Serverless コレクションの名前"
}

variable "index_name" {
  type        = string
  default     = "example-index"
  description = "OpenSearch Serverless コレクション上に作製するindexの名前"
}

# 暗号化ポリシーを作成します
resource "aws_opensearchserverless_security_policy" "example_encryption" {
  name        = "example"
  type        = "encryption"
  description = "encryption security policy"
  policy = jsonencode({
    Rules = [
      {
        Resource = [
          "collection/${var.collection_name}"
        ],
        ResourceType = "collection"
      }
    ],
    AWSOwnedKey = true
  })
}

# ネットワークポリシーを作成します
resource "aws_opensearchserverless_security_policy" "example_network" {
  name        = "example"
  type        = "network"
  description = "network security policy"
  policy = jsonencode([
    {
      Rules = [
        {
          ResourceType = "collection",
          Resource = [
            "collection/${var.collection_name}"
          ]
        }
      ],
      AllowFromPublic = true
    }
    ]
  )
}

# データアクセスポリシーでTerraformを実行しているIAMユーザーのARNを使用するために使います
data "aws_caller_identity" "current" {}

# データアクセスポリシーを作成します
resource "aws_opensearchserverless_access_policy" "example" {
  name        = "example"
  description = "example"
  type        = "data"
  policy = jsonencode([
    {
      Rules = [
        {
          ResourceType = "index",
          Resource = [
            "index/${var.collection_name}/${var.index_name}"
          ],
          Permission = [
            "aoss:CreateIndex",
            "aoss:DeleteIndex",
            "aoss:UpdateIndex",
            "aoss:DescribeIndex",
            "aoss:ReadDocument",
          ]
        }
      ],
      Principal = [
        data.aws_caller_identity.current.arn
      ]
    }
  ])
}

# AWS OpenSearch Serverless コレクション を作成します
resource "aws_opensearchserverless_collection" "example" {
  name             = var.collection_name
  description      = "example collection"
  type             = "SEARCH"
  # `aws_opensearchserverless_collection` resourceのドキュメント(https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/opensearchserverless_collection)にも書いてありますが、
  # AWS OpenSearch Serverless コレクション を作成する前に暗号化ポリシーを作成しておく必要があります。
  # 作成していない場合、terraform applyを実行すると以下のようなエラーが出ます。
  # > ValidationException: No matching security policy of encryption type found for collection name: example_collection. Please create security policy of encryption type for this collection.
  depends_on = [aws_opensearchserverless_security_policy.example_encryption]
}


output "collection_endpoint" {
  value       = aws_opensearchserverless_collection.example.collection_endpoint
  description = "作成されたAWS OpenSearch Serverless コレクション のcollection endpoint"
}

output "collection_name" {
  value       = var.collection_name
  description = "作成されたAmazon OpenSearch Serverless コレクションの名前"
}

output "index_name" {
  value       = var.index_name
  description = "作成されたインデックスの名前"
}

Amazon OpenSearch Serverless コレクションに対してインデックスを作成

作成されたAmazon OpenSearch Serverless コレクションに対してインデックスを作成します。

インデックスの設定例として、今回は日本語検索を扱うものを使います。 これは How to implement Japanese full-text search in Elasticsearch | Elastic Blogを参考にしています。

インデックスの作成にあたり、Terraform Providerとして opensearch-project/terraform-provider-opensearch を使用します。このProviderはOpenSearchのバージョン1.xと2.xどちらにも対応しており、Amazon OpenSearch Serverlessもサポートしています。

terraform-provider-opensearchでは、opensearch_index resourceでインデックスを作成します。 インデックス以外にもこのProviderで作成できるリソースはいくつもあります。詳細はドキュメントを確認してください。

なお、OpenSearchやElasticSearchのインデックス等を管理するTerraform providerとしては、他にもelastic/terraform-provider-elasticstackphillbaker/terraform-provider-elasticsearchがあります。 しかし、前者はOpenSearchをサポートしておらず、後者はOpenSearchのバージョン2.xには対応していません

Terraformのコード

terraform-provider-opensearch では、Amazon OpenSearch Serverless コレクションに対してインデックスを作成するなどの操作には認証情報が必要です。

今回の例では、terraform-provider-aws と同じように、 AWS IAM Userのアクセスキーとシークレットアクセスキーをそれぞれ環境変数AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEYとして設定しているものとします。
その他の認証方法については、ドキュメント(https://registry.terraform.io/providers/opensearch-project/opensearch/2.2.0/docs)を参照してください。

provider "opensearch" {
  # 作成されたコレクションのcollection endpointを指定します  
  url        = aws_opensearchserverless_collection.example.collection_endpoint
  aws_region = "ap-northeast-1"

  # terraformの実行時に以下のようなエラーが出て失敗することがあるため false にしています
  # > Error: HEAD healthcheck failed: This is usually due to network or permission issues. The underlying error isn't accessible, please debug by disabling healthchecks.
  healthcheck = false
}

# インデックスを作成します
resource "opensearch_index" "example" {

  name = var.index_name
  # アナライザーの設定
  analysis_analyzer = jsonencode({
    # kuromoji tokenizerを使ったアナライザーの設定
    example_kuromoji_analyzer = {
      type = "custom"
      char_filter = [
        "icu_normalizer"
      ]
      tokenizer = "kuromoji_tokenizer"
      filter = [
        "kuromoji_baseform",
        "kuromoji_part_of_speech",
        "ja_stop",
        "kuromoji_stemmer",
        "lowercase"
      ]
    }
    # N-gram tokenizerを使ったアナライザーの設定
    example_ngram_analyzer = {
      type = "custom"
      char_filter = [
        "icu_normalizer"
      ]
      tokenizer = "example_ngram_tokenizer"
      filter = [
        "lowercase"
      ]
    }
  })

  analysis_tokenizer = jsonencode({
    example_ngram_tokenizer = {
      type     = "ngram"
      min_gram = 2
      max_gram = 3
      token_chars = [
        "letter",
        "digit"
      ]
    }
  })

  # マッピングの設定
  mappings = jsonencode({
    dynamic = "strict"
    properties = {
      example_field = {
        type     = "text"
        analyzer = "example_kuromoji_analyzer"
        fields = {
          ngram = {
            type     = "text"
            analyzer = "example_ngram_analyzer"
          }
        }
      }
    }
  })

  number_of_shards = 2
  number_of_replicas = 0
}

実際にインデックスが作成されたのかを確認する

実際にインデックスが作成されたのかを確認します。 この確認は、作成したコレクションのcollection endpointに対してGet index APIを呼び出すことで実現します。

ここではAPIを呼び出す際にcurlコマンドを使います。 collection endpointでAPIを呼び出す際に、AWSの署名付きリクエストを送る必要があります。 curlコマンドの --aws-sigv4 オプションと --user オプションを使うことで署名付きリクエストが送れます。

以下のコマンドで、インデックスが作成されたのかを確認します。

# 作成したAmazon OpenSearch Serverless コレクションのcollection endpointをTerraformのoutputを使い取得します
COLLECTION_ENDPOINT="$(terraform output -raw collection_endpoint)"
# 作成されたインデックスの名前をTerraformのoutputを使い取得します
INDEX_NAME="$(terraform output -raw index_name)"

# Get index API を呼び出します
# --aws-sigv4 オプションに設定する値は https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-clients.html#serverless-signing を参考にしています
# --user オプションに設定している値はTerraformの実行時に渡していたのと同じAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY 環境変数です
curl \
--aws-sigv4 "aws:amz:ap-northeast-1:aoss" \
--user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \
"${COLLECTION_ENDPOINT}/${INDEX_NAME}"

得られたレスポンスは以下のようになります。(見やすいようにレスポンスをjqコマンドで整形しています。)

{
  "example-index": {
    "aliases": {},
    "mappings": {
      "dynamic": "strict",
      "properties": {
        "example_field": {
          "type": "text",
          "fields": {
            "ngram": {
              "type": "text",
              "analyzer": "example_ngram_analyzer"
            }
          },
          "analyzer": "example_kuromoji_analyzer"
        }
      }
    },
    "settings": {
      "index": {
        "number_of_shards": "2",
        "provided_name": "example-index",
        "creation_date": "1704868738849",
        "analysis": {
          "analyzer": {
            "example_ngram_analyzer": {
              "filter": [
                "lowercase"
              ],
              "char_filter": [
                "icu_normalizer"
              ],
              "type": "custom",
              "tokenizer": "example_ngram_tokenizer"
            },
            "example_kuromoji_analyzer": {
              "filter": [
                "kuromoji_baseform",
                "kuromoji_part_of_speech",
                "ja_stop",
                "kuromoji_stemmer",
                "lowercase"
              ],
              "char_filter": [
                "icu_normalizer"
              ],
              "type": "custom",
              "tokenizer": "kuromoji_tokenizer"
            }
          },
          "tokenizer": {
            "example_ngram_tokenizer": {
              "token_chars": [
                "letter",
                "digit"
              ],
              "min_gram": "2",
              "type": "ngram",
              "max_gram": "3"
            }
          }
        },
        "number_of_replicas": "0",
        "uuid": "ay4Y8owBZS7FqaybduCp",
        "version": {
          "created": "135217827"
        }
      }
    }
  }
}

インデックスが作成されていることが確認できました。

おわりに

Terraformを使ったAmazon OpenSearch Serverless コレクションとインデックスの作成方法について紹介しました。