Serverless Operations, inc

>_cd /blog/id_7-i4gem1c

title

AWS CDKを使ってAmazon OpenSearch Serverlessをデプロイし、AWS Lambdaから接続する

summary

Amazon OpenSearch ServerlessはAmazon OpenSearch Service のオンデマンドのサーバーレスオプションです。クラスタ管理が不要で大量の検索やログ分析、メトリクス分析などをスケーラブルに実現できます。今回の記事ではAWS CDKを使ってAWS LmabdaとAmazon OpenSearch ServerlessをデプロイしてLambdaファンクションからOpenSearch Serverlessに接続を行ってみましょう。

Amazon OpenSearch Serverlessのデプロイ

まずはOpenSearch ServerlessのcollectionをCDKでデプロイしましょう。collectionとはデータとその処理リソースをグループ化する論理単位です。クラスターベースの OpenSearch における「クラスター」に近い概念ですが、サーバーレス向けに最適化された分離単位です。OpenSearch Serverlessではcollectionの配下に必要なindexを定義していくことになります。

import * as cdk from ‘aws-cdk-lib’
import type { Construct } from ‘constructs’
import * as openSearchServerless from ‘aws-cdk-lib/aws-opensearchserverless’

export interface OssProps extends cdk.StackProps {
  app_name: string
  env_name: string
  env: {
    region: string
  }
}

export class SearchOpenSearchServerlessStack extends cdk.Stack {
  public readonly collection: openSearchServerless.CfnCollection

  constructor(scope: Construct, id: string, props: OssProps) {
    super(scope, id, props)

    const collectionName = `${props.app_name}-${props.env_name}-collection`

    // コレクション作成
    this.collection = new openSearchServerless.CfnCollection(
      this,
      collectionName,
      {
        name: collectionName,
        type: ‘VECTORSEARCH’,
        standbyReplicas: ‘DISABLED’,
      },
    )

    // IAMロール/ポリシー(アクセス制御用)
    const dataAccessPolicy = new openSearchServerless.CfnAccessPolicy(
      this,
      `${props.app_name}-${props.env_name}-idx-policy`,
      {
        name: `${props.app_name}-${props.env_name}-idx-policy`,
        type: ‘data’,
        policy: JSON.stringify([
          {
            Rules: [
              {
                ResourceType: ‘index’,
                Resource: [`index/${collectionName}/*`],
                Permission: [
                  ‘aoss:CreateIndex’,
                  ‘aoss:UpdateIndex’,
                  ‘aoss:DescribeIndex’,
                  ‘aoss:WriteDocument’,
                  ‘aoss:ReadDocument’,
                ],
              },
            ],
            Principal: [
              `arn:aws:iam::${cdk.Stack.of(this).account}:role/${props.app_name}-${props.env_name}-oss-role`,
            ],
          },
        ]),
      },
    )

    const dataEncryptionPolicy = new openSearchServerless.CfnSecurityPolicy(
      this,
      `${props.app_name}-${props.env}-enc-policy`,
      {
        name: `${props.app_name}-${props.env_name}-enc-policy`,
        type: ‘encryption’,
        policy: JSON.stringify({
          Rules: [
            {
              ResourceType: ‘collection’,
              Resource: [`collection/${collectionName}`],
            },
          ],
          AWSOwnedKey: true,
        }),
      },
    )

    const networkPolicy = new openSearchServerless.CfnSecurityPolicy(
      this,
      `${props.app_name}-${props.env_name}-network-policy`,
      {
        name: `${props.app_name}-${props.env_name}-network-policy`,
        type: ‘network’,
        policy: JSON.stringify([
          {
            Rules: [
              {
                ResourceType: ‘collection’,
                Resource: [`collection/${collectionName}`],
              },
              {
                ResourceType: ‘dashboard’,
                Resource: [`collection/${collectionName}`],
              },
            ],
            AllowFromPublic: true,
          },
        ]),
      },
    )

    this.collection.addDependency(dataAccessPolicy)
    this.collection.addDependency(dataEncryptionPolicy)
    this.collection.addDependency(networkPolicy)
  }
}

ポイントとしては、アクセスポリシー、暗号化のポリシー、ネットワークポリシーの設定が必要になるところではないでしょうか。アクセスポリシーにはAWS LambdaにアタッチするIAM RoleのArnを指定するようにしましょう。今回の例では、AWSOwnedKey: true を暗号化ポリシーに指定して、AWS自身が内部的に管理するキーを使用するように指定しています。また、ネットワークポリシーでAllowFromPublic: true を指定しているのでインターネットからのアクセスを許可しています。ここをfalse にしていすることでインターネットからのアクセスを禁止することができ、VPCを経由したアクセスのみが可能となります。

AWS Lambdaのデプロイ

以下がAWS LambdaのCDKスタックです。ポイントになるのはここでもポリシーです。AWS LambdaからOpenSearch ServerlessのエンドポイントURLへリクエストが遅れるようにaoss:APIAccessAll という権限を許可するようにしましょう。らOpenSearch ServerlessのエンドポイントへのARNはCfnCollectionattrArnプロパティにアクセスすることで取得が可能となりますので、それをresourcesに指定します。

