diff --git a/docs/examples/README.md b/docs/examples/README.md index 75d0a589..c6664ac8 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -55,6 +55,7 @@ Available CDK examples: - [Deploy to EC2](cdk/deploy_to_ec2/README.md) - Guide for deploying agents to Amazon EC2 instances - [Deploy to Fargate](cdk/deploy_to_fargate/README.md) - Guide for deploying agents to AWS Fargate +- [Deploy to App Runner](cdk/deploy_to_apprunner/README.md) - Guide for deploying agents to AWS App Runner - [Deploy to Lambda](cdk/deploy_to_lambda/README.md) - Guide for deploying agents to AWS Lambda ### Amazon EKS Example diff --git a/docs/examples/cdk/deploy_to_apprunner/.gitignore b/docs/examples/cdk/deploy_to_apprunner/.gitignore new file mode 100644 index 00000000..97f9badd --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/.gitignore @@ -0,0 +1,41 @@ +*.js +!jest.config.js +*.d.ts +node_modules +.venv/ +output.json + +# Installed dependencies for container image +_dependencies + +# CDK asset staging directory +.cdk.staging +cdk.out +packaging/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# IDE +.idea/ +.vscode/ +*.swp +*.swo diff --git a/docs/examples/cdk/deploy_to_apprunner/README.md b/docs/examples/cdk/deploy_to_apprunner/README.md new file mode 100644 index 00000000..992a39c2 --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/README.md @@ -0,0 +1,139 @@ +# AWS CDK App Runner Deployment Example + +## Introduction + +This is a TypeScript-based CDK (Cloud Development Kit) example that demonstrates how to deploy a Python application to AWS App Runner. The example deploys a weather forecaster application that runs as a containerized service in AWS App Runner. The application is built with FastAPI and provides two weather endpoints: + +1. `/weather` - A standard endpoint that returns weather information based on the provided prompt +2. `/weather-streaming` - A streaming endpoint that delivers weather information in real-time as it's being generated + +## Prerequisites + +- [AWS CLI](https://aws.amazon.com/cli/) installed and configured +- [Node.js](https://nodejs.org/) (v18.x or later) +- Python 3.12 or later +- Either: + - [Podman](https://podman.io/) installed and running + - (or) [Docker](https://www.docker.com/) installed and running +## Project Structure + +- `lib/` - Contains the CDK stack definition in TypeScript +- `bin/` - Contains the CDK app entry point and deployment scripts: + - `cdk-app.ts` - Main CDK application entry point +- `docker/` - Contains the Dockerfile and application code for the container: + - `Dockerfile` - Docker image definition + - `app/` - Application code + - `requirements.txt` - Python dependencies for the container & local development + +## Setup and Deployment + +1. Install dependencies: + +```bash +# Install Node.js dependencies including CDK and TypeScript locally +npm install + +# Create a Python virtual environment (optional but recommended) +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install Python dependencies for the local development +pip install -r ./docker/requirements.txt +``` + +2. Bootstrap your AWS environment (if not already done): + +```bash +npx cdk bootstrap +``` + +3. Ensure podman is started (one time): + +```bash +podman machine init +podman machine start +``` + +4. Package & deploy via CDK: + +```bash +CDK_DOCKER=podman npx cdk deploy +``` + +## Usage + +After deployment, you can access the weather service using the App Runner Service URL that is output after deployment: + +```bash +# Get the service URL from the CDK output +SERVICE_URL=$(aws cloudformation describe-stacks --stack-name AgentAppRunnerStack --query "Stacks[0].Outputs[?ExportName=='AppRunnerServiceUrl'].OutputValue" --output text) +``` + +The service exposes a REST API endpoint that you can call using curl or any HTTP client: + +```bash +# Call the weather service +curl -X POST \ + https://$SERVICE_URL/weather \ + -H 'Content-Type: application/json' \ + -d '{"prompt": "What is the weather in New York?"}' + +# Call the streaming endpoint +curl -X POST \ + https://$SERVICE_URL/weather-streaming \ + -H 'Content-Type: application/json' \ + -d '{"prompt": "What is the weather in New York in Celsius?"}' +``` + +## Local testing (python) + +You can run the python app directly for local testing via: + +```bash +python ./docker/app/app.py +``` + +Then, set the SERVICE_URL to point to your local server + +```bash +SERVICE_URL=127.0.0.1:8000 +``` + +and you can use the curl commands above to test locally. + +## Local testing (container) + +Build & run the container: + +```bash +podman build --platform linux/amd64 -t agent_container ./docker/ + +podman run -p 127.0.0.1:8000:8000 \ + -v ~/.aws:/home/appuser/.aws:ro \ + -e AWS_PROFILE=default \ + -t agent_container +``` + +Then, set the SERVICE_URL to point to your local server + +```bash +SERVICE_URL=127.0.0.1:8000 +``` + +and you can use the curl commands above to test locally. + +## Cleanup + +To remove all resources created by this example: + +```bash +npx cdk destroy +``` + +## Additional Resources + +- [AWS CDK TypeScript Documentation](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-typescript.html) +- [AWS App Runner Documentation](https://docs.aws.amazon.com/apprunner/latest/dg/what-is-apprunner.html) +- [Docker Documentation](https://docs.docker.com/) +- [Podman Documentation](https://podman.io/docs) +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) diff --git a/docs/examples/cdk/deploy_to_apprunner/bin/cdk-app.ts b/docs/examples/cdk/deploy_to_apprunner/bin/cdk-app.ts new file mode 100644 index 00000000..106c15cd --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/bin/cdk-app.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env node +import { App } from "aws-cdk-lib"; +import { AgentAppRunnerStack } from "../lib/agent-apprunner-stack"; + +const app = new App(); + +// prettier-ignore +new AgentAppRunnerStack(app, "AgentAppRunnerStack", { + /* If you don't specify 'env', this stack will be environment-agnostic. + * Account/Region-dependent features and context lookups will not work, + * but a single synthesized template can be deployed anywhere. */ + + /* Uncomment the next line to specialize this stack for the AWS Account + * and Region that are implied by the current CLI configuration. */ + // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + + /* Uncomment the next line if you know exactly what Account and Region you + * want to deploy the stack to. */ + // env: { account: '123456789012', region: 'us-east-1' }, + + /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ +}); diff --git a/docs/examples/cdk/deploy_to_apprunner/cdk.json b/docs/examples/cdk/deploy_to_apprunner/cdk.json new file mode 100644 index 00000000..ae59229c --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/cdk.json @@ -0,0 +1,88 @@ +{ + "app": "npx tsx bin/cdk-app.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "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:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@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, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true + } +} diff --git a/docs/examples/cdk/deploy_to_apprunner/docker/Dockerfile b/docs/examples/cdk/deploy_to_apprunner/docker/Dockerfile new file mode 100644 index 00000000..046cf8e0 --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/docker/Dockerfile @@ -0,0 +1,29 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +# Copy requirements file +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY app/ . + +# Create a non-root user to run the application +RUN useradd -m appuser +USER appuser + +# Expose the port the app runs on +EXPOSE 8000 + +# Command to run the application with Uvicorn +# - workers: 2 worker processes (adjust based on container resources) +# - host: Listen on all interfaces +# - port: 8000 +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"] diff --git a/docs/examples/cdk/deploy_to_apprunner/docker/app/app.py b/docs/examples/cdk/deploy_to_apprunner/docker/app/app.py new file mode 100644 index 00000000..e79107fe --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/docker/app/app.py @@ -0,0 +1,114 @@ +from collections.abc import Callable +from queue import Queue +from threading import Thread +from typing import Iterator, Dict, Optional +from uuid import uuid4 + +from fastapi import FastAPI, Request, Response, HTTPException +from fastapi.responses import StreamingResponse, PlainTextResponse +from pydantic import BaseModel +import uvicorn +from strands import Agent, tool +from strands_tools import http_request +import os + +app = FastAPI(title="Weather API") + +# Define a weather-focused system prompt +WEATHER_SYSTEM_PROMPT = """You are a weather assistant with HTTP capabilities. You can: + +1. Make HTTP requests to the National Weather Service API +2. Process and display weather forecast data +3. Provide weather information for locations in the United States + +When retrieving weather information: +1. First get the coordinates or grid information using https://api.weather.gov/points/{latitude},{longitude} or https://api.weather.gov/points/{zipcode} +2. Then use the returned forecast URL to get the actual forecast + +When displaying responses: +- Format weather data in a human-readable way +- Highlight important information like temperature, precipitation, and alerts +- Handle errors appropriately +- Don't ask follow-up questions + +Always explain the weather conditions clearly and provide context for the forecast. + +At the point where tools are done being invoked and a summary can be presented to the user, invoke the ready_to_summarize +tool and then continue with the summary. +""" + +class PromptRequest(BaseModel): + prompt: str + +@app.get('/health') +def health_check(): + """Health check endpoint for the load balancer.""" + return {"status": "healthy"} + +@app.post('/weather') +async def get_weather(request: PromptRequest): + """Endpoint to get weather information.""" + prompt = request.prompt + + if not prompt: + raise HTTPException(status_code=400, detail="No prompt provided") + + try: + weather_agent = Agent( + system_prompt=WEATHER_SYSTEM_PROMPT, + tools=[http_request], + ) + response = weather_agent(prompt) + content = str(response) + return PlainTextResponse(content=content) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +async def run_weather_agent_and_stream_response(prompt: str): + """ + A helper function to yield summary text chunks one by one as they come in, allowing the web server to emit + them to caller live + """ + is_summarizing = False + + @tool + def ready_to_summarize(): + """ + A tool that is intended to be called by the agent right before summarize the response. + """ + nonlocal is_summarizing + is_summarizing = True + return "Ok - continue providing the summary!" + + weather_agent = Agent( + system_prompt=WEATHER_SYSTEM_PROMPT, + tools=[http_request, ready_to_summarize], + callback_handler=None + ) + + async for item in weather_agent.stream_async(prompt): + if not is_summarizing: + continue + if "data" in item: + yield item['data'] + +@app.post('/weather-streaming') +async def get_weather_streaming(request: PromptRequest): + """Endpoint to stream the weather summary as it comes it, not all at once at the end.""" + try: + prompt = request.prompt + + if not prompt: + raise HTTPException(status_code=400, detail="No prompt provided") + + return StreamingResponse( + run_weather_agent_and_stream_response(prompt), + media_type="text/plain" + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +if __name__ == '__main__': + # Get port from environment variable or default to 8000 + port = int(os.environ.get('PORT', 8000)) + uvicorn.run(app, host='0.0.0.0', port=port) diff --git a/docs/examples/cdk/deploy_to_apprunner/docker/requirements.txt b/docs/examples/cdk/deploy_to_apprunner/docker/requirements.txt new file mode 100644 index 00000000..9cf79ab9 --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/docker/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.115.12 +uvicorn==0.34.2 +pydantic==2.11.4 +strands-agents +strands-agents-tools diff --git a/docs/examples/cdk/deploy_to_apprunner/lib/agent-apprunner-stack.ts b/docs/examples/cdk/deploy_to_apprunner/lib/agent-apprunner-stack.ts new file mode 100644 index 00000000..a7c7866d --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/lib/agent-apprunner-stack.ts @@ -0,0 +1,86 @@ +import { Stack, StackProps } from "aws-cdk-lib"; +import { Construct } from "constructs"; +import * as apprunner from "aws-cdk-lib/aws-apprunner"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as ecr_assets from "aws-cdk-lib/aws-ecr-assets"; +import * as path from "path"; + +export class AgentAppRunnerStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + // Create IAM role for App Runner instance + const instanceRole = new iam.Role(this, "AppRunnerInstanceRole", { + assumedBy: new iam.ServicePrincipal("tasks.apprunner.amazonaws.com"), + }); + + // Add Bedrock permissions + instanceRole.addToPolicy( + new iam.PolicyStatement({ + actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], + resources: ["*"], + }) + ); + + // Create IAM role for App Runner to access ECR + const accessRole = new iam.Role(this, "AppRunnerAccessRole", { + assumedBy: new iam.ServicePrincipal("build.apprunner.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + "service-role/AWSAppRunnerServicePolicyForECRAccess" + ), + ], + }); + + // Build Docker image for x86_64 (App Runner requirement) + const dockerAsset = new ecr_assets.DockerImageAsset(this, "AppRunnerImage", { + directory: path.join(__dirname, "../docker"), + platform: ecr_assets.Platform.LINUX_AMD64, // App Runner requires x86_64 + }); + + // Grant App Runner access to pull the image + dockerAsset.repository.grantPull(accessRole); + + // Create App Runner service + const service = new apprunner.CfnService(this, "AgentAppRunnerService", { + serviceName: "agent-service", + sourceConfiguration: { + authenticationConfiguration: { + accessRoleArn: accessRole.roleArn, + }, + imageRepository: { + imageIdentifier: dockerAsset.imageUri, + imageRepositoryType: "ECR", + imageConfiguration: { + port: "8000", + runtimeEnvironmentVariables: [ + { + name: "LOG_LEVEL", + value: "INFO", + }, + ], + }, + }, + }, + instanceConfiguration: { + cpu: "1 vCPU", + memory: "2 GB", + instanceRoleArn: instanceRole.roleArn, + }, + healthCheckConfiguration: { + protocol: "HTTP", + path: "/health", + interval: 10, + timeout: 5, + healthyThreshold: 1, + unhealthyThreshold: 5, + }, + }); + + // Output the service URL + this.exportValue(service.attrServiceUrl, { + name: "AppRunnerServiceUrl", + description: "The URL of the App Runner service", + }); + } +} diff --git a/docs/examples/cdk/deploy_to_apprunner/package-lock.json b/docs/examples/cdk/deploy_to_apprunner/package-lock.json new file mode 100644 index 00000000..c21341b6 --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/package-lock.json @@ -0,0 +1,1198 @@ +{ + "name": "deploy_to_apprunner", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deploy_to_apprunner", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "2.192.0", + "constructs": "^10.2.0" + }, + "bin": { + "cdk-app": "bin/cdk-app.js" + }, + "devDependencies": { + "@types/node": "22.15.3", + "aws-cdk": "2.1012.0", + "prettier": "~3.5.3", + "tsx": "^4.7.0", + "typescript": "~5.8.3", + "vitest": "^3.1.2" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.233", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "41.2.0", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.3", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.1", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.3", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.2", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/aws-cdk": { + "version": "2.1012.0", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.192.0", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.229", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^41.0.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.0", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.1", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/constructs": { + "version": "10.4.2", + "license": "Apache-2.0" + }, + "node_modules/debug": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.3", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.4.4", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/loupe": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.40.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.1", + "@rollup/rollup-android-arm64": "4.40.1", + "@rollup/rollup-darwin-arm64": "4.40.1", + "@rollup/rollup-darwin-x64": "4.40.1", + "@rollup/rollup-freebsd-arm64": "4.40.1", + "@rollup/rollup-freebsd-x64": "4.40.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", + "@rollup/rollup-linux-arm-musleabihf": "4.40.1", + "@rollup/rollup-linux-arm64-gnu": "4.40.1", + "@rollup/rollup-linux-arm64-musl": "4.40.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-musl": "4.40.1", + "@rollup/rollup-linux-s390x-gnu": "4.40.1", + "@rollup/rollup-linux-x64-gnu": "4.40.1", + "@rollup/rollup-linux-x64-musl": "4.40.1", + "@rollup/rollup-win32-arm64-msvc": "4.40.1", + "@rollup/rollup-win32-ia32-msvc": "4.40.1", + "@rollup/rollup-win32-x64-msvc": "4.40.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.19.3", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.1.2", + "@vitest/mocker": "3.1.2", + "@vitest/pretty-format": "^3.1.2", + "@vitest/runner": "3.1.2", + "@vitest/snapshot": "3.1.2", + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.13", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.1.2", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.1.2", + "@vitest/ui": "3.1.2", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/docs/examples/cdk/deploy_to_apprunner/package.json b/docs/examples/cdk/deploy_to_apprunner/package.json new file mode 100644 index 00000000..2ef280a2 --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/package.json @@ -0,0 +1,30 @@ +{ + "name": "deploy_to_apprunner", + "version": "0.1.0", + "description": "CDK TypeScript project to deploy a sample Agent on AWS App Runner", + "private": true, + "bin": { + "cdk-app": "bin/cdk-app.js" + }, + "scripts": { + "format": "prettier --write .", + "watch": "tsc -w", + "test": "vitest run", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "22.15.3", + "aws-cdk": "2.1012.0", + "prettier": "~3.5.3", + "tsx": "^4.7.0", + "typescript": "~5.8.3", + "vitest": "^3.1.2" + }, + "dependencies": { + "aws-cdk-lib": "2.192.0", + "constructs": "^10.2.0" + }, + "prettier": { + "printWidth": 120 + } +} diff --git a/docs/examples/cdk/deploy_to_apprunner/tsconfig.json b/docs/examples/cdk/deploy_to_apprunner/tsconfig.json new file mode 100644 index 00000000..507608a9 --- /dev/null +++ b/docs/examples/cdk/deploy_to_apprunner/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["node_modules", "cdk.out"] +} diff --git a/docs/examples/cdk/deploy_to_ec2/README.md b/docs/examples/cdk/deploy_to_ec2/README.md index 6ca00238..4709079e 100644 --- a/docs/examples/cdk/deploy_to_ec2/README.md +++ b/docs/examples/cdk/deploy_to_ec2/README.md @@ -120,7 +120,7 @@ Note that this example demonstrates a simple deployment approach with some impor - The application code is deployed only during the initial instance creation via user data script - Updating the application requires implementing a custom update mechanism - The example exposes the application directly on port 8000 without a load balancer -- For production workloads, consider using ECS/Fargate which provides built-in support for application updates, scaling, and high availability +- For production workloads, consider using ECS/Fargate/App Runner which provides built-in support for application updates, scaling, and high availability ## Additional Resources diff --git a/docs/user-guide/deploy/deploy_to_aws_apprunner.md b/docs/user-guide/deploy/deploy_to_aws_apprunner.md new file mode 100644 index 00000000..f26b871f --- /dev/null +++ b/docs/user-guide/deploy/deploy_to_aws_apprunner.md @@ -0,0 +1,300 @@ +# Deploying Strands Agents SDK Agents to AWS App Runner + +AWS App Runner is the easiest way to deploy web applications on AWS, including API services, backend web services, and websites. App Runner eliminates the need for infrastructure management or container orchestration by providing a fully managed platform with automatic integration and delivery pipelines, high performance, scalability, and security. + +AWS App Runner automatically deploys containerized applications with secure HTTPS endpoints while handling infrastructure provisioning, auto-scaling, and TLS certificate management. This makes App Runner an excellent choice for deploying Strands Agents SDK agents as highly available and scalable containerized applications. + +If you're not familiar with the AWS CDK, check out the [official documentation](https://docs.aws.amazon.com/cdk/v2/guide/home.html). + +This guide discusses AWS App Runner integration at a high level - for a complete example project deploying to App Runner, check out the [`deploy_to_apprunner` sample project on GitHub][project_code]. + +## Creating Your Agent in Python + +The core of your App Runner deployment is a containerized FastAPI application that hosts your Strands Agents SDK agent. This Python application initializes your agent and processes incoming HTTP requests. + +The FastAPI application follows these steps: + +1. Define endpoints for agent interactions +2. Create a Strands Agents SDK agent with the specified system prompt and tools +3. Process incoming requests through the agent +4. Return the response back to the client + +Here's an example of a weather forecasting agent application ([`app.py`][app_py]): + +```python +app = FastAPI(title="Weather API") + +# Define a weather-focused system prompt +WEATHER_SYSTEM_PROMPT = """You are a weather assistant with HTTP capabilities. You can: + +1. Make HTTP requests to the National Weather Service API +2. Process and display weather forecast data +3. Provide weather information for locations in the United States + +When retrieving weather information: +1. First get the coordinates or grid information using https://api.weather.gov/points/{latitude},{longitude} or https://api.weather.gov/points/{zipcode} +2. Then use the returned forecast URL to get the actual forecast + +When displaying responses: +- Format weather data in a human-readable way +- Highlight important information like temperature, precipitation, and alerts +- Handle errors appropriately +- Don't ask follow-up questions + +Always explain the weather conditions clearly and provide context for the forecast. + +At the point where tools are done being invoked and a summary can be presented to the user, invoke the ready_to_summarize +tool and then continue with the summary. +""" + +class PromptRequest(BaseModel): + prompt: str + +@app.post('/weather') +async def get_weather(request: PromptRequest): + """Endpoint to get weather information.""" + prompt = request.prompt + + if not prompt: + raise HTTPException(status_code=400, detail="No prompt provided") + + try: + weather_agent = Agent( + system_prompt=WEATHER_SYSTEM_PROMPT, + tools=[http_request], + ) + response = weather_agent(prompt) + content = str(response) + return PlainTextResponse(content=content) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) +``` + +### Streaming responses + +Streaming responses can significantly improve the user experience by providing real-time responses back to the customer. This is especially valuable for longer responses. + +Python web-servers commonly implement streaming through the use of iterators, and the Strands Agents SDK facilitates response streaming via the `stream_async(prompt)` function: + +```python +async def run_weather_agent_and_stream_response(prompt: str): + is_summarizing = False + + @tool + def ready_to_summarize(): + nonlocal is_summarizing + is_summarizing = True + return "Ok - continue providing the summary!" + + weather_agent = Agent( + system_prompt=WEATHER_SYSTEM_PROMPT, + tools=[http_request, ready_to_summarize], + callback_handler=None + ) + + async for item in weather_agent.stream_async(prompt): + if not is_summarizing: + continue + if "data" in item: + yield item['data'] + +@app.route('/weather-streaming', methods=['POST']) +async def get_weather_streaming(request: PromptRequest): + try: + prompt = request.prompt + + if not prompt: + raise HTTPException(status_code=400, detail="No prompt provided") + + return StreamingResponse( + run_weather_agent_and_stream_response(prompt), + media_type="text/plain" + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) +``` + +The implementation above employs a [custom tool](../concepts/tools/python-tools.md#python-tool-decorators) to mark the boundary between information gathering and summary generation phases. This approach ensures that only the final, user-facing content is streamed to the client, maintaining consistency with the non-streaming endpoint while providing the benefits of incremental response delivery. + +## Containerization + +To deploy your agent to App Runner, you need to containerize it using Podman or Docker. The Dockerfile defines how your application is packaged and run. Below is an example Docker file that installs all needed dependencies, the application, and configures the FastAPI server to run via unicorn ([Dockerfile][dockerfile]): + +```dockerfile +FROM public.ecr.aws/docker/library/python:3.12-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY app/ . + +# Create a non-root user to run the application +RUN useradd -m appuser +USER appuser + +# Expose the port the app runs on +EXPOSE 8000 + +# Command to run the application with Uvicorn +# - port: 8000 +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"] +``` + +## Infrastructure + +To deploy the containerized agent to App Runner using the TypeScript CDK, you need to define the infrastructure stack ([agent-apprunner-stack.ts][agent_apprunner_stack]). Much of the configuration follows standard App Runner deployment patterns, but the following code snippet highlights the key components specific to deploying Strands Agents SDK agents: + +```typescript +// Create IAM role for App Runner instance +const instanceRole = new iam.Role(this, "AppRunnerInstanceRole", { + assumedBy: new iam.ServicePrincipal("tasks.apprunner.amazonaws.com"), +}); + +// Add Bedrock permissions +instanceRole.addToPolicy( + new iam.PolicyStatement({ + actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], + resources: ["*"], + }) +); + +// Create IAM role for App Runner to access ECR +const accessRole = new iam.Role(this, "AppRunnerAccessRole", { + assumedBy: new iam.ServicePrincipal("build.apprunner.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + "service-role/AWSAppRunnerServicePolicyForECRAccess" + ), + ], +}); + +// Build Docker image for x86_64 (App Runner requirement) +const dockerAsset = new ecr_assets.DockerImageAsset(this, "AppRunnerImage", { + directory: path.join(__dirname, "../docker"), + platform: ecr_assets.Platform.LINUX_AMD64, // App Runner requires x86_64 +}); + +// Grant App Runner access to pull the image +dockerAsset.repository.grantPull(accessRole); + +// Create App Runner service +const service = new apprunner.CfnService(this, "AgentAppRunnerService", { + serviceName: "agent-service", + sourceConfiguration: { + authenticationConfiguration: { + accessRoleArn: accessRole.roleArn, + }, + imageRepository: { + imageIdentifier: dockerAsset.imageUri, + imageRepositoryType: "ECR", + imageConfiguration: { + port: "8000", + runtimeEnvironmentVariables: [ + { + name: "LOG_LEVEL", + value: "INFO", + }, + ], + }, + }, + }, + instanceConfiguration: { + cpu: "1 vCPU", + memory: "2 GB", + instanceRoleArn: instanceRole.roleArn, + }, + healthCheckConfiguration: { + protocol: "HTTP", + path: "/health", + interval: 10, + timeout: 5, + healthyThreshold: 1, + unhealthyThreshold: 5, + }, +}); + +// Output the service URL +this.exportValue(service.attrServiceUrl, { + name: "AppRunnerServiceUrl", + description: "The URL of the App Runner service", +}); +``` + +The full example ([agent-apprunner-stack.ts][agent_apprunner_stack]): + +1. Creates an instance role with permissions to invoke Bedrock APIs +2. Creates an access role for App Runner to pull images from ECR +3. Builds a Docker image for x86_64 architecture (App Runner requirement) +4. Configures the App Runner service with container settings (port 8000, environment variables) +5. Sets up instance configuration with 1 vCPU and 2 GB memory +6. Configures health checks to monitor service availability +7. Outputs the secure HTTPS service URL for accessing your application + +## Deploying Your Agent & Testing + +Assuming that Python & Node dependencies are already installed, run the CDK and deploy which will also run the Docker file for deployment: + +```bash +# Bootstrap your AWS environment (if not already done) +npx cdk bootstrap + +# Ensure Docker or Podman is running +podman machine start + +# Deploy the stack +CDK_DOCKER=podman npx cdk deploy +``` + +Once deployed, you can test your agent using the Application Load Balancer URL: + +```bash +# Get the service URL from the CDK output +SERVICE_URL=$(aws cloudformation describe-stacks --stack-name AgentAppRunnerStack --query "Stacks[0].Outputs[?ExportName=='AppRunnerServiceUrl'].OutputValue" --output text) + +# Call the weather service +curl -X POST \ + https://$SERVICE_URL/weather \ + -H 'Content-Type: application/json' \ + -d '{"prompt": "What is the weather in New York?"}' + +# Call the streaming endpoint +curl -X POST \ + https://$SERVICE_URL/weather-streaming \ + -H 'Content-Type: application/json' \ + -d '{"prompt": "What is the weather in New York in Celsius?"}' +``` +## Summary + +The above steps covered: + +- Creating a FastAPI application that hosts your Strands Agents SDK agent +- Containerizing your application with Podman +- Creating the CDK infrastructure to deploy to App Runner +- Deploying the agent and infrastructure to an AWS account +- Manually testing the deployed service + +## Complete Example + +For the complete example code, including all files and configurations, see the [`deploy_to_apprunner` sample project on GitHub][project_code]. + +## Related Resources + +- [AWS App Runner Documentation](https://docs.aws.amazon.com/apprunner/latest/dg/what-is-apprunner.html) +- [AWS CDK Documentation](https://docs.aws.amazon.com/cdk/v2/guide/home.html) +- [Podman Documentation](https://docs.podman.io/en/latest/) +- [FastAPI Documentation](https://fastapi.tiangolo.com/) + +[project_code]: {{ docs_repo }}/docs/examples/cdk/deploy_to_apprunner +[app_py]: {{ docs_repo }}/docs/examples/cdk/deploy_to_apprunner/docker/app/app.py +[dockerfile]: {{ docs_repo }}/docs/examples/cdk/deploy_to_apprunner/docker/Dockerfile +[agent_apprunner_stack]: {{ docs_repo }}/docs/examples/cdk/deploy_to_apprunner/lib/agent-apprunner-stack.ts + diff --git a/docs/user-guide/deploy/deploy_to_aws_lambda.md b/docs/user-guide/deploy/deploy_to_aws_lambda.md index 9cf814a3..4cb4e4af 100644 --- a/docs/user-guide/deploy/deploy_to_aws_lambda.md +++ b/docs/user-guide/deploy/deploy_to_aws_lambda.md @@ -8,7 +8,7 @@ This guide discusses Lambda integration at a high level - for a complete example !!! note - This Lambda deployment example does not implement response streaming as described in the [Async Iterators for Streaming](../concepts/streaming/async-iterators.md) documentation. If you need streaming capabilities, consider using the [AWS Fargate deployment](deploy_to_aws_fargate.md) approach which does implement streaming responses. + This Lambda deployment example does not implement response streaming as described in the [Async Iterators for Streaming](../concepts/streaming/async-iterators.md) documentation. If you need streaming capabilities, consider using the [AWS Fargate deployment](deploy_to_aws_fargate.md) or [AWS App Runner deployment](deploy_to_aws_apprunner) approach which does implement streaming responses. ## Creating Your Agent in Python diff --git a/docs/user-guide/deploy/operating-agents-in-production.md b/docs/user-guide/deploy/operating-agents-in-production.md index 97e95e06..f51e7c4c 100644 --- a/docs/user-guide/deploy/operating-agents-in-production.md +++ b/docs/user-guide/deploy/operating-agents-in-production.md @@ -125,6 +125,8 @@ Built-in guides are available for several AWS services: * **AWS Fargate** - Containerized deployment with streaming support, ideal for interactive applications requiring real-time responses or high concurrency. [Learn more](deploy_to_aws_fargate.md) +* **AWS App Runner** - Containerized deployment with streaming support, automated deployment, scaling, and load balancing, ideal for interactive applications requiring real-time responses or high concurrency. [Learn more](deploy_to_aws_apprunner.md) + * **Amazon EKS** - Containerized deployment with streaming support, ideal for interactive applications requiring real-time responses or high concurrency. [Learn more](deploy_to_amazon_eks.md) * **Amazon EC2** - Maximum control and flexibility for high-volume applications or specialized infrastructure requirements. [Learn more](deploy_to_amazon_ec2.md) diff --git a/mkdocs.yml b/mkdocs.yml index 14518565..e57a6a3a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -134,6 +134,7 @@ nav: - Operating Agents in Production: user-guide/deploy/operating-agents-in-production.md - AWS Lambda: user-guide/deploy/deploy_to_aws_lambda.md - AWS Fargate: user-guide/deploy/deploy_to_aws_fargate.md + - AWS App Runner: user-guide/deploy/deploy_to_aws_apprunner.md - Amazon Bedrock AgentCore : user-guide/deploy/deploy_to_bedrock_agentcore.md - Amazon EKS: user-guide/deploy/deploy_to_amazon_eks.md - Amazon EC2: user-guide/deploy/deploy_to_amazon_ec2.md