diff --git a/agentcore-opensearch-serverless-nextgen-cdk/.gitignore b/agentcore-opensearch-serverless-nextgen-cdk/.gitignore new file mode 100644 index 0000000000..745e8db830 --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/.gitignore @@ -0,0 +1,6 @@ +*.js +*.d.ts +node_modules +cdk.out +cdk.context.json +package-lock.json diff --git a/agentcore-opensearch-serverless-nextgen-cdk/README.md b/agentcore-opensearch-serverless-nextgen-cdk/README.md new file mode 100644 index 0000000000..7b07d4e6ef --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/README.md @@ -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 diff --git a/agentcore-opensearch-serverless-nextgen-cdk/bin/app.ts b/agentcore-opensearch-serverless-nextgen-cdk/bin/app.ts new file mode 100644 index 0000000000..90723915a2 --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/bin/app.ts @@ -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', + }, +}); diff --git a/agentcore-opensearch-serverless-nextgen-cdk/cdk.json b/agentcore-opensearch-serverless-nextgen-cdk/cdk.json new file mode 100644 index 0000000000..2ae391ccc1 --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "watch": { + "include": ["**"], + "exclude": ["node_modules", "cdk.out", "**/*.js", "**/*.d.ts"] + } +} diff --git a/agentcore-opensearch-serverless-nextgen-cdk/example-pattern.json b/agentcore-opensearch-serverless-nextgen-cdk/example-pattern.json new file mode 100644 index 0000000000..f14e21355a --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/example-pattern.json @@ -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" + } + ] +} diff --git a/agentcore-opensearch-serverless-nextgen-cdk/lib/agentcore-opensearch-stack.ts b/agentcore-opensearch-serverless-nextgen-cdk/lib/agentcore-opensearch-stack.ts new file mode 100644 index 0000000000..e006e4b923 --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/lib/agentcore-opensearch-stack.ts @@ -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', + }); + } +} diff --git a/agentcore-opensearch-serverless-nextgen-cdk/package.json b/agentcore-opensearch-serverless-nextgen-cdk/package.json new file mode 100644 index 0000000000..c3cb561a51 --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/package.json @@ -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" + } +} diff --git a/agentcore-opensearch-serverless-nextgen-cdk/src/agent/Dockerfile b/agentcore-opensearch-serverless-nextgen-cdk/src/agent/Dockerfile new file mode 100644 index 0000000000..a71cd863ff --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/src/agent/Dockerfile @@ -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"] diff --git a/agentcore-opensearch-serverless-nextgen-cdk/src/agent/agent.py b/agentcore-opensearch-serverless-nextgen-cdk/src/agent/agent.py new file mode 100644 index 0000000000..81ac3e3638 --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/src/agent/agent.py @@ -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() diff --git a/agentcore-opensearch-serverless-nextgen-cdk/src/agent/requirements.txt b/agentcore-opensearch-serverless-nextgen-cdk/src/agent/requirements.txt new file mode 100644 index 0000000000..9635afa0f9 --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/src/agent/requirements.txt @@ -0,0 +1,3 @@ +strands-agents +opensearch-py +requests-aws4auth diff --git a/agentcore-opensearch-serverless-nextgen-cdk/tsconfig.json b/agentcore-opensearch-serverless-nextgen-cdk/tsconfig.json new file mode 100644 index 0000000000..6d757f27f4 --- /dev/null +++ b/agentcore-opensearch-serverless-nextgen-cdk/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "outDir": "./cdk.out", + "rootDir": "." + }, + "exclude": ["node_modules", "cdk.out"] +}