diff --git a/04-infrastructure-as-code/README.md b/04-infrastructure-as-code/README.md new file mode 100644 index 00000000..ff3ccfdd --- /dev/null +++ b/04-infrastructure-as-code/README.md @@ -0,0 +1,245 @@ +# CloudFormation Samples for Amazon Bedrock AgentCore + +CloudFormation templates for deploying Amazon Bedrock AgentCore resources. + +## Overview + +These CloudFormation templates enable you to: +- Deploy AgentCore resources consistently across environments +- Automate infrastructure provisioning with Infrastructure as Code +- Maintain version control of your infrastructure +- Implement AWS best practices for security and monitoring + +## ๐Ÿ“š Available Samples + +### 01. [Hosting MCP Server on AgentCore Runtime](./cloudformation/mcp-server-agentcore-runtime/) + +Deploy a complete MCP (Model Context Protocol) server with automated Docker image building and JWT authentication. + +**What it deploys:** +- Amazon ECR Repository for Docker images +- AWS CodeBuild for automated ARM64 builds +- Amazon Cognito for JWT authentication +- IAM roles with least-privilege policies +- Lambda functions for custom resource automation +- Amazon Bedrock AgentCore Runtime hosting the MCP server + +**Sample MCP Tools:** +- `add_numbers` - Adds two numbers +- `multiply_numbers` - Multiplies two numbers +- `greet_user` - Greets a user by name + +**Deployment time:** ~10-15 minutes +**Estimated cost:** ~$50-100/month + +**Quick start:** +```bash +cd cloudformation/mcp-server-agentcore-runtime +./deploy.sh +./test.sh +``` + +--- + +### 02. [Basic Agent Runtime](./cloudformation/basic-runtime/) + +Deploy a basic AgentCore Runtime with a simple Strands agent - no additional tools or memory. + +**What it deploys:** +- Amazon ECR Repository +- AWS CodeBuild for ARM64 Docker image building +- IAM roles with least-privilege policies +- Lambda functions for automation +- Basic AgentCore Runtime with simple agent + +**Use case:** Simple agent deployment without memory, code interpreter, or browser tools + +**Deployment time:** ~10-15 minutes +**Estimated cost:** ~$50-100/month + +**Quick start:** +```bash +aws cloudformation create-stack \ + --stack-name basic-agent-demo \ + --template-body file://cloudformation/basic-runtime/template.yaml \ + --capabilities CAPABILITY_IAM \ + --region us-west-2 +``` + +--- + +### 03. [Multi-Agent Runtime](./cloudformation/multi-agent-runtime/) + +Deploy a multi-agent system where Agent1 (orchestrator) can invoke Agent2 (specialist) for complex tasks. + +**What it deploys:** +- Two ECR Repositories (one per agent) +- AWS CodeBuild projects for both agents +- IAM roles with agent-to-agent invocation permissions +- Lambda functions for automation +- Two AgentCore Runtimes with agent-to-agent communication + +**Architecture:** +- **Agent1 (Orchestrator)**: Routes requests and delegates to Agent2 +- **Agent2 (Specialist)**: Handles detailed analysis and complex tasks + +**Deployment time:** ~15-20 minutes +**Estimated cost:** ~$100-200/month + +**Quick start:** +```bash +aws cloudformation create-stack \ + --stack-name multi-agent-demo \ + --template-body file://cloudformation/multi-agent-runtime/template.yaml \ + --capabilities CAPABILITY_IAM \ + --region us-west-2 +``` + +--- + +### 04. [End-to-End Weather Agent with Tools and Memory](./cloudformation/end-to-end-weather-agent/) + +Deploy a complete weather-based activity planning agent with browser automation, code interpreter, and memory. + +**What it deploys:** +- Amazon ECR Repository +- AWS CodeBuild for ARM64 Docker image building +- S3 bucket for results storage +- IAM roles with comprehensive permissions +- Lambda functions for automation +- AgentCore Runtime with Strands agent +- **Browser Tool** for web scraping weather data +- **Code Interpreter Tool** for weather analysis +- **Memory** for storing user preferences + +**Features:** +- Scrapes weather data from weather.gov using browser automation +- Analyzes weather conditions using Python code execution +- Stores and retrieves user activity preferences +- Generates personalized activity recommendations +- Saves results to S3 bucket + +**Deployment time:** ~15-20 minutes +**Estimated cost:** ~$100-150/month + +**Quick start:** +```bash +aws cloudformation create-stack \ + --stack-name weather-agent-demo \ + --template-body file://cloudformation/end-to-end-weather-agent/end-to-end-weather-agent.yaml \ + --capabilities CAPABILITY_IAM \ + --region us-west-2 +``` + +--- + +## Prerequisites + +Before deploying any CloudFormation template, ensure you have: + +1. **AWS Account** with appropriate permissions +2. **AWS CLI** installed and configured + ```bash + aws configure + ``` +3. **Access to Amazon Bedrock AgentCore** (preview) +4. **IAM Permissions** to create: + - CloudFormation stacks + - IAM roles and policies + - ECR repositories + - Lambda functions + - CodeBuild projects + - AgentCore resources + - S3 buckets (for weather agent) + +## General Usage Pattern + +Each sample follows a consistent structure: + +```bash +# Deploy +aws cloudformation create-stack \ + --stack-name \ + --template-body file:///template.yaml \ + --capabilities CAPABILITY_IAM \ + --region + +# Monitor deployment +aws cloudformation describe-stacks \ + --stack-name \ + --region + +# Cleanup +aws cloudformation delete-stack \ + --stack-name \ + --region +``` + +Default values: +- Stack name: Varies by sample (see quick start commands) +- Region: `us-west-2` + +## Repository Structure + +``` +04-infrastructure-as-code/ +โ”œโ”€โ”€ README.md # This file +โ””โ”€โ”€ cloudformation/ # CloudFormation samples + โ”œโ”€โ”€ mcp-server-agentcore-runtime/ # MCP Server sample + โ”‚ โ”œโ”€โ”€ deploy.sh # Deployment script + โ”‚ โ”œโ”€โ”€ test.sh # Testing script + โ”‚ โ”œโ”€โ”€ cleanup.sh # Cleanup script + โ”‚ โ”œโ”€โ”€ mcp-server-template.yaml # CloudFormation template + โ”‚ โ”œโ”€โ”€ get_token.py # Authentication helper + โ”‚ โ”œโ”€โ”€ test_mcp_server.py # MCP client test + โ”‚ โ”œโ”€โ”€ README.md # Sample documentation + โ”‚ โ””โ”€โ”€ DETAILED_GUIDE.md # Technical deep-dive + โ”œโ”€โ”€ basic-runtime/ # Basic agent sample + โ”‚ โ””โ”€โ”€ template.yaml # CloudFormation template + โ”œโ”€โ”€ multi-agent-runtime/ # Multi-agent sample + โ”‚ โ””โ”€โ”€ template.yaml # CloudFormation template + โ””โ”€โ”€ end-to-end-weather-agent/ # Weather agent sample + โ””โ”€โ”€ end-to-end-weather-agent.yaml # CloudFormation template +``` + + +### Stack Creation Fails + +Check CloudFormation events: +```bash +aws cloudformation describe-stack-events \ + --stack-name \ + --region +``` + +### Permission Issues + +Ensure your IAM user/role has: +- `CloudFormationFullAccess` or equivalent +- Permissions to create all resources in the template +- `iam:PassRole` for service roles + +### CodeBuild Failures + +Check CodeBuild logs: +```bash +aws codebuild batch-get-builds \ + --ids \ + --region +``` + +### Resource Limits + +Check AWS service quotas: +```bash +aws service-quotas list-service-quotas \ + --service-code +``` + +## Additional Resources + +- [Amazon Bedrock AgentCore Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html) +- [AWS CloudFormation Documentation](https://docs.aws.amazon.com/cloudformation/) +- [CloudFormation Best Practices](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/best-practices.html) +- [CloudFormation Template Reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/AWS_BedrockAgentCore.html) +- [Original Tutorials](../01-tutorials/) diff --git a/04-infrastructure-as-code/cloudformation/basic-runtime/README.md b/04-infrastructure-as-code/cloudformation/basic-runtime/README.md new file mode 100644 index 00000000..b8df4478 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/basic-runtime/README.md @@ -0,0 +1,241 @@ +# Basic AgentCore Runtime + +This CloudFormation template deploys a basic Amazon Bedrock AgentCore Runtime with a simple Strands agent. This is the simplest possible AgentCore deployment, perfect for getting started and understanding the core concepts without additional complexity. + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Prerequisites](#prerequisites) +- [Deployment](#deployment) +- [Testing](#testing) +- [Sample Queries](#sample-queries) +- [Cleanup](#cleanup) +- [Cost Estimate](#cost-estimate) +- [Troubleshooting](#troubleshooting) +- [๐Ÿค Contributing](#-contributing) +- [๐Ÿ“„ License](#-license) + +## Overview + +This template creates a minimal AgentCore deployment that includes: + +- **AgentCore Runtime**: Hosts a simple Strands agent +- **ECR Repository**: Stores the Docker container image +- **IAM Roles**: Provides necessary permissions +- **CodeBuild Project**: Automatically builds the ARM64 Docker image +- **Lambda Functions**: Custom resources for automation + + +This makes it ideal for: +- Learning AgentCore basics +- Quick prototyping +- Understanding the core deployment pattern +- Building a foundation before adding complexity + +## Architecture + +![Basic AgentCore Runtime Architecture](architecture.png) + +The architecture consists of: + +- **User**: Sends questions to the agent and receives responses +- **AWS CodeBuild**: Builds the ARM64 Docker container image with the agent code +- **Amazon ECR Repository**: Stores the container image +- **AgentCore Runtime**: Hosts the Basic Agent container + - **Basic Agent**: Simple Strands agent that processes user queries + - Invokes Amazon Bedrock LLMs to generate responses +- **IAM Roles**: + - IAM role for CodeBuild (builds and pushes images) + - IAM role for Agent Execution (runtime permissions) +- **Amazon Bedrock LLMs**: Provides the AI model capabilities for the agent + +## Prerequisites + +### AWS Account Setup + +1. **AWS Account**: You need an active AWS account with appropriate permissions + - [Create AWS Account](https://aws.amazon.com/account/) + - [AWS Console Access](https://aws.amazon.com/console/) + +2. **AWS CLI**: Install and configure AWS CLI with your credentials + - [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) + - [Configure AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) + + ```bash + aws configure + ``` + +3. **Bedrock Model Access**: Enable access to Amazon Bedrock models in your AWS region + - Navigate to [Amazon Bedrock Console](https://console.aws.amazon.com/bedrock/) + - Go to "Model access" and request access to: + - Anthropic Claude models (recommended: Claude 3.5 Sonnet or Claude 3 Haiku) + - [Bedrock Model Access Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) + +4. **Required Permissions**: Your AWS user/role needs permissions for: + - CloudFormation stack operations + - ECR repository management + - IAM role creation + - Lambda function creation + - CodeBuild project creation + - BedrockAgentCore resource creation + +## Deployment + +### Option 1: Using the Deploy Script (Recommended) + +```bash +# Make the script executable +chmod +x deploy.sh + +# Deploy the stack +./deploy.sh +``` + +The script will: +1. Deploy the CloudFormation stack +2. Wait for stack creation to complete +3. Display the AgentCore Runtime ID + +### Option 2: Using AWS CLI + +```bash +# Deploy the stack +aws cloudformation create-stack \ + --stack-name basic-agent-demo \ + --template-body file://template.yaml \ + --capabilities CAPABILITY_NAMED_IAM \ + --region us-west-2 + +# Wait for stack creation +aws cloudformation wait stack-create-complete \ + --stack-name basic-agent-demo \ + --region us-west-2 + +# Get the Runtime ID +aws cloudformation describe-stacks \ + --stack-name basic-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`AgentRuntimeId`].OutputValue' \ + --output text +``` + +### Option 3: Using AWS Console + +1. Navigate to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) +2. Click "Create stack" โ†’ "With new resources" +3. Upload the `template.yaml` file +4. Enter stack name: `basic-agent-demo` +5. Review parameters (or use defaults) +6. Check "I acknowledge that AWS CloudFormation might create IAM resources" +7. Click "Create stack" + +### Deployment Time + +- **Expected Duration**: 10-15 minutes +- **Main Steps**: + - Stack creation: ~2 minutes + - Docker image build (CodeBuild): ~8-10 minutes + - Runtime provisioning: ~2-3 minutes + +## Testing + +### Using AWS CLI + +```bash +# Get the Runtime ID from stack outputs +RUNTIME_ID=$(aws cloudformation describe-stacks \ + --stack-name basic-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`AgentRuntimeId`].OutputValue' \ + --output text) + +# Invoke the agent +aws bedrock-agentcore invoke-agent-runtime \ + --agent-runtime-id $RUNTIME_ID \ + --qualifier DEFAULT \ + --payload '{"prompt": "What is 2+2?"}' \ + --region us-west-2 \ + response.json + +# View the response +cat response.json +``` + +### Using AWS Console + +1. Navigate to [Bedrock AgentCore Console](https://console.aws.amazon.com/bedrock-agentcore/) +2. Go to "Runtimes" in the left navigation +3. Find your runtime (name starts with `basic_agent_demo_`) +4. Click on the runtime name +5. Click "Test" button +6. Enter test payload: + ```json + { + "prompt": "What is 2+2?" + } + ``` +7. Click "Invoke" + + + +## Sample Queries + +Try these queries to test your basic agent: + +1. **Simple Math**: + ```json + {"prompt": "What is 2+2?"} + ``` + +2. **General Knowledge**: + ```json + {"prompt": "What is the capital of France?"} + ``` + +3. **Explanation Request**: + ```json + {"prompt": "Explain what Amazon Bedrock is in simple terms"} + ``` + +4. **Creative Task**: + ```json + {"prompt": "Write a haiku about cloud computing"} + ``` + +5. **Reasoning**: + ```json + {"prompt": "If I have 5 apples and give away 2, how many do I have left?"} + ``` + +## Cleanup + +### Using the Cleanup Script (Recommended) + +```bash +# Make the script executable +chmod +x cleanup.sh + +# Delete the stack +./cleanup.sh +``` + +### Using AWS CLI + +```bash +aws cloudformation delete-stack \ + --stack-name basic-agent-demo \ + --region us-west-2 + +# Wait for deletion to complete +aws cloudformation wait stack-delete-complete \ + --stack-name basic-agent-demo \ + --region us-west-2 +``` + +### Using AWS Console + +1. Navigate to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) +2. Select the `basic-agent-demo` stack +3. Click "Delete" +4. Confirm deletion diff --git a/04-infrastructure-as-code/cloudformation/basic-runtime/architecture.png b/04-infrastructure-as-code/cloudformation/basic-runtime/architecture.png new file mode 100644 index 00000000..8ef7ebc3 Binary files /dev/null and b/04-infrastructure-as-code/cloudformation/basic-runtime/architecture.png differ diff --git a/04-infrastructure-as-code/cloudformation/basic-runtime/cleanup.sh b/04-infrastructure-as-code/cloudformation/basic-runtime/cleanup.sh new file mode 100755 index 00000000..fab86363 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/basic-runtime/cleanup.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Cleanup script for Basic Agent Runtime CloudFormation stack +# This script deletes the CloudFormation stack and all associated resources + +set -e + +# Configuration +STACK_NAME="${1:-basic-agent-demo}" +REGION="${2:-us-west-2}" + +echo "==========================================" +echo "Cleaning up Basic Agent Runtime" +echo "==========================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "==========================================" + +# Confirm deletion +read -p "Are you sure you want to delete the stack '$STACK_NAME'? (yes/no): " CONFIRM + +if [ "$CONFIRM" != "yes" ]; then + echo "Cleanup cancelled." + exit 0 +fi + +echo "" +echo "Deleting CloudFormation stack..." +aws cloudformation delete-stack \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + +if [ $? -eq 0 ]; then + echo "" + echo "โœ“ Stack deletion initiated successfully!" + echo "" + echo "Waiting for stack deletion to complete..." + echo "This may take a few minutes..." + echo "" + + aws cloudformation wait stack-delete-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + + if [ $? -eq 0 ]; then + echo "" + echo "==========================================" + echo "โœ“ Stack deleted successfully!" + echo "==========================================" + echo "" + echo "All resources have been cleaned up." + echo "" + else + echo "" + echo "โœ— Stack deletion failed or timed out" + echo "Check the CloudFormation console for details" + exit 1 + fi +else + echo "" + echo "โœ— Failed to initiate stack deletion" + exit 1 +fi diff --git a/04-infrastructure-as-code/cloudformation/basic-runtime/deploy.sh b/04-infrastructure-as-code/cloudformation/basic-runtime/deploy.sh new file mode 100755 index 00000000..bdb196e0 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/basic-runtime/deploy.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# Deploy script for Basic Agent Runtime CloudFormation stack +# This script deploys a basic AgentCore Runtime with a simple Strands agent + +set -e + +# Configuration +STACK_NAME="${1:-basic-agent-demo}" +REGION="${2:-us-west-2}" +TEMPLATE_FILE="template.yaml" + +echo "==========================================" +echo "Deploying Basic Agent Runtime" +echo "==========================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "==========================================" + +# Check if template file exists +if [ ! -f "$TEMPLATE_FILE" ]; then + echo "Error: Template file '$TEMPLATE_FILE' not found!" + exit 1 +fi + +# Deploy the CloudFormation stack +echo "" +echo "Creating CloudFormation stack..." +aws cloudformation create-stack \ + --stack-name "$STACK_NAME" \ + --template-body file://"$TEMPLATE_FILE" \ + --capabilities CAPABILITY_NAMED_IAM \ + --region "$REGION" + +if [ $? -eq 0 ]; then + echo "" + echo "โœ“ Stack creation initiated successfully!" + echo "" + echo "Waiting for stack creation to complete..." + echo "This will take approximately 10-15 minutes..." + echo "" + + aws cloudformation wait stack-create-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + + if [ $? -eq 0 ]; then + echo "" + echo "==========================================" + echo "โœ“ Stack deployed successfully!" + echo "==========================================" + echo "" + echo "Stack Outputs:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs' \ + --output table \ + --region "$REGION" + echo "" + echo "Agent Runtime ID:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`AgentRuntimeId`].OutputValue' \ + --output text \ + --region "$REGION" + echo "" + echo "To delete this stack, run:" + echo " ./cleanup.sh $STACK_NAME $REGION" + echo "" + else + echo "" + echo "โœ— Stack creation failed or timed out" + echo "Check the CloudFormation console for details" + exit 1 + fi +else + echo "" + echo "โœ— Failed to initiate stack creation" + exit 1 +fi diff --git a/04-infrastructure-as-code/cloudformation/basic-runtime/template.yaml b/04-infrastructure-as-code/cloudformation/basic-runtime/template.yaml new file mode 100644 index 00000000..8539ba69 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/basic-runtime/template.yaml @@ -0,0 +1,591 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: "Basic AgentCore deployment - Simple agent runtime without memory, code interpreter, or browser" + +# ============================================================================ +# PARAMETERS SECTION +# ============================================================================ +Parameters: + # Agent Configuration + AgentName: + Type: String + Default: "BasicAgent" + Description: "Name for the agent runtime" + AllowedPattern: "^[a-zA-Z][a-zA-Z0-9_]{0,47}$" + ConstraintDescription: "Must start with a letter, max 48 characters, alphanumeric and underscores only" + + # Container Configuration + ImageTag: + Type: String + Default: "latest" + Description: "Tag for the Docker image" + + # Network Configuration + NetworkMode: + Type: String + Default: "PUBLIC" + Description: "Network mode for AgentCore resources" + AllowedValues: + - PUBLIC + - PRIVATE + + # ECR Configuration + ECRRepositoryName: + Type: String + Default: "basic-agent" + Description: "Name of the ECR repository" + +# ============================================================================ +# METADATA SECTION +# ============================================================================ +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Agent Configuration" + Parameters: + - AgentName + - NetworkMode + - Label: + default: "Container Configuration" + Parameters: + - ECRRepositoryName + - ImageTag + ParameterLabels: + AgentName: + default: "Agent Name" + NetworkMode: + default: "Network Mode" + ECRRepositoryName: + default: "ECR Repository Name" + ImageTag: + default: "Image Tag" + +# ============================================================================ +# RESOURCES SECTION +# ============================================================================ +Resources: + # ======================================================================== + # ECR MODULE - Container Registry + # ======================================================================== + + ECRRepository: + Type: AWS::ECR::Repository + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + RepositoryName: !Sub "${AWS::StackName}-${ECRRepositoryName}" + ImageTagMutability: MUTABLE + EmptyOnDelete: true + ImageScanningConfiguration: + ScanOnPush: true + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + - Sid: AllowPullFromAccount + Effect: Allow + Principal: + AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-ecr-repository" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: ECR + + # ======================================================================== + # IAM MODULE - Security and Permissions + # ======================================================================== + + # Agent Execution Role + AgentExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-agent-execution-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: AssumeRolePolicy + Effect: Allow + Principal: + Service: bedrock-agentcore.amazonaws.com + Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: !Ref AWS::AccountId + ArnLike: + aws:SourceArn: !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:*" + ManagedPolicyArns: + - arn:aws:iam::aws:policy/BedrockAgentCoreFullAccess + Policies: + - PolicyName: AgentCoreExecutionPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: ECRImageAccess + Effect: Allow + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + - ecr:BatchCheckLayerAvailability + Resource: !GetAtt ECRRepository.Arn + - Sid: ECRTokenAccess + Effect: Allow + Action: + - ecr:GetAuthorizationToken + Resource: "*" + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:DescribeLogStreams + - logs:CreateLogGroup + - logs:DescribeLogGroups + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "*" + - Sid: XRayTracing + Effect: Allow + Action: + - xray:PutTraceSegments + - xray:PutTelemetryRecords + - xray:GetSamplingRules + - xray:GetSamplingTargets + Resource: "*" + - Sid: CloudWatchMetrics + Effect: Allow + Resource: "*" + Action: cloudwatch:PutMetricData + Condition: + StringEquals: + cloudwatch:namespace: bedrock-agentcore + - Sid: GetAgentAccessToken + Effect: Allow + Action: + - bedrock-agentcore:GetWorkloadAccessToken + - bedrock-agentcore:GetWorkloadAccessTokenForJWT + - bedrock-agentcore:GetWorkloadAccessTokenForUserId + Resource: + - !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default" + - !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default/workload-identity/*" + - Sid: BedrockModelInvocation + Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-agent-execution-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # CodeBuild Service Role + CodeBuildRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-codebuild-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: CodeBuildPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*" + - Sid: ECRAccess + Effect: Allow + Action: + - ecr:BatchCheckLayerAvailability + - ecr:GetDownloadUrlForLayer + - ecr:BatchGetImage + - ecr:GetAuthorizationToken + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + Resource: + - !GetAtt ECRRepository.Arn + - "*" + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-codebuild-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # Lambda Custom Resource Role + CustomResourceRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-custom-resource-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: CustomResourcePolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: ECRAccess + Effect: Allow + Action: + - ecr:ListImages + - ecr:BatchDeleteImage + - ecr:GetAuthorizationToken + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + Resource: !GetAtt ECRRepository.Arn + - Sid: CodeBuildAccess + Effect: Allow + Action: + - codebuild:StartBuild + - codebuild:BatchGetBuilds + - codebuild:BatchGetProjects + Resource: !GetAtt AgentImageBuildProject.Arn + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-custom-resource-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # ======================================================================== + # LAMBDA MODULE - Custom Resources + # ======================================================================== + + CodeBuildTriggerFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub "${AWS::StackName}-codebuild-trigger" + Description: "Triggers CodeBuild projects as CloudFormation custom resource" + Handler: index.handler + Role: !GetAtt CustomResourceRole.Arn + Runtime: python3.9 + Timeout: 900 + Code: + ZipFile: | + import boto3 + import cfnresponse + import json + import logging + import time + + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + def handler(event, context): + logger.info('Received event: %s', json.dumps(event)) + + try: + if event['RequestType'] == 'Delete': + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + return + + project_name = event['ResourceProperties']['ProjectName'] + wait_for_completion = event['ResourceProperties'].get('WaitForCompletion', 'true').lower() == 'true' + + logger.info(f"Attempting to start CodeBuild project: {project_name}") + logger.info(f"Wait for completion: {wait_for_completion}") + + # Start the CodeBuild project + codebuild = boto3.client('codebuild') + + # First, verify the project exists + try: + project_info = codebuild.batch_get_projects(names=[project_name]) + if not project_info['projects']: + raise Exception(f"CodeBuild project '{project_name}' not found") + logger.info(f"CodeBuild project '{project_name}' found") + except Exception as e: + logger.error(f"Error checking project existence: {str(e)}") + raise + + response = codebuild.start_build(projectName=project_name) + build_id = response['build']['id'] + + logger.info(f"Successfully started build: {build_id}") + + if not wait_for_completion: + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'BuildId': build_id, + 'Status': 'STARTED' + }) + return + + # Wait for the build to complete + max_wait_time = context.get_remaining_time_in_millis() / 1000 - 30 # Leave 30s buffer + start_time = time.time() + + while True: + if time.time() - start_time > max_wait_time: + error_message = f"Build {build_id} timed out" + logger.error(error_message) + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_message}) + return + + build_response = codebuild.batch_get_builds(ids=[build_id]) + build_status = build_response['builds'][0]['buildStatus'] + + if build_status == 'SUCCEEDED': + logger.info(f"Build {build_id} succeeded") + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'BuildId': build_id, + 'Status': build_status + }) + return + elif build_status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']: + error_message = f"Build {build_id} failed with status: {build_status}" + logger.error(error_message) + + # Get build logs for debugging + try: + logs_info = build_response['builds'][0].get('logs', {}) + if logs_info.get('groupName') and logs_info.get('streamName'): + logger.info(f"Build logs available in CloudWatch") + except Exception as log_error: + logger.warning(f"Could not get log information: {log_error}") + + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': error_message, + 'BuildId': build_id + }) + return + + logger.info(f"Build {build_id} status: {build_status}") + time.sleep(30) # Check every 30 seconds + + except Exception as e: + logger.error('Error: %s', str(e)) + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': str(e) + }) + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-codebuild-trigger" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: Lambda + + # ======================================================================== + # CODEBUILD MODULE - Container Image Building + # ======================================================================== + + AgentImageBuildProject: + Type: AWS::CodeBuild::Project + Properties: + Name: !Sub "${AWS::StackName}-basic-agent-build" + Description: !Sub "Build basic agent Docker image for ${AWS::StackName}" + ServiceRole: !GetAtt CodeBuildRole.Arn + Artifacts: + Type: NO_ARTIFACTS + Environment: + Type: ARM_CONTAINER + ComputeType: BUILD_GENERAL1_LARGE + Image: aws/codebuild/amazonlinux2-aarch64-standard:3.0 + PrivilegedMode: true + EnvironmentVariables: + - Name: AWS_DEFAULT_REGION + Value: !Ref AWS::Region + - Name: AWS_ACCOUNT_ID + Value: !Ref AWS::AccountId + - Name: IMAGE_REPO_NAME + Value: !Ref ECRRepository + - Name: IMAGE_TAG + Value: !Ref ImageTag + - Name: STACK_NAME + Value: !Ref AWS::StackName + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + pre_build: + commands: + - echo Logging in to Amazon ECR... + - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com + build: + commands: + - echo Build started on `date` + - echo Building the Docker image for basic agent ARM64... + + # Step 1.1: Create requirements.txt + - | + cat > requirements.txt << 'EOF' + strands-agents + boto3 + bedrock-agentcore + EOF + + # Step 1.2: Create my_agent.py (simplified basic version) + - | + cat > my_agent.py << 'EOF' + from strands import Agent + import os + from bedrock_agentcore.runtime import BedrockAgentCoreApp + + app = BedrockAgentCoreApp() + + def create_basic_agent() -> Agent: + """Create a basic agent with simple functionality""" + system_prompt = """You are a helpful assistant. Answer questions clearly and concisely.""" + + return Agent( + system_prompt=system_prompt, + name="BasicAgent" + ) + + @app.entrypoint + async def invoke(payload=None): + """Main entrypoint for the agent""" + try: + # Get the query from payload + query = payload.get("prompt", "Hello, how are you?") if payload else "Hello, how are you?" + + # Create and use the agent + agent = create_basic_agent() + response = agent(query) + + return { + "status": "success", + "response": response.message['content'][0]['text'] + } + + except Exception as e: + return { + "status": "error", + "error": str(e) + } + + if __name__ == "__main__": + app.run() + EOF + + # Step 1.3: Create Dockerfile + - | + cat > Dockerfile << 'EOF' + FROM public.ecr.aws/docker/library/python:3.11-slim + WORKDIR /app + + COPY requirements.txt requirements.txt + RUN pip install -r requirements.txt + RUN pip install aws-opentelemetry-distro>=0.10.1 + + ENV AWS_REGION=us-west-2 + ENV AWS_DEFAULT_REGION=us-west-2 + + # Create non-root user + RUN useradd -m -u 1000 bedrock_agentcore + USER bedrock_agentcore + + EXPOSE 8080 + EXPOSE 8000 + + COPY . . + + CMD ["opentelemetry-instrument", "python", "-m", "my_agent"] + EOF + + # Step 1.4: Build the image + - echo Building ARM64 image... + - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . + - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + + post_build: + commands: + - echo Build completed on `date` + - echo Pushing the Docker image... + - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + - echo ARM64 Docker image pushed successfully + + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-basic-build" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: CodeBuild + + # CUSTOM RESOURCE - Trigger Image Build + TriggerImageBuild: + Type: Custom::CodeBuildTrigger + DependsOn: + - ECRRepository + - AgentImageBuildProject + - CodeBuildTriggerFunction + Properties: + ServiceToken: !GetAtt CodeBuildTriggerFunction.Arn + ProjectName: !Ref AgentImageBuildProject + WaitForCompletion: "true" + + # ======================================================================== + # AGENTCORE MODULE - Runtime Only (No Tools) + # ======================================================================== + + # AgentCore Runtime + AgentRuntime: + Type: AWS::BedrockAgentCore::Runtime + DependsOn: + - TriggerImageBuild + Properties: + AgentRuntimeName: !Sub + - "${StackNameUnderscore}_${AgentName}" + - StackNameUnderscore: !Join ["_", !Split ["-", !Ref "AWS::StackName"]] + AgentRuntimeArtifact: + ContainerConfiguration: + ContainerUri: !Sub "${ECRRepository.RepositoryUri}:${ImageTag}" + RoleArn: !GetAtt AgentExecutionRole.Arn + NetworkConfiguration: + NetworkMode: !Ref NetworkMode + Description: !Sub "Basic agent runtime for ${AWS::StackName}" + +# ============================================================================ +# OUTPUTS SECTION +# ============================================================================ +Outputs: + # AGENTCORE MODULE OUTPUTS + AgentRuntimeId: + Description: "ID of the created agent runtime" + Value: !GetAtt AgentRuntime.AgentRuntimeId + Export: + Name: !Sub "${AWS::StackName}-AgentRuntimeId" + + ECRRepositoryUri: + Description: "URI of the ECR repository" + Value: !GetAtt ECRRepository.RepositoryUri + Export: + Name: !Sub "${AWS::StackName}-ECRRepositoryUri" + + AgentExecutionRoleArn: + Description: "ARN of the agent execution role" + Value: !GetAtt AgentExecutionRole.Arn + Export: + Name: !Sub "${AWS::StackName}-AgentExecutionRoleArn" \ No newline at end of file diff --git a/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/README.md b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/README.md new file mode 100644 index 00000000..0ffd5616 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/README.md @@ -0,0 +1,371 @@ +# End-to-End Weather Agent with Tools and Memory + +This CloudFormation template deploys a complete Amazon Bedrock AgentCore Runtime with a sophisticated weather-based activity planning agent. This demonstrates the full power of AgentCore by integrating Browser tool, Code Interpreter, Memory, and S3 storage in a single deployment. + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Prerequisites](#prerequisites) +- [Deployment](#deployment) +- [Testing](#testing) +- [Sample Queries](#sample-queries) +- [How It Works](#how-it-works) +- [Cleanup](#cleanup) +- [Cost Estimate](#cost-estimate) +- [Troubleshooting](#troubleshooting) +- [๐Ÿค Contributing](#-contributing) +- [๐Ÿ“„ License](#-license) + +## Overview + +This template creates a comprehensive AgentCore deployment that showcases: + +### Core Components + +- **AgentCore Runtime**: Hosts a Strands agent with multiple tools +- **Browser Tool**: Web automation for scraping weather data from weather.gov +- **Code Interpreter**: Python code execution for weather analysis +- **Memory**: Stores user activity preferences +- **S3 Bucket**: Stores generated activity recommendations +- **ECR Repository**: Container image storage +- **IAM Roles**: Comprehensive permissions for all components + +### Agent Capabilities + +The Weather Activity Planner agent can: + +1. **Scrape Weather Data**: Uses browser automation to fetch 8-day forecasts from weather.gov +2. **Analyze Weather**: Generates and executes Python code to classify days as GOOD/OK/POOR +3. **Retrieve Preferences**: Accesses user activity preferences from memory +4. **Generate Recommendations**: Creates personalized activity suggestions based on weather and preferences +5. **Store Results**: Saves recommendations as Markdown files in S3 + +### Use Cases + +- Weather-based activity planning +- Automated web scraping and data analysis +- Multi-tool agent orchestration +- Memory-driven personalization +- Asynchronous task processing + +## Architecture + +![End-to-End Weather Agent Architecture](architecture.png) + +The architecture demonstrates a complete AgentCore deployment with multiple integrated tools: + +**Core Components:** +- **User**: Sends weather-based activity planning queries +- **AWS CodeBuild**: Builds the ARM64 Docker container image with the agent code +- **Amazon ECR Repository**: Stores the container image +- **AgentCore Runtime**: Hosts the Weather Activity Planner Agent + - **Weather Agent**: Strands agent that orchestrates multiple tools + - Invokes Amazon Bedrock LLMs for reasoning and code generation +- **Browser Tool**: Web automation for scraping weather data from weather.gov +- **Code Interpreter Tool**: Executes Python code for weather analysis +- **Memory**: Stores user activity preferences (30-day retention) +- **S3 Bucket**: Stores generated activity recommendations +- **IAM Roles**: Comprehensive permissions for all components + +**Workflow:** +1. User sends query: "What should I do this weekend in Richmond VA?" +2. Agent extracts city and uses Browser Tool to scrape 8-day forecast +3. Agent generates Python code and uses Code Interpreter to classify weather +4. Agent retrieves user preferences from Memory +5. Agent generates personalized recommendations +6. Agent stores results in S3 bucket using use_aws tool + +## Prerequisites + +### AWS Account Setup + +1. **AWS Account**: You need an active AWS account with appropriate permissions + - [Create AWS Account](https://aws.amazon.com/account/) + - [AWS Console Access](https://aws.amazon.com/console/) + +2. **AWS CLI**: Install and configure AWS CLI with your credentials + - [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) + - [Configure AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) + + ```bash + aws configure + ``` + +3. **Bedrock Model Access**: Enable access to Amazon Bedrock models in your AWS region + - Navigate to [Amazon Bedrock Console](https://console.aws.amazon.com/bedrock/) + - [Bedrock Model Access Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) + +4. **Required Permissions**: Your AWS user/role needs permissions for: + - CloudFormation stack operations + - ECR repository management + - IAM role creation + - Lambda function creation + - CodeBuild project creation + - BedrockAgentCore resource creation (Runtime, Browser, CodeInterpreter, Memory) + - S3 bucket creation + +## Deployment + +### Option 1: Using the Deploy Script (Recommended) + +```bash +# Make the script executable +chmod +x deploy.sh + +# Deploy the stack +./deploy.sh +``` + +The script will: +1. Deploy the CloudFormation stack +2. Wait for stack creation to complete +3. Display all resource IDs (Runtime, Browser, CodeInterpreter, Memory, S3 Bucket) + +### Option 2: Using AWS CLI + +```bash +# Deploy the stack +aws cloudformation create-stack \ + --stack-name weather-agent-demo \ + --template-body file://end-to-end-weather-agent.yaml \ + --capabilities CAPABILITY_NAMED_IAM \ + --region us-west-2 + +# Wait for stack creation +aws cloudformation wait stack-create-complete \ + --stack-name weather-agent-demo \ + --region us-west-2 + +# Get all outputs +aws cloudformation describe-stacks \ + --stack-name weather-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs' +``` + +### Option 3: Using AWS Console + +1. Navigate to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) +2. Click "Create stack" โ†’ "With new resources" +3. Upload the `end-to-end-weather-agent.yaml` file +4. Enter stack name: `weather-agent-demo` +5. Review parameters (or use defaults) +6. Check "I acknowledge that AWS CloudFormation might create IAM resources" +7. Click "Create stack" + +### Deployment Time + +- **Expected Duration**: 15-20 minutes +- **Main Steps**: + - Stack creation: ~2 minutes + - Docker image build (CodeBuild): ~10-12 minutes + - Runtime and tools provisioning: ~3-5 minutes + - Memory initialization: ~1 minute + +## Testing + +### Using AWS CLI + +```bash +# Get the Runtime ID from stack outputs +RUNTIME_ID=$(aws cloudformation describe-stacks \ + --stack-name weather-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`AgentRuntimeId`].OutputValue' \ + --output text) + +# Get the S3 bucket name +BUCKET_NAME=$(aws cloudformation describe-stacks \ + --stack-name weather-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`ResultsBucket`].OutputValue' \ + --output text) + +# Invoke the agent +aws bedrock-agentcore invoke-agent-runtime \ + --agent-runtime-id $RUNTIME_ID \ + --qualifier DEFAULT \ + --payload '{"prompt": "What should I do this weekend in Richmond VA?"}' \ + --region us-west-2 \ + response.json + +# View the immediate response +cat response.json + +# Wait a few minutes for processing, then check S3 for results +aws s3 ls s3://$BUCKET_NAME/ + +# Download the results +aws s3 cp s3://$BUCKET_NAME/results.md ./results.md +cat results.md +``` + +### Using AWS Console + +1. Navigate to [Bedrock AgentCore Console](https://console.aws.amazon.com/bedrock-agentcore/) +2. Go to "Runtimes" in the left navigation +3. Find your runtime (name starts with `weather_agent_demo_`) +4. Click on the runtime name +5. Click "Test" button +6. Enter test payload: + ```json + { + "prompt": "What should I do this weekend in Richmond VA?" + } + ``` +7. Click "Invoke" +8. View the immediate response +9. Wait 2-3 minutes for background processing +10. Navigate to [S3 Console](https://console.aws.amazon.com/s3/) to download results.md from the results bucket + +## Sample Queries + +Try these queries to test the weather agent: + +1. **Weekend Planning**: + ```json + {"prompt": "What should I do this weekend in Richmond VA?"} + ``` + +2. **Specific City**: + ```json + {"prompt": "Plan activities for next week in San Francisco"} + ``` + +3. **Different Location**: + ```json + {"prompt": "What outdoor activities can I do in Seattle this week?"} + ``` + +4. **Vacation Planning**: + ```json + {"prompt": "I'm visiting Austin next week. What should I plan based on the weather?"} + ``` + +## How It Works + +### Step-by-Step Workflow + +1. **User Query**: "What should I do this weekend in Richmond VA?" + +2. **City Extraction**: Agent extracts "Richmond VA" from the query + +3. **Weather Scraping** (Browser Tool): + - Navigates to weather.gov + - Searches for Richmond VA + - Clicks "Printable Forecast" + - Extracts 8-day forecast data (date, high, low, conditions, wind, precipitation) + - Returns JSON array of weather data + +4. **Code Generation** (LLM): + - Agent generates Python code to classify weather days + - Classification rules: + - GOOD: 65-80ยฐF, clear, no rain + - OK: 55-85ยฐF, partly cloudy, slight rain + - POOR: <55ยฐF or >85ยฐF, cloudy/rainy + +5. **Code Execution** (Code Interpreter): + - Executes the generated Python code + - Returns list of tuples: `[('2025-09-16', 'GOOD'), ('2025-09-17', 'OK'), ...]` + +6. **Preference Retrieval** (Memory): + - Fetches user activity preferences from memory + - Preferences stored by weather type: + ```json + { + "good_weather": ["hiking", "beach volleyball", "outdoor picnic"], + "ok_weather": ["walking tours", "outdoor dining", "park visits"], + "poor_weather": ["indoor museums", "shopping", "restaurants"] + } + ``` + +7. **Recommendation Generation** (LLM): + - Combines weather analysis with user preferences + - Creates day-by-day activity recommendations + - Formats as Markdown document + +8. **Storage** (S3 via use_aws tool): + - Saves recommendations to S3 bucket as `results.md` + - User can download and review recommendations + +### Asynchronous Processing + +The agent runs asynchronously to handle long-running tasks: +- Immediate response: "Processing started..." +- Background processing: Completes all steps +- Results available in S3 after ~2-3 minutes + +## Cleanup + +### Using the Cleanup Script (Recommended) + +```bash +# Make the script executable +chmod +x cleanup.sh + +# Delete the stack +./cleanup.sh +``` + +**Note**: If cleanup fails due to active browser sessions, see the AWS CLI cleanup method below for manual session termination. + +### Using AWS CLI + +```bash +# Step 1: Empty the S3 bucket (required before deletion) +BUCKET_NAME=$(aws cloudformation describe-stacks \ + --stack-name weather-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`ResultsBucket`].OutputValue' \ + --output text) + +aws s3 rm s3://$BUCKET_NAME --recursive + +# Step 2: Terminate any active browser sessions +# Get the Browser ID +BROWSER_ID=$(aws cloudformation describe-stacks \ + --stack-name weather-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`BrowserId`].OutputValue' \ + --output text) + +# List active sessions +aws bedrock-agentcore list-browser-sessions \ + --browser-id $BROWSER_ID \ + --region us-west-2 + +# Terminate each active session (replace SESSION_ID with actual session ID from list command) +# Repeat this command for each active session +aws bedrock-agentcore terminate-browser-session \ + --browser-id $BROWSER_ID \ + --session-id SESSION_ID \ + --region us-west-2 + +# Step 3: Delete the stack +aws cloudformation delete-stack \ + --stack-name weather-agent-demo \ + --region us-west-2 + +# Wait for deletion to complete +aws cloudformation wait stack-delete-complete \ + --stack-name weather-agent-demo \ + --region us-west-2 +``` + +**Important**: Browser sessions are automatically created when the agent uses the browser tool. Always terminate active sessions before deleting the stack to avoid deletion failures. + +### Using AWS Console + +1. Navigate to [S3 Console](https://console.aws.amazon.com/s3/) +2. Find the bucket (name format: `-results-`, e.g., `weather-agent-demo-results-123456789012`) +3. Empty the bucket +4. Navigate to [Bedrock AgentCore Console](https://console.aws.amazon.com/bedrock-agentcore/) +5. Go to "Browsers" in the left navigation +6. Find your browser (name starts with `weather_agent_demo_browser`) +7. Click on the browser name +8. In the "Sessions" tab, terminate any active sessions +9. Navigate to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) +10. Select the `weather-agent-demo` stack +11. Click "Delete" +12. Confirm deletion diff --git a/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/architecture.png b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/architecture.png new file mode 100644 index 00000000..a4e25a64 Binary files /dev/null and b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/architecture.png differ diff --git a/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/cleanup.sh b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/cleanup.sh new file mode 100755 index 00000000..f6e73aa7 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/cleanup.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Cleanup script for Weather Agent Runtime CloudFormation stack +# This script deletes the CloudFormation stack and all associated resources + +set -e + +# Configuration +STACK_NAME="${1:-weather-agent-demo}" +REGION="${2:-us-west-2}" + +echo "==========================================" +echo "Cleaning up Weather Agent Runtime" +echo "==========================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "==========================================" + +# Confirm deletion +read -p "Are you sure you want to delete the stack '$STACK_NAME'? (yes/no): " CONFIRM + +if [ "$CONFIRM" != "yes" ]; then + echo "Cleanup cancelled." + exit 0 +fi + +echo "" +echo "Deleting CloudFormation stack..." +aws cloudformation delete-stack \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + +if [ $? -eq 0 ]; then + echo "" + echo "โœ“ Stack deletion initiated successfully!" + echo "" + echo "Waiting for stack deletion to complete..." + echo "This may take a few minutes..." + echo "" + + aws cloudformation wait stack-delete-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + + if [ $? -eq 0 ]; then + echo "" + echo "==========================================" + echo "โœ“ Stack deleted successfully!" + echo "==========================================" + echo "" + echo "All resources have been cleaned up." + echo "" + else + echo "" + echo "โœ— Stack deletion failed or timed out" + echo "Check the CloudFormation console for details" + exit 1 + fi +else + echo "" + echo "โœ— Failed to initiate stack deletion" + exit 1 +fi diff --git a/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/deploy.sh b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/deploy.sh new file mode 100755 index 00000000..f25498ec --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/deploy.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# Deploy script for Weather Agent Runtime CloudFormation stack +# This script deploys a complete weather agent with browser, code interpreter, and memory + +set -e + +# Configuration +STACK_NAME="${1:-weather-agent-demo}" +REGION="${2:-us-west-2}" +TEMPLATE_FILE="end-to-end-weather-agent.yaml" + +echo "==========================================" +echo "Deploying Weather Agent Runtime" +echo "==========================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "==========================================" + +# Check if template file exists +if [ ! -f "$TEMPLATE_FILE" ]; then + echo "Error: Template file '$TEMPLATE_FILE' not found!" + exit 1 +fi + +# Deploy the CloudFormation stack +echo "" +echo "Creating CloudFormation stack..." +aws cloudformation create-stack \ + --stack-name "$STACK_NAME" \ + --template-body file://"$TEMPLATE_FILE" \ + --capabilities CAPABILITY_NAMED_IAM \ + --region "$REGION" + +if [ $? -eq 0 ]; then + echo "" + echo "โœ“ Stack creation initiated successfully!" + echo "" + echo "Waiting for stack creation to complete..." + echo "This will take approximately 15-20 minutes..." + echo "(Building Docker image, deploying agent with browser, code interpreter, and memory)" + echo "" + + aws cloudformation wait stack-create-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + + if [ $? -eq 0 ]; then + echo "" + echo "==========================================" + echo "โœ“ Stack deployed successfully!" + echo "==========================================" + echo "" + echo "Stack Outputs:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs' \ + --output table \ + --region "$REGION" + echo "" + echo "Agent Runtime ID:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`AgentRuntimeId`].OutputValue' \ + --output text \ + --region "$REGION" + echo "" + echo "Browser ID:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`BrowserId`].OutputValue' \ + --output text \ + --region "$REGION" + echo "" + echo "Code Interpreter ID:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`CodeInterpreterId`].OutputValue' \ + --output text \ + --region "$REGION" + echo "" + echo "Memory ID:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`MemoryId`].OutputValue' \ + --output text \ + --region "$REGION" + echo "" + echo "Results Bucket:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`ResultsBucket`].OutputValue' \ + --output text \ + --region "$REGION" + echo "" + echo "To delete this stack, run:" + echo " ./cleanup.sh $STACK_NAME $REGION" + echo "" + else + echo "" + echo "โœ— Stack creation failed or timed out" + echo "Check the CloudFormation console for details" + exit 1 + fi +else + echo "" + echo "โœ— Failed to initiate stack creation" + exit 1 +fi diff --git a/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/end-to-end-weather-agent.yaml b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/end-to-end-weather-agent.yaml new file mode 100644 index 00000000..cd2f75fd --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/end-to-end-weather-agent/end-to-end-weather-agent.yaml @@ -0,0 +1,1074 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: "Complete AgentCore deployment - Single stack with modular organization" + +# ============================================================================ +# PARAMETERS SECTION +# ============================================================================ +Parameters: + # Agent Configuration + AgentName: + Type: String + Default: "TestAgent" + Description: "Name for the agent runtime" + AllowedPattern: "^[a-zA-Z][a-zA-Z0-9_]{0,47}$" + ConstraintDescription: "Must start with a letter, max 48 characters, alphanumeric and underscores only" + + # Container Configuration + ImageTag: + Type: String + Default: "latest" + Description: "Tag for the Docker image" + + # Network Configuration + NetworkMode: + Type: String + Default: "PUBLIC" + Description: "Network mode for AgentCore resources" + AllowedValues: + - PUBLIC + - PRIVATE + + # ECR Configuration + ECRRepositoryName: + Type: String + Default: "agent-cfn-weather" + Description: "Name of the ECR repository" + + # Memory Configuration + MemoryName: + Type: String + Default: "TestAgentCoreMemoryWeather" + Description: "Name for the AgentCore memory resource" + AllowedPattern: "^[a-zA-Z][a-zA-Z0-9_]{0,47}$" + ConstraintDescription: "Must start with a letter, max 48 characters, alphanumeric and underscores only" + +# ============================================================================ +# METADATA SECTION +# ============================================================================ +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Agent Configuration" + Parameters: + - AgentName + - NetworkMode + - Label: + default: "Container Configuration" + Parameters: + - ECRRepositoryName + - ImageTag + - Label: + default: "Memory Configuration" + Parameters: + - MemoryName + ParameterLabels: + AgentName: + default: "Agent Name" + NetworkMode: + default: "Network Mode" + ECRRepositoryName: + default: "ECR Repository Name" + ImageTag: + default: "Image Tag" + MemoryName: + default: "Memory Name" + +# ============================================================================ +# RESOURCES SECTION - ORGANIZED BY MODULE +# ============================================================================ +Resources: + # ======================================================================== + # ECR MODULE - Container Registry + # ======================================================================== + + ECRRepository: + Type: AWS::ECR::Repository + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + RepositoryName: !Sub "${AWS::StackName}-${ECRRepositoryName}" + ImageTagMutability: MUTABLE + EmptyOnDelete: true + ImageScanningConfiguration: + ScanOnPush: true + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + - Sid: AllowPullFromAccount + Effect: Allow + Principal: + AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-ecr-repository" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: ECR + + # ======================================================================== + # S3 MODULE - Results Storage + # ======================================================================== + + ResultsBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub "${AWS::StackName}-results-${AWS::AccountId}" + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + VersioningConfiguration: + Status: Enabled + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-results-bucket" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: S3 + + # ======================================================================== + # IAM MODULE - Security and Permissions + # ======================================================================== + + # Agent Execution Role + AgentExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-agent-execution-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: AssumeRolePolicy + Effect: Allow + Principal: + Service: bedrock-agentcore.amazonaws.com + Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: !Ref AWS::AccountId + ArnLike: + aws:SourceArn: !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:*" + ManagedPolicyArns: + - arn:aws:iam::aws:policy/BedrockAgentCoreFullAccess + Policies: + - PolicyName: AgentCoreExecutionPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: ECRImageAccess + Effect: Allow + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + - ecr:BatchCheckLayerAvailability + Resource: !GetAtt ECRRepository.Arn + - Sid: ECRTokenAccess + Effect: Allow + Action: + - ecr:GetAuthorizationToken + Resource: "*" + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:DescribeLogStreams + - logs:CreateLogGroup + - logs:DescribeLogGroups + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "*" + - Sid: XRayTracing + Effect: Allow + Action: + - xray:PutTraceSegments + - xray:PutTelemetryRecords + - xray:GetSamplingRules + - xray:GetSamplingTargets + Resource: "*" + - Sid: CloudWatchMetrics + Effect: Allow + Resource: "*" + Action: cloudwatch:PutMetricData + Condition: + StringEquals: + cloudwatch:namespace: bedrock-agentcore + - Sid: GetAgentAccessToken + Effect: Allow + Action: + - bedrock-agentcore:GetWorkloadAccessToken + - bedrock-agentcore:GetWorkloadAccessTokenForJWT + - bedrock-agentcore:GetWorkloadAccessTokenForUserId + Resource: + - !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default" + - !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default/workload-identity/*" + - Sid: BedrockModelInvocation + Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + - Sid: S3ResultsAccess + Effect: Allow + Action: + - s3:PutObject + - s3:GetObject + - s3:DeleteObject + Resource: !Sub "${ResultsBucket.Arn}/*" + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-agent-execution-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # CodeBuild Service Role + CodeBuildRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-codebuild-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: CodeBuildPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*" + - Sid: ECRAccess + Effect: Allow + Action: + - ecr:BatchCheckLayerAvailability + - ecr:GetDownloadUrlForLayer + - ecr:BatchGetImage + - ecr:GetAuthorizationToken + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + Resource: + - !GetAtt ECRRepository.Arn + - "*" + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-codebuild-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # Lambda Custom Resource Role + CustomResourceRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-custom-resource-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: CustomResourcePolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: ECRAccess + Effect: Allow + Action: + - ecr:ListImages + - ecr:BatchDeleteImage + - ecr:GetAuthorizationToken + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + Resource: !GetAtt ECRRepository.Arn + - Sid: CodeBuildAccess + Effect: Allow + Action: + - codebuild:StartBuild + - codebuild:BatchGetBuilds + - codebuild:BatchGetProjects + Resource: !GetAtt AgentImageBuildProject.Arn + - Sid: BedrockAgentCoreMemoryAccess + Effect: Allow + Action: + - bedrock-agentcore:CreateEvent + - bedrock-agentcore:ListEvents + - bedrock-agentcore:GetMemory + Resource: "*" + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-custom-resource-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # ======================================================================== + # LAMBDA MODULE - Custom Resources + # ======================================================================== + + CodeBuildTriggerFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub "${AWS::StackName}-codebuild-trigger" + Description: "Triggers CodeBuild projects as CloudFormation custom resource" + Handler: index.handler + Role: !GetAtt CustomResourceRole.Arn + Runtime: python3.9 + Timeout: 900 + Code: + ZipFile: | + import boto3 + import cfnresponse + import json + import logging + import time + + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + def handler(event, context): + logger.info('Received event: %s', json.dumps(event)) + + try: + if event['RequestType'] == 'Delete': + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + return + + project_name = event['ResourceProperties']['ProjectName'] + wait_for_completion = event['ResourceProperties'].get('WaitForCompletion', 'true').lower() == 'true' + + logger.info(f"Attempting to start CodeBuild project: {project_name}") + logger.info(f"Wait for completion: {wait_for_completion}") + + # Start the CodeBuild project + codebuild = boto3.client('codebuild') + + # First, verify the project exists + try: + project_info = codebuild.batch_get_projects(names=[project_name]) + if not project_info['projects']: + raise Exception(f"CodeBuild project '{project_name}' not found") + logger.info(f"CodeBuild project '{project_name}' found") + except Exception as e: + logger.error(f"Error checking project existence: {str(e)}") + raise + + response = codebuild.start_build(projectName=project_name) + build_id = response['build']['id'] + + logger.info(f"Successfully started build: {build_id}") + + if not wait_for_completion: + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'BuildId': build_id, + 'Status': 'STARTED' + }) + return + + # Wait for the build to complete + max_wait_time = context.get_remaining_time_in_millis() / 1000 - 30 # Leave 30s buffer + start_time = time.time() + + while True: + if time.time() - start_time > max_wait_time: + error_message = f"Build {build_id} timed out" + logger.error(error_message) + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_message}) + return + + build_response = codebuild.batch_get_builds(ids=[build_id]) + build_status = build_response['builds'][0]['buildStatus'] + + if build_status == 'SUCCEEDED': + logger.info(f"Build {build_id} succeeded") + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'BuildId': build_id, + 'Status': build_status + }) + return + elif build_status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']: + error_message = f"Build {build_id} failed with status: {build_status}" + logger.error(error_message) + + # Get build logs for debugging + try: + logs_info = build_response['builds'][0].get('logs', {}) + if logs_info.get('groupName') and logs_info.get('streamName'): + logger.info(f"Build logs available in CloudWatch") + except Exception as log_error: + logger.warning(f"Could not get log information: {log_error}") + + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': error_message, + 'BuildId': build_id + }) + return + + logger.info(f"Build {build_id} status: {build_status}") + time.sleep(30) # Check every 30 seconds + + except Exception as e: + logger.error('Error: %s', str(e)) + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': str(e) + }) + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-codebuild-trigger" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: Lambda + + MemoryInitializerFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub "${AWS::StackName}-memory-initializer" + Description: "Initializes AgentCore Memory with default entries after memory creation" + Handler: index.handler + Role: !GetAtt CustomResourceRole.Arn + Runtime: python3.9 + Timeout: 300 + Code: + ZipFile: | + import boto3 + import cfnresponse + import json + import logging + import time + from datetime import datetime + + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + def handler(event, context): + logger.info('Received event: %s', json.dumps(event)) + + try: + if event['RequestType'] == 'Delete': + # No cleanup needed for memory entries + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + return + + memory_id = event['ResourceProperties']['MemoryId'] + region = event['ResourceProperties'].get('Region', 'us-west-2') + timestamp = datetime.utcnow().isoformat() + 'Z' + + logger.info(f"Initializing memory entries for Memory ID: {memory_id}") + + activity_preferences = { + "good_weather": ["hiking", "beach volleyball", "outdoor picnic", "farmers market", "gardening", "photography", "bird watching"], + "ok_weather": ["walking tours", "outdoor dining", "park visits", "museums"], + "poor_weather": ["indoor museums", "shopping", "restaurants", "movies"] + } + + # Convert the dictionary to a JSON string for storage in the blob + activity_preferences_json = json.dumps(activity_preferences) + + # Initialize the bedrock-agentcore client + client = boto3.client('bedrock-agentcore', region_name=region) + + response = client.create_event( + memoryId=memory_id, + actorId="user123", + sessionId="session456", + eventTimestamp=timestamp, + payload=[ + { + 'blob': activity_preferences_json, + } + ] + ) + logger.info(f"Successfully created memory event: {response}") + + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'MemoryId': memory_id, + 'Status': 'INITIALIZED' + }) + + except Exception as e: + logger.error('Error initializing memory: %s', str(e)) + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': str(e) + }) + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-memory-initializer" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: Lambda + + # ======================================================================== + # CODEBUILD MODULE - Container Image Building + # ======================================================================== + + AgentImageBuildProject: + Type: AWS::CodeBuild::Project + Properties: + Name: !Sub "${AWS::StackName}-strands-agent-build" + Description: !Sub "Build Strands agent Docker image for ${AWS::StackName}" + ServiceRole: !GetAtt CodeBuildRole.Arn + Artifacts: + Type: NO_ARTIFACTS + Environment: + Type: ARM_CONTAINER + ComputeType: BUILD_GENERAL1_LARGE + Image: aws/codebuild/amazonlinux2-aarch64-standard:3.0 + PrivilegedMode: true + EnvironmentVariables: + - Name: AWS_DEFAULT_REGION + Value: !Ref AWS::Region + - Name: AWS_ACCOUNT_ID + Value: !Ref AWS::AccountId + - Name: IMAGE_REPO_NAME + Value: !Ref ECRRepository + - Name: IMAGE_TAG + Value: !Ref ImageTag + - Name: STACK_NAME + Value: !Ref AWS::StackName + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + pre_build: + commands: + - echo Logging in to Amazon ECR... + - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com + build: + commands: + - echo Build started on `date` + - echo Building the Docker image for default agent ARM64... + + # Step 1.1: Create requirements.txt + - | + cat > requirements.txt << 'EOF' + strands-agents + strands-agents-tools + uv + boto3 + bedrock-agentcore + bedrock-agentcore-starter-toolkit + browser-use==0.3.2 + langchain-aws>=0.1.0 + rich + EOF + + # Step 1.2: Create my_agent.py + - | + cat > my_agent.py << 'EOF' + from strands import Agent, tool + from strands_tools import use_aws + from typing import Dict, Any + import json + import os + import asyncio + from contextlib import suppress + + from bedrock_agentcore.tools.browser_client import BrowserClient + from browser_use import Agent as BrowserAgent + from browser_use.browser.session import BrowserSession + from browser_use.browser import BrowserProfile + from langchain_aws import ChatBedrockConverse + from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter + from bedrock_agentcore.memory import MemoryClient + from rich.console import Console + import re + + from bedrock_agentcore.runtime import BedrockAgentCoreApp + app = BedrockAgentCoreApp() + + console = Console() + + # Configuration + BROWSER_ID = os.getenv('BROWSER_ID', "agentcore_dev_browser-Df3lyxkbjo") + CODE_INTERPRETER_ID = os.getenv('CODE_INTERPRETER_ID', "agentcore_dev_code_interpreter-IqIg8bqnKn") + MEMORY_ID = os.getenv('MEMORY_ID', "agentcore_dev_TestAgentCoreMemory-N7LCAH8ZCK") + RESULTS_BUCKET = os.getenv('RESULTS_BUCKET', "default-results-bucket") + region = 'us-west-2' + + # Async helper functions + async def run_browser_task(browser_session, bedrock_chat, task: str) -> str: + """Run a browser automation task using browser_use""" + try: + console.print(f"[blue]๐Ÿค– Executing browser task:[/blue] {task[:100]}...") + + agent = BrowserAgent( + task=task, + llm=bedrock_chat, + browser=browser_session + ) + + result = await agent.run() + console.print("[green]โœ… Browser task completed successfully![/green]") + + if 'done' in result.last_action() and 'text' in result.last_action()['done']: + return result.last_action()['done']['text'] + else: + raise ValueError("NO Data") + + except Exception as e: + console.print(f"[red]โŒ Browser task error: {e}[/red]") + raise + + async def initialize_browser_session(): + """Initialize Browser-use session with AgentCore WebSocket connection""" + try: + client = BrowserClient(region) + client.start(identifier=BROWSER_ID) + + ws_url, headers = client.generate_ws_headers() + console.print(f"[cyan]๐Ÿ”— Browser WebSocket URL: {ws_url[:50]}...[/cyan]") + + browser_profile = BrowserProfile( + headers=headers, + timeout=150000, + ) + + browser_session = BrowserSession( + cdp_url=ws_url, + browser_profile=browser_profile, + keep_alive=True + ) + + console.print("[cyan]๐Ÿ”„ Initializing browser session...[/cyan]") + await browser_session.start() + + bedrock_chat = ChatBedrockConverse( + model_id="us.anthropic.claude-3-7-sonnet-20250219-v1:0", + region_name="us-west-2" + ) + + console.print("[green]โœ… Browser session initialized and ready[/green]") + return browser_session, bedrock_chat, client + + except Exception as e: + console.print(f"[red]โŒ Failed to initialize browser session: {e}[/red]") + raise + + # Tools for Strands Agent + @tool + async def get_weather_data(city: str) -> Dict[str, Any]: + """Get weather data for a city using browser automation""" + browser_session = None + + try: + console.print(f"[cyan]๐ŸŒ Getting weather data for {city}[/cyan]") + + browser_session, bedrock_chat, browser_client = await initialize_browser_session() + + task = f"""Instruction: Extract 8-Day Weather Forecast for {city} from weather.gov + Steps: + - Go to https://weather.gov. + - Enter โ€œ{city}โ€ into the search box and Click on `GO` to execute the search. + - On the local forecast page, click the "Printable Forecast" link. + - Wait for the printable forecast page to load completely. + - For each day in the forecast, extract these fields: + - date (format YYYY-MM-DD) + - high (highest temperature) + - low (lowest temperature) + - conditions (short weather summary, e.g., "Clear") + - wind (wind speed as an integer; use mph or km/h as consistent) + - precip (precipitation chance or amount, zero if none) + - Format the extracted data as a JSON array of daily forecast objects, e.g.: + ```json + [ + {{ + "date": "2025-09-17", + "high": 78, + "low": 62, + "conditions": "Clear", + "wind": 10, + "precip": 80 + }}, + {{ + "date": "2025-09-18", + "high": 82, + "low": 65, + "conditions": "Partly Cloudy", + "wind": 10, + "precip": 80 + + }} + // ... Repeat for each day ... + ]``` + + - Return only this JSON array as the final output. + + Additional Notes: + Use null or 0 if any numeric value is missing. + Avoid scraping ads, navigation, or unrelated page elements. + If "Printable Forecast" is missing, fallback to the main forecast page. + Include error handling (e.g., return an empty array if forecast data isn't found). + Confirm the city name matches the requested location before returning results. + """ + + result = await run_browser_task(browser_session, bedrock_chat, task) + + if browser_client : + browser_client.stop() + + return { + "status": "success", + "content": [{"text": result}] + } + + except Exception as e: + console.print(f"[red]โŒ Error getting weather data: {e}[/red]") + return { + "status": "error", + "content": [{"text": f"Error getting weather data: {str(e)}"}] + } + + finally: + if browser_session: + console.print("[yellow]๐Ÿ”Œ Closing browser session...[/yellow]") + with suppress(Exception): + await browser_session.close() + console.print("[green]โœ… Browser session closed[/green]") + + @tool + def generate_analysis_code(weather_data: str) -> Dict[str, Any]: + """Generate Python code for weather classification""" + try: + query = f"""Create Python code to classify weather days as GOOD/OK/POOR: + + Rules: + - GOOD: 65-80ยฐF, clear conditions, no rain + - OK: 55-85ยฐF, partly cloudy, slight rain chance + - POOR: <55ยฐF or >85ยฐF, cloudy/rainy + + Weather data: + {weather_data} + + Store weather data stored in python variable for using it in python code + + Return code that outputs list of tuples: [('2025-09-16', 'GOOD'), ('2025-09-17', 'OK'), ...]""" + + agent = Agent() + result = agent(query) + + pattern = r'```(?:json|python)\n(.*?)\n```' + match = re.search(pattern, result.message['content'][0]['text'], re.DOTALL) + python_code = match.group(1).strip() if match else result.message['content'][0]['text'] + + return {"status": "success", "content": [{"text": python_code}]} + except Exception as e: + return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]} + + @tool + def execute_code(python_code: str) -> Dict[str, Any]: + """Execute Python code using AgentCore Code Interpreter""" + try: + code_client = CodeInterpreter('us-west-2') + code_client.start(identifier=CODE_INTERPRETER_ID) + + response = code_client.invoke("executeCode", { + "code": python_code, + "language": "python", + "clearContext": True + }) + + for event in response["stream"]: + code_execute_result = json.dumps(event["result"]) + + analysis_results = json.loads(code_execute_result) + console.print("Analysis results:", analysis_results) + + return {"status": "success", "content": [{"text": str(analysis_results)}]} + + except Exception as e: + return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]} + + @tool + def get_activity_preferences() -> Dict[str, Any]: + """Get activity preferences from memory""" + try: + client = MemoryClient(region_name='us-west-2') + response = client.list_events( + memory_id=MEMORY_ID, + actor_id="user123", + session_id="session456", + max_results=50, + include_payload=True + ) + + preferences = response[0]["payload"][0]['blob'] if response else "No preferences found" + return {"status": "success", "content": [{"text": preferences}]} + except Exception as e: + return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]} + + def create_weather_agent() -> Agent: + """Create the weather agent with all tools""" + system_prompt = f"""You are a Weather-Based Activity Planning Assistant. + + When a user asks about activities for a location, follow below stepes Sequentially: + 1. Extract city from user query + 2. Call get_weather_data(city) to get weather information + 3. Call generate_analysis_code(weather_data) to create classification code + 4. Call execute_code(python_code) to get Day Type ( GOOD, OK , POOR ) for forecasting dates. + 5. Call get_activity_preferences() to get user preferences + 6. Generate Activity Recommendations based on weather and preferences that you have recieved from previous steps + 7. Generate the comprehensive Markdown file (results.md) and store it in S3 Bucket : {RESULTS_BUCKET} through use_aws tool. + + IMPORTANT: Provide complete recommendations and end your response. Do NOT ask follow-up questions or wait for additional input.""" + + return Agent( + tools=[get_weather_data, generate_analysis_code, execute_code, get_activity_preferences, use_aws], + system_prompt=system_prompt, + name="WeatherActivityPlanner" + ) + + @app.async_task + async def async_main(query=None): + """Async main function""" + console.print("๐ŸŒค๏ธ Weather-Based Activity Planner - Async Version") + console.print("=" * 30) + + agent = create_weather_agent() + + query = query or "What should I do this weekend in Richmond VA?" + console.print(f"\n[bold blue]๐Ÿ” Query:[/bold blue] {query}") + console.print("-" * 50) + + try: + os.environ["BYPASS_TOOL_CONSENT"] = "True" + result = agent(query) + + return { + "status": "completed", + "result": result.message['content'][0]['text'] + } + + except Exception as e: + console.print(f"[red]โŒ Error: {e}[/red]") + import traceback + traceback.print_exc() + return { + "status": "error", + "error": str(e) + } + + @app.entrypoint + async def invoke(payload=None): + try: + # change + query = payload.get("prompt") + + asyncio.create_task(async_main(query)) + + msg = ( + "Processing started ... " + f"You can monitor status in CloudWatch logs at /aws/bedrock-agentcore/runtimes/ ....." + f"You can see the result at {RESULTS_BUCKET} ...." + ) + + return { + "status": "Started", + "message": msg + } + + except Exception as e: + return {"error": str(e)} + + if __name__ == "__main__": + app.run() + EOF + + # Step 1.3: Create Dockerfile + - | + cat > Dockerfile << 'EOF' + FROM public.ecr.aws/docker/library/python:3.11-slim + WORKDIR /app + + COPY requirements.txt requirements.txt + RUN pip install -r requirements.txt + RUN pip install aws-opentelemetry-distro>=0.10.1 + + ENV AWS_REGION=us-west-2 + ENV AWS_DEFAULT_REGION=us-west-2 + + # Create non-root user + RUN useradd -m -u 1000 bedrock_agentcore + USER bedrock_agentcore + + EXPOSE 8080 + EXPOSE 8000 + + COPY . . + + CMD ["opentelemetry-instrument", "python", "-m", "my_agent"] + + EOF + + # Step 1.5: Build the image + - echo Building ARM64 image... + - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . + - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + + post_build: + commands: + - echo Build completed on `date` + - echo Pushing the Docker image... + - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + - echo ARM64 Docker image pushed successfully + + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-strands-build" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: CodeBuild + + # CUSTOM RESOURCE - Trigger Image Build + TriggerImageBuild: + Type: Custom::CodeBuildTrigger + DependsOn: + - ECRRepository + - AgentImageBuildProject + - CodeBuildTriggerFunction + Properties: + ServiceToken: !GetAtt CodeBuildTriggerFunction.Arn + ProjectName: !Ref AgentImageBuildProject + WaitForCompletion: "true" + + # ======================================================================== + # AGENTCORE MODULE - Runtime and Tools + # ======================================================================== + + # AgentCore Runtime + AgentRuntime: + Type: AWS::BedrockAgentCore::Runtime + DependsOn: + - TriggerImageBuild + - BrowserTool + - CodeInterpreterTool + - BasicMemory + Properties: + AgentRuntimeName: !Sub + - "${StackNameUnderscore}_${AgentName}" + - StackNameUnderscore: !Join ["_", !Split ["-", !Ref "AWS::StackName"]] + AgentRuntimeArtifact: + ContainerConfiguration: + ContainerUri: !Sub "${ECRRepository.RepositoryUri}:${ImageTag}" + RoleArn: !GetAtt AgentExecutionRole.Arn + NetworkConfiguration: + NetworkMode: !Ref NetworkMode + Description: !Sub "Strands agent runtime for ${AWS::StackName}" + EnvironmentVariables: + BROWSER_ID: !GetAtt BrowserTool.BrowserId + CODE_INTERPRETER_ID: !GetAtt CodeInterpreterTool.CodeInterpreterId + MEMORY_ID: !GetAtt BasicMemory.MemoryId + RESULTS_BUCKET: !Ref ResultsBucket + + + # Browser Tool + BrowserTool: + Type: AWS::BedrockAgentCore::BrowserCustom + Properties: + Name: !Sub + - "${StackNameUnderscore}_browser" + - StackNameUnderscore: !Join ["_", !Split ["-", !Ref "AWS::StackName"]] + Description: !Sub "Browser tool for ${AWS::StackName} web automation" + NetworkConfiguration: + NetworkMode: !Ref NetworkMode + RecordingConfig: + Enabled: false + + # Code Interpreter Tool + CodeInterpreterTool: + Type: AWS::BedrockAgentCore::CodeInterpreterCustom + Properties: + Name: !Sub + - "${StackNameUnderscore}_code_interpreter" + - StackNameUnderscore: !Join ["_", !Split ["-", !Ref "AWS::StackName"]] + Description: !Sub "Code interpreter tool for ${AWS::StackName} code execution" + NetworkConfiguration: + NetworkMode: !Ref NetworkMode + + # Basic Memory + BasicMemory: + Type: AWS::BedrockAgentCore::Memory + Properties: + Name: !Sub + - "${StackNameUnderscore}_${MemoryName}" + - StackNameUnderscore: !Join ["_", !Split ["-", !Ref "AWS::StackName"]] + Description: !Sub "Memory created for ${AWS::StackName} integration testing" + EventExpiryDuration: 30 + + # ======================================================================== + # CUSTOM RESOURCE - Initialize Memory with Default Entries + # ======================================================================== + + InitializeMemoryEntries: + Type: Custom::MemoryInitializer + DependsOn: + - BasicMemory + - MemoryInitializerFunction + Properties: + ServiceToken: !GetAtt MemoryInitializerFunction.Arn + MemoryId: !GetAtt BasicMemory.MemoryId + Region: !Ref AWS::Region + +# ============================================================================ +# OUTPUTS SECTION - ORGANIZED BY MODULE +# ============================================================================ +Outputs: + # AGENTCORE MODULE OUTPUTS + AgentRuntimeId: + Description: "ID of the created agent runtime" + Value: !GetAtt AgentRuntime.AgentRuntimeId + Export: + Name: !Sub "${AWS::StackName}-AgentRuntimeId" + + BrowserId: + Description: ID of the created browser + Value: !GetAtt BrowserTool.BrowserId + Export: + Name: !Sub "${AWS::StackName}-BrowserId" + + CodeInterpreterId: + Description: ID of the created code interpreter + Value: !GetAtt CodeInterpreterTool.CodeInterpreterId + Export: + Name: !Sub "${AWS::StackName}-CodeInterpreterId" + + MemoryId: + Description: "ID of the created memory" + Value: !GetAtt BasicMemory.MemoryId + Export: + Name: !Sub "${AWS::StackName}-MemoryId" + + ResultsBucket: + Description: "S3 bucket for storing agent results" + Value: !Ref ResultsBucket + Export: + Name: !Sub "${AWS::StackName}-ResultsBucket" diff --git a/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/README.md b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/README.md new file mode 100644 index 00000000..14bd8ad1 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/README.md @@ -0,0 +1,181 @@ +# Hosting MCP Server on AgentCore Runtime - CloudFormation + +## Overview + +This CloudFormation template deploys an MCP (Model Context Protocol) server on Amazon Bedrock AgentCore Runtime. It demonstrates how to host MCP tools on AgentCore Runtime using infrastructure as code, with automated deployment scripts for a streamlined experience. + +The template uses the Amazon Bedrock AgentCore Python SDK to wrap agent functions as an MCP server compatible with Amazon Bedrock AgentCore. It handles the MCP server details so you can focus on your agent's core functionality. + +When hosting tools, the Amazon Bedrock AgentCore Python SDK implements the [Stateless Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) transport protocol with the `MCP-Session-Id` header for session isolation. Your MCP server will be hosted on port `8000` and provide one invocation path: the `mcp-POST` endpoint. + +### Tutorial Details + +| Information | Details | +|:--------------------|:----------------------------------------------------------| +| Tutorial type | Hosting Tools | +| Tool type | MCP server | +| Tutorial components | CloudFormation, AgentCore Runtime, MCP server | +| Tutorial vertical | Cross-vertical | +| Example complexity | Easy | +| SDK used | Amazon BedrockAgentCore Python SDK and MCP Client | + +### Architecture + +![MCP Server AgentCore Runtime Architecture](architecture.png) + +This CloudFormation template deploys a simple MCP server with 3 tools: `add_numbers`, `multiply_numbers`, and `greet_user`. + +The architecture consists of: + +- **User/MCP Client**: Sends requests to the MCP server with JWT authentication +- **Amazon Cognito**: Provides JWT-based authentication + - User Pool with pre-created test user (testuser/MyPassword123!) + - User Pool Client for application access +- **AWS CodeBuild**: Builds the ARM64 Docker container image with the MCP server +- **Amazon ECR Repository**: Stores the container image +- **AgentCore Runtime**: Hosts the MCP Server + - **MCP Server**: Exposes three tools via HTTP transport + - `add_numbers`: Adds two numbers + - `multiply_numbers`: Multiplies two numbers + - `greet_user`: Greets a user by name + - Validates JWT tokens from Cognito + - Processes MCP tool invocations +- **IAM Roles**: + - IAM role for CodeBuild (builds and pushes images) + - IAM role for AgentCore Runtime (runtime permissions) + +### Key Features + +* **One-Command Deployment** - Automated scripts handle everything +* **Complete Infrastructure** - Full infrastructure as code +* **Secure by Default** - JWT authentication with Cognito +* **Automated Build** - CodeBuild creates ARM64 Docker images +* **Easy Testing** - Automated test script included +* **Simple Cleanup** - One command removes all resources + +## What Gets Deployed + +The CloudFormation stack creates: + +- **Amazon ECR Repository** - Stores the MCP server Docker image +- **AWS CodeBuild Project** - Builds ARM64 Docker image automatically +- **Amazon Cognito User Pool** - JWT authentication +- **Cognito User Pool Client** - Application client configuration +- **Cognito User** - Pre-created test user (testuser/MyPassword123!) +- **IAM Roles** - Least-privilege permissions for all services +- **Lambda Functions** - Custom resource automation +- **Amazon Bedrock AgentCore Runtime** - Hosts the MCP server + +**MCP Server Tools**: +- `add_numbers` - Adds two numbers together +- `multiply_numbers` - Multiplies two numbers +- `greet_user` - Greets a user by name + +## Prerequisites + +- AWS CLI configured with appropriate credentials +- AWS account with permissions to create: + - CloudFormation stacks + - ECR repositories + - CodeBuild projects + - Cognito User Pools + - IAM roles and policies + - Lambda functions + - Bedrock AgentCore Runtime +- Python 3.8+ (for testing) +- `boto3` and `mcp` Python packages (installed automatically by test script) + +## Quick Start + +### 1. Deploy the Stack + +```bash +cd 04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime +./deploy.sh +``` + +The deployment takes approximately **10-15 minutes** and includes: +- Creating all AWS resources +- Building the Docker image +- Pushing to ECR +- Starting the AgentCore Runtime + +### 2. Test the MCP Server + +After deployment completes: + +```bash +./test.sh +``` + +This will: +- Authenticate with Cognito +- Test all three MCP tools +- Display the results + +### 3. Cleanup + +When you're done: + +```bash +./cleanup.sh +``` + +This removes all created resources. + +## Understanding the Components + +#### Authentication Flow + +1. User authenticates with Cognito using username/password +2. Cognito returns an access token (JWT) +3. Access token is passed as Bearer token to AgentCore Runtime +4. AgentCore Runtime validates the token with Cognito +5. If valid, the MCP server processes the request + +#### MCP Server Implementation + +The MCP server is embedded in the CodeBuild buildspec and includes: + +```python +from bedrock_agentcore.mcp import MCPServer + +server = MCPServer() + +@server.tool() +def add_numbers(a: int, b: int) -> int: + """Add two numbers together.""" + return a + b + +@server.tool() +def multiply_numbers(a: int, b: int) -> int: + """Multiply two numbers together.""" + return a * b + +@server.tool() +def greet_user(name: str) -> str: + """Greet a user by name.""" + return f"Hello, {name}!" +``` + +#### Docker Image Build + +CodeBuild automatically: +1. Creates a Python 3.12 ARM64 environment +2. Installs dependencies +3. Creates the MCP server code +4. Builds the Docker image +5. Pushes to ECR +6. Triggers AgentCore Runtime update + + + + + +## Additional Resources + +- [Amazon Bedrock AgentCore Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html) +- [Model Context Protocol Specification](https://modelcontextprotocol.io/) +- [CloudFormation Template Reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/AWS_BedrockAgentCore.html) +- [Original Tutorial](../../01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/) +- [Detailed Technical Guide](DETAILED_GUIDE.md) diff --git a/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/architecture.png b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/architecture.png new file mode 100644 index 00000000..2bab62d9 Binary files /dev/null and b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/architecture.png differ diff --git a/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/cleanup.sh b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/cleanup.sh new file mode 100755 index 00000000..7998b8c0 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/cleanup.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Cleanup script for MCP Server deployment + +set -e + +STACK_NAME="${1:-mcp-server-demo}" +REGION="${2:-us-west-2}" + +echo "==========================================" +echo "MCP Server Cleanup Script" +echo "==========================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "" + +read -p "โš ๏ธ This will delete all resources. Continue? (y/N): " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Cleanup cancelled" + exit 0 +fi + +echo "" +echo "๐Ÿ—‘๏ธ Deleting CloudFormation stack..." +aws cloudformation delete-stack \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + +echo "โœ“ Stack deletion initiated" +echo "" +echo "โณ Waiting for stack deletion to complete..." +aws cloudformation wait stack-delete-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + +echo "" +echo "==========================================" +echo "โœ… Cleanup Complete!" +echo "==========================================" +echo "" +echo "All resources have been deleted." +echo "" diff --git a/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/deploy.sh b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/deploy.sh new file mode 100755 index 00000000..908a1043 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/deploy.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Streamlined deployment script for MCP Server on AgentCore Runtime + +set -e + +STACK_NAME="${1:-mcp-server-demo}" +REGION="${2:-us-west-2}" + +echo "==========================================" +echo "MCP Server Deployment Script" +echo "==========================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "" + +# Deploy CloudFormation stack +echo "๐Ÿ“ฆ Deploying CloudFormation stack..." +aws cloudformation create-stack \ + --stack-name "$STACK_NAME" \ + --template-body file://mcp-server-template.yaml \ + --capabilities CAPABILITY_NAMED_IAM \ + --region "$REGION" + +echo "โœ“ Stack creation initiated" +echo "" + +# Wait for stack to complete +echo "โณ Waiting for stack to complete (this takes ~10-15 minutes)..." +aws cloudformation wait stack-create-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + +echo "โœ“ Stack deployment complete!" +echo "" + +# Get stack outputs +echo "๐Ÿ“‹ Retrieving stack outputs..." +CLIENT_ID=$(aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`CognitoUserPoolClientId`].OutputValue' \ + --output text \ + --region "$REGION") + +AGENT_ARN=$(aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`MCPServerRuntimeArn`].OutputValue' \ + --output text \ + --region "$REGION") + +echo "" +echo "==========================================" +echo "โœ… Deployment Complete!" +echo "==========================================" +echo "" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "Client ID: $CLIENT_ID" +echo "Agent ARN: $AGENT_ARN" +echo "" +echo "Test Credentials:" +echo " Username: testuser" +echo " Password: MyPassword123!" +echo "" +echo "==========================================" +echo "Next Steps:" +echo "==========================================" +echo "" +echo "Test your MCP server:" +echo " ./test.sh $STACK_NAME $REGION" +echo "" diff --git a/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/get_token.py b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/get_token.py new file mode 100644 index 00000000..196059c2 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/get_token.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +Simple Cognito Authentication Script +Matches the approach from the original tutorial +""" + +import boto3 +import sys + + +def get_token(client_id, username, password, region=None): + """Get authentication token from Cognito.""" + # Use provided region or default from environment/config + if region: + cognito_client = boto3.client("cognito-idp", region_name=region) + else: + cognito_client = boto3.client("cognito-idp") + + try: + auth_response = cognito_client.initiate_auth( + ClientId=client_id, + AuthFlow="USER_PASSWORD_AUTH", + AuthParameters={"USERNAME": username, "PASSWORD": password}, + ) + + return auth_response["AuthenticationResult"]["AccessToken"] + + except Exception as e: + print(f"Error: {e}") + print("Troubleshooting:") + print(" - Verify the Client ID is correct") + print(" - Ensure you're using the correct region") + print(" - Check that the user exists and password is correct") + print(" - Verify USER_PASSWORD_AUTH is enabled for this client") + sys.exit(1) + + +def main(): + if len(sys.argv) < 4 or len(sys.argv) > 5: + print("Usage: python get_token.py [region]") + print("\nExamples:") + print(" python get_token.py abc123xyz testuser MyPassword123!") + print(" python get_token.py abc123xyz testuser MyPassword123! us-west-2") + sys.exit(1) + + client_id = sys.argv[1] + username = sys.argv[2] + password = sys.argv[3] + region = sys.argv[4] if len(sys.argv) == 5 else None + + if region: + print(f"Authenticating with Cognito in region {region}...") + else: + print("Authenticating with Cognito...") + + token = get_token(client_id, username, password, region) + + print("\n" + "=" * 70) + print("Authentication Successful!") + print("=" * 70) + print("\nAccess Token:") + print(token) + print("\n" + "=" * 70) + print("Export Command:") + print("=" * 70) + print(f'\nexport JWT_TOKEN="{token}"') + print("\nThen use in curl:") + print('curl -H "Authorization: Bearer $JWT_TOKEN" ') + print() + + +if __name__ == "__main__": + main() diff --git a/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/mcp-server-template.yaml b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/mcp-server-template.yaml new file mode 100644 index 00000000..d976eb56 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/mcp-server-template.yaml @@ -0,0 +1,725 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: "MCP Server on AgentCore Runtime - Deploy an MCP server with custom tools (add_numbers, multiply_numbers, greet_user)" + +# ============================================================================ +# PARAMETERS SECTION +# ============================================================================ +Parameters: + # Agent Configuration + AgentName: + Type: String + Default: "MCPServerAgent" + Description: "Name for the MCP server runtime" + AllowedPattern: "^[a-zA-Z][a-zA-Z0-9_]{0,47}$" + ConstraintDescription: "Must start with a letter, max 48 characters, alphanumeric and underscores only" + + # Container Configuration + ImageTag: + Type: String + Default: "latest" + Description: "Tag for the Docker image" + + # Network Configuration + NetworkMode: + Type: String + Default: "PUBLIC" + Description: "Network mode for AgentCore resources" + AllowedValues: + - PUBLIC + - PRIVATE + + # ECR Configuration + ECRRepositoryName: + Type: String + Default: "mcp-server" + Description: "Name of the ECR repository" + +# ============================================================================ +# METADATA SECTION +# ============================================================================ +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Agent Configuration" + Parameters: + - AgentName + - NetworkMode + - Label: + default: "Container Configuration" + Parameters: + - ECRRepositoryName + - ImageTag + ParameterLabels: + AgentName: + default: "Agent Name" + NetworkMode: + default: "Network Mode" + ECRRepositoryName: + default: "ECR Repository Name" + ImageTag: + default: "Image Tag" + +# ============================================================================ +# RESOURCES SECTION +# ============================================================================ +Resources: + # ======================================================================== + # ECR MODULE - Container Registry + # ======================================================================== + + ECRRepository: + Type: AWS::ECR::Repository + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + RepositoryName: !Sub "${AWS::StackName}-${ECRRepositoryName}" + ImageTagMutability: MUTABLE + EmptyOnDelete: true + ImageScanningConfiguration: + ScanOnPush: true + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + - Sid: AllowPullFromAccount + Effect: Allow + Principal: + AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-ecr-repository" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: ECR + + # ======================================================================== + # COGNITO MODULE - Authentication + # ======================================================================== + + CognitoUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: !Sub "${AWS::StackName}-user-pool" + Policies: + PasswordPolicy: + MinimumLength: 8 + RequireUppercase: false + RequireLowercase: false + RequireNumbers: false + RequireSymbols: false + Schema: + - Name: email + AttributeDataType: String + Required: false + Mutable: true + UserPoolTags: + Name: !Sub "${AWS::StackName}-user-pool" + StackName: !Ref AWS::StackName + Module: Cognito + + CognitoUserPoolClient: + Type: AWS::Cognito::UserPoolClient + Properties: + ClientName: !Sub "${AWS::StackName}-client" + UserPoolId: !Ref CognitoUserPool + GenerateSecret: false + ExplicitAuthFlows: + - ALLOW_USER_PASSWORD_AUTH + - ALLOW_REFRESH_TOKEN_AUTH + PreventUserExistenceErrors: ENABLED + + CognitoUser: + Type: AWS::Cognito::UserPoolUser + Properties: + UserPoolId: !Ref CognitoUserPool + Username: testuser + MessageAction: SUPPRESS + + SetCognitoUserPassword: + Type: Custom::CognitoSetPassword + DependsOn: CognitoUser + Properties: + ServiceToken: !GetAtt CognitoPasswordSetterFunction.Arn + UserPoolId: !Ref CognitoUserPool + Username: testuser + Password: MyPassword123! + + # ======================================================================== + # IAM MODULE - Security and Permissions + # ======================================================================== + + # Agent Execution Role + AgentExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-agent-execution-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: AssumeRolePolicy + Effect: Allow + Principal: + Service: bedrock-agentcore.amazonaws.com + Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: !Ref AWS::AccountId + ArnLike: + aws:SourceArn: !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:*" + ManagedPolicyArns: + - arn:aws:iam::aws:policy/BedrockAgentCoreFullAccess + Policies: + - PolicyName: AgentCoreExecutionPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: ECRImageAccess + Effect: Allow + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + - ecr:BatchCheckLayerAvailability + Resource: !GetAtt ECRRepository.Arn + - Sid: ECRTokenAccess + Effect: Allow + Action: + - ecr:GetAuthorizationToken + Resource: "*" + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:DescribeLogStreams + - logs:CreateLogGroup + - logs:DescribeLogGroups + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "*" + - Sid: XRayTracing + Effect: Allow + Action: + - xray:PutTraceSegments + - xray:PutTelemetryRecords + - xray:GetSamplingRules + - xray:GetSamplingTargets + Resource: "*" + - Sid: CloudWatchMetrics + Effect: Allow + Resource: "*" + Action: cloudwatch:PutMetricData + Condition: + StringEquals: + cloudwatch:namespace: bedrock-agentcore + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-agent-execution-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # CodeBuild Service Role + CodeBuildRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-codebuild-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: CodeBuildPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*" + - Sid: ECRAccess + Effect: Allow + Action: + - ecr:BatchCheckLayerAvailability + - ecr:GetDownloadUrlForLayer + - ecr:BatchGetImage + - ecr:GetAuthorizationToken + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + Resource: + - !GetAtt ECRRepository.Arn + - "*" + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-codebuild-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # Lambda Custom Resource Role + CustomResourceRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-custom-resource-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: CustomResourcePolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CodeBuildAccess + Effect: Allow + Action: + - codebuild:StartBuild + - codebuild:BatchGetBuilds + - codebuild:BatchGetProjects + Resource: !GetAtt MCPServerImageBuildProject.Arn + - Sid: CognitoAccess + Effect: Allow + Action: + - cognito-idp:AdminSetUserPassword + Resource: !GetAtt CognitoUserPool.Arn + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-custom-resource-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # ======================================================================== + # LAMBDA MODULE - Custom Resources + # ======================================================================== + + CodeBuildTriggerFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub "${AWS::StackName}-codebuild-trigger" + Description: "Triggers CodeBuild projects as CloudFormation custom resource" + Handler: index.handler + Role: !GetAtt CustomResourceRole.Arn + Runtime: python3.9 + Timeout: 900 + Code: + ZipFile: | + import boto3 + import cfnresponse + import json + import logging + import time + + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + def handler(event, context): + logger.info('Received event: %s', json.dumps(event)) + + try: + if event['RequestType'] == 'Delete': + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + return + + project_name = event['ResourceProperties']['ProjectName'] + wait_for_completion = event['ResourceProperties'].get('WaitForCompletion', 'true').lower() == 'true' + + logger.info(f"Attempting to start CodeBuild project: {project_name}") + logger.info(f"Wait for completion: {wait_for_completion}") + + codebuild = boto3.client('codebuild') + + try: + project_info = codebuild.batch_get_projects(names=[project_name]) + if not project_info['projects']: + raise Exception(f"CodeBuild project '{project_name}' not found") + logger.info(f"CodeBuild project '{project_name}' found") + except Exception as e: + logger.error(f"Error checking project existence: {str(e)}") + raise + + response = codebuild.start_build(projectName=project_name) + build_id = response['build']['id'] + + logger.info(f"Successfully started build: {build_id}") + + if not wait_for_completion: + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'BuildId': build_id, + 'Status': 'STARTED' + }) + return + + max_wait_time = context.get_remaining_time_in_millis() / 1000 - 30 + start_time = time.time() + + while True: + if time.time() - start_time > max_wait_time: + error_message = f"Build {build_id} timed out" + logger.error(error_message) + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_message}) + return + + build_response = codebuild.batch_get_builds(ids=[build_id]) + build_status = build_response['builds'][0]['buildStatus'] + + if build_status == 'SUCCEEDED': + logger.info(f"Build {build_id} succeeded") + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'BuildId': build_id, + 'Status': build_status + }) + return + elif build_status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']: + error_message = f"Build {build_id} failed with status: {build_status}" + logger.error(error_message) + + try: + logs_info = build_response['builds'][0].get('logs', {}) + if logs_info.get('groupName') and logs_info.get('streamName'): + logger.info(f"Build logs available in CloudWatch") + except Exception as log_error: + logger.warning(f"Could not get log information: {log_error}") + + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': error_message, + 'BuildId': build_id + }) + return + + logger.info(f"Build {build_id} status: {build_status}") + time.sleep(30) + + except Exception as e: + logger.error('Error: %s', str(e)) + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': str(e) + }) + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-codebuild-trigger" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: Lambda + + CognitoPasswordSetterFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub "${AWS::StackName}-cognito-password-setter" + Description: "Sets Cognito user password" + Handler: index.handler + Role: !GetAtt CustomResourceRole.Arn + Runtime: python3.9 + Timeout: 300 + Code: + ZipFile: | + import boto3 + import cfnresponse + import json + import logging + + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + def handler(event, context): + logger.info('Received event: %s', json.dumps(event)) + + try: + if event['RequestType'] == 'Delete': + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + return + + user_pool_id = event['ResourceProperties']['UserPoolId'] + username = event['ResourceProperties']['Username'] + password = event['ResourceProperties']['Password'] + + cognito = boto3.client('cognito-idp') + + # Set permanent password + cognito.admin_set_user_password( + UserPoolId=user_pool_id, + Username=username, + Password=password, + Permanent=True + ) + + logger.info(f"Password set successfully for user: {username}") + + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'Status': 'SUCCESS' + }) + + except Exception as e: + logger.error('Error: %s', str(e)) + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': str(e) + }) + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-cognito-password-setter" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: Lambda + + # ======================================================================== + # CODEBUILD MODULE - Container Image Building + # ======================================================================== + + MCPServerImageBuildProject: + Type: AWS::CodeBuild::Project + Properties: + Name: !Sub "${AWS::StackName}-mcp-server-build" + Description: !Sub "Build MCP server Docker image for ${AWS::StackName}" + ServiceRole: !GetAtt CodeBuildRole.Arn + Artifacts: + Type: NO_ARTIFACTS + Environment: + Type: ARM_CONTAINER + ComputeType: BUILD_GENERAL1_LARGE + Image: aws/codebuild/amazonlinux2-aarch64-standard:3.0 + PrivilegedMode: true + EnvironmentVariables: + - Name: AWS_DEFAULT_REGION + Value: !Ref AWS::Region + - Name: AWS_ACCOUNT_ID + Value: !Ref AWS::AccountId + - Name: IMAGE_REPO_NAME + Value: !Ref ECRRepository + - Name: IMAGE_TAG + Value: !Ref ImageTag + - Name: STACK_NAME + Value: !Ref AWS::StackName + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + pre_build: + commands: + - echo Logging in to Amazon ECR... + - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com + build: + commands: + - echo Build started on `date` + - echo Building the Docker image for MCP server ARM64... + + # Create requirements.txt + - | + cat > requirements.txt << 'EOF' + mcp>=1.10.0 + boto3 + bedrock-agentcore + EOF + + # Create mcp_server.py + - | + cat > mcp_server.py << 'EOF' + from mcp.server.fastmcp import FastMCP + from starlette.responses import JSONResponse + + mcp = FastMCP(host="0.0.0.0", stateless_http=True) + + @mcp.tool() + def add_numbers(a: int, b: int) -> int: + """Add two numbers together""" + return a + b + + @mcp.tool() + def multiply_numbers(a: int, b: int) -> int: + """Multiply two numbers together""" + return a * b + + @mcp.tool() + def greet_user(name: str) -> str: + """Greet a user by name""" + return f"Hello, {name}! Nice to meet you." + + if __name__ == "__main__": + mcp.run(transport="streamable-http") + EOF + + # Create Dockerfile + - | + cat > Dockerfile << 'EOF' + FROM public.ecr.aws/docker/library/python:3.11-slim + WORKDIR /app + + COPY requirements.txt requirements.txt + RUN pip install -r requirements.txt + + ENV AWS_REGION=us-west-2 + ENV AWS_DEFAULT_REGION=us-west-2 + + # Create non-root user + RUN useradd -m -u 1000 bedrock_agentcore + USER bedrock_agentcore + + EXPOSE 8000 + + COPY . . + + CMD ["python", "-m", "mcp_server"] + EOF + + # Build the image + - echo Building ARM64 image... + - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . + - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + + post_build: + commands: + - echo Build completed on `date` + - echo Pushing the Docker image... + - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + - echo ARM64 Docker image pushed successfully + + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-mcp-server-build" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: CodeBuild + + # CUSTOM RESOURCE - Trigger Image Build + TriggerImageBuild: + Type: Custom::CodeBuildTrigger + DependsOn: + - ECRRepository + - MCPServerImageBuildProject + - CodeBuildTriggerFunction + Properties: + ServiceToken: !GetAtt CodeBuildTriggerFunction.Arn + ProjectName: !Ref MCPServerImageBuildProject + WaitForCompletion: "true" + + # ======================================================================== + # AGENTCORE MODULE - MCP Server Runtime + # ======================================================================== + + MCPServerRuntime: + Type: AWS::BedrockAgentCore::Runtime + DependsOn: + - TriggerImageBuild + Properties: + AgentRuntimeName: !Sub + - "${StackNameUnderscore}_${AgentName}" + - StackNameUnderscore: !Join ["_", !Split ["-", !Ref "AWS::StackName"]] + AgentRuntimeArtifact: + ContainerConfiguration: + ContainerUri: !Sub "${ECRRepository.RepositoryUri}:${ImageTag}" + RoleArn: !GetAtt AgentExecutionRole.Arn + NetworkConfiguration: + NetworkMode: !Ref NetworkMode + ProtocolConfiguration: MCP + AuthorizerConfiguration: + CustomJWTAuthorizer: + AllowedClients: + - !Ref CognitoUserPoolClient + DiscoveryUrl: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}/.well-known/openid-configuration" + Description: !Sub "MCP server runtime for ${AWS::StackName}" + +# ============================================================================ +# OUTPUTS SECTION +# ============================================================================ +Outputs: + # AGENTCORE MODULE OUTPUTS + MCPServerRuntimeId: + Description: "ID of the created MCP server runtime" + Value: !GetAtt MCPServerRuntime.AgentRuntimeId + Export: + Name: !Sub "${AWS::StackName}-MCPServerRuntimeId" + + MCPServerRuntimeArn: + Description: "ARN of the created MCP server runtime" + Value: !GetAtt MCPServerRuntime.AgentRuntimeArn + Export: + Name: !Sub "${AWS::StackName}-MCPServerRuntimeArn" + + MCPServerInvocationURL: + Description: "URL to invoke the MCP server" + Value: !Sub + - "https://bedrock-agentcore.${AWS::Region}.amazonaws.com/runtimes/${EncodedArn}/invocations?qualifier=DEFAULT" + - EncodedArn: !Join + - "" + - - !Select [0, !Split [":", !GetAtt MCPServerRuntime.AgentRuntimeArn]] + - "%3A" + - !Select [1, !Split [":", !GetAtt MCPServerRuntime.AgentRuntimeArn]] + - "%3A" + - !Select [2, !Split [":", !GetAtt MCPServerRuntime.AgentRuntimeArn]] + - "%3A" + - !Select [3, !Split [":", !GetAtt MCPServerRuntime.AgentRuntimeArn]] + - "%3A" + - !Select [4, !Split [":", !GetAtt MCPServerRuntime.AgentRuntimeArn]] + - "%3A" + - !Select [5, !Split [":", !GetAtt MCPServerRuntime.AgentRuntimeArn]] + - "%2F" + - !Select [1, !Split ["/", !GetAtt MCPServerRuntime.AgentRuntimeArn]] + Export: + Name: !Sub "${AWS::StackName}-MCPServerInvocationURL" + + # ECR OUTPUTS + ECRRepositoryUri: + Description: "URI of the ECR repository" + Value: !GetAtt ECRRepository.RepositoryUri + Export: + Name: !Sub "${AWS::StackName}-ECRRepositoryUri" + + # IAM OUTPUTS + AgentExecutionRoleArn: + Description: "ARN of the agent execution role" + Value: !GetAtt AgentExecutionRole.Arn + Export: + Name: !Sub "${AWS::StackName}-AgentExecutionRoleArn" + + # COGNITO OUTPUTS + CognitoUserPoolId: + Description: "ID of the Cognito User Pool" + Value: !Ref CognitoUserPool + Export: + Name: !Sub "${AWS::StackName}-CognitoUserPoolId" + + CognitoUserPoolClientId: + Description: "ID of the Cognito User Pool Client" + Value: !Ref CognitoUserPoolClient + Export: + Name: !Sub "${AWS::StackName}-CognitoUserPoolClientId" + + CognitoDiscoveryUrl: + Description: "Cognito OIDC Discovery URL" + Value: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}/.well-known/openid-configuration" + Export: + Name: !Sub "${AWS::StackName}-CognitoDiscoveryUrl" + + # AUTHENTICATION INFO + TestUsername: + Description: "Test username for authentication" + Value: "testuser" + + TestPassword: + Description: "Test password for authentication" + Value: "MyPassword123!" + + GetTokenCommand: + Description: "Command to get authentication token" + Value: !Sub | + python get_token.py ${CognitoUserPoolClient} testuser MyPassword123! diff --git a/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/test.sh b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/test.sh new file mode 100755 index 00000000..140b29ed --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/test.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Streamlined testing script for MCP Server + +set -e + +STACK_NAME="${1:-mcp-server-demo}" +REGION="${2:-us-west-2}" + +echo "==========================================" +echo "MCP Server Testing Script" +echo "==========================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "" + +# Get stack outputs +echo "๐Ÿ“‹ Retrieving stack configuration..." +CLIENT_ID=$(aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`CognitoUserPoolClientId`].OutputValue' \ + --output text \ + --region "$REGION") + +AGENT_ARN=$(aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`MCPServerRuntimeArn`].OutputValue' \ + --output text \ + --region "$REGION") + +if [ -z "$CLIENT_ID" ] || [ -z "$AGENT_ARN" ]; then + echo "โŒ Error: Could not retrieve stack outputs" + echo " Make sure the stack '$STACK_NAME' exists in region '$REGION'" + exit 1 +fi + +echo "โœ“ Configuration retrieved" +echo "" + +# Get authentication token +echo "๐Ÿ” Getting authentication token..." +TOKEN_OUTPUT=$(python get_token.py "$CLIENT_ID" testuser MyPassword123! "$REGION" 2>&1) + +# Extract token from output (it's the line after "Access Token:") +JWT_TOKEN=$(echo "$TOKEN_OUTPUT" | grep -A 1 "Access Token:" | tail -n 1 | tr -d '[:space:]') + +if [ -z "$JWT_TOKEN" ]; then + echo "โŒ Error: Could not get authentication token" + echo "$TOKEN_OUTPUT" + exit 1 +fi + +echo "โœ“ Authentication successful" +echo "" + +# Test MCP server +echo "๐Ÿงช Testing MCP server..." +echo "" +python test_mcp_server.py "$AGENT_ARN" "$JWT_TOKEN" "$REGION" + +echo "" +echo "==========================================" +echo "โœ… Testing Complete!" +echo "==========================================" diff --git a/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/test_mcp_server.py b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/test_mcp_server.py new file mode 100644 index 00000000..96b91f96 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/mcp-server-agentcore-runtime/test_mcp_server.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +Test script for deployed MCP server +Uses the MCP Python client library to properly communicate with the server +""" + +import asyncio +import sys +from datetime import timedelta +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + + +async def test_mcp_server(agent_arn, bearer_token, region): + """Test the deployed MCP server.""" + + # Encode the ARN for URL + encoded_arn = agent_arn.replace(":", "%3A").replace("/", "%2F") + mcp_url = f"https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT" + + headers = { + "authorization": f"Bearer {bearer_token}", + "Content-Type": "application/json", + } + + print(f"Connecting to: {mcp_url}") + print() + + try: + async with streamablehttp_client( + mcp_url, headers, timeout=timedelta(seconds=120), terminate_on_close=False + ) as (read_stream, write_stream, _): + async with ClientSession(read_stream, write_stream) as session: + print("๐Ÿ”„ Initializing MCP session...") + await session.initialize() + print("โœ“ MCP session initialized\n") + + print("๐Ÿ”„ Listing available tools...") + tool_result = await session.list_tools() + + print("\n๐Ÿ“‹ Available MCP Tools:") + print("=" * 50) + for tool in tool_result.tools: + print(f"๐Ÿ”ง {tool.name}: {tool.description}") + + print("\n๐Ÿงช Testing MCP Tools:") + print("=" * 50) + + # Test add_numbers + print("\nโž• Testing add_numbers(5, 3)...") + add_result = await session.call_tool( + name="add_numbers", arguments={"a": 5, "b": 3} + ) + print(f" Result: {add_result.content[0].text}") + + # Test multiply_numbers + print("\nโœ–๏ธ Testing multiply_numbers(4, 7)...") + multiply_result = await session.call_tool( + name="multiply_numbers", arguments={"a": 4, "b": 7} + ) + print(f" Result: {multiply_result.content[0].text}") + + # Test greet_user + print("\n๐Ÿ‘‹ Testing greet_user('Alice')...") + greet_result = await session.call_tool( + name="greet_user", arguments={"name": "Alice"} + ) + print(f" Result: {greet_result.content[0].text}") + + print("\nโœ… MCP tool testing completed!") + + except Exception as e: + print(f"โŒ Error: {e}") + sys.exit(1) + + +def main(): + if len(sys.argv) != 4: + print("Usage: python test_mcp_server.py ") + print("\nExample:") + print( + " python test_mcp_server.py arn:aws:bedrock-agentcore:... eyJraWQiOiJ... us-west-2" + ) + sys.exit(1) + + agent_arn = sys.argv[1] + bearer_token = sys.argv[2] + region = sys.argv[3] + + asyncio.run(test_mcp_server(agent_arn, bearer_token, region)) + + +if __name__ == "__main__": + main() diff --git a/04-infrastructure-as-code/cloudformation/multi-agent-runtime/README.md b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/README.md new file mode 100644 index 00000000..82798534 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/README.md @@ -0,0 +1,295 @@ +# Multi-Agent AgentCore Runtime + +This CloudFormation template demonstrates a multi-agent architecture where one agent (orchestrator) can invoke another agent (specialist) to handle complex tasks. This pattern is useful for building sophisticated AI systems with specialized capabilities. + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Prerequisites](#prerequisites) +- [Deployment](#deployment) +- [Testing](#testing) +- [Sample Queries](#sample-queries) +- [Cleanup](#cleanup) +- [Cost Estimate](#cost-estimate) +- [Troubleshooting](#troubleshooting) +- [๐Ÿค Contributing](#-contributing) +- [๐Ÿ“„ License](#-license) + +## Overview + +This template creates a two-agent system that demonstrates agent-to-agent communication: + +### Agent 1: Orchestrator Agent +- **Role**: Main entry point for user queries +- **Capabilities**: + - Handles simple queries directly + - Delegates complex tasks to Agent 2 + - Has a tool to invoke Agent 2's runtime +- **Use Cases**: Routing, task delegation, simple Q&A + +### Agent 2: Specialist Agent +- **Role**: Expert agent for detailed analysis +- **Capabilities**: + - Provides in-depth analytical responses + - Handles complex reasoning tasks + - Focuses on accuracy and completeness +- **Use Cases**: Data analysis, expert knowledge, detailed explanations + +### Key Features + +- **Multi-Agent Communication**: Agent 1 can invoke Agent 2 using `bedrock-agentcore:InvokeAgentRuntime` +- **Automatic Orchestration**: Agent 1 decides when to delegate based on query complexity +- **Independent Deployment**: Each agent has its own ECR repository and runtime +- **Modular Architecture**: Easy to extend with additional specialized agents + +## Architecture + +![Multi-Agent AgentCore Runtime Architecture](architecture.png) + +The architecture consists of: + +- **User**: Sends questions to Agent 1 (Orchestrator) and receives responses +- **Agent 1 - Orchestrator Agent**: + - **AWS CodeBuild**: Builds the ARM64 Docker container image for Agent 1 + - **Amazon ECR Repository**: Stores Agent 1's container image + - **AgentCore Runtime**: Hosts the Orchestrator Agent + - Routes simple queries directly + - Delegates complex queries to Agent 2 using the `call_specialist_agent` tool + - Invokes Amazon Bedrock LLMs for reasoning + - **IAM Role**: Permissions to invoke Agent 2's runtime and access Bedrock +- **Agent 2 - Specialist Agent**: + - **AWS CodeBuild**: Builds the ARM64 Docker container image for Agent 2 + - **Amazon ECR Repository**: Stores Agent 2's container image + - **AgentCore Runtime**: Hosts the Specialist Agent + - Provides detailed analysis and expert responses + - Invokes Amazon Bedrock LLMs for in-depth reasoning + - **IAM Role**: Standard runtime permissions and Bedrock access +- **Amazon Bedrock LLMs**: Provides AI model capabilities for both agents +- **Agent-to-Agent Communication**: Agent 1 can invoke Agent 2's runtime via `bedrock-agentcore:InvokeAgentRuntime` API + +## Prerequisites + +### AWS Account Setup + +1. **AWS Account**: You need an active AWS account with appropriate permissions + - [Create AWS Account](https://aws.amazon.com/account/) + - [AWS Console Access](https://aws.amazon.com/console/) + +2. **AWS CLI**: Install and configure AWS CLI with your credentials + - [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) + - [Configure AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) + + ```bash + aws configure + ``` + +3. **Bedrock Model Access**: Enable access to Amazon Bedrock models in your AWS region + - Navigate to [Amazon Bedrock Console](https://console.aws.amazon.com/bedrock/) + - Go to "Model access" and request access to: + - Anthropic Claude models + - [Bedrock Model Access Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) + +4. **Required Permissions**: Your AWS user/role needs permissions for: + - CloudFormation stack operations + - ECR repository management + - IAM role creation + - Lambda function creation + - CodeBuild project creation + - BedrockAgentCore resource creation + +## Deployment + +### Option 1: Using the Deploy Script (Recommended) + +```bash +# Make the script executable +chmod +x deploy.sh + +# Deploy the stack +./deploy.sh +``` + +The script will: +1. Deploy the CloudFormation stack +2. Wait for stack creation to complete +3. Display both Agent Runtime IDs + +### Option 2: Using AWS CLI + +```bash +# Deploy the stack +aws cloudformation create-stack \ + --stack-name multi-agent-demo \ + --template-body file://template.yaml \ + --capabilities CAPABILITY_NAMED_IAM \ + --region us-west-2 + +# Wait for stack creation +aws cloudformation wait stack-create-complete \ + --stack-name multi-agent-demo \ + --region us-west-2 + +# Get the Runtime IDs +aws cloudformation describe-stacks \ + --stack-name multi-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs' +``` + +### Option 3: Using AWS Console + +1. Navigate to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) +2. Click "Create stack" โ†’ "With new resources" +3. Upload the `template.yaml` file +4. Enter stack name: `multi-agent-demo` +5. Review parameters (or use defaults) +6. Check "I acknowledge that AWS CloudFormation might create IAM resources" +7. Click "Create stack" + + + + +## Testing + +### Test Agent 1 (Orchestrator) + +Agent 1 is your main entry point. It will handle simple queries directly or delegate to Agent 2 for complex tasks. + +#### Using AWS CLI + +```bash +# Get Agent1 Runtime ID +AGENT1_ID=$(aws cloudformation describe-stacks \ + --stack-name multi-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`Agent1RuntimeId`].OutputValue' \ + --output text) + +# Test with a simple query (Agent1 handles directly) +aws bedrock-agentcore invoke-agent-runtime \ + --agent-runtime-id $AGENT1_ID \ + --qualifier DEFAULT \ + --payload '{"prompt": "Hello, how are you?"}' \ + --region us-west-2 \ + response.json + +# Test with a complex query (Agent1 delegates to Agent2) +aws bedrock-agentcore invoke-agent-runtime \ + --agent-runtime-id $AGENT1_ID \ + --qualifier DEFAULT \ + --payload '{"prompt": "Provide a detailed analysis of cloud computing benefits"}' \ + --region us-west-2 \ + response.json + +cat response.json +``` + +### Using AWS Console + +1. Navigate to [Bedrock AgentCore Console](https://console.aws.amazon.com/bedrock-agentcore/) +2. Go to "Runtimes" in the left navigation +3. Find Agent1 runtime (name starts with `multi_agent_demo_OrchestratorAgent`) +4. Click on the runtime name +5. Click "Test" button +6. Enter test payload: + ```json + { + "prompt": "Hello, how are you?" + } + ``` +7. Click "Invoke" + +### Test Agent 2 (Specialist) Directly + +You can also test Agent 2 directly to see its specialized capabilities. + +```bash +# Get Agent2 Runtime ID +AGENT2_ID=$(aws cloudformation describe-stacks \ + --stack-name multi-agent-demo \ + --region us-west-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`Agent2RuntimeId`].OutputValue' \ + --output text) + +# Invoke Agent2 directly +aws bedrock-agentcore invoke-agent-runtime \ + --agent-runtime-id $AGENT2_ID \ + --qualifier DEFAULT \ + --payload '{"prompt": "Explain quantum computing in detail"}' \ + --region us-west-2 \ + response.json +``` + +## Sample Queries + +### Queries that Agent 1 Handles Directly + +These simple queries don't require specialist knowledge: + +1. **Greetings**: + ```json + {"prompt": "Hello, how are you?"} + ``` + +2. **Simple Math**: + ```json + {"prompt": "What is 5 + 3?"} + ``` + + +### Queries that Trigger Agent 2 Delegation + +These complex queries require expert analysis: + +1. **Detailed Analysis**: + ```json + {"prompt": "Provide a detailed analysis of the benefits and drawbacks of serverless architecture"} + ``` + +2. **Expert Knowledge**: + ```json + {"prompt": "Explain the CAP theorem and its implications for distributed systems"} + ``` + +3. **Complex Reasoning**: + ```json + {"prompt": "Compare and contrast different machine learning algorithms for time series forecasting"} + ``` + +4. **In-depth Explanation**: + ```json + {"prompt": "Provide expert analysis on best practices for securing cloud infrastructure"} + ``` + +## Cleanup + +### Using the Cleanup Script (Recommended) + +```bash +# Make the script executable +chmod +x cleanup.sh + +# Delete the stack +./cleanup.sh +``` + +### Using AWS CLI + +```bash +aws cloudformation delete-stack \ + --stack-name multi-agent-demo \ + --region us-west-2 + +# Wait for deletion to complete +aws cloudformation wait stack-delete-complete \ + --stack-name multi-agent-demo \ + --region us-west-2 +``` + +### Using AWS Console + +1. Navigate to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) +2. Select the `multi-agent-demo` stack +3. Click "Delete" +4. Confirm deletion diff --git a/04-infrastructure-as-code/cloudformation/multi-agent-runtime/architecture.png b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/architecture.png new file mode 100644 index 00000000..0a4fbe24 Binary files /dev/null and b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/architecture.png differ diff --git a/04-infrastructure-as-code/cloudformation/multi-agent-runtime/cleanup.sh b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/cleanup.sh new file mode 100755 index 00000000..9a4a4afd --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/cleanup.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Cleanup script for Multi-Agent Runtime CloudFormation stack +# This script deletes the CloudFormation stack and all associated resources + +set -e + +# Configuration +STACK_NAME="${1:-multi-agent-demo}" +REGION="${2:-us-west-2}" + +echo "==========================================" +echo "Cleaning up Multi-Agent Runtime" +echo "==========================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "==========================================" + +# Confirm deletion +read -p "Are you sure you want to delete the stack '$STACK_NAME'? (yes/no): " CONFIRM + +if [ "$CONFIRM" != "yes" ]; then + echo "Cleanup cancelled." + exit 0 +fi + +echo "" +echo "Deleting CloudFormation stack..." +aws cloudformation delete-stack \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + +if [ $? -eq 0 ]; then + echo "" + echo "โœ“ Stack deletion initiated successfully!" + echo "" + echo "Waiting for stack deletion to complete..." + echo "This may take a few minutes..." + echo "" + + aws cloudformation wait stack-delete-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + + if [ $? -eq 0 ]; then + echo "" + echo "==========================================" + echo "โœ“ Stack deleted successfully!" + echo "==========================================" + echo "" + echo "All resources have been cleaned up." + echo "" + else + echo "" + echo "โœ— Stack deletion failed or timed out" + echo "Check the CloudFormation console for details" + exit 1 + fi +else + echo "" + echo "โœ— Failed to initiate stack deletion" + exit 1 +fi diff --git a/04-infrastructure-as-code/cloudformation/multi-agent-runtime/deploy.sh b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/deploy.sh new file mode 100755 index 00000000..6cd0c361 --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/deploy.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# Deploy script for Multi-Agent Runtime CloudFormation stack +# This script deploys two AgentCore Runtimes where Agent1 can invoke Agent2 + +set -e + +# Configuration +STACK_NAME="${1:-multi-agent-demo}" +REGION="${2:-us-west-2}" +TEMPLATE_FILE="template.yaml" + +echo "==========================================" +echo "Deploying Multi-Agent Runtime" +echo "==========================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "==========================================" + +# Check if template file exists +if [ ! -f "$TEMPLATE_FILE" ]; then + echo "Error: Template file '$TEMPLATE_FILE' not found!" + exit 1 +fi + +# Deploy the CloudFormation stack +echo "" +echo "Creating CloudFormation stack..." +aws cloudformation create-stack \ + --stack-name "$STACK_NAME" \ + --template-body file://"$TEMPLATE_FILE" \ + --capabilities CAPABILITY_NAMED_IAM \ + --region "$REGION" + +if [ $? -eq 0 ]; then + echo "" + echo "โœ“ Stack creation initiated successfully!" + echo "" + echo "Waiting for stack creation to complete..." + echo "This will take approximately 15-20 minutes..." + echo "(Building two Docker images and deploying two agents)" + echo "" + + aws cloudformation wait stack-create-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + + if [ $? -eq 0 ]; then + echo "" + echo "==========================================" + echo "โœ“ Stack deployed successfully!" + echo "==========================================" + echo "" + echo "Stack Outputs:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs' \ + --output table \ + --region "$REGION" + echo "" + echo "Agent1 (Orchestrator) Runtime ARN:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`Agent1RuntimeArn`].OutputValue' \ + --output text \ + --region "$REGION" + echo "" + echo "Agent2 (Specialist) Runtime ARN:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query 'Stacks[0].Outputs[?OutputKey==`Agent2RuntimeArn`].OutputValue' \ + --output text \ + --region "$REGION" + echo "" + echo "To delete this stack, run:" + echo " ./cleanup.sh $STACK_NAME $REGION" + echo "" + else + echo "" + echo "โœ— Stack creation failed or timed out" + echo "Check the CloudFormation console for details" + exit 1 + fi +else + echo "" + echo "โœ— Failed to initiate stack creation" + exit 1 +fi diff --git a/04-infrastructure-as-code/cloudformation/multi-agent-runtime/template.yaml b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/template.yaml new file mode 100644 index 00000000..af3e77ee --- /dev/null +++ b/04-infrastructure-as-code/cloudformation/multi-agent-runtime/template.yaml @@ -0,0 +1,1009 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: "Multi-Agent AgentCore deployment - Two agents where agent1 orchestrates and calls agent2" + +# ============================================================================ +# PARAMETERS SECTION +# ============================================================================ +Parameters: + # Agent Configuration + Agent1Name: + Type: String + Default: "OrchestratorAgent" + Description: "Name for the orchestrator agent runtime (agent1)" + AllowedPattern: "^[a-zA-Z][a-zA-Z0-9_]{0,47}$" + ConstraintDescription: "Must start with a letter, max 48 characters, alphanumeric and underscores only" + + Agent2Name: + Type: String + Default: "SpecialistAgent" + Description: "Name for the specialist agent runtime (agent2)" + AllowedPattern: "^[a-zA-Z][a-zA-Z0-9_]{0,47}$" + ConstraintDescription: "Must start with a letter, max 48 characters, alphanumeric and underscores only" + + # Container Configuration + ImageTag: + Type: String + Default: "latest" + Description: "Tag for the Docker images" + + # Network Configuration + NetworkMode: + Type: String + Default: "PUBLIC" + Description: "Network mode for AgentCore resources" + AllowedValues: + - PUBLIC + - PRIVATE + + # ECR Configuration + ECRRepositoryName: + Type: String + Default: "multi-agent" + Description: "Base name of the ECR repositories" + +# ============================================================================ +# METADATA SECTION +# ============================================================================ +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Agent Configuration" + Parameters: + - Agent1Name + - Agent2Name + - NetworkMode + - Label: + default: "Container Configuration" + Parameters: + - ECRRepositoryName + - ImageTag + ParameterLabels: + Agent1Name: + default: "Agent 1 Name (Orchestrator)" + Agent2Name: + default: "Agent 2 Name (Specialist)" + NetworkMode: + default: "Network Mode" + ECRRepositoryName: + default: "ECR Repository Base Name" + ImageTag: + default: "Image Tag" + +# ============================================================================ +# RESOURCES SECTION +# ============================================================================ +Resources: + # ======================================================================== + # ECR MODULE - Container Registry + # ======================================================================== + + ECRRepositoryAgent1: + Type: AWS::ECR::Repository + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + RepositoryName: !Sub "${AWS::StackName}-${ECRRepositoryName}-agent1" + ImageTagMutability: MUTABLE + EmptyOnDelete: true + ImageScanningConfiguration: + ScanOnPush: true + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + - Sid: AllowPullFromAccount + Effect: Allow + Principal: + AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-ecr-repository-agent1" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: ECR + + ECRRepositoryAgent2: + Type: AWS::ECR::Repository + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + RepositoryName: !Sub "${AWS::StackName}-${ECRRepositoryName}-agent2" + ImageTagMutability: MUTABLE + EmptyOnDelete: true + ImageScanningConfiguration: + ScanOnPush: true + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + - Sid: AllowPullFromAccount + Effect: Allow + Principal: + AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-ecr-repository-agent2" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: ECR + + # ======================================================================== + # IAM MODULE - Security and Permissions + # ======================================================================== + + # Agent1 Execution Role (with permissions to invoke Agent2) + Agent1ExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-agent1-execution-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: AssumeRolePolicy + Effect: Allow + Principal: + Service: bedrock-agentcore.amazonaws.com + Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: !Ref AWS::AccountId + ArnLike: + aws:SourceArn: !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:*" + ManagedPolicyArns: + - arn:aws:iam::aws:policy/BedrockAgentCoreFullAccess + Policies: + - PolicyName: Agent1ExecutionPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: ECRImageAccess + Effect: Allow + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + - ecr:BatchCheckLayerAvailability + Resource: !GetAtt ECRRepositoryAgent1.Arn + - Sid: ECRTokenAccess + Effect: Allow + Action: + - ecr:GetAuthorizationToken + Resource: "*" + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:DescribeLogStreams + - logs:CreateLogGroup + - logs:DescribeLogGroups + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "*" + - Sid: XRayTracing + Effect: Allow + Action: + - xray:PutTraceSegments + - xray:PutTelemetryRecords + - xray:GetSamplingRules + - xray:GetSamplingTargets + Resource: "*" + - Sid: CloudWatchMetrics + Effect: Allow + Resource: "*" + Action: cloudwatch:PutMetricData + Condition: + StringEquals: + cloudwatch:namespace: bedrock-agentcore + - Sid: GetAgentAccessToken + Effect: Allow + Action: + - bedrock-agentcore:GetWorkloadAccessToken + - bedrock-agentcore:GetWorkloadAccessTokenForJWT + - bedrock-agentcore:GetWorkloadAccessTokenForUserId + Resource: + - !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default" + - !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default/workload-identity/*" + - Sid: BedrockModelInvocation + Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + - Sid: InvokeAgent2Runtime + Effect: Allow + Action: + - bedrock-agentcore:InvokeAgentRuntime + Resource: !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:runtime/*" + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-agent1-execution-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # Agent2 Execution Role (basic permissions) + Agent2ExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-agent2-execution-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: AssumeRolePolicy + Effect: Allow + Principal: + Service: bedrock-agentcore.amazonaws.com + Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: !Ref AWS::AccountId + ArnLike: + aws:SourceArn: !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:*" + ManagedPolicyArns: + - arn:aws:iam::aws:policy/BedrockAgentCoreFullAccess + Policies: + - PolicyName: Agent2ExecutionPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: ECRImageAccess + Effect: Allow + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + - ecr:BatchCheckLayerAvailability + Resource: !GetAtt ECRRepositoryAgent2.Arn + - Sid: ECRTokenAccess + Effect: Allow + Action: + - ecr:GetAuthorizationToken + Resource: "*" + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:DescribeLogStreams + - logs:CreateLogGroup + - logs:DescribeLogGroups + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "*" + - Sid: XRayTracing + Effect: Allow + Action: + - xray:PutTraceSegments + - xray:PutTelemetryRecords + - xray:GetSamplingRules + - xray:GetSamplingTargets + Resource: "*" + - Sid: CloudWatchMetrics + Effect: Allow + Resource: "*" + Action: cloudwatch:PutMetricData + Condition: + StringEquals: + cloudwatch:namespace: bedrock-agentcore + - Sid: GetAgentAccessToken + Effect: Allow + Action: + - bedrock-agentcore:GetWorkloadAccessToken + - bedrock-agentcore:GetWorkloadAccessTokenForJWT + - bedrock-agentcore:GetWorkloadAccessTokenForUserId + Resource: + - !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default" + - !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default/workload-identity/*" + - Sid: BedrockModelInvocation + Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: "*" + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-agent2-execution-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # CodeBuild Service Role + CodeBuildRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-codebuild-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: CodeBuildPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*" + - Sid: ECRAccess + Effect: Allow + Action: + - ecr:BatchCheckLayerAvailability + - ecr:GetDownloadUrlForLayer + - ecr:BatchGetImage + - ecr:GetAuthorizationToken + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + Resource: + - !GetAtt ECRRepositoryAgent1.Arn + - !GetAtt ECRRepositoryAgent2.Arn + - "*" + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-codebuild-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # Lambda Custom Resource Role + CustomResourceRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-custom-resource-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: CustomResourcePolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: ECRAccess + Effect: Allow + Action: + - ecr:ListImages + - ecr:BatchDeleteImage + - ecr:GetAuthorizationToken + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + Resource: + - !GetAtt ECRRepositoryAgent1.Arn + - !GetAtt ECRRepositoryAgent2.Arn + - Sid: CodeBuildAccess + Effect: Allow + Action: + - codebuild:StartBuild + - codebuild:BatchGetBuilds + - codebuild:BatchGetProjects + Resource: + - !GetAtt Agent1ImageBuildProject.Arn + - !GetAtt Agent2ImageBuildProject.Arn + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-custom-resource-role" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: IAM + + # ======================================================================== + # LAMBDA MODULE - Custom Resources + # ======================================================================== + + CodeBuildTriggerFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Sub "${AWS::StackName}-codebuild-trigger" + Description: "Triggers CodeBuild projects as CloudFormation custom resource" + Handler: index.handler + Role: !GetAtt CustomResourceRole.Arn + Runtime: python3.9 + Timeout: 900 + Code: + ZipFile: | + import boto3 + import cfnresponse + import json + import logging + import time + + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + def handler(event, context): + logger.info('Received event: %s', json.dumps(event)) + + try: + if event['RequestType'] == 'Delete': + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + return + + project_name = event['ResourceProperties']['ProjectName'] + wait_for_completion = event['ResourceProperties'].get('WaitForCompletion', 'true').lower() == 'true' + + logger.info(f"Attempting to start CodeBuild project: {project_name}") + logger.info(f"Wait for completion: {wait_for_completion}") + + # Start the CodeBuild project + codebuild = boto3.client('codebuild') + + # First, verify the project exists + try: + project_info = codebuild.batch_get_projects(names=[project_name]) + if not project_info['projects']: + raise Exception(f"CodeBuild project '{project_name}' not found") + logger.info(f"CodeBuild project '{project_name}' found") + except Exception as e: + logger.error(f"Error checking project existence: {str(e)}") + raise + + response = codebuild.start_build(projectName=project_name) + build_id = response['build']['id'] + + logger.info(f"Successfully started build: {build_id}") + + if not wait_for_completion: + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'BuildId': build_id, + 'Status': 'STARTED' + }) + return + + # Wait for the build to complete + max_wait_time = context.get_remaining_time_in_millis() / 1000 - 30 # Leave 30s buffer + start_time = time.time() + + while True: + if time.time() - start_time > max_wait_time: + error_message = f"Build {build_id} timed out" + logger.error(error_message) + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_message}) + return + + build_response = codebuild.batch_get_builds(ids=[build_id]) + build_status = build_response['builds'][0]['buildStatus'] + + if build_status == 'SUCCEEDED': + logger.info(f"Build {build_id} succeeded") + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'BuildId': build_id, + 'Status': build_status + }) + return + elif build_status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']: + error_message = f"Build {build_id} failed with status: {build_status}" + logger.error(error_message) + + # Get build logs for debugging + try: + logs_info = build_response['builds'][0].get('logs', {}) + if logs_info.get('groupName') and logs_info.get('streamName'): + logger.info(f"Build logs available in CloudWatch") + except Exception as log_error: + logger.warning(f"Could not get log information: {log_error}") + + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': error_message, + 'BuildId': build_id + }) + return + + logger.info(f"Build {build_id} status: {build_status}") + time.sleep(30) # Check every 30 seconds + + except Exception as e: + logger.error('Error: %s', str(e)) + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': str(e) + }) + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-codebuild-trigger" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: Lambda + + # ======================================================================== + # CODEBUILD MODULE - Container Image Building + # ======================================================================== + + # Agent2 Build Project (build first as it's independent) + Agent2ImageBuildProject: + Type: AWS::CodeBuild::Project + Properties: + Name: !Sub "${AWS::StackName}-agent2-build" + Description: !Sub "Build agent2 Docker image for ${AWS::StackName}" + ServiceRole: !GetAtt CodeBuildRole.Arn + Artifacts: + Type: NO_ARTIFACTS + Environment: + Type: ARM_CONTAINER + ComputeType: BUILD_GENERAL1_LARGE + Image: aws/codebuild/amazonlinux2-aarch64-standard:3.0 + PrivilegedMode: true + EnvironmentVariables: + - Name: AWS_DEFAULT_REGION + Value: !Ref AWS::Region + - Name: AWS_ACCOUNT_ID + Value: !Ref AWS::AccountId + - Name: IMAGE_REPO_NAME + Value: !Ref ECRRepositoryAgent2 + - Name: IMAGE_TAG + Value: !Ref ImageTag + - Name: STACK_NAME + Value: !Ref AWS::StackName + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + pre_build: + commands: + - echo Logging in to Amazon ECR... + - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com + build: + commands: + - echo Build started on `date` + - echo Building the Docker image for agent2 ARM64... + + # Create requirements.txt + - | + cat > requirements.txt << 'EOF' + strands-agents + boto3>=1.40.0 + botocore>=1.40.0 + bedrock-agentcore + EOF + + # Create agent2.py - specialist agent that handles specific tasks + - | + cat > agent2.py << 'EOF' + from strands import Agent + import os + from bedrock_agentcore.runtime import BedrockAgentCoreApp + + app = BedrockAgentCoreApp() + + def create_specialist_agent() -> Agent: + """Create a specialist agent that handles specific analytical tasks""" + system_prompt = """You are a specialist analytical agent. + You are an expert at analyzing data and providing detailed insights. + When asked questions, provide thorough, well-reasoned responses with specific details. + Focus on accuracy and completeness in your answers.""" + + return Agent( + system_prompt=system_prompt, + name="SpecialistAgent" + ) + + @app.entrypoint + async def invoke(payload=None): + """Main entrypoint for agent2""" + try: + # Get the query from payload + query = payload.get("prompt", "Hello") if payload else "Hello" + + # Create and use the specialist agent + agent = create_specialist_agent() + response = agent(query) + + return { + "status": "success", + "agent": "agent2", + "response": response.message['content'][0]['text'] + } + + except Exception as e: + return { + "status": "error", + "agent": "agent2", + "error": str(e) + } + + if __name__ == "__main__": + app.run() + EOF + + # Create Dockerfile + - | + cat > Dockerfile << 'EOF' + FROM public.ecr.aws/docker/library/python:3.11-slim + WORKDIR /app + + COPY requirements.txt requirements.txt + RUN pip install -r requirements.txt + RUN pip install aws-opentelemetry-distro>=0.10.1 + + # Region will be set by AgentCore runtime environment automatically + + # Create non-root user + RUN useradd -m -u 1000 bedrock_agentcore + USER bedrock_agentcore + + EXPOSE 8080 + EXPOSE 8000 + + COPY . . + + CMD ["opentelemetry-instrument", "python", "-m", "agent2"] + EOF + + # Build the image + - echo Building ARM64 image... + - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . + - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + + post_build: + commands: + - echo Build completed on `date` + - echo Pushing the Docker image... + - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + - echo ARM64 Docker image pushed successfully + + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-agent2-build" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: CodeBuild + + # Agent1 Build Project (orchestrator that calls agent2) + Agent1ImageBuildProject: + Type: AWS::CodeBuild::Project + Properties: + Name: !Sub "${AWS::StackName}-agent1-build" + Description: !Sub "Build agent1 Docker image for ${AWS::StackName}" + ServiceRole: !GetAtt CodeBuildRole.Arn + Artifacts: + Type: NO_ARTIFACTS + Environment: + Type: ARM_CONTAINER + ComputeType: BUILD_GENERAL1_LARGE + Image: aws/codebuild/amazonlinux2-aarch64-standard:3.0 + PrivilegedMode: true + EnvironmentVariables: + - Name: AWS_DEFAULT_REGION + Value: !Ref AWS::Region + - Name: AWS_ACCOUNT_ID + Value: !Ref AWS::AccountId + - Name: IMAGE_REPO_NAME + Value: !Ref ECRRepositoryAgent1 + - Name: IMAGE_TAG + Value: !Ref ImageTag + - Name: STACK_NAME + Value: !Ref AWS::StackName + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + pre_build: + commands: + - echo Logging in to Amazon ECR... + - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com + build: + commands: + - echo Build started on `date` + - echo Building the Docker image for agent1 ARM64... + + # Create requirements.txt + - | + cat > requirements.txt << 'EOF' + strands-agents + boto3>=1.40.0 + botocore>=1.40.0 + bedrock-agentcore + EOF + + # Create agent1.py - orchestrator agent with tool to call agent2 + - | + cat > agent1.py << 'EOF' + from strands import Agent, tool + from typing import Dict, Any + import boto3 + import json + import os + from bedrock_agentcore.runtime import BedrockAgentCoreApp + + app = BedrockAgentCoreApp() + + # Environment variable for Agent2 ARN (will be set by CloudFormation) + AGENT2_ARN = os.getenv('AGENT2_ARN', '') + + def invoke_agent2(query: str) -> str: + """Helper function to invoke agent2 using boto3""" + import uuid + try: + # Get region from environment or use default + region = os.getenv('AWS_REGION', 'us-west-2') + agentcore_client = boto3.client('bedrock-agentcore', region_name=region) + + # Invoke agent2 runtime (using AWS sample format) + response = agentcore_client.invoke_agent_runtime( + agentRuntimeArn=AGENT2_ARN, + qualifier="DEFAULT", + payload=json.dumps({"prompt": query}) + ) + + # Handle streaming response (text/event-stream) + if "text/event-stream" in response.get("contentType", ""): + result = "" + for line in response["response"].iter_lines(chunk_size=10): + if line: + line = line.decode("utf-8") + # Remove 'data: ' prefix if present + if line.startswith("data: "): + line = line[6:] + result += line + return result + + # Handle JSON response + elif response.get("contentType") == "application/json": + content = [] + for chunk in response.get("response", []): + content.append(chunk.decode('utf-8')) + response_data = json.loads(''.join(content)) + return json.dumps(response_data) + + # Handle other response types + else: + response_body = response['response'].read() + return response_body.decode('utf-8') + + except Exception as e: + import traceback + error_details = traceback.format_exc() + return f"Error invoking agent2: {str(e)}\nDetails: {error_details}" + + @tool + def call_specialist_agent(query: str) -> Dict[str, Any]: + """ + Call the specialist agent (agent2) for detailed analysis or complex tasks. + Use this tool when you need expert analysis or detailed information. + + Args: + query: The question or task to send to the specialist agent + + Returns: + The specialist agent's response + """ + result = invoke_agent2(query) + return { + "status": "success", + "content": [{"text": result}] + } + + def create_orchestrator_agent() -> Agent: + """Create the orchestrator agent with the tool to call agent2""" + system_prompt = """You are an orchestrator agent. + You can handle simple queries directly, but for complex analytical tasks, + you should delegate to the specialist agent using the call_specialist_agent tool. + + Use the specialist agent when: + - The query requires detailed analysis + - The query is about complex topics + - The user explicitly asks for expert analysis + + Handle simple queries (greetings, basic questions) yourself.""" + + return Agent( + tools=[call_specialist_agent], + system_prompt=system_prompt, + name="OrchestratorAgent" + ) + + @app.entrypoint + async def invoke(payload=None): + """Main entrypoint for agent1""" + try: + # Get the query from payload + query = payload.get("prompt", "Hello, how are you?") if payload else "Hello, how are you?" + + # Create and use the orchestrator agent + agent = create_orchestrator_agent() + response = agent(query) + + return { + "status": "success", + "agent": "agent1", + "response": response.message['content'][0]['text'] + } + + except Exception as e: + return { + "status": "error", + "agent": "agent1", + "error": str(e) + } + + if __name__ == "__main__": + app.run() + EOF + + # Create Dockerfile + - | + cat > Dockerfile << 'EOF' + FROM public.ecr.aws/docker/library/python:3.11-slim + WORKDIR /app + + COPY requirements.txt requirements.txt + RUN pip install -r requirements.txt + RUN pip install aws-opentelemetry-distro>=0.10.1 + + # Region will be set by AgentCore runtime environment automatically + + # Create non-root user + RUN useradd -m -u 1000 bedrock_agentcore + USER bedrock_agentcore + + EXPOSE 8080 + EXPOSE 8000 + + COPY . . + + CMD ["opentelemetry-instrument", "python", "-m", "agent1"] + EOF + + # Build the image + - echo Building ARM64 image... + - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . + - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + + post_build: + commands: + - echo Build completed on `date` + - echo Pushing the Docker image... + - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG + - echo ARM64 Docker image pushed successfully + + Tags: + - Key: Name + Value: !Sub "${AWS::StackName}-agent1-build" + - Key: StackName + Value: !Ref AWS::StackName + - Key: Module + Value: CodeBuild + + # CUSTOM RESOURCE - Trigger Agent2 Image Build + TriggerAgent2ImageBuild: + Type: Custom::CodeBuildTrigger + DependsOn: + - ECRRepositoryAgent2 + - Agent2ImageBuildProject + - CodeBuildTriggerFunction + Properties: + ServiceToken: !GetAtt CodeBuildTriggerFunction.Arn + ProjectName: !Ref Agent2ImageBuildProject + WaitForCompletion: "true" + + # CUSTOM RESOURCE - Trigger Agent1 Image Build + TriggerAgent1ImageBuild: + Type: Custom::CodeBuildTrigger + DependsOn: + - ECRRepositoryAgent1 + - Agent1ImageBuildProject + - CodeBuildTriggerFunction + Properties: + ServiceToken: !GetAtt CodeBuildTriggerFunction.Arn + ProjectName: !Ref Agent1ImageBuildProject + WaitForCompletion: "true" + + # ======================================================================== + # AGENTCORE MODULE - Runtime Resources + # ======================================================================== + + # Agent2 Runtime (deploy first as agent1 depends on it) + Agent2Runtime: + Type: AWS::BedrockAgentCore::Runtime + DependsOn: + - TriggerAgent2ImageBuild + Properties: + AgentRuntimeName: !Sub + - "${StackNameUnderscore}_${Agent2Name}" + - StackNameUnderscore: !Join ["_", !Split ["-", !Ref "AWS::StackName"]] + AgentRuntimeArtifact: + ContainerConfiguration: + ContainerUri: !Sub "${ECRRepositoryAgent2.RepositoryUri}:${ImageTag}" + RoleArn: !GetAtt Agent2ExecutionRole.Arn + NetworkConfiguration: + NetworkMode: !Ref NetworkMode + Description: !Sub "Specialist agent runtime for ${AWS::StackName}" + + # Agent1 Runtime (orchestrator with agent2 ARN as environment variable) + Agent1Runtime: + Type: AWS::BedrockAgentCore::Runtime + DependsOn: + - TriggerAgent1ImageBuild + - Agent2Runtime + Properties: + AgentRuntimeName: !Sub + - "${StackNameUnderscore}_${Agent1Name}" + - StackNameUnderscore: !Join ["_", !Split ["-", !Ref "AWS::StackName"]] + AgentRuntimeArtifact: + ContainerConfiguration: + ContainerUri: !Sub "${ECRRepositoryAgent1.RepositoryUri}:${ImageTag}" + RoleArn: !GetAtt Agent1ExecutionRole.Arn + NetworkConfiguration: + NetworkMode: !Ref NetworkMode + Description: !Sub "Orchestrator agent runtime for ${AWS::StackName}" + EnvironmentVariables: + AGENT2_ARN: !GetAtt Agent2Runtime.AgentRuntimeArn + +# ============================================================================ +# OUTPUTS SECTION +# ============================================================================ +Outputs: + # AGENT1 (ORCHESTRATOR) OUTPUTS + Agent1RuntimeId: + Description: "ID of agent1 (orchestrator) runtime" + Value: !GetAtt Agent1Runtime.AgentRuntimeId + Export: + Name: !Sub "${AWS::StackName}-Agent1RuntimeId" + + Agent1RuntimeArn: + Description: "ARN of agent1 (orchestrator) runtime" + Value: !GetAtt Agent1Runtime.AgentRuntimeArn + Export: + Name: !Sub "${AWS::StackName}-Agent1RuntimeArn" + + Agent1ECRRepositoryUri: + Description: "URI of the ECR repository for agent1" + Value: !GetAtt ECRRepositoryAgent1.RepositoryUri + Export: + Name: !Sub "${AWS::StackName}-Agent1ECRRepositoryUri" + + Agent1ExecutionRoleArn: + Description: "ARN of agent1 execution role" + Value: !GetAtt Agent1ExecutionRole.Arn + Export: + Name: !Sub "${AWS::StackName}-Agent1ExecutionRoleArn" + + # AGENT2 (SPECIALIST) OUTPUTS + Agent2RuntimeId: + Description: "ID of agent2 (specialist) runtime" + Value: !GetAtt Agent2Runtime.AgentRuntimeId + Export: + Name: !Sub "${AWS::StackName}-Agent2RuntimeId" + + Agent2RuntimeArn: + Description: "ARN of agent2 (specialist) runtime" + Value: !GetAtt Agent2Runtime.AgentRuntimeArn + Export: + Name: !Sub "${AWS::StackName}-Agent2RuntimeArn" + + Agent2ECRRepositoryUri: + Description: "URI of the ECR repository for agent2" + Value: !GetAtt ECRRepositoryAgent2.RepositoryUri + Export: + Name: !Sub "${AWS::StackName}-Agent2ECRRepositoryUri" + + Agent2ExecutionRoleArn: + Description: "ARN of agent2 execution role" + Value: !GetAtt Agent2ExecutionRole.Arn + Export: + Name: !Sub "${AWS::StackName}-Agent2ExecutionRoleArn"