🔎

AWS OpenSearch Serverless を Lambda から使ってみる

2023/04/18に公開

AWS OpenSearch Serverless

先日、一般公開された Amazon OpenSearch Serverless を AWS Lambda(Golang) から使ってみました。
OpenSearch とは AWS 版の Elasticsearch です。Serverless ということなのでインフラ管理のほとんどを AWS 側に任せることができるサービスになっています。
この記事では AWS Lambda から OpenSearch Serverless にアクセスしてドキュメントを検索するところまで実装してみました。こちらで用意したサンプルリポジトリの内容に沿って進めていきます。

OpenSearch Serverless のコレクション作成

いままでの OpenSearch はドメインという名前でインスタンスを作成していましたが、サーバーレス版だとコレクションという名前になっています。
他にもセキュリティ用のポリシーをいくつか設定する必要があるようです。今回は以下のポリシーを使いました。

  • データアクセスポリシー - データアクセスポリシーはインデックス作成やドキュメント検索など OpenSearch の操作の権限を管理します。
  • 暗号化ポリシー - 保管したデータを暗号化して保護するための設定です。コレクションを作成するときは有効な暗号化ポリシーがないといけません。
  • ネットワークポリシー - ダッシュボードやエンドポイントへのアクセスを制限できます。インターネット経由でアクセス可能にするか、VPC エンドポイントを使ったプライベートアクセスに制限するか選択できます。

これらのポリシーをコレクションに紐づけることで OpenSearch Serverless を使う準備ができます。

まずは CloudFormation でコレクションやポリシー、Lambda 用のロールを作成します。各ポリシーは緩めの設定になっているのでプロダクト環境で使用する場合は適宜変更してください。

make deploy-stack
template.yaml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  SampleCollection:
    Type: 'AWS::OpenSearchServerless::Collection'
    Properties:
      Name: sample-collection
      Type: SEARCH
    DependsOn: SampleEncryptionPolicy
  SampleEncryptionPolicy:
    Type: 'AWS::OpenSearchServerless::SecurityPolicy'
    Properties:
      Name: sample-encryption-policy
      Type: encryption
      Policy: >-
        {
          "Rules":[
            {"ResourceType":"collection","Resource":["collection/sample-collection"]}
          ],
          "AWSOwnedKey":true
        }
  SampleNetworkPolicy:
    Type: 'AWS::OpenSearchServerless::SecurityPolicy'
    Properties:
      Name: sample-network-policy
      Type: network
      Policy: >-
        [
          {
            "Rules":[
              {"ResourceType":"collection","Resource":["collection/sample-collection"]}
            ],
            "AllowFromPublic":true
          }
        ]
  SampleAccessPolicy:
    Type: 'AWS::OpenSearchServerless::AccessPolicy'
    Properties:
      Name: sample-access-policy
      Type: data
      Policy: !Sub >-
        [
          {
            "Rules":[
              {"ResourceType":"index","Resource":["index/sample-collection/*"],"Permission":["aoss:*"]},
              {"ResourceType":"collection","Resource":["collection/sample-collection"],"Permission":["aoss:*"]}
            ],
            "Principal":["arn:aws:iam::${AWS::AccountId}:role/sample-aoss-role"]
          }
        ]
  SampleAossRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: sample-aoss-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
Outputs:
  Endpoint:
    Value: !GetAtt SampleCollection.CollectionEndpoint
  Role:
    Value: !GetAtt SampleAossRole.Arn
  • コレクション

  • データアクセスポリシー

  • ネットワークポリシー

AWS Lambda からアクセスする

次に OpenSearch Serverless へアクセスする AWS Lambda をデプロイします。
Makefile の変数を自分の環境向けに更新した後 make deploy-lambda でデプロイできます。

Makefile
AossEndpoint:=https://xxxxxxxxxxxxxxxxxxxx.ap-northeast-1.aoss.amazonaws.com
LambdaRole:=arn:aws:iam::123456789012:role/sample-aoss-role
make deploy-lambda

Lambda のコードは Golang で書きました。github.com/opensearch-project/opensearch-go/v2 パッケージがサーバーレスに対応しているようなので利用しています。実行している内容は以下のようになります。

  1. OpenSearch クライアント生成
  2. test-index の ID=1 に {"title": "タイトル"} という内容のドキュメント追加
  3. test-index の ID=1 のドキュメントを取得してレスポンスとして返す
main.go
package main

import (
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go-v2/config"
	search "github.com/opensearch-project/opensearch-go/v2"
	searchapi "github.com/opensearch-project/opensearch-go/v2/opensearchapi"
	requestsigner "github.com/opensearch-project/opensearch-go/v2/signer/awsv2"
)

func main() {
	lambda.Start(handler)
}

func handler(ctx context.Context) (string, error) {
	endpoint := os.Getenv("AOSS_ENDPOINT")
	if endpoint == "" {
		return "", errors.New("AOSS_ENDPOINT is empty")
	}

	awsCfg, err := config.LoadDefaultConfig(ctx)
	if err != nil {
		return "", err
	}

	signer, err := requestsigner.NewSignerWithService(awsCfg, "aoss")
	if err != nil {
		return "", err
	}

	client, err := search.NewClient(search.Config{
		Addresses: []string{endpoint},
		Signer:    signer,
	})
	if err != nil {
		return "", err
	}

	indexReq := searchapi.IndexRequest{
		Index:      "test-index",
		DocumentID: "1",
		Body:       strings.NewReader(`{"title": "タイトル"}`),
	}
	_, err = indexReq.Do(ctx, client)
	if err != nil {
		return "", err
	}

	getReq := searchapi.GetRequest{
		Index:      "test-index",
		DocumentID: "1",
	}
	getRes, err := getReq.Do(ctx, client)
	if err != nil {
		return "", err
	}
	defer getRes.Body.Close()
	if getRes.StatusCode != http.StatusOK {
		return "", fmt.Errorf("StatusCode=%d", getRes.StatusCode)
	}
	body, err := ioutil.ReadAll(getRes.Body)
	if err != nil {
		return "", err
	}
	return string(body), nil
}

Lambda がデプロイできたら実行してみます。実行すると Lambda のレスポンス内容が response.json に出力されます。

make invoke
response.json
"{\"_index\":\"test-index\", ... ,\"_source\":{\"title\": \"タイトル\"}}"

リソース削除

これまでデプロイしたリソースを make delete で削除できます。

make delete

参考

OpenSearch Documentation
OpenSearch Go Client

株式会社ROBONの技術ブログ

Discussion