From 976ed65a870a96d6e651ec12327c0c8d2c601d1c Mon Sep 17 00:00:00 2001 From: Rakshith Rao Date: Tue, 27 Jan 2026 20:58:07 +0100 Subject: [PATCH 1/9] feat(cloudfront-agentcore-runtime-cdk): Add CloudFront to AgentCore Runtime pattern - Add new CloudFront to Amazon Bedrock AgentCore Runtime CDK pattern with OAuth 2.0 authentication - Implement three agent runtime services supporting A2A, HTTP, and MCP protocols - Add CloudFront distribution with path-based routing to appropriate agent endpoints - Include Amazon Cognito User Pool for JWT token authentication - Add agent code implementations with Docker containerization for each protocol type - Include comprehensive README with deployment and testing instructions - Add test scripts for validating A2A, HTTP, and MCP protocol endpoints - Add project configuration files (cdk.json, requirements.txt, .gitignore) - Add architecture documentation and example pattern configuration - Update root .gitignore to exclude .history directory - This pattern demonstrates global edge caching, DDoS protection, and centralized access logging through CloudFront --- .gitignore | 4 +- cloudfront-agentcore-runtime-cdk/.gitignore | 10 + cloudfront-agentcore-runtime-cdk/README.md | 139 ++++++++++++++ .../agent-code/a2a/Dockerfile | 20 ++ .../agent-code/a2a/agent.py | 44 +++++ .../agent-code/a2a/requirements.txt | 4 + .../agent-code/http/Dockerfile | 20 ++ .../agent-code/http/agent.py | 17 ++ .../agent-code/http/requirements.txt | 2 + .../agent-code/mcp/Dockerfile | 20 ++ .../agent-code/mcp/agent.py | 18 ++ .../agent-code/mcp/requirements.txt | 1 + cloudfront-agentcore-runtime-cdk/app.py | 26 +++ cloudfront-agentcore-runtime-cdk/cdk.json | 79 ++++++++ .../example-pattern.json | 81 ++++++++ .../images/architecture.png | Bin 0 -> 38499 bytes .../infra/__init__.py | 0 .../infra/agentcore_stack.py | 179 ++++++++++++++++++ .../infra/cloudfront_stack.py | 133 +++++++++++++ .../requirements.txt | 2 + .../test/.cognito_config | 4 + .../test/get_token.sh | 46 +++++ .../test/test_a2a.py | 92 +++++++++ .../test/test_http.py | 69 +++++++ .../test/test_mcp.py | 59 ++++++ 25 files changed, 1068 insertions(+), 1 deletion(-) create mode 100644 cloudfront-agentcore-runtime-cdk/.gitignore create mode 100644 cloudfront-agentcore-runtime-cdk/README.md create mode 100644 cloudfront-agentcore-runtime-cdk/agent-code/a2a/Dockerfile create mode 100644 cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py create mode 100644 cloudfront-agentcore-runtime-cdk/agent-code/a2a/requirements.txt create mode 100644 cloudfront-agentcore-runtime-cdk/agent-code/http/Dockerfile create mode 100644 cloudfront-agentcore-runtime-cdk/agent-code/http/agent.py create mode 100644 cloudfront-agentcore-runtime-cdk/agent-code/http/requirements.txt create mode 100644 cloudfront-agentcore-runtime-cdk/agent-code/mcp/Dockerfile create mode 100644 cloudfront-agentcore-runtime-cdk/agent-code/mcp/agent.py create mode 100644 cloudfront-agentcore-runtime-cdk/agent-code/mcp/requirements.txt create mode 100644 cloudfront-agentcore-runtime-cdk/app.py create mode 100644 cloudfront-agentcore-runtime-cdk/cdk.json create mode 100644 cloudfront-agentcore-runtime-cdk/example-pattern.json create mode 100644 cloudfront-agentcore-runtime-cdk/images/architecture.png create mode 100644 cloudfront-agentcore-runtime-cdk/infra/__init__.py create mode 100644 cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py create mode 100644 cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py create mode 100644 cloudfront-agentcore-runtime-cdk/requirements.txt create mode 100644 cloudfront-agentcore-runtime-cdk/test/.cognito_config create mode 100755 cloudfront-agentcore-runtime-cdk/test/get_token.sh create mode 100755 cloudfront-agentcore-runtime-cdk/test/test_a2a.py create mode 100755 cloudfront-agentcore-runtime-cdk/test/test_http.py create mode 100755 cloudfront-agentcore-runtime-cdk/test/test_mcp.py diff --git a/.gitignore b/.gitignore index f1e3d74228..d335f4e714 100644 --- a/.gitignore +++ b/.gitignore @@ -237,4 +237,6 @@ terraform.rc *.idea # This file gets generated as part of Maven shade plugin which can be ignored -dependency-reduced-pom.xml \ No newline at end of file +dependency-reduced-pom.xml + +.history diff --git a/cloudfront-agentcore-runtime-cdk/.gitignore b/cloudfront-agentcore-runtime-cdk/.gitignore new file mode 100644 index 0000000000..3a13cdab5d --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/.gitignore @@ -0,0 +1,10 @@ +*.swp +package-lock.json +__pycache__ +.pytest_cache +.venv +*.egg-info +*.pyc +cdk.out +.DS_Store +.bedrock_agentcore diff --git a/cloudfront-agentcore-runtime-cdk/README.md b/cloudfront-agentcore-runtime-cdk/README.md new file mode 100644 index 0000000000..4b234601ea --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/README.md @@ -0,0 +1,139 @@ +# CloudFront to Amazon Bedrock AgentCore Runtime + +This pattern demonstrates how to proxy requests to Amazon Bedrock AgentCore Runtime through CloudFront with OAuth 2.0 authentication, supporting all three [AgentCore Runtime service contracts](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html): A2A, HTTP, and MCP protocols. + +Benefits of using CloudFront in front of AgentCore Runtime: +- Global edge caching and low-latency access +- DDoS protection via AWS Shield Standard (included) +- Optional AWS WAF integration for IP rate limiting, geo-blocking, and bot protection +- Custom domain support with SSL/TLS certificates +- Request/response transformation via CloudFront Functions +- Centralized access logging and monitoring + +Learn more about this pattern at [Serverless Land Patterns](https://serverlessland.com/patterns/cloudfront-agentcore-runtime-cdk) + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the AWS Pricing page for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Pre-requisites + +- [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +- [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed and configured +- [Docker](https://docs.docker.com/get-docker/) installed and running +- [Python 3.12](https://www.python.org/downloads/) installed + +## Deployment Instructions + +1. Clone the GitHub repository: + + ```shell + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/cloudfront-agentcore-runtime-cdk + ``` + +2. Create and activate a Python virtual environment: + + ```shell + python3 -m venv .venv + source .venv/bin/activate + ``` + +3. Install the required dependencies: + + ```shell + pip3 install -r requirements.txt + ``` + +4. Bootstrap CDK (if not already done): + + ```shell + cdk bootstrap aws:///us-west-2 aws:///us-east-1 + ``` + +5. Deploy the stacks: + + ```shell + cdk deploy --all + ``` + +## How it works + +This pattern creates: + +1. **Amazon Bedrock AgentCore Runtimes** - Three agents supporting A2A, HTTP, and MCP protocols +2. **Amazon Cognito User Pool** for JWT authentication +3. **CloudFront Distribution** that proxies requests to AgentCore Runtimes: + - `/a2a/*` - A2A protocol agent + - `/rest/*` - HTTP protocol agent + - `/mcp/*` - MCP protocol agent + +**Architecture Flow:** +1. Client sends request to CloudFront with Bearer token +2. CloudFront Function strips path prefix (/a2a, /rest, /mcp) +3. Request is forwarded to the appropriate AgentCore Runtime +4. AgentCore validates JWT token +5. Response is returned through the same path + +## Testing + +1. Get Pool ID and Client ID from the CDK outputs after running the `cdk deploy` command. Look for `UserPoolId` and `UserPoolClientId` in the outputs. + +2. Get a bearer token: + + ```shell + cd test + ./get_token.sh + ``` + + Example output: + ``` + Cognito Token Generator + Get Pool ID and Client ID from CDK stack outputs + + Pool ID []: us-west-2_xxxxxx + Client ID []: xxxxxxxxxxxxxxxxxx + Username []: testuser + Password: + Region [us-west-2]: + export BEARER_TOKEN="eyJraWQiOi..." + ``` + +3. Set environment variables (get DistributionUrl from AgentcoreCloudFrontStack outputs): + + ```shell + export CF_URL="https://.cloudfront.net" + export BEARER_TOKEN="" + ``` + +4. Test A2A protocol: + + ```shell + python test_a2a.py + ``` + +5. Test HTTP protocol: + + ```shell + python test_http.py + ``` + +6. Test MCP protocol (requires `mcp` package): + + ```shell + python test_mcp.py + ``` + +## Cleanup + +Delete the stacks: + +```bash +cdk destroy --all +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/a2a/Dockerfile b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/Dockerfile new file mode 100644 index 0000000000..ee192fa327 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/Dockerfile @@ -0,0 +1,20 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +RUN useradd -m -u 1000 bedrock_agentcore +USER bedrock_agentcore + +EXPOSE 9000 + +COPY . . + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:9000/ping || exit 1 + +CMD ["python", "agent.py"] diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py new file mode 100644 index 0000000000..6979f60c4d --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py @@ -0,0 +1,44 @@ +import logging +import os +from strands import Agent +from strands.multiagent.a2a import A2AServer +from a2a.types import AgentSkill +import uvicorn +from fastapi import FastAPI + +logging.basicConfig(level=logging.INFO) + +runtime_url = os.environ.get('AGENTCORE_RUNTIME_URL', 'http://127.0.0.1:9000/') + +strands_agent = Agent( + name="Test Agent", + description="A helpful assistant that answers questions clearly and concisely.", + callback_handler=None +) + +a2a_server = A2AServer( + agent=strands_agent, + http_url=runtime_url, + serve_at_root=True, + enable_a2a_compliant_streaming=True, + skills=[ + AgentSkill( + id="general-assistant", + name="General Assistant", + description="Answers questions and provides helpful information", + tags=["assistant", "qa"], + examples=["What is the capital of France?"] + ) + ] +) + +a2a_app = a2a_server.to_fastapi_app() + +@a2a_app.get("/ping") +def ping(): + return {"status": "healthy"} + +app = a2a_app + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=9000) diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/a2a/requirements.txt b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/requirements.txt new file mode 100644 index 0000000000..1e4729f37b --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/requirements.txt @@ -0,0 +1,4 @@ +strands-agents[a2a] +bedrock-agentcore +uvicorn +fastapi diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/http/Dockerfile b/cloudfront-agentcore-runtime-cdk/agent-code/http/Dockerfile new file mode 100644 index 0000000000..acc74f75fc --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/http/Dockerfile @@ -0,0 +1,20 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +RUN useradd -m -u 1000 bedrock_agentcore +USER bedrock_agentcore + +EXPOSE 8080 + +COPY . . + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/ping || exit 1 + +CMD ["python", "agent.py"] diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/http/agent.py b/cloudfront-agentcore-runtime-cdk/agent-code/http/agent.py new file mode 100644 index 0000000000..54406e73fd --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/http/agent.py @@ -0,0 +1,17 @@ +from strands import Agent +from bedrock_agentcore import BedrockAgentCoreApp + +app = BedrockAgentCoreApp() +agent = Agent() + +@app.entrypoint +async def agent_invocation(payload): + user_message = payload.get( + "prompt", "No prompt found in input, please provide a json payload with prompt key" + ) + stream = agent.stream_async(user_message) + async for event in stream: + yield event + +if __name__ == "__main__": + app.run() diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/http/requirements.txt b/cloudfront-agentcore-runtime-cdk/agent-code/http/requirements.txt new file mode 100644 index 0000000000..2bdadd46e6 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/http/requirements.txt @@ -0,0 +1,2 @@ +strands-agents +bedrock-agentcore diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/mcp/Dockerfile b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/Dockerfile new file mode 100644 index 0000000000..e1dcaba31e --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/Dockerfile @@ -0,0 +1,20 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +RUN useradd -m -u 1000 bedrock_agentcore +USER bedrock_agentcore + +EXPOSE 8000 + +COPY . . + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/mcp || exit 1 + +CMD ["python", "agent.py"] diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/mcp/agent.py b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/agent.py new file mode 100644 index 0000000000..ef495b3f8b --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/agent.py @@ -0,0 +1,18 @@ +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP(host="0.0.0.0", stateless_http=True) + +@mcp.tool() +def add_numbers(a: int, b: int) -> int: + return a + b + +@mcp.tool() +def multiply_numbers(a: int, b: int) -> int: + return a * b + +@mcp.tool() +def greet_user(name: str) -> str: + return f"Hello, {name}!" + +if __name__ == "__main__": + mcp.run(transport="streamable-http") diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/mcp/requirements.txt b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/requirements.txt new file mode 100644 index 0000000000..6664c6da98 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/requirements.txt @@ -0,0 +1 @@ +mcp diff --git a/cloudfront-agentcore-runtime-cdk/app.py b/cloudfront-agentcore-runtime-cdk/app.py new file mode 100644 index 0000000000..de5926737d --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/app.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import aws_cdk as cdk +import os + +from infra.agentcore_stack import AgentcoreStack +from infra.cloudfront_stack import CloudFrontStack + +app = cdk.App() + +agentcore_stack = AgentcoreStack(app, "AgentcoreStack", + env=cdk.Environment(region=os.environ.get("CDK_DEFAULT_REGION", "us-west-2")), + cross_region_references=True +) + +cloudfront_stack = CloudFrontStack(app, "AgentcoreCloudFrontStack", + env=cdk.Environment(region="us-east-1"), + cross_region_references=True, + a2a_agent_runtime_arn=agentcore_stack.a2a_agent_runtime_arn, + http_agent_runtime_arn=agentcore_stack.http_agent_runtime_arn, + mcp_agent_runtime_arn=agentcore_stack.mcp_agent_runtime_arn, + agent_runtime_region=os.environ.get("CDK_DEFAULT_REGION", "us-west-2")) + +cloudfront_stack.add_dependency(agentcore_stack) + +app.synth() diff --git a/cloudfront-agentcore-runtime-cdk/cdk.json b/cloudfront-agentcore-runtime-cdk/cdk.json new file mode 100644 index 0000000000..98290be503 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/cdk.json @@ -0,0 +1,79 @@ +{ + "app": ".venv/bin/python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "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-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:enableEmrServicePolicyV2": 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:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true + } +} diff --git a/cloudfront-agentcore-runtime-cdk/example-pattern.json b/cloudfront-agentcore-runtime-cdk/example-pattern.json new file mode 100644 index 0000000000..25ddc74379 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/example-pattern.json @@ -0,0 +1,81 @@ +{ + "title": "CloudFront to Amazon Bedrock AgentCore Runtime", + "description": "This pattern demonstrates how to proxy requests to Amazon Bedrock AgentCore Runtime through CloudFront with OAuth 2.0 authentication, supporting all three AgentCore Runtime service contracts: A2A, HTTP, and MCP protocols.", + "language": "Python", + "level": "300", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern creates a CloudFront distribution that proxies requests to three AgentCore Runtimes (A2A, HTTP, MCP protocols).", + "CloudFront Functions strip path prefixes (/a2a, /rest, /mcp) before forwarding to the appropriate AgentCore Runtime.", + "AgentCore validates JWT tokens for OAuth 2.0 authentication.", + "Benefits include: global edge caching, DDoS protection via AWS Shield, optional WAF integration for rate limiting and geo-blocking, custom domain support, and centralized logging." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/cloudfront-agentcore-runtime-cdk", + "templateURL": "serverless-patterns/cloudfront-agentcore-runtime-cdk", + "projectFolder": "cloudfront-agentcore-runtime-cdk", + "templateFile": "app.py" + } + }, + "resources": { + "bullets": [ + { + "text": "AgentCore Runtime Service Contracts", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html" + }, + { + "text": "Amazon Bedrock AgentCore Runtime Documentation", + "link": "https://aws.github.io/bedrock-agentcore-starter-toolkit/" + }, + { + "text": "CloudFront Functions Documentation", + "link": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html" + }, + { + "text": "Using AWS WAF with CloudFront", + "link": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-awswaf.html" + } + ] + }, + "deploy": { + "text": [ + "python3 -m venv .venv", + "source .venv/bin/activate", + "pip3 install -r requirements.txt", + "cdk bootstrap aws:///us-west-2 aws:///us-east-1", + "cdk deploy --all" + ] + }, + "testing": { + "text": [ + "Get a bearer token: cd test && ./get_token.sh", + "Set environment variables: export CF_URL=\"https://<distribution-id>.cloudfront.net\" && export BEARER_TOKEN=\"<token>\"", + "Test A2A protocol: python test_a2a.py", + "Test HTTP protocol: python test_http.py", + "Test MCP protocol: pip install mcp && python test_mcp.py" + ] + }, + "cleanup": { + "text": [ + "Delete the stacks: cdk destroy --all" + ] + }, + "authors": [ + { + "name": "Rakshith Rao", + "image": "https://serverlessland.com/assets/images/resources/contributors/rakshith-rao.png", + "bio": "I am a Senior Solutions Architect at AWS and help our strategic customers build and operate their key workloads on AWS.", + "linkedin": "rakshithrao" + }, + { + "name": "Biswanath Mukherjee", + "image": "https://serverlessland.com/assets/images/resources/contributors/biswanath-mukherjee.jpg", + "bio": "I am a Sr. Solutions Architect working at AWS India.", + "linkedin": "biswanathmukherjee" + } + ] +} diff --git a/cloudfront-agentcore-runtime-cdk/images/architecture.png b/cloudfront-agentcore-runtime-cdk/images/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..ad4d99e97bccba54706260e779eee908dd190287 GIT binary patch literal 38499 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ú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 literal 0 HcmV?d00001 diff --git a/cloudfront-agentcore-runtime-cdk/infra/__init__.py b/cloudfront-agentcore-runtime-cdk/infra/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py b/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py new file mode 100644 index 0000000000..e6d01fb429 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 + +from aws_cdk import ( + Stack, + CfnOutput, + RemovalPolicy, + aws_iam as iam, + aws_ecr_assets as ecr_assets, + aws_bedrockagentcore as bedrockagentcore, + aws_cognito as cognito, +) +from constructs import Construct + + +class AgentcoreStack(Stack): + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + user_pool = cognito.UserPool(self, "UserPool", + user_pool_name=f"{self.stack_name}-user-pool", + self_sign_up_enabled=False, + sign_in_aliases=cognito.SignInAliases(username=True), + password_policy=cognito.PasswordPolicy(min_length=8), + removal_policy=RemovalPolicy.DESTROY + ) + + user_pool_client = cognito.UserPoolClient(self, "UserPoolClient", + user_pool=user_pool, + user_pool_client_name=f"{self.stack_name}-client", + generate_secret=False, + auth_flows=cognito.AuthFlow(user_password=True, custom=True) + ) + + a2a_docker_image = ecr_assets.DockerImageAsset(self, "A2aAgentImage", + directory="./agent-code/a2a", + platform=ecr_assets.Platform.LINUX_ARM64 + ) + + http_docker_image = ecr_assets.DockerImageAsset(self, "HttpAgentImage", + directory="./agent-code/http", + platform=ecr_assets.Platform.LINUX_ARM64 + ) + + mcp_docker_image = ecr_assets.DockerImageAsset(self, "McpAgentImage", + directory="./agent-code/mcp", + platform=ecr_assets.Platform.LINUX_ARM64 + ) + + a2a_agent_name = f"{self.stack_name.replace('-', '_')}_A2a_Agent" + + agent_role = iam.Role(self, "AgentCoreRole", + assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com", + conditions={ + "StringEquals": {"aws:SourceAccount": self.account}, + "ArnLike": {"aws:SourceArn": f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:*"} + } + ), + inline_policies={ + "AgentCorePolicy": iam.PolicyDocument(statements=[ + iam.PolicyStatement( + sid="ECRImageAccess", + actions=["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"], + resources=[a2a_docker_image.repository.repository_arn, http_docker_image.repository.repository_arn, mcp_docker_image.repository.repository_arn] + ), + iam.PolicyStatement( + sid="ECRTokenAccess", + actions=["ecr:GetAuthorizationToken"], + resources=["*"] + ), + iam.PolicyStatement( + actions=["logs:DescribeLogStreams", "logs:CreateLogGroup"], + resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:/aws/bedrock-agentcore/runtimes/*"] + ), + iam.PolicyStatement( + actions=["logs:DescribeLogGroups"], + resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:*"] + ), + iam.PolicyStatement( + actions=["logs:CreateLogStream", "logs:PutLogEvents"], + resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"] + ), + iam.PolicyStatement( + actions=["xray:PutTraceSegments", "xray:PutTelemetryRecords", "xray:GetSamplingRules", "xray:GetSamplingTargets"], + resources=["*"] + ), + iam.PolicyStatement( + actions=["cloudwatch:PutMetricData"], + resources=["*"], + conditions={"StringEquals": {"cloudwatch:namespace": "bedrock-agentcore"}} + ), + iam.PolicyStatement( + sid="GetAgentAccessToken", + actions=["bedrock-agentcore:GetWorkloadAccessToken", "bedrock-agentcore:GetWorkloadAccessTokenForJWT", "bedrock-agentcore:GetWorkloadAccessTokenForUserId"], + resources=[ + f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:workload-identity-directory/default", + f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:workload-identity-directory/default/workload-identity/{a2a_agent_name}-*" + ] + ), + iam.PolicyStatement( + sid="BedrockModelInvocation", + actions=["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], + resources=["arn:aws:bedrock:*::foundation-model/*", f"arn:aws:bedrock:{self.region}:{self.account}:*"] + ) + ]) + } + ) + + discovery_url = f"https://cognito-idp.{self.region}.amazonaws.com/{user_pool.user_pool_id}/.well-known/openid-configuration" + + a2a_agent_runtime = bedrockagentcore.CfnRuntime(self, "AgentRuntime", + agent_runtime_name=a2a_agent_name, + agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty( + container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty( + container_uri=a2a_docker_image.image_uri + ) + ), + network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(network_mode="PUBLIC"), + protocol_configuration="A2A", + role_arn=agent_role.role_arn, + environment_variables={"AWS_DEFAULT_REGION": self.region}, + authorizer_configuration=bedrockagentcore.CfnRuntime.AuthorizerConfigurationProperty( + custom_jwt_authorizer=bedrockagentcore.CfnRuntime.CustomJWTAuthorizerConfigurationProperty( + discovery_url=discovery_url, + allowed_clients=[user_pool_client.user_pool_client_id] + ) + ) + ) + + http_agent_name = f"{self.stack_name.replace('-', '_')}_Http_Agent" + http_agent_runtime = bedrockagentcore.CfnRuntime(self, "HttpAgentRuntime", + agent_runtime_name=http_agent_name, + agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty( + container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty( + container_uri=http_docker_image.image_uri + ) + ), + network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(network_mode="PUBLIC"), + protocol_configuration="HTTP", + role_arn=agent_role.role_arn, + environment_variables={"AWS_DEFAULT_REGION": self.region}, + authorizer_configuration=bedrockagentcore.CfnRuntime.AuthorizerConfigurationProperty( + custom_jwt_authorizer=bedrockagentcore.CfnRuntime.CustomJWTAuthorizerConfigurationProperty( + discovery_url=discovery_url, + allowed_clients=[user_pool_client.user_pool_client_id] + ) + ) + ) + + mcp_agent_name = f"{self.stack_name.replace('-', '_')}_Mcp_Agent" + mcp_agent_runtime = bedrockagentcore.CfnRuntime(self, "McpAgentRuntime", + agent_runtime_name=mcp_agent_name, + agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty( + container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty( + container_uri=mcp_docker_image.image_uri + ) + ), + network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(network_mode="PUBLIC"), + protocol_configuration="MCP", + role_arn=agent_role.role_arn, + environment_variables={"AWS_DEFAULT_REGION": self.region}, + authorizer_configuration=bedrockagentcore.CfnRuntime.AuthorizerConfigurationProperty( + custom_jwt_authorizer=bedrockagentcore.CfnRuntime.CustomJWTAuthorizerConfigurationProperty( + discovery_url=discovery_url, + allowed_clients=[user_pool_client.user_pool_client_id] + ) + ) + ) + + self.a2a_agent_runtime_arn = a2a_agent_runtime.attr_agent_runtime_arn + self.http_agent_runtime_arn = http_agent_runtime.attr_agent_runtime_arn + self.mcp_agent_runtime_arn = mcp_agent_runtime.attr_agent_runtime_arn + + CfnOutput(self, "A2aAgentRuntimeArn", value=a2a_agent_runtime.attr_agent_runtime_arn) + CfnOutput(self, "HttpAgentRuntimeArn", value=http_agent_runtime.attr_agent_runtime_arn) + CfnOutput(self, "McpAgentRuntimeArn", value=mcp_agent_runtime.attr_agent_runtime_arn) + CfnOutput(self, "UserPoolId", value=user_pool.user_pool_id) + CfnOutput(self, "UserPoolClientId", value=user_pool_client.user_pool_client_id) + CfnOutput(self, "CognitoDiscoveryUrl", value=f"https://cognito-idp.{self.region}.amazonaws.com/{user_pool.user_pool_id}/.well-known/openid-configuration") diff --git a/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py b/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py new file mode 100644 index 0000000000..cb76c9ed25 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +from aws_cdk import ( + Stack, + CfnOutput, + Fn, + aws_cloudfront as cloudfront, + aws_cloudfront_origins as origins, +) +from constructs import Construct + + +class CloudFrontStack(Stack): + + def __init__(self, scope: Construct, construct_id: str, a2a_agent_runtime_arn: str, http_agent_runtime_arn: str, mcp_agent_runtime_arn: str, agent_runtime_region: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + a2a_encoded_arn = Fn.join("", Fn.split(":", Fn.join("%3A", Fn.split(":", a2a_agent_runtime_arn)))) + a2a_encoded_arn = Fn.join("", Fn.split("/", Fn.join("%2F", Fn.split("/", a2a_encoded_arn)))) + + http_encoded_arn = Fn.join("", Fn.split(":", Fn.join("%3A", Fn.split(":", http_agent_runtime_arn)))) + http_encoded_arn = Fn.join("", Fn.split("/", Fn.join("%2F", Fn.split("/", http_encoded_arn)))) + + mcp_encoded_arn = Fn.join("", Fn.split(":", Fn.join("%3A", Fn.split(":", mcp_agent_runtime_arn)))) + mcp_encoded_arn = Fn.join("", Fn.split("/", Fn.join("%2F", Fn.split("/", mcp_encoded_arn)))) + + a2a_agentcore_origin = origins.HttpOrigin( + f"bedrock-agentcore.{agent_runtime_region}.amazonaws.com", + origin_path=Fn.join("", ["/runtimes/", a2a_encoded_arn, "/invocations"]), + protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY + ) + + http_agentcore_origin = origins.HttpOrigin( + f"bedrock-agentcore.{agent_runtime_region}.amazonaws.com", + origin_path=Fn.join("", ["/runtimes/", http_encoded_arn]), + protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY + ) + + mcp_agentcore_origin = origins.HttpOrigin( + f"bedrock-agentcore.{agent_runtime_region}.amazonaws.com", + origin_path=Fn.join("", ["/runtimes/", mcp_encoded_arn]), + protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY + ) + + strip_a2a_prefix_fn = cloudfront.Function(self, "StripA2aPrefixFunction", + code=cloudfront.FunctionCode.from_inline(""" +function handler(event) { + var request = event.request; + request.uri = request.uri.replace(/^\\/a2a/, ''); + if (request.uri === '') request.uri = '/'; + return request; +} +"""), + runtime=cloudfront.FunctionRuntime.JS_2_0 + ) + + strip_http_prefix_fn = cloudfront.Function(self, "StripHttpPrefixFunction", + code=cloudfront.FunctionCode.from_inline(""" +function handler(event) { + var request = event.request; + request.uri = request.uri.replace(/^\\/rest/, ''); + if (request.uri === '') request.uri = '/'; + return request; +} +"""), + runtime=cloudfront.FunctionRuntime.JS_2_0 + ) + + strip_mcp_prefix_fn = cloudfront.Function(self, "StripMcpPrefixFunction", + code=cloudfront.FunctionCode.from_inline(""" +function handler(event) { + var request = event.request; + request.uri = request.uri.replace(/^\\/mcp/, ''); + if (request.uri === '') request.uri = '/'; + return request; +} +"""), + runtime=cloudfront.FunctionRuntime.JS_2_0 + ) + + dummy_origin = origins.HttpOrigin("aws.amazon.com") + + distribution = cloudfront.Distribution(self, "Distribution", + default_behavior=cloudfront.BehaviorOptions( + origin=dummy_origin, + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED + ), + additional_behaviors={ + "/a2a/*": cloudfront.BehaviorOptions( + origin=a2a_agentcore_origin, + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, + origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, + function_associations=[ + cloudfront.FunctionAssociation( + function=strip_a2a_prefix_fn, + event_type=cloudfront.FunctionEventType.VIEWER_REQUEST + ) + ] + ), + "/rest/*": cloudfront.BehaviorOptions( + origin=http_agentcore_origin, + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, + origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, + function_associations=[ + cloudfront.FunctionAssociation( + function=strip_http_prefix_fn, + event_type=cloudfront.FunctionEventType.VIEWER_REQUEST + ) + ] + ), + "/mcp/*": cloudfront.BehaviorOptions( + origin=mcp_agentcore_origin, + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, + origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, + function_associations=[ + cloudfront.FunctionAssociation( + function=strip_mcp_prefix_fn, + event_type=cloudfront.FunctionEventType.VIEWER_REQUEST + ) + ] + ) + }, + price_class=cloudfront.PriceClass.PRICE_CLASS_100 + ) + + CfnOutput(self, "DistributionUrl", value=f"https://{distribution.distribution_domain_name}") diff --git a/cloudfront-agentcore-runtime-cdk/requirements.txt b/cloudfront-agentcore-runtime-cdk/requirements.txt new file mode 100644 index 0000000000..520efb5493 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/requirements.txt @@ -0,0 +1,2 @@ +aws-cdk-lib>=2.220.0 +constructs>=10.0.0,<11.0.0 diff --git a/cloudfront-agentcore-runtime-cdk/test/.cognito_config b/cloudfront-agentcore-runtime-cdk/test/.cognito_config new file mode 100644 index 0000000000..c1cdf26a27 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/.cognito_config @@ -0,0 +1,4 @@ +POOL_ID= +CLIENT_ID= +USERNAME=test +REGION=us-west-2 diff --git a/cloudfront-agentcore-runtime-cdk/test/get_token.sh b/cloudfront-agentcore-runtime-cdk/test/get_token.sh new file mode 100755 index 0000000000..dba90ca188 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/get_token.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +echo "Cognito Token Generator" +echo "Get Pool ID and Client ID from CDK stack outputs" +echo "" + +CONFIG_FILE=".cognito_config" + +if [ -f "$CONFIG_FILE" ]; then + source "$CONFIG_FILE" +fi + +read -p "Pool ID [$POOL_ID]: " input && POOL_ID=${input:-$POOL_ID} +read -p "Client ID [$CLIENT_ID]: " input && CLIENT_ID=${input:-$CLIENT_ID} +read -p "Username [$USERNAME]: " input && USERNAME=${input:-$USERNAME} +read -sp "Password: " PASSWORD +echo +read -p "Region [${REGION:-us-west-2}]: " input && REGION=${input:-${REGION:-us-west-2}} + +cat > "$CONFIG_FILE" << EOF +POOL_ID=$POOL_ID +CLIENT_ID=$CLIENT_ID +USERNAME=$USERNAME +REGION=$REGION +EOF + +aws cognito-idp admin-create-user \ + --user-pool-id $POOL_ID \ + --username $USERNAME \ + --region $REGION \ + --message-action SUPPRESS > /dev/null 2>&1 || true + +aws cognito-idp admin-set-user-password \ + --user-pool-id $POOL_ID \ + --username $USERNAME \ + --password $PASSWORD \ + --region $REGION \ + --permanent > /dev/null 2>&1 || true + +BEARER_TOKEN=$(aws cognito-idp initiate-auth \ + --client-id "$CLIENT_ID" \ + --auth-flow USER_PASSWORD_AUTH \ + --auth-parameters USERNAME=$USERNAME,PASSWORD=$PASSWORD \ + --region $REGION | jq -r '.AuthenticationResult.AccessToken') + +echo "export BEARER_TOKEN=\"$BEARER_TOKEN\"" diff --git a/cloudfront-agentcore-runtime-cdk/test/test_a2a.py b/cloudfront-agentcore-runtime-cdk/test/test_a2a.py new file mode 100755 index 0000000000..91a8197e9a --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/test_a2a.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +import requests +import json +import uuid +import sys + +def debug_response(resp): + print(f" [DEBUG] Status: {resp.status_code}") + print(f" [DEBUG] Headers: {dict(resp.headers)}") + try: + print(f" [DEBUG] Body: {json.dumps(resp.json(), indent=2)}") + except: + print(f" [DEBUG] Body: {resp.text[:500]}") + +def test_agent_card(base_url, headers): + print("\n[TEST] /.well-known/agent-card.json (GET)") + print(f" [DEBUG] URL: {base_url}/a2a/.well-known/agent-card.json") + resp = requests.get(f"{base_url}/a2a/.well-known/agent-card.json", headers=headers) + debug_response(resp) + assert resp.status_code == 200, f"Agent card failed: {resp.status_code}" + data = resp.json() + required_fields = ["name", "description", "skills"] + for field in required_fields: + assert field in data, f"Missing {field} in agent card" + assert len(data["skills"]) > 0, "Agent must have at least one skill" + print(f" [PASS] Agent '{data['name']}' with {len(data['skills'])} skill(s)") + +def test_message_send(base_url, headers): + print("\n[TEST] / (POST message/send)") + print(f" [DEBUG] URL: {base_url}/a2a/") + payload = { + "jsonrpc": "2.0", + "id": str(uuid.uuid4()), + "method": "message/send", + "params": { + "message": { + "role": "user", + "parts": [{"kind": "text", "text": "Hello, what is 1+1?"}], + "messageId": str(uuid.uuid4()) + } + } + } + print(f" [DEBUG] Request: {json.dumps(payload, indent=2)}") + resp = requests.post(f"{base_url}/a2a/", headers=headers, json=payload) + debug_response(resp) + assert resp.status_code == 200, f"Message send failed: {resp.status_code}" + data = resp.json() + assert "jsonrpc" in data, "Missing jsonrpc in response" + assert data["jsonrpc"] == "2.0", "Invalid jsonrpc version" + assert "id" in data, "Missing id in response" + if "error" in data: + print(f" [WARN] Error response: {data['error']}") + else: + assert "result" in data, "Missing result in response" + print(f" [PASS] Got valid JSON-RPC response") + +def main(): + import os + base_url = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else os.environ.get("CF_URL", "").rstrip("/") + token = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("BEARER_TOKEN", "") + + if not base_url or not token: + print("Usage: python test_a2a.py ") + print("Or set CF_URL and BEARER_TOKEN environment variables") + sys.exit(1) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + print("=" * 60) + print("A2A Protocol Validation") + print("=" * 60) + print(f"Base URL: {base_url}") + print(f"Token: {token[:20]}...") + + try: + test_agent_card(base_url, headers) + test_message_send(base_url, headers) + print("\n" + "=" * 60) + print("All tests passed!") + print("=" * 60) + except AssertionError as e: + print(f"\n[FAIL] {e}") + sys.exit(1) + except Exception as e: + print(f"\n[ERROR] {e}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/cloudfront-agentcore-runtime-cdk/test/test_http.py b/cloudfront-agentcore-runtime-cdk/test/test_http.py new file mode 100755 index 0000000000..97641d39a9 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/test_http.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +import requests +import json +import sys + +def debug_response(resp): + print(f" [DEBUG] Status: {resp.status_code}") + print(f" [DEBUG] Headers: {dict(resp.headers)}") + try: + print(f" [DEBUG] Body: {resp.text[:500]}") + except: + print(f" [DEBUG] Body: {resp.text}") + +def test_invocations(base_url, headers): + print("\n[TEST] /rest/invocations (POST)") + print(f" [DEBUG] URL: {base_url}/rest/invocations") + payload = {"prompt": "What is 2+2?"} + print(f" [DEBUG] Request: {json.dumps(payload, indent=2)}") + resp = requests.post(f"{base_url}/rest/invocations", headers=headers, json=payload, stream=True) + print(f" [DEBUG] Status: {resp.status_code}") + print(f" [DEBUG] Headers: {dict(resp.headers)}") + assert resp.status_code == 200, f"Invocations failed: {resp.status_code}" + + content_type = resp.headers.get("content-type", "") + if "text/event-stream" in content_type: + print(" [DEBUG] Streaming response (SSE):") + for line in resp.iter_lines(): + if line: + print(f" {line.decode('utf-8')}") + print(f" [PASS] Got streaming response") + else: + print(f" [DEBUG] Body: {resp.text[:500]}") + print(f" [PASS] Got response") + +def main(): + import os + base_url = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else os.environ.get("CF_URL", "").rstrip("/") + token = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("BEARER_TOKEN", "") + + if not base_url or not token: + print("Usage: python test_http.py ") + print("Or set CF_URL and BEARER_TOKEN environment variables") + sys.exit(1) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + print("=" * 60) + print("HTTP Protocol Validation") + print("=" * 60) + print(f"Base URL: {base_url}") + print(f"Token: {token[:20]}...") + + try: + test_invocations(base_url, headers) + print("\n" + "=" * 60) + print("All tests passed!") + print("=" * 60) + except AssertionError as e: + print(f"\n[FAIL] {e}") + sys.exit(1) + except Exception as e: + print(f"\n[ERROR] {e}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/cloudfront-agentcore-runtime-cdk/test/test_mcp.py b/cloudfront-agentcore-runtime-cdk/test/test_mcp.py new file mode 100755 index 0000000000..042c9fe3a5 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/test_mcp.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import asyncio +import os +import sys + +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +async def main(): + base_url = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else os.environ.get("CF_URL", "").rstrip("/") + token = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("BEARER_TOKEN", "") + + if not base_url or not token: + print("Usage: python test_mcp.py ") + print("Or set CF_URL and BEARER_TOKEN environment variables") + sys.exit(1) + + print("=" * 60) + print("MCP Protocol Validation") + print("=" * 60) + print(f"Base URL: {base_url}") + print(f"Token: {token[:20]}...") + + mcp_url = f"{base_url}/mcp/invocations" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + print(f"\n[TEST] MCP Connection") + print(f" [DEBUG] URL: {mcp_url}") + + try: + async with streamablehttp_client(mcp_url, headers, timeout=120, terminate_on_close=False) as ( + read_stream, + write_stream, + _, + ): + async with ClientSession(read_stream, write_stream) as session: + await session.initialize() + print(" [PASS] MCP session initialized") + + print("\n[TEST] List Tools") + tools = await session.list_tools() + print(f" [PASS] Found {len(tools.tools)} tools:") + for tool in tools.tools: + print(f" - {tool.name}") + + print("\n[TEST] Call add_numbers tool") + result = await session.call_tool("add_numbers", {"a": 5, "b": 3}) + print(f" [PASS] Result: {result.content}") + + print("\n" + "=" * 60) + print("All tests passed!") + print("=" * 60) + except Exception as e: + print(f"\n[FAIL] {e}") + sys.exit(1) + +if __name__ == "__main__": + asyncio.run(main()) From e0dbc109aba65d9bee811fe1409040abfb73d2b1 Mon Sep 17 00:00:00 2001 From: Rakshith Rao Date: Tue, 27 Jan 2026 21:29:12 +0100 Subject: [PATCH 2/9] feat(cloudfront-agentcore-runtime-cdk): Update deployment and testing documentation --- cloudfront-agentcore-runtime-cdk/README.md | 13 ++++++------- cloudfront-agentcore-runtime-cdk/app.py | 6 +++--- cloudfront-agentcore-runtime-cdk/requirements.txt | 2 ++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cloudfront-agentcore-runtime-cdk/README.md b/cloudfront-agentcore-runtime-cdk/README.md index 4b234601ea..f03e25050e 100644 --- a/cloudfront-agentcore-runtime-cdk/README.md +++ b/cloudfront-agentcore-runtime-cdk/README.md @@ -45,13 +45,7 @@ Important: this application uses various AWS services and there are costs associ pip3 install -r requirements.txt ``` -4. Bootstrap CDK (if not already done): - - ```shell - cdk bootstrap aws:///us-west-2 aws:///us-east-1 - ``` - -5. Deploy the stacks: +4. Deploy the stacks: ```shell cdk deploy --all @@ -112,6 +106,11 @@ This pattern creates: python test_a2a.py ``` + You can also use tools like [A2A Inspector](https://a2a-inspector.vercel.app/) with the agent card URL: + ``` + https://.cloudfront.net/a2a/ + ``` + 5. Test HTTP protocol: ```shell diff --git a/cloudfront-agentcore-runtime-cdk/app.py b/cloudfront-agentcore-runtime-cdk/app.py index de5926737d..83eeea8337 100644 --- a/cloudfront-agentcore-runtime-cdk/app.py +++ b/cloudfront-agentcore-runtime-cdk/app.py @@ -8,13 +8,13 @@ app = cdk.App() -agentcore_stack = AgentcoreStack(app, "AgentcoreStack", +agentcore_stack = AgentcoreStack(app, "AgentCoreAgentsStack", env=cdk.Environment(region=os.environ.get("CDK_DEFAULT_REGION", "us-west-2")), cross_region_references=True ) -cloudfront_stack = CloudFrontStack(app, "AgentcoreCloudFrontStack", - env=cdk.Environment(region="us-east-1"), +cloudfront_stack = CloudFrontStack(app, "CloudFrontToAgentCoreStack", + env=cdk.Environment(region=os.environ.get("CDK_DEFAULT_REGION", "us-west-2")), cross_region_references=True, a2a_agent_runtime_arn=agentcore_stack.a2a_agent_runtime_arn, http_agent_runtime_arn=agentcore_stack.http_agent_runtime_arn, diff --git a/cloudfront-agentcore-runtime-cdk/requirements.txt b/cloudfront-agentcore-runtime-cdk/requirements.txt index 520efb5493..1db8dd37fe 100644 --- a/cloudfront-agentcore-runtime-cdk/requirements.txt +++ b/cloudfront-agentcore-runtime-cdk/requirements.txt @@ -1,2 +1,4 @@ aws-cdk-lib>=2.220.0 constructs>=10.0.0,<11.0.0 +requests>=2.25.0 +mcp>=1.0.0 From fd1bf0f10aed51bd941ec5b21ab79466c34f7773 Mon Sep 17 00:00:00 2001 From: Rakshith Rao Date: Fri, 30 Jan 2026 12:06:48 +0100 Subject: [PATCH 3/9] feat(cloudfront-agentcore-runtime-cdk): Add CloudFront URL configuration for A2A agent discovery --- cloudfront-agentcore-runtime-cdk/README.md | 13 ++++- .../agent-code/a2a/agent.py | 2 +- cloudfront-agentcore-runtime-cdk/app.py | 10 ++-- .../infra/agentcore_stack.py | 4 -- .../scripts/update_a2a_cloudfront_url.sh | 55 +++++++++++++++++++ 5 files changed, 74 insertions(+), 10 deletions(-) create mode 100755 cloudfront-agentcore-runtime-cdk/scripts/update_a2a_cloudfront_url.sh diff --git a/cloudfront-agentcore-runtime-cdk/README.md b/cloudfront-agentcore-runtime-cdk/README.md index f03e25050e..ed740eb283 100644 --- a/cloudfront-agentcore-runtime-cdk/README.md +++ b/cloudfront-agentcore-runtime-cdk/README.md @@ -71,7 +71,7 @@ This pattern creates: ## Testing -1. Get Pool ID and Client ID from the CDK outputs after running the `cdk deploy` command. Look for `UserPoolId` and `UserPoolClientId` in the outputs. +1. Get Pool ID, Client ID, and CloudFront URL from the CDK outputs after running the `cdk deploy` command. Look for `UserPoolId`, `UserPoolClientId`, and `DistributionUrl` in the outputs. 2. Get a bearer token: @@ -123,6 +123,17 @@ This pattern creates: python test_mcp.py ``` +## Optional: Update A2A Agent Card URL + +If you want A2A clients to discover your agent via CloudFront instead of the direct AgentCore Runtime URL, run this script after deployment: + +```shell +cd .. +./scripts/update_a2a_cloudfront_url.sh +``` + +This configures the A2A agent to advertise the CloudFront URL in its agent card (`/.well-known/agent-card.json`). This is required when using tools like [A2A Inspector](https://a2a-inspector.vercel.app/) or other A2A-compatible clients that rely on the agent card for endpoint discovery. + ## Cleanup Delete the stacks: diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py index 6979f60c4d..bc46952d94 100644 --- a/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py +++ b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py @@ -8,7 +8,7 @@ logging.basicConfig(level=logging.INFO) -runtime_url = os.environ.get('AGENTCORE_RUNTIME_URL', 'http://127.0.0.1:9000/') +runtime_url = os.environ.get('CLOUDFRONT_URL', os.environ.get('AGENTCORE_RUNTIME_URL', 'http://127.0.0.1:9000/')) strands_agent = Agent( name="Test Agent", diff --git a/cloudfront-agentcore-runtime-cdk/app.py b/cloudfront-agentcore-runtime-cdk/app.py index 83eeea8337..242ebad150 100644 --- a/cloudfront-agentcore-runtime-cdk/app.py +++ b/cloudfront-agentcore-runtime-cdk/app.py @@ -8,19 +8,21 @@ app = cdk.App() +region = os.environ.get("CDK_DEFAULT_REGION", "us-west-2") + agentcore_stack = AgentcoreStack(app, "AgentCoreAgentsStack", - env=cdk.Environment(region=os.environ.get("CDK_DEFAULT_REGION", "us-west-2")), + env=cdk.Environment(region=region), cross_region_references=True ) cloudfront_stack = CloudFrontStack(app, "CloudFrontToAgentCoreStack", - env=cdk.Environment(region=os.environ.get("CDK_DEFAULT_REGION", "us-west-2")), + env=cdk.Environment(region=region), cross_region_references=True, a2a_agent_runtime_arn=agentcore_stack.a2a_agent_runtime_arn, http_agent_runtime_arn=agentcore_stack.http_agent_runtime_arn, mcp_agent_runtime_arn=agentcore_stack.mcp_agent_runtime_arn, - agent_runtime_region=os.environ.get("CDK_DEFAULT_REGION", "us-west-2")) - + agent_runtime_region=region +) cloudfront_stack.add_dependency(agentcore_stack) app.synth() diff --git a/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py b/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py index e6d01fb429..60ac05f5da 100644 --- a/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py +++ b/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py @@ -171,9 +171,5 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: self.http_agent_runtime_arn = http_agent_runtime.attr_agent_runtime_arn self.mcp_agent_runtime_arn = mcp_agent_runtime.attr_agent_runtime_arn - CfnOutput(self, "A2aAgentRuntimeArn", value=a2a_agent_runtime.attr_agent_runtime_arn) - CfnOutput(self, "HttpAgentRuntimeArn", value=http_agent_runtime.attr_agent_runtime_arn) - CfnOutput(self, "McpAgentRuntimeArn", value=mcp_agent_runtime.attr_agent_runtime_arn) CfnOutput(self, "UserPoolId", value=user_pool.user_pool_id) CfnOutput(self, "UserPoolClientId", value=user_pool_client.user_pool_client_id) - CfnOutput(self, "CognitoDiscoveryUrl", value=f"https://cognito-idp.{self.region}.amazonaws.com/{user_pool.user_pool_id}/.well-known/openid-configuration") diff --git a/cloudfront-agentcore-runtime-cdk/scripts/update_a2a_cloudfront_url.sh b/cloudfront-agentcore-runtime-cdk/scripts/update_a2a_cloudfront_url.sh new file mode 100755 index 0000000000..d6cd5cbdd4 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/scripts/update_a2a_cloudfront_url.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -e + +REGION=${AWS_DEFAULT_REGION:-us-west-2} + +echo "Fetching CloudFront distribution..." +DISTRIBUTION_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[*].[Id, Origins.Items[0].DomainName] | [?contains([1], 'bedrock-agentcore')] | [0][0]" --output text --no-cli-pager | grep -v "^None$" | head -1) +if [ -z "$DISTRIBUTION_ID" ]; then + echo "Error: Could not find CloudFront distribution for AgentCore" + exit 1 +fi +CF_DOMAIN=$(aws cloudfront get-distribution --id $DISTRIBUTION_ID --query "Distribution.DomainName" --output text --no-cli-pager) +CF_URL="https://${CF_DOMAIN}" + +echo "Fetching A2A agent runtime..." +RUNTIME_ID=$(aws bedrock-agentcore-control list-agent-runtimes --region $REGION --query "agentRuntimes[?contains(agentRuntimeName, 'A2a_Agent')].agentRuntimeId" --output text --no-cli-pager | grep -v "^None$" | head -1) +if [ -z "$RUNTIME_ID" ]; then + echo "Error: Could not find A2A agent runtime" + exit 1 +fi + +echo "Found runtime: $RUNTIME_ID" + +RUNTIME_INFO=$(aws bedrock-agentcore-control get-agent-runtime --agent-runtime-id $RUNTIME_ID --region $REGION --no-cli-pager) +CONTAINER_URI=$(echo $RUNTIME_INFO | jq -r '.agentRuntimeArtifact.containerConfiguration.containerUri') +ROLE_ARN=$(echo $RUNTIME_INFO | jq -r '.roleArn') +DISCOVERY_URL=$(echo $RUNTIME_INFO | jq -r '.authorizerConfiguration.customJWTAuthorizer.discoveryUrl') +CLIENT_ID=$(echo $RUNTIME_INFO | jq -r '.authorizerConfiguration.customJWTAuthorizer.allowedClients[0]') + +echo "Updating A2A agent with CloudFront URL: ${CF_URL}/a2a/" + +aws bedrock-agentcore-control update-agent-runtime \ + --agent-runtime-id $RUNTIME_ID \ + --agent-runtime-artifact containerConfiguration={containerUri=$CONTAINER_URI} \ + --role-arn $ROLE_ARN \ + --network-configuration networkMode=PUBLIC \ + --protocol-configuration serverProtocol=A2A \ + --authorizer-configuration "customJWTAuthorizer={discoveryUrl=$DISCOVERY_URL,allowedClients=$CLIENT_ID}" \ + --environment-variables "AWS_DEFAULT_REGION=$REGION,CLOUDFRONT_URL=${CF_URL}/a2a/" \ + --region $REGION \ + --no-cli-pager + +echo "Waiting for runtime to be ready..." +while true; do + STATUS=$(aws bedrock-agentcore-control get-agent-runtime --agent-runtime-id $RUNTIME_ID --region $REGION --query "status" --output text --no-cli-pager) + if [ "$STATUS" == "READY" ]; then + echo "Runtime is ready." + break + fi + echo "Status: $STATUS. Waiting..." + sleep 5 +done + +echo "Done." From 6e957b90deb3ec5c7211dd35d22fbacce447b1a6 Mon Sep 17 00:00:00 2001 From: Rakshith Rao Date: Fri, 30 Jan 2026 12:11:21 +0100 Subject: [PATCH 4/9] docs(cloudfront-agentcore-runtime-cdk): Simplify agent card documentation --- cloudfront-agentcore-runtime-cdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudfront-agentcore-runtime-cdk/README.md b/cloudfront-agentcore-runtime-cdk/README.md index ed740eb283..b7c43c55bb 100644 --- a/cloudfront-agentcore-runtime-cdk/README.md +++ b/cloudfront-agentcore-runtime-cdk/README.md @@ -132,7 +132,7 @@ cd .. ./scripts/update_a2a_cloudfront_url.sh ``` -This configures the A2A agent to advertise the CloudFront URL in its agent card (`/.well-known/agent-card.json`). This is required when using tools like [A2A Inspector](https://a2a-inspector.vercel.app/) or other A2A-compatible clients that rely on the agent card for endpoint discovery. +This configures the A2A agent to advertise the CloudFront URL in its agent card (`/.well-known/agent-card.json`). This is required when using A2A-compatible clients that rely on the agent card for endpoint discovery. ## Cleanup From 15645276d5efba273ed5f471eb8d473b6755e160 Mon Sep 17 00:00:00 2001 From: Rakshith Rao Date: Fri, 30 Jan 2026 12:24:34 +0100 Subject: [PATCH 5/9] refactor(cloudfront-agentcore-runtime-cdk): Remove redundant agent_runtime_region parameter --- cloudfront-agentcore-runtime-cdk/app.py | 3 +-- .../images/architecture.png | Bin 38499 -> 20633 bytes .../infra/cloudfront_stack.py | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cloudfront-agentcore-runtime-cdk/app.py b/cloudfront-agentcore-runtime-cdk/app.py index 242ebad150..f2f560bc36 100644 --- a/cloudfront-agentcore-runtime-cdk/app.py +++ b/cloudfront-agentcore-runtime-cdk/app.py @@ -20,8 +20,7 @@ cross_region_references=True, a2a_agent_runtime_arn=agentcore_stack.a2a_agent_runtime_arn, http_agent_runtime_arn=agentcore_stack.http_agent_runtime_arn, - mcp_agent_runtime_arn=agentcore_stack.mcp_agent_runtime_arn, - agent_runtime_region=region + mcp_agent_runtime_arn=agentcore_stack.mcp_agent_runtime_arn ) cloudfront_stack.add_dependency(agentcore_stack) diff --git a/cloudfront-agentcore-runtime-cdk/images/architecture.png b/cloudfront-agentcore-runtime-cdk/images/architecture.png index ad4d99e97bccba54706260e779eee908dd190287..d5a48fc73b4b07170b5cc4b0c8ddca7a0f705469 100644 GIT binary patch literal 20633 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ú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 diff --git a/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py b/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py index cb76c9ed25..b2b182a7b1 100644 --- a/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py +++ b/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py @@ -12,7 +12,7 @@ class CloudFrontStack(Stack): - def __init__(self, scope: Construct, construct_id: str, a2a_agent_runtime_arn: str, http_agent_runtime_arn: str, mcp_agent_runtime_arn: str, agent_runtime_region: str, **kwargs) -> None: + def __init__(self, scope: Construct, construct_id: str, a2a_agent_runtime_arn: str, http_agent_runtime_arn: str, mcp_agent_runtime_arn: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) a2a_encoded_arn = Fn.join("", Fn.split(":", Fn.join("%3A", Fn.split(":", a2a_agent_runtime_arn)))) @@ -25,19 +25,19 @@ def __init__(self, scope: Construct, construct_id: str, a2a_agent_runtime_arn: s mcp_encoded_arn = Fn.join("", Fn.split("/", Fn.join("%2F", Fn.split("/", mcp_encoded_arn)))) a2a_agentcore_origin = origins.HttpOrigin( - f"bedrock-agentcore.{agent_runtime_region}.amazonaws.com", + f"bedrock-agentcore.{self.region}.amazonaws.com", origin_path=Fn.join("", ["/runtimes/", a2a_encoded_arn, "/invocations"]), protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY ) http_agentcore_origin = origins.HttpOrigin( - f"bedrock-agentcore.{agent_runtime_region}.amazonaws.com", + f"bedrock-agentcore.{self.region}.amazonaws.com", origin_path=Fn.join("", ["/runtimes/", http_encoded_arn]), protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY ) mcp_agentcore_origin = origins.HttpOrigin( - f"bedrock-agentcore.{agent_runtime_region}.amazonaws.com", + f"bedrock-agentcore.{self.region}.amazonaws.com", origin_path=Fn.join("", ["/runtimes/", mcp_encoded_arn]), protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY ) From 4919b71afbae24373497a6af2fc75fab1c625ccf Mon Sep 17 00:00:00 2001 From: Rakshith Rao Date: Thu, 26 Mar 2026 09:51:19 +0100 Subject: [PATCH 6/9] docs(cloudfront-agentcore-runtime-cdk): Add clarifying comments for wildcard IAM resources --- cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py b/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py index 60ac05f5da..56d8f1a66f 100644 --- a/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py +++ b/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py @@ -66,7 +66,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: iam.PolicyStatement( sid="ECRTokenAccess", actions=["ecr:GetAuthorizationToken"], - resources=["*"] + resources=["*"] # ecr:GetAuthorizationToken does not support resource-level permissions, see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonelasticcontainerregistry.html ), iam.PolicyStatement( actions=["logs:DescribeLogStreams", "logs:CreateLogGroup"], @@ -82,11 +82,11 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ), iam.PolicyStatement( actions=["xray:PutTraceSegments", "xray:PutTelemetryRecords", "xray:GetSamplingRules", "xray:GetSamplingTargets"], - resources=["*"] + resources=["*"] # X-Ray actions do not support resource-level permissions, see https://docs.aws.amazon.com/service-authorization/latest/reference/list_awsx-ray.html ), iam.PolicyStatement( actions=["cloudwatch:PutMetricData"], - resources=["*"], + resources=["*"], # cloudwatch:PutMetricData does not support resource-level permissions, scoped via condition key; see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazoncloudwatch.html conditions={"StringEquals": {"cloudwatch:namespace": "bedrock-agentcore"}} ), iam.PolicyStatement( From 2983ef716391ef918b8a93d4aef5dafa78c1a48f Mon Sep 17 00:00:00 2001 From: Rakshith Rao Date: Thu, 26 Mar 2026 09:52:09 +0100 Subject: [PATCH 7/9] docs(cloudfront-agentcore-runtime-cdk): Add post-deployment verification steps --- cloudfront-agentcore-runtime-cdk/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cloudfront-agentcore-runtime-cdk/README.md b/cloudfront-agentcore-runtime-cdk/README.md index b7c43c55bb..a3bdbd39c0 100644 --- a/cloudfront-agentcore-runtime-cdk/README.md +++ b/cloudfront-agentcore-runtime-cdk/README.md @@ -51,6 +51,14 @@ Important: this application uses various AWS services and there are costs associ cdk deploy --all ``` +5. After `cdk deploy --all` completes, verify the deployment: + 1. Check the CDK outputs for `UserPoolId`, `UserPoolClientId`, and `DistributionUrl`. + 2. Verify the CloudFront distribution is deployed: + ```shell + aws cloudfront list-distributions --query "DistributionList.Items[*].[Id,Status,DomainName]" --output table + ``` + 3. Confirm the distribution status shows "Deployed". + ## How it works This pattern creates: From 3c1b8bcbc4e3b32ffbdc6b4a44e4211d18a4ef73 Mon Sep 17 00:00:00 2001 From: Rakshith Rao Date: Thu, 26 Mar 2026 10:38:12 +0100 Subject: [PATCH 8/9] feat(cloudfront-agentcore-runtime-cdk): Require explicit CDK_DEFAULT_REGION configuration --- cloudfront-agentcore-runtime-cdk/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloudfront-agentcore-runtime-cdk/app.py b/cloudfront-agentcore-runtime-cdk/app.py index f2f560bc36..c795058b1d 100644 --- a/cloudfront-agentcore-runtime-cdk/app.py +++ b/cloudfront-agentcore-runtime-cdk/app.py @@ -8,7 +8,9 @@ app = cdk.App() -region = os.environ.get("CDK_DEFAULT_REGION", "us-west-2") +region = os.environ.get("CDK_DEFAULT_REGION") +if not region: + raise ValueError("CDK_DEFAULT_REGION environment variable must be set. Run: export CDK_DEFAULT_REGION=") agentcore_stack = AgentcoreStack(app, "AgentCoreAgentsStack", env=cdk.Environment(region=region), From 5ad28379454b5fdb310364a6fa4e4f295cb990f2 Mon Sep 17 00:00:00 2001 From: Rakshith Rao Date: Thu, 26 Mar 2026 10:40:47 +0100 Subject: [PATCH 9/9] docs(cloudfront-agentcore-runtime-cdk): Update AWS service naming conventions --- cloudfront-agentcore-runtime-cdk/README.md | 2 +- cloudfront-agentcore-runtime-cdk/example-pattern.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudfront-agentcore-runtime-cdk/README.md b/cloudfront-agentcore-runtime-cdk/README.md index a3bdbd39c0..ad21b2c31d 100644 --- a/cloudfront-agentcore-runtime-cdk/README.md +++ b/cloudfront-agentcore-runtime-cdk/README.md @@ -7,7 +7,7 @@ Benefits of using CloudFront in front of AgentCore Runtime: - DDoS protection via AWS Shield Standard (included) - Optional AWS WAF integration for IP rate limiting, geo-blocking, and bot protection - Custom domain support with SSL/TLS certificates -- Request/response transformation via CloudFront Functions +- Request/response transformation via Amazon CloudFront Functions - Centralized access logging and monitoring Learn more about this pattern at [Serverless Land Patterns](https://serverlessland.com/patterns/cloudfront-agentcore-runtime-cdk) diff --git a/cloudfront-agentcore-runtime-cdk/example-pattern.json b/cloudfront-agentcore-runtime-cdk/example-pattern.json index 25ddc74379..e696c1d105 100644 --- a/cloudfront-agentcore-runtime-cdk/example-pattern.json +++ b/cloudfront-agentcore-runtime-cdk/example-pattern.json @@ -1,5 +1,5 @@ { - "title": "CloudFront to Amazon Bedrock AgentCore Runtime", + "title": "Amazon CloudFront to Amazon Bedrock AgentCore Runtime", "description": "This pattern demonstrates how to proxy requests to Amazon Bedrock AgentCore Runtime through CloudFront with OAuth 2.0 authentication, supporting all three AgentCore Runtime service contracts: A2A, HTTP, and MCP protocols.", "language": "Python", "level": "300",