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はCfnCollection
のattrArn
プロパティにアクセスすることで取得が可能となりますので、それを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”: “テストテスト”
}
}
}
]
}
}