import * as cdk from ‘aws-cdk-lib’
import type { Construct } from ‘constructs’
import type * as opensearchserverless from ‘aws-cdk-lib/aws-opensearchserverless’
import { NodejsFunction } from ‘aws-cdk-lib/aws-lambda-nodejs’
import * as lambda from ‘aws-cdk-lib/aws-lambda’
import * as iam from ‘aws-cdk-lib/aws-iam’

export interface SfProps extends cdk.StackProps {
  app_name: string
  env_name: string
  openSearchServerless: opensearchserverless.CfnCollection
  env: {
    region: string
  }
}

export class LambdaFunctionsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: SfProps) {
    super(scope, id, props)

    // LambdaにアタッチするIAM Role
    const ossFunctionRole = new iam.Role(
        this,
        `${props.app_name}-${props.env_name}-oss-role`,
        {
          assumedBy: new iam.ServicePrincipal(‘lambda.amazonaws.com’),
          roleName: `${props.app_name}-${props.env_name}-oss-role`,
          description: ‘Lambda role for ossFunction’,
          inlinePolicies: {
            LambdaPolicy: new iam.PolicyDocument({
              statements: [
                new iam.PolicyStatement({
                  effect: iam.Effect.ALLOW,
                  actions: [
                    ‘logs:CreateLogGroup’,
                    ‘logs:CreateLogStream’,
                    ‘logs:PutLogEvents’,
                  ],
                  resources: [‘*’],
                }),
                new iam.PolicyStatement({
                  effect: iam.Effect.ALLOW,
                  actions: [‘aoss:APIAccessAll’],
                  resources: [props.openSearchServerless.attrArn],
                }),
              ],
            }),
          },
        },
      )

    // OpenSearchServerlessに接続するファンクション
    const Function = new NodejsFunction(
      this,
      `${props.app_name}-${props.env_name}-oss-function`,
      {
        functionName: `${props.app_name}-${props.env_name}-oss-function`,
        runtime: lambda.Runtime.NODEJS_22_X,
        entry: ‘src/OssFunction.ts’,
        handler: ‘handler’,
        environment: {
          OPENSEARCHSERVERLESS_URL: props.openSearchServerless.attrId,
          NODE_OPTIONS: ‘--enable-source-maps’,
        },
        memorySize: 128,
        timeout: cdk.Duration.minutes(15),
        logRetention: 3,
        role: ossFunctionRole,
        bundling: {
          forceDockerBundling: false,
          sourceMap: true,
        },
      },
    )
  }
}

Lambdaファンクションの実装

LambdaからOpenSearch ServerlessにデータをPUTして、それをすぐに検索して取り出すファンクションを実装しています。なお、OpenSearch Serverlessはidを用いたデータのやり取りをサポートしていません。クライアント側からid指定してデータを投入したり、idを指定してデータをGETするといったことはできなくなっています。

また、エンドポイントへの認証はAWS Signature Version 4署名リクエストを作成する必要があります。opensearchのライブラリが署名の作成までサポートしてくれるので、これでクライアントを作成し、OpenSearch Serverlessにアクセスできるようにします。

import { defaultProvider } from ‘@aws-sdk/credential-provider-node’
import { Client } from ‘@opensearch-project/opensearch’
import { AwsSigv4Signer } from ‘@opensearch-project/opensearch/aws’
import type { Handler } from ‘aws-lambda’

const ossClient = new Client({
  ...AwsSigv4Signer({
    region: process.env.AWS_REGION,
    service: ‘aoss’,
    getCredentials: () => {
      const credentialsProvider = defaultProvider()
      return credentialsProvider()
    },
  }),
  node: `https://${process.env.OPENSEARCHSERVERLESS_URL}.${process.env.AWS_REGION}.aoss.amazonaws.com`,
})


export const handler: Handler = async (event) => {
    const indexName = ‘test-idex’
    const document = {
      document: {
        title: ‘タイトル‘,
        content: ‘テストテスト’
      },
    }
  
    // ドキュメントを登録 (put)
    const putResponse = await ossClient.index({
      index: indexName,
      body: document
    })
  
    console.log(‘Indexed document:’, putResponse)
  
    // ドキュメントを取得 (search)
    const searchResponse = await ossClient.search({
      index: indexName,
      body: {
        query: {
          match: {
            ‘document.title’: ‘タイトル’,
          },
        },
      },
    })
  
    console.log(‘Retrieved document:’, JSON.stringify(searchResponse.body))
}

このLambdaファンクションをデプロイして実行すると、以下のように登録したデータを取り出すことが出来ました。

{
    “took”: 39,
    “timed_out”: false,
    “_shards”: {
        “total”: 0,
        “successful”: 0,
        “skipped”: 0,
        “failed”: 0
    },
    “hits”: {
        “total”: {
            “value”: 1,
            “relation”: “eq”
        },
        “max_score”: 0.2876821,
        “hits”: [
            {
                “_index”: “test-idx”,
                “_id”: “1%3A0%3ASHTcMpYBvyhEBRYK6Yyl”,
                “_score”: 0.2876821,
                “_source”: {
                    “document”: {
                        “title”: “タイトル“,
                        “content”: “テストテスト”
                    }
                }
            }
        ]
    }
}

Written by
CEO

堀家 隆宏

Takahiro Horike

  • Facebook->
  • X->
  • GitHub->

Share

Facebook->X->
Back
to list
<-