Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions agentcore-opensearch-serverless-nextgen-cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.js
*.d.ts
node_modules
cdk.out
cdk.context.json
package-lock.json
99 changes: 99 additions & 0 deletions agentcore-opensearch-serverless-nextgen-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Amazon Bedrock AgentCore with Amazon OpenSearch Serverless NextGen

This pattern deploys an Amazon Bedrock AgentCore Runtime running a containerized Python Strands agent that searches an Amazon OpenSearch Serverless NextGen collection using opensearch-py.

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/agentcore-opensearch-serverless-nextgen-cdk

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

## Requirements

- [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed
- [Node.js 20+](https://nodejs.org/en/download/) installed
- [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed (`npm install -g aws-cdk`)
- [Docker](https://docs.docker.com/get-docker/) installed and running
- CDK bootstrapped in your target account and region: `cdk bootstrap aws://ACCOUNT-NUMBER/REGION`

## Supported Regions

Amazon Bedrock AgentCore is available in: **us-east-1**, **us-west-2**, **eu-central-1**, **ap-southeast-2**.

## Deployment Instructions

1. Clone the repository and navigate to the pattern directory:
```bash
git clone https://github.com/aws-samples/serverless-patterns
cd serverless-patterns/agentcore-opensearch-serverless-nextgen-cdk
```

2. Install dependencies:
```bash
npm install
```

3. Build and push the agent Docker image to ECR:
```bash
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export AWS_REGION=us-east-1

# Create ECR repo (CDK will also create it, but we need it for the initial push)
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com

# Build and push
cd src/agent
docker build -t agentcore-opensearch-agent .
docker tag agentcore-opensearch-agent:latest $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/agentcore-opensearch-agent:latest
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/agentcore-opensearch-agent:latest
cd ../..
```

4. Deploy the stack:
```bash
export CDK_DEFAULT_ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
export CDK_DEFAULT_REGION=us-east-1
cdk deploy
```

## How it works

- **Amazon OpenSearch Serverless NextGen** collection group with scale-to-zero provides a SEARCH-type collection for document storage and retrieval.
- **Amazon Bedrock AgentCore Runtime** hosts a containerized Python agent built with the Strands Agents framework.
- The agent uses the `search_documents` tool backed by opensearch-py to query the OpenSearch Serverless collection.
- IAM roles grant the runtime permissions to invoke Amazon Bedrock models (Claude Sonnet) and access the OpenSearch Serverless collection via `aoss:APIAccessAll`.

## Testing

After deployment, index a sample document into the OpenSearch collection:

```bash
COLLECTION_ENDPOINT=$(aws cloudformation describe-stacks --stack-name AgentcoreOpensearchServerlessNextgenStack --query 'Stacks[0].Outputs[?OutputKey==`CollectionEndpoint`].OutputValue' --output text)

curl -X PUT "$COLLECTION_ENDPOINT/documents" -H "Content-Type: application/json" --aws-sigv4 "aws:amz:us-east-1:aoss" --user "" -d '{}'

curl -X POST "$COLLECTION_ENDPOINT/documents/_doc" -H "Content-Type: application/json" --aws-sigv4 "aws:amz:us-east-1:aoss" --user "" -d '{"title": "Hello World", "content": "This is a test document for the AgentCore search agent."}'
```

Then invoke the AgentCore Runtime endpoint to test the search agent.

## Cleanup

⚠️ **Warning**: Deleting the stack will permanently remove the OpenSearch Serverless collection and all indexed data. The ECR repository and all pushed images will also be deleted.

```bash
cdk destroy
```

## Architecture

```
User → AgentCore Runtime → Strands Agent → opensearch-py → OpenSearch Serverless NextGen Collection
Bedrock (Claude Sonnet) for reasoning
```

----
Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
15 changes: 15 additions & 0 deletions agentcore-opensearch-serverless-nextgen-cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { AgentcoreOpensearchStack } from '../lib/agentcore-opensearch-stack';

if (!process.env.CDK_DEFAULT_ACCOUNT) {
throw new Error('CDK_DEFAULT_ACCOUNT environment variable is required. Run: export CDK_DEFAULT_ACCOUNT=$(aws sts get-caller-identity --query Account --output text)');
}

const app = new cdk.App();
new AgentcoreOpensearchStack(app, 'AgentcoreOpensearchServerlessNextgenStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'us-east-1',
},
});
7 changes: 7 additions & 0 deletions agentcore-opensearch-serverless-nextgen-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"watch": {
"include": ["**"],
"exclude": ["node_modules", "cdk.out", "**/*.js", "**/*.d.ts"]
}
}
43 changes: 43 additions & 0 deletions agentcore-opensearch-serverless-nextgen-cdk/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"title": "Amazon Bedrock AgentCore with Amazon OpenSearch Serverless NextGen",
"description": "Deploy a Bedrock AgentCore Runtime running a Python Strands agent that searches an OpenSearch Serverless NextGen collection.",
"language": "TypeScript",
"level": "300",
"framework": "AWS CDK",
"introBox": {
"headline": "How it works",
"text": [
"This pattern deploys an Amazon Bedrock AgentCore Runtime with a containerized Python Strands agent.",
"The agent uses opensearch-py to search an Amazon OpenSearch Serverless NextGen collection (SEARCH type).",
"The CDK stack provisions the OpenSearch Serverless NextGen collection group, collection, security policies, ECR repository, IAM role, and AgentCore Runtime."
]
},
"gitHub": {
"template": {
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/agentcore-opensearch-serverless-nextgen-cdk",
"templateURL": "serverless-patterns/agentcore-opensearch-serverless-nextgen-cdk",
"projectFolder": "agentcore-opensearch-serverless-nextgen-cdk",
"templateFile": "lib/agentcore-opensearch-stack.ts"
}
},
"resources": {
"bullets": [
{ "text": "Amazon Bedrock AgentCore", "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html" },
{ "text": "Amazon OpenSearch Serverless", "link": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless.html" }
]
},
"deploy": {
"text": ["cdk deploy"],
"code": ["cdk deploy"]
},
"cleanup": {
"text": ["cdk destroy"],
"code": ["cdk destroy"]
},
"authors": [
{
"name": "Nithin Chandran R",
"bio": "Cloud Application Architect at AWS"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as opensearchserverless from 'aws-cdk-lib/aws-opensearchserverless';

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

const collectionName = 'agentcore-search';
const collectionGroupName = 'agentcore-search-group';

// OpenSearch Serverless NextGen Collection Group
const collectionGroup = new opensearchserverless.CfnCollectionGroup(this, 'CollectionGroup', {
name: collectionGroupName,
description: 'NextGen collection group for AgentCore search',
standbyReplicas: 'DISABLED',
});
// CollectionGroup itself implies NextGen architecture

// Encryption policy
const encryptionPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'EncryptionPolicy', {
name: 'agentcore-search-enc',
type: 'encryption',
policy: JSON.stringify({
Rules: [{ ResourceType: 'collection', Resource: [`collection/${collectionName}`] }],
AWSOwnedKey: true,
}),
});

// Network policy
const networkPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'NetworkPolicy', {
name: 'agentcore-search-net',
type: 'network',
policy: JSON.stringify([{
Rules: [
{ ResourceType: 'collection', Resource: [`collection/${collectionName}`] },
{ ResourceType: 'dashboard', Resource: [`collection/${collectionName}`] },
],
AllowFromPublic: true,
}]),
});

// OpenSearch Serverless Collection
const collection = new opensearchserverless.CfnCollection(this, 'Collection', {
name: collectionName,
type: 'SEARCH',
description: 'Search collection for AgentCore agent',
});
collection.addDependency(encryptionPolicy);
collection.addDependency(networkPolicy);
collection.addDependency(collectionGroup);

// ECR Repository (pre-created, imported)
const ecrRepo = ecr.Repository.fromRepositoryName(this, 'AgentEcrRepo', 'agentcore-opensearch-agent');

// IAM Role for AgentCore Runtime (inlinePolicies ensures permissions exist before AgentCore validates)
const runtimeRole = new iam.Role(this, 'AgentCoreRuntimeRole', {
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Role for AgentCore Runtime to access Bedrock and OpenSearch',
inlinePolicies: {
AgentCorePermissions: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['bedrock:InvokeModel'],
resources: ['arn:aws:bedrock:*::foundation-model/*'],
}),
new iam.PolicyStatement({
actions: ['aoss:APIAccessAll'],
resources: ['*'],
}),
new iam.PolicyStatement({
actions: ['ecr:GetAuthorizationToken'],
resources: ['*'],
}),
new iam.PolicyStatement({
actions: ['ecr:BatchGetImage', 'ecr:GetDownloadUrlForLayer'],
resources: [ecrRepo.repositoryArn],
}),
],
}),
},
});

