diff --git a/.gitignore b/.gitignore index acf76b81..86baaee2 100644 --- a/.gitignore +++ b/.gitignore @@ -226,6 +226,8 @@ pyrightconfig.json lambda.zip .kiro/ +### CDK artifacts +cdk.out/ ### Bedrock AgentCore ### .bedrock_agentcore/ .bedrock_agentcore.yaml diff --git a/04-infrastructure-as-code/README.md b/04-infrastructure-as-code/README.md index ff3ccfdd..78f94a26 100644 --- a/04-infrastructure-as-code/README.md +++ b/04-infrastructure-as-code/README.md @@ -1,16 +1,20 @@ -# CloudFormation Samples for Amazon Bedrock AgentCore +# Infrastructure as Code Samples for Amazon Bedrock AgentCore -CloudFormation templates for deploying Amazon Bedrock AgentCore resources. +CloudFormation templates and AWS CDK stacks for deploying Amazon Bedrock AgentCore resources. ## Overview -These CloudFormation templates enable you to: +These Infrastructure as Code samples 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 +Choose your preferred approach: +- **[CloudFormation](./cloudformation/)** - YAML/JSON templates for declarative infrastructure +- **[CDK](./cdk/)** - Python code for programmatic infrastructure + +## 📚 CloudFormation Samples ### 01. [Hosting MCP Server on AgentCore Runtime](./cloudformation/mcp-server-agentcore-runtime/) @@ -133,6 +137,37 @@ aws cloudformation create-stack \ --- +## 📚 CDK Samples + +### 01. [Basic Agent Runtime](./cdk/basic-runtime/) + +Deploy a basic AgentCore Runtime with a simple Strands agent using AWS CDK - no additional tools or memory. + +**What it deploys:** +- Docker image asset built from local code +- IAM role with least-privilege policies for AgentCore +- Basic AgentCore Runtime with simple agent + +**Architecture highlights:** +- Uses `DockerImageAsset` for container image building (no CodeBuild needed) +- Separates IAM role into its own construct (`AgentCoreRole`) +- Uses `CfnRuntime` directly from `aws_bedrockagentcore` +- Much cleaner than the CloudFormation equivalent + +**Use case:** Simple agent deployment without memory, code interpreter, or browser tools + +**Deployment time:** ~5-10 minutes +**Estimated cost:** ~$50-100/month + +**Quick start:** +```bash +cd cdk/basic-runtime +pip install -r requirements.txt +cdk deploy +``` + +--- + ## Prerequisites Before deploying any CloudFormation template, ensure you have: @@ -143,7 +178,12 @@ Before deploying any CloudFormation template, ensure you have: aws configure ``` 3. **Access to Amazon Bedrock AgentCore** (preview) -4. **IAM Permissions** to create: +4. **For CDK samples**: Python 3.8+, AWS CDK v2 installed, and **CDK version 2.218.0 or later** (for BedrockAgentCore support) + ```bash + npm install -g aws-cdk + pip install aws-cdk-lib==2.218.0 constructs>=10.0.79 + ``` +5. **IAM Permissions** to create: - CloudFormation stacks - IAM roles and policies - ECR repositories @@ -184,22 +224,36 @@ Default values: ``` 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 +├── 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 +└── cdk/ # CDK samples + └── basic-runtime/ # Basic agent CDK sample + ├── app.py # CDK app entry point + ├── basic_runtime_stack.py # Stack definition + ├── requirements.txt # Python dependencies + ├── cdk.json # CDK configuration + ├── README.md # Sample documentation + ├── infra-utils/ # Infrastructure utilities + │ ├── agentcore_role.py # Dedicated role construct + │ └── build_trigger_lambda.py # Lambda function for CodeBuild trigger + └── agent-code/ # Agent source code + ├── Dockerfile + ├── basic_agent.py + └── requirements.txt ``` diff --git a/04-infrastructure-as-code/cdk/basic-runtime/README.md b/04-infrastructure-as-code/cdk/basic-runtime/README.md new file mode 100644 index 00000000..fbbd444c --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/README.md @@ -0,0 +1,252 @@ +# Basic AgentCore Runtime - CDK + +This CDK stack 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) +- [Troubleshooting](#troubleshooting) + +## Overview + +This CDK stack 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 +- **S3 Assets**: Source code packaging and deployment + +This makes it ideal for: +- Learning AgentCore basics +- Quick prototyping +- Understanding the core deployment pattern +- Building a foundation before adding complexity + +## Architecture + +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. **Python 3.10+** and **AWS CDK v2** installed + ```bash + # Install CDK + npm install -g aws-cdk + + # Verify installation + cdk --version + ``` + +4. **CDK version 2.218.0 or later** (for BedrockAgentCore support) + +5. **Bedrock Model Access**: Enable access to Amazon Bedrock models in your AWS region + - [Bedrock Model Access Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) + +6. **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 + - S3 bucket operations (for CDK assets) + +## Deployment + +### Option 1: Quick Deploy (Recommended) + +```bash +# Install dependencies +pip install -r requirements.txt + +# Bootstrap CDK (first time only) +cdk bootstrap + +# Deploy +cdk deploy +``` + +### Option 2: Step by Step + +```bash +# 1. Create and activate Python virtual environment +python3 -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# 2. Install Python dependencies +pip install -r requirements.txt + +# 2. Bootstrap CDK in your account/region (first time only) +cdk bootstrap + +# 3. Synthesize the CloudFormation template (optional) +cdk synth + +# 4. Deploy the stack +cdk deploy --require-approval never + +# 5. Get outputs +cdk list +``` + +### Deployment Time + +- **Expected Duration**: 3-5 minutes + +## Testing + +### Using AWS CLI + +```bash +# Get the Runtime ARN from CDK outputs +RUNTIME_ARN=$(aws cloudformation describe-stacks \ + --stack-name BasicAgentDemo \ + --region us-east-1 \ + --query 'Stacks[0].Outputs[?OutputKey==`AgentRuntimeArn`].OutputValue' \ + --output text) + +# Invoke the agent +aws bedrock-agentcore invoke-agent-runtime \ + --agent-runtime-arn $RUNTIME_ARN \ + --qualifier DEFAULT \ + --payload $(echo '{"prompt": "What is 2+2?"}' | base64) \ + 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 `BasicAgentDemo_`) +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 CDK (Recommended) + +```bash +cdk destroy +``` + +### Using AWS CLI + +```bash +aws cloudformation delete-stack \ + --stack-name BasicAgentDemo \ + --region us-east-1 + +# Wait for deletion to complete +aws cloudformation wait stack-delete-complete \ + --stack-name BasicAgentDemo \ + --region us-east-1 +``` + +### Using AWS Console + +1. Navigate to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) +2. Select the `BasicAgentDemo` stack +3. Click "Delete" +4. Confirm deletion + +## Troubleshooting + +### CDK Bootstrap Required + +If you see bootstrap errors: +```bash +cdk bootstrap aws://ACCOUNT-NUMBER/REGION +``` + +### Permission Issues + +Ensure your IAM user/role has: +- `CDKToolkit` permissions or equivalent +- Permissions to create all resources in the stack +- `iam:PassRole` for service roles + +### Python Dependencies + +Install dependencies in the project directory: +```bash +pip install -r requirements.txt +``` + +### Build Failures + +Check CodeBuild logs in the AWS Console: +1. Go to CodeBuild console +2. Find the build project (name contains "basic-agent-build") +3. Check build history and logs diff --git a/04-infrastructure-as-code/cdk/basic-runtime/agent-code/Dockerfile b/04-infrastructure-as-code/cdk/basic-runtime/agent-code/Dockerfile new file mode 100644 index 00000000..1b6cf32a --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/agent-code/Dockerfile @@ -0,0 +1,24 @@ +FROM public.ecr.aws/docker/library/python:3.11-slim + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip install --no-cache-dir -r requirements.txt && \ + pip install --no-cache-dir 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 . . + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/ping || exit 1 + +CMD ["opentelemetry-instrument", "python", "-m", "basic_agent"] diff --git a/04-infrastructure-as-code/cdk/basic-runtime/agent-code/basic_agent.py b/04-infrastructure-as-code/cdk/basic-runtime/agent-code/basic_agent.py new file mode 100644 index 00000000..88986beb --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/agent-code/basic_agent.py @@ -0,0 +1,39 @@ +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() diff --git a/04-infrastructure-as-code/cdk/basic-runtime/agent-code/requirements.txt b/04-infrastructure-as-code/cdk/basic-runtime/agent-code/requirements.txt new file mode 100644 index 00000000..3ba698e4 --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/agent-code/requirements.txt @@ -0,0 +1,3 @@ +strands-agents +boto3 +bedrock-agentcore diff --git a/04-infrastructure-as-code/cdk/basic-runtime/app.py b/04-infrastructure-as-code/cdk/basic-runtime/app.py new file mode 100644 index 00000000..d99819be --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/app.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +import aws_cdk as cdk +from basic_runtime_stack import BasicRuntimeStack + +app = cdk.App() +BasicRuntimeStack(app, "BasicAgentDemo") + +app.synth() diff --git a/04-infrastructure-as-code/cdk/basic-runtime/basic_runtime_stack.py b/04-infrastructure-as-code/cdk/basic-runtime/basic_runtime_stack.py new file mode 100644 index 00000000..fbc4de0a --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/basic_runtime_stack.py @@ -0,0 +1,214 @@ +from aws_cdk import ( + Stack, + aws_ecr as ecr, + aws_codebuild as codebuild, + aws_iam as iam, + aws_lambda as lambda_, + aws_s3_assets as s3_assets, + aws_bedrockagentcore as bedrockagentcore, + CustomResource, + CfnParameter, + CfnOutput, + Duration, + RemovalPolicy +) +from constructs import Construct +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), 'infra_utils')) +from agentcore_role import AgentCoreRole + +class BasicRuntimeStack(Stack): + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + # Parameters + agent_name = CfnParameter(self, "AgentName", + type="String", + default="BasicAgent", + description="Name for the agent runtime" + ) + + image_tag = CfnParameter(self, "ImageTag", + type="String", + default="latest", + description="Tag for the Docker image" + ) + + network_mode = CfnParameter(self, "NetworkMode", + type="String", + default="PUBLIC", + description="Network mode for AgentCore resources", + allowed_values=["PUBLIC", "PRIVATE"] + ) + + # ECR Repository + ecr_repository = ecr.Repository(self, "ECRRepository", + repository_name=f"{self.stack_name.lower()}-basic-agent", + image_tag_mutability=ecr.TagMutability.MUTABLE, + removal_policy=RemovalPolicy.DESTROY, + empty_on_delete=True, + image_scan_on_push=True + ) + + # S3 Asset for source code + source_asset = s3_assets.Asset(self, "SourceAsset", + path="./agent-code" + ) + + # CodeBuild Role + codebuild_role = iam.Role(self, "CodeBuildRole", + role_name=f"{self.stack_name}-codebuild-role", + assumed_by=iam.ServicePrincipal("codebuild.amazonaws.com"), + inline_policies={ + "CodeBuildPolicy": iam.PolicyDocument( + statements=[ + iam.PolicyStatement( + sid="CloudWatchLogs", + effect=iam.Effect.ALLOW, + actions=[ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:/aws/codebuild/*"] + ), + iam.PolicyStatement( + sid="ECRAccess", + effect=iam.Effect.ALLOW, + actions=[ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:GetAuthorizationToken", + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload" + ], + resources=[ecr_repository.repository_arn, "*"] + ), + iam.PolicyStatement( + sid="S3SourceAccess", + effect=iam.Effect.ALLOW, + actions=["s3:GetObject"], + resources=[f"{source_asset.bucket.bucket_arn}/*"] + ) + ] + ) + } + ) + + # CodeBuild Project + build_project = codebuild.Project(self, "AgentImageBuildProject", + project_name=f"{self.stack_name}-basic-agent-build", + description=f"Build basic agent Docker image for {self.stack_name}", + role=codebuild_role, + environment=codebuild.BuildEnvironment( + build_image=codebuild.LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0, + compute_type=codebuild.ComputeType.LARGE, + privileged=True + ), + source=codebuild.Source.s3( + bucket=source_asset.bucket, + path=source_asset.s3_object_key + ), + build_spec=codebuild.BuildSpec.from_object({ + "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...", + "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" + ] + } + } + }), + environment_variables={ + "AWS_DEFAULT_REGION": codebuild.BuildEnvironmentVariable(value=self.region), + "AWS_ACCOUNT_ID": codebuild.BuildEnvironmentVariable(value=self.account), + "IMAGE_REPO_NAME": codebuild.BuildEnvironmentVariable(value=ecr_repository.repository_name), + "IMAGE_TAG": codebuild.BuildEnvironmentVariable(value=image_tag.value_as_string), + "STACK_NAME": codebuild.BuildEnvironmentVariable(value=self.stack_name) + } + ) + + # Lambda function to trigger and wait for CodeBuild + build_trigger_function = lambda_.Function(self, "BuildTriggerFunction", + runtime=lambda_.Runtime.PYTHON_3_9, + handler="infra_utils.build_trigger_lambda.handler", + timeout=Duration.minutes(15), + code=lambda_.Code.from_asset(".", exclude=["*.pyc", "__pycache__", "cdk.out"]), + initial_policy=[ + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=["codebuild:StartBuild", "codebuild:BatchGetBuilds"], + resources=[build_project.project_arn] + ) + ] + ) + + # Custom Resource using the Lambda function + trigger_build = CustomResource(self, "TriggerImageBuild", + service_token=build_trigger_function.function_arn, + properties={ + "ProjectName": build_project.project_name + } + ) + + # Create AgentCore execution role + agent_role = AgentCoreRole(self, "AgentCoreRole") + + # Create AgentCore Runtime + agent_runtime = bedrockagentcore.CfnRuntime(self, "AgentRuntime", + agent_runtime_name=f"{self.stack_name.replace('-', '_')}_{agent_name.value_as_string}", + agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty( + container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty( + container_uri=f"{ecr_repository.repository_uri}:{image_tag.value_as_string}" + ) + ), + network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty( + network_mode=network_mode.value_as_string + ), + protocol_configuration="HTTP", + role_arn=agent_role.role_arn, + description=f"Basic agent runtime for {self.stack_name}", + environment_variables={ + "AWS_DEFAULT_REGION": self.region + } + ) + + agent_runtime.node.add_dependency(trigger_build) + + # Outputs + CfnOutput(self, "AgentRuntimeId", + description="ID of the created agent runtime", + value=agent_runtime.attr_agent_runtime_id + ) + + CfnOutput(self, "AgentRuntimeArn", + description="ARN of the created agent runtime", + value=agent_runtime.attr_agent_runtime_arn + ) + + CfnOutput(self, "AgentRoleArn", + description="ARN of the agent execution role", + value=agent_role.role_arn + ) diff --git a/04-infrastructure-as-code/cdk/basic-runtime/cdk.json b/04-infrastructure-as-code/cdk/basic-runtime/cdk.json new file mode 100644 index 00000000..ca4a6fac --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/cdk.json @@ -0,0 +1,60 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__pycache__", + "**/*.pyc" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableLogging": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableLogging": true, + "@aws-cdk/aws-nordicapis-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForSourceAction": true + } +} diff --git a/04-infrastructure-as-code/cdk/basic-runtime/infra_utils/__init__.py b/04-infrastructure-as-code/cdk/basic-runtime/infra_utils/__init__.py new file mode 100644 index 00000000..3a2dd782 --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/infra_utils/__init__.py @@ -0,0 +1 @@ +# Infrastructure utilities for CDK stacks diff --git a/04-infrastructure-as-code/cdk/basic-runtime/infra_utils/agentcore_role.py b/04-infrastructure-as-code/cdk/basic-runtime/infra_utils/agentcore_role.py new file mode 100644 index 00000000..9f661af6 --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/infra_utils/agentcore_role.py @@ -0,0 +1,93 @@ +from aws_cdk import ( + aws_iam as iam, + Stack +) +from constructs import Construct + +class AgentCoreRole(iam.Role): + def __init__(self, scope: Construct, construct_id: str, **kwargs): + region = Stack.of(scope).region + account_id = Stack.of(scope).account + + super().__init__(scope, construct_id, + assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"), + inline_policies={ + "AgentCorePolicy": iam.PolicyDocument( + statements=[ + iam.PolicyStatement( + sid="ECRImageAccess", + effect=iam.Effect.ALLOW, + actions=[ + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchCheckLayerAvailability" + ], + resources=[f"arn:aws:ecr:{region}:{account_id}:repository/*"] + ), + iam.PolicyStatement( + sid="ECRTokenAccess", + effect=iam.Effect.ALLOW, + actions=["ecr:GetAuthorizationToken"], + resources=["*"] + ), + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=[ + "logs:DescribeLogStreams", + "logs:CreateLogGroup", + "logs:DescribeLogGroups", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + resources=[f"arn:aws:logs:{region}:{account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"] + ), + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=[ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + "xray:GetSamplingRules", + "xray:GetSamplingTargets" + ], + resources=["*"] + ), + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=["cloudwatch:PutMetricData"], + resources=["*"], + conditions={ + "StringEquals": { + "cloudwatch:namespace": "bedrock-agentcore" + } + } + ), + iam.PolicyStatement( + sid="GetAgentAccessToken", + effect=iam.Effect.ALLOW, + actions=[ + "bedrock-agentcore:GetWorkloadAccessToken", + "bedrock-agentcore:GetWorkloadAccessTokenForJWT", + "bedrock-agentcore:GetWorkloadAccessTokenForUserId" + ], + resources=[ + f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default", + f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default/workload-identity/*" + ] + ), + iam.PolicyStatement( + sid="BedrockModelInvocation", + effect=iam.Effect.ALLOW, + actions=[ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ], + resources=[ + "arn:aws:bedrock:*::foundation-model/*", + f"arn:aws:bedrock:{region}:{account_id}:*" + ] + ) + ] + ) + }, + **kwargs + ) diff --git a/04-infrastructure-as-code/cdk/basic-runtime/infra_utils/build_trigger_lambda.py b/04-infrastructure-as-code/cdk/basic-runtime/infra_utils/build_trigger_lambda.py new file mode 100644 index 00000000..a203e0a0 --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/infra_utils/build_trigger_lambda.py @@ -0,0 +1,93 @@ +import boto3 +import json +import logging +import time +import urllib3 + +# Note: cfnresponse is only available for inline Lambda code in CloudFormation. +# When using CDK with Code.from_asset(), we need to include our own copy. +# This is the standard AWS-provided cfnresponse module embedded directly. + +class cfnresponse: + SUCCESS = "SUCCESS" + FAILED = "FAILED" + + @staticmethod + def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None): + responseUrl = event['ResponseURL'] + print(responseUrl) + + responseBody = { + 'Status': responseStatus, + 'Reason': reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name), + 'PhysicalResourceId': physicalResourceId or context.log_stream_name, + 'StackId': event['StackId'], + 'RequestId': event['RequestId'], + 'LogicalResourceId': event['LogicalResourceId'], + 'NoEcho': noEcho, + 'Data': responseData + } + + json_responseBody = json.dumps(responseBody) + print("Response body:") + print(json_responseBody) + + headers = { + 'content-type': '', + 'content-length': str(len(json_responseBody)) + } + + try: + http = urllib3.PoolManager() + response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody) + print("Status code:", response.status) + except Exception as e: + print("send(..) failed executing http.request(..):", e) + +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'] + + codebuild = boto3.client('codebuild') + + # Start build + response = codebuild.start_build(projectName=project_name) + build_id = response['build']['id'] + logger.info(f"Started build: {build_id}") + + # Wait for completion + 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: + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': 'Build timeout'}) + 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}) + return + elif build_status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']: + logger.error(f"Build {build_id} failed with status: {build_status}") + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': f'Build failed: {build_status}'}) + 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)}) diff --git a/04-infrastructure-as-code/cdk/basic-runtime/requirements.txt b/04-infrastructure-as-code/cdk/basic-runtime/requirements.txt new file mode 100644 index 00000000..36b463de --- /dev/null +++ b/04-infrastructure-as-code/cdk/basic-runtime/requirements.txt @@ -0,0 +1,2 @@ +aws-cdk-lib==2.218.0 +constructs>=10.0.79 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 11841732..94fd23f3 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -43,3 +43,4 @@ - Omar Elkharbotly - Chintan Patel - Shreyas Subramanian +- David Kaleko