// Data access policy
new opensearchserverless.CfnAccessPolicy(this, 'DataAccessPolicy', {
name: 'agentcore-search-access',
type: 'data',
policy: JSON.stringify([{
Rules: [
{ ResourceType: 'collection', Resource: [`collection/${collectionName}`], Permission: ['aoss:*'] },
{ ResourceType: 'index', Resource: [`index/${collectionName}/*`], Permission: ['aoss:*'] },
],
Principal: [runtimeRole.roleArn],
}]),
});

// AgentCore Runtime
const containerUri = cdk.Fn.sub(
'${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/agentcore-opensearch-agent:latest'
);

new cdk.CfnResource(this, 'AgentCoreRuntime', {
type: 'AWS::BedrockAgentCore::Runtime',
properties: {
AgentRuntimeName: 'agentcore_opensearch_agent',
AgentRuntimeArtifact: {
ContainerConfiguration: {
ContainerUri: containerUri,
},
},
NetworkConfiguration: {
NetworkMode: 'PUBLIC',
},
RoleArn: runtimeRole.roleArn,
},
});

// Outputs
new cdk.CfnOutput(this, 'CollectionEndpoint', {
value: collection.attrCollectionEndpoint,
description: 'OpenSearch Serverless Collection Endpoint',
});

new cdk.CfnOutput(this, 'EcrRepositoryUri', {
value: ecrRepo.repositoryUri,
description: 'ECR Repository URI for agent Docker image',
});
}
}
20 changes: 20 additions & 0 deletions agentcore-opensearch-serverless-nextgen-cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "agentcore-opensearch-serverless-nextgen-cdk",
"version": "1.0.0",
"bin": {
"app": "bin/app.ts"
},
"scripts": {
"build": "tsc",
"cdk": "cdk"
},
"devDependencies": {
"typescript": "~5.4.0",
"ts-node": "^10.9.0",
"aws-cdk": "^2.258.0"
},
"dependencies": {
"aws-cdk-lib": "^2.258.0",
"constructs": "^10.0.0"
}
}
10 changes: 10 additions & 0 deletions agentcore-opensearch-serverless-nextgen-cdk/src/agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM --platform=linux/arm64 public.ecr.aws/lambda/python:3.12

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY agent.py .

CMD ["python", "agent.py"]
58 changes: 58 additions & 0 deletions agentcore-opensearch-serverless-nextgen-cdk/src/agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import os
import boto3
from opensearchpy import OpenSearch, RequestsHttpConnection
from requests_aws4auth import AWS4Auth
from strands import Agent, tool

region = os.environ.get("AWS_REGION", "us-east-1")
collection_endpoint = os.environ["COLLECTION_ENDPOINT"]
host = collection_endpoint.replace("https://", "")

credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(
credentials.access_key,
credentials.secret_key,
region,
"aoss",
session_token=credentials.token,
)

client = OpenSearch(
hosts=[{"host": host, "port": 443}],
http_auth=awsauth,
use_ssl=True,
verify_certs=True,
connection_class=RequestsHttpConnection,
)


@tool
def search_documents(query: str, index: str = "documents") -> str:
"""Search documents in the OpenSearch Serverless collection.

Args:
query: The search query string.
index: The index to search in. Defaults to 'documents'.

Returns:
Search results as a formatted string.
"""
body = {"query": {"multi_match": {"query": query, "fields": ["*"]}}, "size": 5}
response = client.search(index=index, body=body)
hits = response["hits"]["hits"]
if not hits:
return "No documents found."
results = []
for hit in hits:
results.append(f"Score: {hit['_score']}, Source: {hit['_source']}")
return "\n".join(results)


agent = Agent(
model="us.anthropic.claude-sonnet-4-20250514-v1:0",
tools=[search_documents],
system_prompt="You are a helpful search assistant. Use the search_documents tool to find information in the OpenSearch collection.",
)

if __name__ == "__main__":
agent.serve()
Loading