chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Terraform Infrastructure
|
||||
|
||||
Equivalent of `../infra-cdk/` using Terraform.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cd infra-terraform
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
# Edit terraform.tfvars — set stack_name_base, backend_pattern, aws_region
|
||||
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
After apply, run the frontend deploy from the repo root:
|
||||
|
||||
```bash
|
||||
python3 scripts/deploy-frontend.py
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# S3 Remote Backend Configuration
|
||||
# =============================================================================
|
||||
#
|
||||
# To enable remote state storage:
|
||||
# 1. Create S3 bucket and DynamoDB table (see commands below)
|
||||
# 2. Copy this file: cp backend.tf.example backend.tf
|
||||
# 3. Update bucket/table names below
|
||||
# 4. Run: terraform init -migrate-state
|
||||
#
|
||||
# Prerequisites (run once):
|
||||
# aws s3 mb s3://YOUR-BUCKET-NAME --region us-east-1
|
||||
# aws dynamodb create-table \
|
||||
# --table-name terraform-locks \
|
||||
# --attribute-definitions AttributeName=LockID,AttributeType=S \
|
||||
# --key-schema AttributeName=LockID,KeyType=HASH \
|
||||
# --billing-mode PAY_PER_REQUEST \
|
||||
# --region us-east-1
|
||||
# =============================================================================
|
||||
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "YOUR-TERRAFORM-STATE-BUCKET" # Change this
|
||||
key = "fast/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
dynamodb_table = "terraform-locks"
|
||||
encrypt = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Data Sources
|
||||
# =============================================================================
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
|
||||
# =============================================================================
|
||||
# Local Values
|
||||
# =============================================================================
|
||||
|
||||
locals {
|
||||
# Account and region information
|
||||
account_id = data.aws_caller_identity.current.account_id
|
||||
region = data.aws_region.current.id
|
||||
|
||||
# Common tags applied to all resources via provider default_tags
|
||||
common_tags = {
|
||||
Project = var.stack_name_base
|
||||
ManagedBy = "Terraform"
|
||||
Repository = "fullstack-agentcore-solution-template"
|
||||
}
|
||||
|
||||
# SSM parameter paths
|
||||
ssm_parameter_prefix = "/${var.stack_name_base}"
|
||||
|
||||
# Log retention in days
|
||||
log_retention_days = 7
|
||||
|
||||
# S3 lifecycle rules
|
||||
staging_bucket_expiry_days = 30
|
||||
access_logs_expiry_days = 90
|
||||
|
||||
# API Gateway settings
|
||||
api_throttling_rate_limit = 100
|
||||
api_throttling_burst_limit = 200
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Provider Configuration
|
||||
# =============================================================================
|
||||
|
||||
provider "aws" {
|
||||
default_tags {
|
||||
tags = local.common_tags
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# DEPLOYMENT ORDER:
|
||||
# 1. Amplify Hosting - Creates app and gets predictable URL
|
||||
# 2. Cognito - Uses Amplify URL for callback URLs
|
||||
# 3. Backend Resources (Memory, Gateway, Runtime, Feedback API)
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Module: Amplify Hosting (Frontend)
|
||||
# =============================================================================
|
||||
# Creates:
|
||||
# - S3 bucket for access logs
|
||||
# - S3 bucket for frontend staging
|
||||
# - Amplify App (WEB platform)
|
||||
# - Amplify Branch (main, PRODUCTION)
|
||||
|
||||
module "amplify_hosting" {
|
||||
source = "./modules/amplify-hosting"
|
||||
|
||||
stack_name_base = var.stack_name_base
|
||||
|
||||
staging_bucket_expiry_days = local.staging_bucket_expiry_days
|
||||
access_logs_expiry_days = local.access_logs_expiry_days
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Module: Cognito (Authentication)
|
||||
# =============================================================================
|
||||
# Creates:
|
||||
# - User Pool with password policy and invitation templates
|
||||
# - User Pool Domain with managed login V2 branding
|
||||
# - Web Client (for frontend OAuth)
|
||||
# - Admin User (optional)
|
||||
|
||||
module "cognito" {
|
||||
source = "./modules/cognito"
|
||||
|
||||
stack_name_base = var.stack_name_base
|
||||
admin_user_email = var.admin_user_email
|
||||
|
||||
# Use the predictable Amplify URL from the app_url output
|
||||
amplify_url = module.amplify_hosting.app_url
|
||||
|
||||
depends_on = [module.amplify_hosting]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Module: Backend (AgentCore + Feedback API)
|
||||
# =============================================================================
|
||||
# Creates:
|
||||
# - AgentCore Memory (IAM role + memory resource)
|
||||
# - M2M Authentication (resource server + machine client)
|
||||
# - AgentCore Gateway (Lambda, IAM, gateway, target)
|
||||
# - AgentCore Runtime (ECR, IAM, runtime)
|
||||
# - Feedback API (DynamoDB, Lambda, API Gateway)
|
||||
# - SSM Parameters & Secrets Manager
|
||||
|
||||
module "backend" {
|
||||
source = "./modules/backend"
|
||||
|
||||
stack_name_base = var.stack_name_base
|
||||
backend_pattern = var.backend_pattern
|
||||
backend_deployment_type = var.backend_deployment_type
|
||||
backend_network_mode = var.backend_network_mode
|
||||
|
||||
# VPC configuration (for VPC mode)
|
||||
backend_vpc_id = var.backend_vpc_id
|
||||
backend_vpc_subnet_ids = var.backend_vpc_subnet_ids
|
||||
backend_vpc_security_group_ids = var.backend_vpc_security_group_ids
|
||||
|
||||
# Cognito configuration
|
||||
user_pool_id = module.cognito.user_pool_id
|
||||
user_pool_arn = module.cognito.user_pool_arn
|
||||
web_client_id = module.cognito.web_client_id
|
||||
cognito_domain_url = module.cognito.cognito_domain_url
|
||||
|
||||
# Frontend URL for CORS
|
||||
frontend_url = module.amplify_hosting.app_url
|
||||
|
||||
# Optional overrides
|
||||
log_retention_days = local.log_retention_days
|
||||
throttling_rate_limit = local.api_throttling_rate_limit
|
||||
throttling_burst_limit = local.api_throttling_burst_limit
|
||||
|
||||
depends_on = [module.cognito, module.amplify_hosting]
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Data Sources
|
||||
# =============================================================================
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
|
||||
# =============================================================================
|
||||
# Local Values
|
||||
# =============================================================================
|
||||
|
||||
locals {
|
||||
account_id = data.aws_caller_identity.current.account_id
|
||||
region = data.aws_region.current.id
|
||||
|
||||
app_name = "${var.stack_name_base}-frontend"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# S3 Bucket for Access Logs
|
||||
# =============================================================================
|
||||
|
||||
resource "aws_s3_bucket" "access_logs" {
|
||||
bucket_prefix = "${lower(var.stack_name_base)}-access-logs-"
|
||||
force_destroy = true
|
||||
|
||||
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_public_access_block" "access_logs" {
|
||||
bucket = aws_s3_bucket.access_logs.id
|
||||
|
||||
block_public_acls = true
|
||||
block_public_policy = true
|
||||
ignore_public_acls = true
|
||||
restrict_public_buckets = true
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_lifecycle_configuration" "access_logs" {
|
||||
bucket = aws_s3_bucket.access_logs.id
|
||||
|
||||
rule {
|
||||
id = "DeleteOldAccessLogs"
|
||||
status = "Enabled"
|
||||
|
||||
expiration {
|
||||
days = var.access_logs_expiry_days
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# S3 Bucket for Staging (Amplify Deployments)
|
||||
# =============================================================================
|
||||
|
||||
resource "aws_s3_bucket" "staging" {
|
||||
bucket_prefix = "${lower(var.stack_name_base)}-staging-"
|
||||
force_destroy = true
|
||||
|
||||
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "staging" {
|
||||
bucket = aws_s3_bucket.staging.id
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_public_access_block" "staging" {
|
||||
bucket = aws_s3_bucket.staging.id
|
||||
|
||||
block_public_acls = true
|
||||
block_public_policy = true
|
||||
ignore_public_acls = true
|
||||
restrict_public_buckets = true
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_lifecycle_configuration" "staging" {
|
||||
bucket = aws_s3_bucket.staging.id
|
||||
|
||||
rule {
|
||||
id = "DeleteOldDeployments"
|
||||
status = "Enabled"
|
||||
|
||||
expiration {
|
||||
days = var.staging_bucket_expiry_days
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_logging" "staging" {
|
||||
bucket = aws_s3_bucket.staging.id
|
||||
|
||||
target_bucket = aws_s3_bucket.access_logs.id
|
||||
target_prefix = "staging-bucket-access-logs/"
|
||||
}
|
||||
|
||||
# Bucket policy: Allow Amplify service access
|
||||
resource "aws_s3_bucket_policy" "staging" {
|
||||
bucket = aws_s3_bucket.staging.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Sid = "AmplifyAccess"
|
||||
Effect = "Allow"
|
||||
Principal = {
|
||||
Service = "amplify.amazonaws.com"
|
||||
}
|
||||
Action = [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectVersion"
|
||||
]
|
||||
Resource = "${aws_s3_bucket.staging.arn}/*"
|
||||
},
|
||||
{
|
||||
Sid = "DenyInsecureConnections"
|
||||
Effect = "Deny"
|
||||
Principal = "*"
|
||||
Action = "s3:*"
|
||||
Resource = [
|
||||
aws_s3_bucket.staging.arn,
|
||||
"${aws_s3_bucket.staging.arn}/*"
|
||||
]
|
||||
Condition = {
|
||||
Bool = {
|
||||
"aws:SecureTransport" = "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Amplify App
|
||||
# =============================================================================
|
||||
# Note: This creates a manual deployment app (no Git integration)
|
||||
# Frontend deployments are handled via the deploy-frontend.py script
|
||||
# Environment variables are set at deployment time, not at app creation
|
||||
|
||||
resource "aws_amplify_app" "frontend" {
|
||||
name = local.app_name
|
||||
platform = var.platform
|
||||
description = "${var.stack_name_base} - React/Next.js Frontend"
|
||||
|
||||
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Amplify Branch (main)
|
||||
# =============================================================================
|
||||
|
||||
resource "aws_amplify_branch" "main" {
|
||||
app_id = aws_amplify_app.frontend.id
|
||||
branch_name = "main"
|
||||
stage = "PRODUCTION"
|
||||
|
||||
description = "Main production branch"
|
||||
|
||||
# Enable auto-build on push (if using Git integration)
|
||||
enable_auto_build = false
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
output "app_id" {
|
||||
description = "Amplify App ID"
|
||||
value = aws_amplify_app.frontend.id
|
||||
}
|
||||
|
||||
output "app_arn" {
|
||||
description = "Amplify App ARN"
|
||||
value = aws_amplify_app.frontend.arn
|
||||
}
|
||||
|
||||
output "default_domain" {
|
||||
description = "Amplify default domain"
|
||||
value = aws_amplify_app.frontend.default_domain
|
||||
}
|
||||
|
||||
output "app_url" {
|
||||
description = "Full Amplify app URL (main branch) - predictable format"
|
||||
value = "https://main.${aws_amplify_app.frontend.id}.amplifyapp.com"
|
||||
}
|
||||
|
||||
output "branch_name" {
|
||||
description = "Main branch name"
|
||||
value = aws_amplify_branch.main.branch_name
|
||||
}
|
||||
|
||||
output "staging_bucket_name" {
|
||||
description = "S3 staging bucket name for deployments"
|
||||
value = aws_s3_bucket.staging.bucket
|
||||
}
|
||||
|
||||
output "staging_bucket_arn" {
|
||||
description = "S3 staging bucket ARN"
|
||||
value = aws_s3_bucket.staging.arn
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
variable "stack_name_base" {
|
||||
description = "Base name for all resources."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "platform" {
|
||||
description = "Platform type for Amplify app (WEB or WEB_COMPUTE)."
|
||||
type = string
|
||||
default = "WEB"
|
||||
}
|
||||
|
||||
variable "staging_bucket_expiry_days" {
|
||||
description = "Number of days before staging bucket objects expire."
|
||||
type = number
|
||||
default = 30
|
||||
}
|
||||
|
||||
variable "access_logs_expiry_days" {
|
||||
description = "Number of days before access log objects expire."
|
||||
type = number
|
||||
default = 90
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.82.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Machine-to-Machine (M2M) Authentication
|
||||
# Maps to: backend-stack.ts createMachineAuthentication()
|
||||
# =============================================================================
|
||||
|
||||
# Resource Server for M2M Authentication
|
||||
# Defines API scopes that machine clients can request access to
|
||||
|
||||
resource "aws_cognito_resource_server" "gateway" {
|
||||
identifier = "${var.stack_name_base}-gateway"
|
||||
name = "${var.stack_name_base}-gateway-resource-server"
|
||||
user_pool_id = var.user_pool_id
|
||||
|
||||
scope {
|
||||
scope_name = "read"
|
||||
scope_description = "Read access to gateway"
|
||||
}
|
||||
|
||||
scope {
|
||||
scope_name = "write"
|
||||
scope_description = "Write access to gateway"
|
||||
}
|
||||
}
|
||||
|
||||
# Machine Client for AgentCore Gateway authentication
|
||||
# Uses OAuth2 Client Credentials flow for service-to-service auth
|
||||
|
||||
resource "aws_cognito_user_pool_client" "machine" {
|
||||
name = "${var.stack_name_base}-machine-client"
|
||||
user_pool_id = var.user_pool_id
|
||||
|
||||
# Secret required for client credentials flow
|
||||
generate_secret = true
|
||||
|
||||
# OAuth configuration for M2M
|
||||
allowed_oauth_flows = ["client_credentials"]
|
||||
allowed_oauth_flows_user_pool_client = true
|
||||
|
||||
# Resource server scopes
|
||||
allowed_oauth_scopes = [
|
||||
"${aws_cognito_resource_server.gateway.identifier}/read",
|
||||
"${aws_cognito_resource_server.gateway.identifier}/write"
|
||||
]
|
||||
|
||||
# Supported identity providers
|
||||
supported_identity_providers = ["COGNITO"]
|
||||
|
||||
# Token validity for M2M
|
||||
access_token_validity = 1
|
||||
|
||||
token_validity_units {
|
||||
access_token = "hours"
|
||||
}
|
||||
|
||||
depends_on = [aws_cognito_resource_server.gateway]
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
resource "aws_cloudwatch_log_group" "copilotkit_runtime" {
|
||||
name = "/aws/lambda/${var.stack_name_base}-copilotkit-runtime"
|
||||
retention_in_days = local.log_retention_days
|
||||
}
|
||||
|
||||
data "aws_ssm_parameter" "langgraph_runtime_arn" {
|
||||
name = "/langgraph-stack/runtime-arn"
|
||||
}
|
||||
|
||||
data "aws_ssm_parameter" "strands_runtime_arn" {
|
||||
name = "/strands-stack/runtime-arn"
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "copilotkit_runtime_assume_role" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["lambda.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "copilotkit_runtime" {
|
||||
name = "${var.stack_name_base}-copilotkit-runtime-role"
|
||||
assume_role_policy = data.aws_iam_policy_document.copilotkit_runtime_assume_role.json
|
||||
description = "Execution role for CopilotKit runtime Lambda"
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "copilotkit_runtime_policy" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
]
|
||||
resources = ["${aws_cloudwatch_log_group.copilotkit_runtime.arn}:*"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "copilotkit_runtime" {
|
||||
name = "${var.stack_name_base}-copilotkit-runtime-policy"
|
||||
role = aws_iam_role.copilotkit_runtime.id
|
||||
policy = data.aws_iam_policy_document.copilotkit_runtime_policy.json
|
||||
}
|
||||
|
||||
resource "null_resource" "copilotkit_runtime_build" {
|
||||
triggers = {
|
||||
source_hash = sha256(join("", [
|
||||
for f in fileset(local.copilotkit_runtime_source_path, "**") :
|
||||
filesha256("${local.copilotkit_runtime_source_path}/${f}")
|
||||
if !endswith(f, "/")
|
||||
]))
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
command = <<-EOT
|
||||
set -e
|
||||
BUILD_DIR="${path.module}/artifacts/copilotkit_runtime_build"
|
||||
rm -rf "$BUILD_DIR"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
cp -R ${local.copilotkit_runtime_source_path}/src "$BUILD_DIR/"
|
||||
cp ${local.copilotkit_runtime_source_path}/package.json "$BUILD_DIR/"
|
||||
cp ${local.copilotkit_runtime_source_path}/package-lock.json "$BUILD_DIR/"
|
||||
cp ${local.copilotkit_runtime_source_path}/tsconfig.json "$BUILD_DIR/"
|
||||
cd "$BUILD_DIR"
|
||||
npm ci --no-audit --no-fund
|
||||
npm run build
|
||||
npm prune --omit=dev
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
data "archive_file" "copilotkit_runtime" {
|
||||
type = "zip"
|
||||
source_dir = "${path.module}/artifacts/copilotkit_runtime_build"
|
||||
output_path = "${path.module}/artifacts/copilotkit_runtime.zip"
|
||||
excludes = ["__pycache__", "*.pyc"]
|
||||
|
||||
depends_on = [null_resource.copilotkit_runtime_build]
|
||||
}
|
||||
|
||||
resource "aws_lambda_function" "copilotkit_runtime" {
|
||||
function_name = "${var.stack_name_base}-copilotkit-runtime"
|
||||
role = aws_iam_role.copilotkit_runtime.arn
|
||||
handler = "dist/index.handler"
|
||||
runtime = "nodejs20.x"
|
||||
architectures = ["arm64"]
|
||||
timeout = 30
|
||||
memory_size = 1024
|
||||
|
||||
filename = data.archive_file.copilotkit_runtime.output_path
|
||||
source_code_hash = data.archive_file.copilotkit_runtime.output_base64sha256
|
||||
|
||||
environment {
|
||||
variables = {
|
||||
AGENTCORE_AG_UI_URL = "https://bedrock-agentcore.${local.region}.amazonaws.com/runtimes/${urlencode(var.backend_pattern == "strands-single-agent" ? data.aws_ssm_parameter.strands_runtime_arn.value : data.aws_ssm_parameter.langgraph_runtime_arn.value)}/invocations"
|
||||
COPILOTKIT_AGENT_NAME = var.backend_pattern
|
||||
LANGGRAPH_AGENTCORE_AG_UI_URL = "https://bedrock-agentcore.${local.region}.amazonaws.com/runtimes/${urlencode(data.aws_ssm_parameter.langgraph_runtime_arn.value)}/invocations"
|
||||
STRANDS_AGENTCORE_AG_UI_URL = "https://bedrock-agentcore.${local.region}.amazonaws.com/runtimes/${urlencode(data.aws_ssm_parameter.strands_runtime_arn.value)}/invocations"
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
aws_cloudwatch_log_group.copilotkit_runtime,
|
||||
aws_iam_role_policy.copilotkit_runtime
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_api" "copilotkit_runtime" {
|
||||
name = "${var.stack_name_base}-copilotkit-runtime-api"
|
||||
protocol_type = "HTTP"
|
||||
|
||||
cors_configuration {
|
||||
allow_headers = ["content-type", "authorization"]
|
||||
allow_methods = ["GET", "OPTIONS", "POST"]
|
||||
allow_origins = [var.frontend_url, "http://localhost:3000"]
|
||||
max_age = 300
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_integration" "copilotkit_runtime" {
|
||||
api_id = aws_apigatewayv2_api.copilotkit_runtime.id
|
||||
integration_type = "AWS_PROXY"
|
||||
integration_uri = aws_lambda_function.copilotkit_runtime.invoke_arn
|
||||
integration_method = "POST"
|
||||
payload_format_version = "2.0"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "copilotkit_runtime_root_get" {
|
||||
api_id = aws_apigatewayv2_api.copilotkit_runtime.id
|
||||
route_key = "GET /copilotkit"
|
||||
target = "integrations/${aws_apigatewayv2_integration.copilotkit_runtime.id}"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "copilotkit_runtime_root_post" {
|
||||
api_id = aws_apigatewayv2_api.copilotkit_runtime.id
|
||||
route_key = "POST /copilotkit"
|
||||
target = "integrations/${aws_apigatewayv2_integration.copilotkit_runtime.id}"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "copilotkit_runtime_proxy_get" {
|
||||
api_id = aws_apigatewayv2_api.copilotkit_runtime.id
|
||||
route_key = "GET /copilotkit/{proxy+}"
|
||||
target = "integrations/${aws_apigatewayv2_integration.copilotkit_runtime.id}"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "copilotkit_runtime_proxy_post" {
|
||||
api_id = aws_apigatewayv2_api.copilotkit_runtime.id
|
||||
route_key = "POST /copilotkit/{proxy+}"
|
||||
target = "integrations/${aws_apigatewayv2_integration.copilotkit_runtime.id}"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_stage" "copilotkit_runtime" {
|
||||
api_id = aws_apigatewayv2_api.copilotkit_runtime.id
|
||||
name = "prod"
|
||||
auto_deploy = true
|
||||
}
|
||||
|
||||
resource "aws_lambda_permission" "copilotkit_runtime" {
|
||||
statement_id = "AllowCopilotKitRuntimeInvoke"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.copilotkit_runtime.function_name
|
||||
principal = "apigateway.amazonaws.com"
|
||||
source_arn = "${aws_apigatewayv2_api.copilotkit_runtime.execution_arn}/*/*"
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# AgentCore Gateway
|
||||
# Maps to: backend-stack.ts createAgentCoreGateway()
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IAM Role for Gateway
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
data "aws_iam_policy_document" "gateway_assume_role" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["bedrock-agentcore.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "gateway" {
|
||||
name = "${var.stack_name_base}-gateway-role"
|
||||
assume_role_policy = data.aws_iam_policy_document.gateway_assume_role.json
|
||||
description = "Role for AgentCore Gateway"
|
||||
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "gateway_policy" {
|
||||
# Bedrock permissions (region-agnostic)
|
||||
statement {
|
||||
sid = "BedrockInvoke"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"bedrock:InvokeModel",
|
||||
"bedrock:InvokeModelWithResponseStream"
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:bedrock:*::foundation-model/*",
|
||||
"arn:aws:bedrock:*:${local.account_id}:inference-profile/*"
|
||||
]
|
||||
}
|
||||
|
||||
# SSM parameter access
|
||||
statement {
|
||||
sid = "SSMAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ssm:GetParameter",
|
||||
"ssm:GetParameters"
|
||||
]
|
||||
resources = ["arn:aws:ssm:${local.region}:${local.account_id}:parameter/${var.stack_name_base}/*"]
|
||||
}
|
||||
|
||||
# Cognito permissions
|
||||
statement {
|
||||
sid = "CognitoAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"cognito-idp:DescribeUserPoolClient",
|
||||
"cognito-idp:InitiateAuth"
|
||||
]
|
||||
resources = [var.user_pool_arn]
|
||||
}
|
||||
|
||||
# CloudWatch Logs
|
||||
statement {
|
||||
sid = "CloudWatchLogs"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"logs:CreateLogGroup",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
]
|
||||
resources = ["arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/bedrock-agentcore/*"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "gateway" {
|
||||
name = "${var.stack_name_base}-gateway-policy"
|
||||
role = aws_iam_role.gateway.id
|
||||
policy = data.aws_iam_policy_document.gateway_policy.json
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Wait for IAM permission propagation
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "time_sleep" "gateway_iam_propagation" {
|
||||
create_duration = "10s"
|
||||
|
||||
depends_on = [aws_iam_role_policy.gateway]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# AgentCore Gateway
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "aws_bedrockagentcore_gateway" "main" {
|
||||
name = "${var.stack_name_base}-gateway"
|
||||
role_arn = aws_iam_role.gateway.arn
|
||||
description = "AgentCore Gateway with MCP protocol and JWT authentication"
|
||||
|
||||
protocol_type = "MCP"
|
||||
protocol_configuration {
|
||||
mcp {
|
||||
supported_versions = ["2025-03-26"]
|
||||
}
|
||||
}
|
||||
|
||||
authorizer_type = "CUSTOM_JWT"
|
||||
authorizer_configuration {
|
||||
custom_jwt_authorizer {
|
||||
discovery_url = local.oidc_discovery_url
|
||||
allowed_clients = [aws_cognito_user_pool_client.machine.id]
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [time_sleep.gateway_iam_propagation]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Data Sources (shared across all backend resources)
|
||||
# =============================================================================
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
|
||||
# =============================================================================
|
||||
# Local Values
|
||||
# =============================================================================
|
||||
|
||||
locals {
|
||||
account_id = data.aws_caller_identity.current.account_id
|
||||
region = data.aws_region.current.id
|
||||
|
||||
# Normalized stack name (lowercase, hyphens only)
|
||||
stack_name_normalized = lower(replace(var.stack_name_base, "_", "-"))
|
||||
|
||||
# Stack name for resource naming (underscores for some AWS resources)
|
||||
stack_name_underscore = replace(var.stack_name_base, "-", "_")
|
||||
|
||||
# Agent name used in runtime naming
|
||||
agent_name = "FASTAgent"
|
||||
|
||||
# Runtime name (underscores required by AgentCore)
|
||||
runtime_name = "${local.stack_name_underscore}_${local.agent_name}"
|
||||
|
||||
# Memory name (unique within account/region)
|
||||
# Must match ^[a-zA-Z][a-zA-Z0-9_]{0,47}$ - no hyphens allowed
|
||||
memory_name = "${local.stack_name_underscore}_memory"
|
||||
|
||||
# OIDC discovery URL for Cognito JWT authorizer
|
||||
oidc_discovery_url = "https://cognito-idp.${local.region}.amazonaws.com/${var.user_pool_id}/.well-known/openid-configuration"
|
||||
|
||||
# Lambda Powertools layer ARN (region-specific, Python 3.13, ARM64)
|
||||
powertools_layer_arn = "arn:aws:lambda:${local.region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-arm64:18"
|
||||
|
||||
# Deployment type flags
|
||||
is_docker = var.backend_deployment_type == "docker"
|
||||
is_zip = var.backend_deployment_type == "zip"
|
||||
|
||||
# Pattern flags
|
||||
is_claude_agent_sdk = contains(["claude-agent-sdk-single-agent", "claude-agent-sdk-multi-agent"], var.backend_pattern)
|
||||
|
||||
# Project paths (for zip packaging)
|
||||
project_root = "${path.module}/../../.."
|
||||
pattern_dir = "${local.project_root}/patterns/${var.backend_pattern}"
|
||||
|
||||
# Zip deployment configuration
|
||||
zip_entry_point = ["opentelemetry-instrument", "basic_agent.py"]
|
||||
zip_packager_lambda_source_path = "${path.module}/../../lambdas/zip-packager"
|
||||
|
||||
# Lambda source paths
|
||||
feedback_lambda_source_path = "${path.module}/../../../infra-cdk/lambdas/feedback"
|
||||
copilotkit_runtime_source_path = "${path.module}/../../../infra-cdk/lambdas/copilotkit-runtime"
|
||||
|
||||
# SSM parameter paths
|
||||
ssm_parameter_prefix = "/${var.stack_name_base}"
|
||||
|
||||
# Log retention in days
|
||||
log_retention_days = var.log_retention_days
|
||||
|
||||
# API Gateway settings
|
||||
api_throttling_rate_limit = var.throttling_rate_limit
|
||||
api_throttling_burst_limit = var.throttling_burst_limit
|
||||
api_cache_ttl_seconds = 300
|
||||
|
||||
# Memory event expiry (hardcoded in CDK backend-stack.ts)
|
||||
memory_event_expiry_days = 30
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# AgentCore Memory
|
||||
# Maps to: backend-stack.ts createAgentCoreRuntime() - memory section
|
||||
# =============================================================================
|
||||
|
||||
# IAM Role for Memory Execution
|
||||
# Role assumed by AgentCore Memory service for processing operations
|
||||
|
||||
data "aws_iam_policy_document" "memory_assume_role" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["bedrock-agentcore.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "memory_execution" {
|
||||
name = "${var.stack_name_base}-memory-execution-role"
|
||||
assume_role_policy = data.aws_iam_policy_document.memory_assume_role.json
|
||||
description = "Execution role for AgentCore Memory"
|
||||
}
|
||||
|
||||
# Attach the AWS managed policy for Bedrock model inference
|
||||
# Required for long-term memory strategies that use model processing
|
||||
resource "aws_iam_role_policy_attachment" "memory_bedrock_policy" {
|
||||
role = aws_iam_role.memory_execution.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/AmazonBedrockAgentCoreMemoryBedrockModelInferenceExecutionRolePolicy"
|
||||
}
|
||||
|
||||
# Persistent memory resource for AI agent interactions
|
||||
# Configured with short-term memory (conversation history) as default
|
||||
resource "aws_bedrockagentcore_memory" "main" {
|
||||
name = local.memory_name
|
||||
event_expiry_duration = local.memory_event_expiry_days
|
||||
description = "Short-term memory for ${var.stack_name_base} agent"
|
||||
|
||||
# Memory execution role for model processing (required for long-term strategies)
|
||||
memory_execution_role_arn = aws_iam_role.memory_execution.arn
|
||||
|
||||
tags = {
|
||||
Name = "${var.stack_name_base}_Memory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# OAuth2 Credential Provider
|
||||
# Maps to: backend-stack.ts createOAuth2CredentialProvider()
|
||||
# =============================================================================
|
||||
# Creates Lambda function that manages OAuth2 Credential Provider lifecycle
|
||||
# for AgentCore Runtime to authenticate with AgentCore Gateway.
|
||||
# Uses CloudFormation Custom Resource pattern via null_resource invocation.
|
||||
#
|
||||
# Background:
|
||||
# AgentCore doesn't have a native Terraform/CloudFormation resource for OAuth2
|
||||
# Credential Provider yet. This Lambda calls the bedrock-agentcore-control API
|
||||
# directly to create/update/delete the provider. The Custom Resource pattern
|
||||
# is used to avoid logging sensitive credentials in CloudWatch (client secret
|
||||
# is read from Secrets Manager at runtime).
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# CloudWatch Log Group for OAuth2 Provider Lambda
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "aws_cloudwatch_log_group" "oauth2_provider" {
|
||||
name = "/aws/lambda/${var.stack_name_base}-oauth2-provider"
|
||||
retention_in_days = 7
|
||||
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IAM Role for OAuth2 Provider Lambda
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
data "aws_iam_policy_document" "oauth2_provider_assume_role" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["lambda.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "oauth2_provider" {
|
||||
name = "${var.stack_name_base}-oauth2-provider-role"
|
||||
assume_role_policy = data.aws_iam_policy_document.oauth2_provider_assume_role.json
|
||||
|
||||
}
|
||||
|
||||
# IAM Policy for OAuth2 Provider Lambda
|
||||
data "aws_iam_policy_document" "oauth2_provider_policy" {
|
||||
# CloudWatch Logs
|
||||
statement {
|
||||
sid = "CloudWatchLogsAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
]
|
||||
resources = ["${aws_cloudwatch_log_group.oauth2_provider.arn}:*"]
|
||||
}
|
||||
|
||||
# Read Machine Client Secret
|
||||
# Lambda needs to read the machine client secret to register OAuth2 provider
|
||||
statement {
|
||||
sid = "ReadMachineClientSecret"
|
||||
effect = "Allow"
|
||||
actions = ["secretsmanager:GetSecretValue"]
|
||||
resources = [aws_secretsmanager_secret.machine_client_secret.arn]
|
||||
}
|
||||
|
||||
# OAuth2 Credential Provider Operations
|
||||
# Note: Need both vault-level and nested resource permissions because:
|
||||
# - CreateOauth2CredentialProvider checks permission on vault itself (token-vault/default)
|
||||
# - Also checks permission on the nested resource path (token-vault/default/oauth2credentialprovider/*)
|
||||
statement {
|
||||
sid = "OAuth2CredentialProviderOperations"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"bedrock-agentcore:CreateOauth2CredentialProvider",
|
||||
"bedrock-agentcore:GetOauth2CredentialProvider",
|
||||
"bedrock-agentcore:UpdateOauth2CredentialProvider",
|
||||
"bedrock-agentcore:DeleteOauth2CredentialProvider"
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:token-vault/default",
|
||||
"arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:token-vault/default/oauth2credentialprovider/*"
|
||||
]
|
||||
}
|
||||
|
||||
# Token Vault Operations
|
||||
# Note: Need both exact match (default) and wildcard (default/*) because:
|
||||
# - AWS checks permission on the vault container itself (token-vault/default)
|
||||
# - AWS also checks permission on resources inside (token-vault/default/*)
|
||||
statement {
|
||||
sid = "TokenVaultOperations"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"bedrock-agentcore:CreateTokenVault",
|
||||
"bedrock-agentcore:GetTokenVault",
|
||||
"bedrock-agentcore:DeleteTokenVault"
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:token-vault/default",
|
||||
"arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:token-vault/default/*"
|
||||
]
|
||||
}
|
||||
|
||||
# Token Vault Secret Management
|
||||
# Lambda creates secrets in AgentCore Identity namespace for Token Vault
|
||||
statement {
|
||||
sid = "TokenVaultSecretManagement"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"secretsmanager:CreateSecret",
|
||||
"secretsmanager:DeleteSecret",
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:PutSecretValue"
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:secretsmanager:${local.region}:${local.account_id}:secret:bedrock-agentcore-identity!default/oauth2/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "oauth2_provider" {
|
||||
name = "${var.stack_name_base}-oauth2-provider-policy"
|
||||
role = aws_iam_role.oauth2_provider.id
|
||||
policy = data.aws_iam_policy_document.oauth2_provider_policy.json
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Lambda Function for OAuth2 Provider Lifecycle
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Package the Lambda code
|
||||
data "archive_file" "oauth2_provider" {
|
||||
type = "zip"
|
||||
source_file = "${path.module}/../../../infra-cdk/lambdas/oauth2-provider/index.py"
|
||||
output_path = "${path.module}/artifacts/oauth2-provider.zip"
|
||||
}
|
||||
|
||||
resource "aws_lambda_function" "oauth2_provider" {
|
||||
filename = data.archive_file.oauth2_provider.output_path
|
||||
function_name = "${var.stack_name_base}-oauth2-provider"
|
||||
role = aws_iam_role.oauth2_provider.arn
|
||||
handler = "index.handler"
|
||||
source_code_hash = data.archive_file.oauth2_provider.output_base64sha256
|
||||
runtime = "python3.13"
|
||||
timeout = 300 # 5 minutes
|
||||
|
||||
|
||||
depends_on = [
|
||||
aws_cloudwatch_log_group.oauth2_provider,
|
||||
aws_iam_role_policy.oauth2_provider
|
||||
]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Custom Resource Invocation via null_resource
|
||||
# Simulates CloudFormation Custom Resource by invoking Lambda directly
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "null_resource" "invoke_oauth2_provider" {
|
||||
# Recreate when any of these values change
|
||||
triggers = {
|
||||
provider_name = "${var.stack_name_base}-runtime-gateway-auth"
|
||||
client_id = aws_cognito_user_pool_client.machine.id
|
||||
client_secret = aws_secretsmanager_secret_version.machine_client_secret.version_id
|
||||
discovery_url = local.oidc_discovery_url
|
||||
function_name = aws_lambda_function.oauth2_provider.function_name
|
||||
region = local.region
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
interpreter = ["bash", "-c"]
|
||||
command = <<-EOT
|
||||
set -e
|
||||
|
||||
# Build the CloudFormation Custom Resource payload
|
||||
PAYLOAD=$(cat <<'PAYLOAD_EOF'
|
||||
{
|
||||
"RequestType": "Create",
|
||||
"ResourceProperties": {
|
||||
"ProviderName": "${self.triggers.provider_name}",
|
||||
"ClientSecretArn": "${aws_secretsmanager_secret.machine_client_secret.arn}",
|
||||
"DiscoveryUrl": "${self.triggers.discovery_url}",
|
||||
"ClientId": "${self.triggers.client_id}"
|
||||
}
|
||||
}
|
||||
PAYLOAD_EOF
|
||||
)
|
||||
|
||||
echo "Invoking OAuth2 provider Lambda: ${self.triggers.function_name}"
|
||||
|
||||
# Invoke Lambda (--cli-binary-format raw-in-base64-out ensures JSON payload is accepted)
|
||||
aws lambda invoke \
|
||||
--function-name ${self.triggers.function_name} \
|
||||
--cli-binary-format raw-in-base64-out \
|
||||
--payload "$PAYLOAD" \
|
||||
--region ${self.triggers.region} \
|
||||
/tmp/oauth2_provider_response.json
|
||||
|
||||
# Check for errors
|
||||
if grep -q "FunctionError" /tmp/oauth2_provider_response.json; then
|
||||
echo "ERROR: OAuth2 provider creation failed"
|
||||
cat /tmp/oauth2_provider_response.json
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OAuth2 provider created successfully"
|
||||
cat /tmp/oauth2_provider_response.json
|
||||
EOT
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
when = destroy
|
||||
interpreter = ["bash", "-c"]
|
||||
command = <<-EOT
|
||||
# Build the CloudFormation Custom Resource Delete payload
|
||||
PAYLOAD=$(cat <<'PAYLOAD_EOF'
|
||||
{
|
||||
"RequestType": "Delete",
|
||||
"PhysicalResourceId": "${self.triggers.provider_name}",
|
||||
"ResourceProperties": {
|
||||
"ProviderName": "${self.triggers.provider_name}"
|
||||
}
|
||||
}
|
||||
PAYLOAD_EOF
|
||||
)
|
||||
|
||||
echo "Deleting OAuth2 provider: ${self.triggers.provider_name}"
|
||||
|
||||
# Invoke Lambda for deletion (ignore errors if already deleted)
|
||||
aws lambda invoke \
|
||||
--function-name ${self.triggers.function_name} \
|
||||
--cli-binary-format raw-in-base64-out \
|
||||
--payload "$PAYLOAD" \
|
||||
--region ${self.triggers.region} \
|
||||
/tmp/oauth2_provider_delete.json || true
|
||||
|
||||
echo "OAuth2 provider deletion completed"
|
||||
cat /tmp/oauth2_provider_delete.json || true
|
||||
EOT
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
aws_lambda_function.oauth2_provider,
|
||||
aws_cognito_user_pool_client.machine,
|
||||
aws_secretsmanager_secret_version.machine_client_secret
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Memory Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "memory_arn" {
|
||||
description = "AgentCore Memory ARN"
|
||||
value = aws_bedrockagentcore_memory.main.arn
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Gateway Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "gateway_id" {
|
||||
description = "AgentCore Gateway ID"
|
||||
value = aws_bedrockagentcore_gateway.main.gateway_id
|
||||
}
|
||||
|
||||
output "gateway_arn" {
|
||||
description = "AgentCore Gateway ARN"
|
||||
value = aws_bedrockagentcore_gateway.main.gateway_arn
|
||||
}
|
||||
|
||||
output "gateway_url" {
|
||||
description = "AgentCore Gateway URL"
|
||||
value = aws_bedrockagentcore_gateway.main.gateway_url
|
||||
}
|
||||
|
||||
output "gateway_target_id" {
|
||||
description = "AgentCore Gateway Target ID"
|
||||
value = aws_bedrockagentcore_gateway_target.sample_tool.target_id
|
||||
}
|
||||
|
||||
output "tool_lambda_arn" {
|
||||
description = "Sample tool Lambda function ARN"
|
||||
value = aws_lambda_function.sample_tool.arn
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Runtime Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "runtime_id" {
|
||||
description = "AgentCore Runtime ID"
|
||||
value = aws_bedrockagentcore_agent_runtime.main.agent_runtime_id
|
||||
}
|
||||
|
||||
output "runtime_arn" {
|
||||
description = "AgentCore Runtime ARN"
|
||||
value = aws_bedrockagentcore_agent_runtime.main.agent_runtime_arn
|
||||
}
|
||||
|
||||
output "runtime_role_arn" {
|
||||
description = "AgentCore Runtime execution role ARN"
|
||||
value = aws_iam_role.runtime.arn
|
||||
}
|
||||
|
||||
output "copilotkit_runtime_url" {
|
||||
description = "CopilotKit runtime endpoint URL"
|
||||
value = "${aws_apigatewayv2_stage.copilotkit_runtime.invoke_url}/copilotkit"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Machine Client Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "machine_client_id" {
|
||||
description = "Cognito Machine Client ID (for M2M authentication)"
|
||||
value = aws_cognito_user_pool_client.machine.id
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# AgentCore Runtime
|
||||
# Maps to: backend-stack.ts createAgentCoreRuntime() - runtime section
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ECR Repository (for container image)
|
||||
# Only created if container_uri is not provided
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "aws_ecr_repository" "agent" {
|
||||
count = local.is_docker && var.container_uri == null ? 1 : 0
|
||||
|
||||
name = "${var.stack_name_base}-agent-runtime"
|
||||
image_tag_mutability = "MUTABLE"
|
||||
force_delete = true
|
||||
|
||||
image_scanning_configuration {
|
||||
scan_on_push = true
|
||||
}
|
||||
|
||||
encryption_configuration {
|
||||
encryption_type = "AES256"
|
||||
}
|
||||
}
|
||||
|
||||
# ECR Lifecycle policy to keep only recent images
|
||||
resource "aws_ecr_lifecycle_policy" "agent" {
|
||||
count = local.is_docker && var.container_uri == null ? 1 : 0
|
||||
|
||||
repository = aws_ecr_repository.agent[0].name
|
||||
|
||||
policy = jsonencode({
|
||||
rules = [
|
||||
{
|
||||
rulePriority = 1
|
||||
description = "Keep only 5 most recent images"
|
||||
selection = {
|
||||
tagStatus = "any"
|
||||
countType = "imageCountMoreThan"
|
||||
countNumber = 5
|
||||
}
|
||||
action = {
|
||||
type = "expire"
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Docker Build & Push (docker mode only)
|
||||
# Automatically builds and pushes the agent container image during apply
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Content hash for Docker image change detection — triggers rebuild and runtime replacement.
|
||||
# Always created (no count) so the runtime's replace_triggered_by can reference it in both modes.
|
||||
# In zip mode the value is static ("zip"), so it never triggers a replacement.
|
||||
resource "terraform_data" "docker_image_hash" {
|
||||
input = local.is_docker && var.container_uri == null ? sha256(join("", concat(
|
||||
[filesha256("${local.pattern_dir}/Dockerfile")],
|
||||
[filesha256("${local.pattern_dir}/requirements.txt")],
|
||||
[for f in fileset(local.pattern_dir, "**/*.py") : filesha256("${local.pattern_dir}/${f}")],
|
||||
[for f in fileset("${local.project_root}/patterns/utils", "**/*.py") : filesha256("${local.project_root}/patterns/utils/${f}")],
|
||||
[for f in fileset("${local.project_root}/gateway", "**/*.py") : filesha256("${local.project_root}/gateway/${f}")],
|
||||
[for f in fileset("${local.project_root}/tools", "**/*.py") : filesha256("${local.project_root}/tools/${f}")],
|
||||
[filesha256("${local.project_root}/pyproject.toml")],
|
||||
))) : "zip"
|
||||
}
|
||||
|
||||
resource "null_resource" "docker_build_push" {
|
||||
count = local.is_docker && var.container_uri == null ? 1 : 0
|
||||
|
||||
triggers = {
|
||||
content_hash = terraform_data.docker_image_hash.output
|
||||
repository_url = aws_ecr_repository.agent[0].repository_url
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
interpreter = ["bash", "-c"]
|
||||
command = <<-EOT
|
||||
set -e
|
||||
|
||||
ECR_REPO="${aws_ecr_repository.agent[0].repository_url}"
|
||||
REGION="${local.region}"
|
||||
ACCOUNT_ID="${local.account_id}"
|
||||
PROJECT_ROOT="${local.project_root}"
|
||||
DOCKERFILE="patterns/${var.backend_pattern}/Dockerfile"
|
||||
|
||||
# Verify Docker is running
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "ERROR: Docker is not running. Please start Docker Desktop." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ECR login
|
||||
echo "Logging into ECR..."
|
||||
aws ecr get-login-password --region "$REGION" | \
|
||||
docker login --username AWS --password-stdin "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"
|
||||
|
||||
# Build ARM64 image
|
||||
echo "Building Docker image (ARM64)..."
|
||||
cd "$PROJECT_ROOT"
|
||||
docker build \
|
||||
--platform linux/arm64 \
|
||||
-f "$DOCKERFILE" \
|
||||
-t "$ECR_REPO:latest" \
|
||||
.
|
||||
|
||||
# Push to ECR
|
||||
echo "Pushing image to ECR..."
|
||||
docker push "$ECR_REPO:latest"
|
||||
|
||||
echo "SUCCESS: Image pushed to $ECR_REPO:latest"
|
||||
EOT
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
aws_ecr_repository.agent[0],
|
||||
aws_ecr_lifecycle_policy.agent[0]
|
||||
]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IAM Role for AgentCore Runtime
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
data "aws_iam_policy_document" "runtime_assume_role" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["bedrock-agentcore.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "runtime" {
|
||||
name = "${var.stack_name_base}-agentcore-runtime-role"
|
||||
assume_role_policy = data.aws_iam_policy_document.runtime_assume_role.json
|
||||
description = "Execution role for AgentCore Runtime"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IAM Policy Document
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
data "aws_iam_policy_document" "runtime_policy" {
|
||||
# ECRImageAccess (docker mode only)
|
||||
dynamic "statement" {
|
||||
for_each = local.is_docker ? [1] : []
|
||||
content {
|
||||
sid = "ECRImageAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchCheckLayerAvailability"
|
||||
]
|
||||
resources = ["arn:aws:ecr:${local.region}:${local.account_id}:repository/*"]
|
||||
}
|
||||
}
|
||||
|
||||
# ECRTokenAccess (docker mode only)
|
||||
dynamic "statement" {
|
||||
for_each = local.is_docker ? [1] : []
|
||||
content {
|
||||
sid = "ECRTokenAccess"
|
||||
effect = "Allow"
|
||||
actions = ["ecr:GetAuthorizationToken"]
|
||||
resources = ["*"]
|
||||
}
|
||||
}
|
||||
|
||||
# S3 Agent Code Access (zip mode only)
|
||||
dynamic "statement" {
|
||||
for_each = local.is_zip ? [1] : []
|
||||
content {
|
||||
sid = "S3AgentCodeAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"s3:GetObject",
|
||||
"s3:GetBucketLocation"
|
||||
]
|
||||
resources = [
|
||||
aws_s3_bucket.agent_code[0].arn,
|
||||
"${aws_s3_bucket.agent_code[0].arn}/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# CloudWatchLogsGroupAccess
|
||||
statement {
|
||||
sid = "CloudWatchLogsGroupAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"logs:DescribeLogStreams",
|
||||
"logs:CreateLogGroup"
|
||||
]
|
||||
resources = ["arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"]
|
||||
}
|
||||
|
||||
# CloudWatchLogsDescribeGroups
|
||||
statement {
|
||||
sid = "CloudWatchLogsDescribeGroups"
|
||||
effect = "Allow"
|
||||
actions = ["logs:DescribeLogGroups"]
|
||||
resources = ["arn:aws:logs:${local.region}:${local.account_id}:log-group:*"]
|
||||
}
|
||||
|
||||
# CloudWatchLogsStreamAccess
|
||||
statement {
|
||||
sid = "CloudWatchLogsStreamAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
]
|
||||
resources = ["arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"]
|
||||
}
|
||||
|
||||
# X-Ray Tracing
|
||||
statement {
|
||||
sid = "XRayTracing"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"xray:PutTraceSegments",
|
||||
"xray:PutTelemetryRecords",
|
||||
"xray:GetSamplingRules",
|
||||
"xray:GetSamplingTargets"
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
# CloudWatch Metrics
|
||||
statement {
|
||||
sid = "CloudWatchMetrics"
|
||||
effect = "Allow"
|
||||
actions = ["cloudwatch:PutMetricData"]
|
||||
resources = ["*"]
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "cloudwatch:namespace"
|
||||
values = ["bedrock-agentcore"]
|
||||
}
|
||||
}
|
||||
|
||||
# GetAgentAccessToken
|
||||
statement {
|
||||
sid = "GetAgentAccessToken"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"bedrock-agentcore:GetWorkloadAccessToken",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:workload-identity-directory/default",
|
||||
"arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:workload-identity-directory/default/workload-identity/*"
|
||||
]
|
||||
}
|
||||
|
||||
# BedrockModelInvocation
|
||||
statement {
|
||||
sid = "BedrockModelInvocation"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"bedrock:InvokeModel",
|
||||
"bedrock:InvokeModelWithResponseStream"
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:bedrock:*::foundation-model/*",
|
||||
"arn:aws:bedrock:${local.region}:${local.account_id}:*"
|
||||
]
|
||||
}
|
||||
|
||||
# SecretsManagerOAuth2Access
|
||||
# Runtime needs to read OAuth2 credentials from Token Vault secret
|
||||
# created by AgentCore Identity (not the machine client secret directly)
|
||||
statement {
|
||||
sid = "SecretsManagerOAuth2Access"
|
||||
effect = "Allow"
|
||||
actions = ["secretsmanager:GetSecretValue"]
|
||||
resources = [
|
||||
"arn:aws:secretsmanager:${local.region}:${local.account_id}:secret:bedrock-agentcore-identity!default/oauth2/${var.stack_name_base}-runtime-gateway-auth*"
|
||||
]
|
||||
}
|
||||
|
||||
# MemoryResourceAccess - references memory resource directly (no variable passing)
|
||||
statement {
|
||||
sid = "MemoryResourceAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"bedrock-agentcore:CreateEvent",
|
||||
"bedrock-agentcore:GetEvent",
|
||||
"bedrock-agentcore:ListEvents",
|
||||
"bedrock-agentcore:RetrieveMemoryRecords"
|
||||
]
|
||||
resources = [aws_bedrockagentcore_memory.main.arn]
|
||||
}
|
||||
|
||||
# SSMParameterAccess
|
||||
statement {
|
||||
sid = "SSMParameterAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ssm:GetParameter",
|
||||
"ssm:GetParameters"
|
||||
]
|
||||
resources = ["arn:aws:ssm:${local.region}:${local.account_id}:parameter/${var.stack_name_base}/*"]
|
||||
}
|
||||
|
||||
# CodeInterpreterAccess
|
||||
statement {
|
||||
sid = "CodeInterpreterAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"bedrock-agentcore:StartCodeInterpreterSession",
|
||||
"bedrock-agentcore:StopCodeInterpreterSession",
|
||||
"bedrock-agentcore:InvokeCodeInterpreter"
|
||||
]
|
||||
resources = ["arn:aws:bedrock-agentcore:${local.region}:aws:code-interpreter/*"]
|
||||
}
|
||||
|
||||
# OAuth2CredentialProviderAccess
|
||||
# The @requires_access_token decorator performs a two-stage process:
|
||||
# GetOauth2CredentialProvider - Looks up provider metadata
|
||||
# GetResourceOauth2Token - Fetches the actual access token from Token Vault
|
||||
statement {
|
||||
sid = "OAuth2CredentialProviderAccess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"bedrock-agentcore:GetOauth2CredentialProvider",
|
||||
"bedrock-agentcore:GetResourceOauth2Token"
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:oauth2-credential-provider/*",
|
||||
"arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:token-vault/*",
|
||||
"arn:aws:bedrock-agentcore:${local.region}:${local.account_id}:workload-identity-directory/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "runtime" {
|
||||
name = "${var.stack_name_base}-agentcore-runtime-policy"
|
||||
role = aws_iam_role.runtime.id
|
||||
policy = data.aws_iam_policy_document.runtime_policy.json
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Default Security Group (for VPC mode, when none provided)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
locals {
|
||||
# Use user-provided security groups, or fall back to the auto-created default
|
||||
effective_security_group_ids = (
|
||||
var.backend_network_mode == "VPC" && length(var.backend_vpc_security_group_ids) == 0
|
||||
? [aws_security_group.runtime_default[0].id]
|
||||
: var.backend_vpc_security_group_ids
|
||||
)
|
||||
}
|
||||
|
||||
resource "aws_security_group" "runtime_default" {
|
||||
count = var.backend_network_mode == "VPC" && length(var.backend_vpc_security_group_ids) == 0 ? 1 : 0
|
||||
|
||||
name = "${var.stack_name_base}-agentcore-runtime-sg"
|
||||
description = "Default security group for AgentCore Runtime VPC deployment"
|
||||
vpc_id = var.backend_vpc_id
|
||||
|
||||
tags = {
|
||||
Name = "${var.stack_name_base}-agentcore-runtime-sg"
|
||||
}
|
||||
}
|
||||
|
||||
# Self-referencing ingress rule: allows HTTPS traffic between runtime and VPC endpoints
|
||||
resource "aws_vpc_security_group_ingress_rule" "runtime_default_https" {
|
||||
count = var.backend_network_mode == "VPC" && length(var.backend_vpc_security_group_ids) == 0 ? 1 : 0
|
||||
|
||||
security_group_id = aws_security_group.runtime_default[0].id
|
||||
referenced_security_group_id = aws_security_group.runtime_default[0].id
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
ip_protocol = "tcp"
|
||||
description = "Allow HTTPS from self (VPC endpoint access)"
|
||||
}
|
||||
|
||||
# Egress rule: allow all outbound traffic (matches CDK allowAllOutbound: true)
|
||||
resource "aws_vpc_security_group_egress_rule" "runtime_default_all" {
|
||||
count = var.backend_network_mode == "VPC" && length(var.backend_vpc_security_group_ids) == 0 ? 1 : 0
|
||||
|
||||
security_group_id = aws_security_group.runtime_default[0].id
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
ip_protocol = "-1"
|
||||
description = "Allow all outbound traffic"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# AgentCore Runtime
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "aws_bedrockagentcore_agent_runtime" "main" {
|
||||
agent_runtime_name = local.runtime_name
|
||||
role_arn = aws_iam_role.runtime.arn
|
||||
description = "${var.backend_pattern} agent runtime for ${var.stack_name_base}"
|
||||
|
||||
# Artifact configuration (docker or zip)
|
||||
agent_runtime_artifact {
|
||||
# Docker mode: container image from ECR or custom URI
|
||||
dynamic "container_configuration" {
|
||||
for_each = local.is_docker ? [1] : []
|
||||
content {
|
||||
container_uri = var.container_uri != null ? var.container_uri : "${aws_ecr_repository.agent[0].repository_url}:latest"
|
||||
}
|
||||
}
|
||||
|
||||
# Zip mode: S3 Python package
|
||||
dynamic "code_configuration" {
|
||||
for_each = local.is_zip ? [1] : []
|
||||
content {
|
||||
runtime = "PYTHON_3_12"
|
||||
entry_point = local.zip_entry_point
|
||||
code {
|
||||
s3 {
|
||||
bucket = aws_s3_bucket.agent_code[0].id
|
||||
prefix = "deployment_package.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Network configuration
|
||||
# PUBLIC: Runtime is accessible over the public internet (default).
|
||||
# VPC: Runtime is deployed into a user-provided VPC for private network isolation.
|
||||
# The user must ensure their VPC has the necessary VPC endpoints for AWS services.
|
||||
# See docs/DEPLOYMENT.md for the full list of required VPC endpoints.
|
||||
network_configuration {
|
||||
network_mode = var.backend_network_mode
|
||||
|
||||
dynamic "network_mode_config" {
|
||||
for_each = var.backend_network_mode == "VPC" ? [1] : []
|
||||
content {
|
||||
subnets = var.backend_vpc_subnet_ids
|
||||
security_groups = local.effective_security_group_ids
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# JWT authorizer configuration (Cognito)
|
||||
authorizer_configuration {
|
||||
custom_jwt_authorizer {
|
||||
discovery_url = local.oidc_discovery_url
|
||||
allowed_clients = [var.web_client_id]
|
||||
}
|
||||
}
|
||||
|
||||
# Protocol configuration (HTTP for agent communication)
|
||||
protocol_configuration {
|
||||
server_protocol = "HTTP"
|
||||
}
|
||||
|
||||
# Request header configuration (allowlist Authorization for JWT sub claim)
|
||||
request_header_configuration {
|
||||
request_header_allowlist = ["Authorization"]
|
||||
}
|
||||
|
||||
# Environment variables for the runtime
|
||||
environment_variables = merge(
|
||||
{
|
||||
AWS_REGION = local.region
|
||||
AWS_DEFAULT_REGION = local.region
|
||||
MEMORY_ID = aws_bedrockagentcore_memory.main.id
|
||||
STACK_NAME = var.stack_name_base
|
||||
GATEWAY_CREDENTIAL_PROVIDER_NAME = "${var.stack_name_base}-runtime-gateway-auth"
|
||||
},
|
||||
# claude-agent-sdk patterns require CLAUDE_CODE_USE_BEDROCK=1
|
||||
local.is_claude_agent_sdk ? { CLAUDE_CODE_USE_BEDROCK = "1" } : {}
|
||||
)
|
||||
|
||||
# Force runtime replacement when agent code changes (zip or docker)
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = !local.is_claude_agent_sdk || local.is_docker
|
||||
error_message = "claude-agent-sdk patterns require Docker deployment (backend_deployment_type = \"docker\") because they need Node.js and the claude-code CLI installed at build time."
|
||||
}
|
||||
precondition {
|
||||
condition = var.backend_network_mode != "VPC" || (var.backend_vpc_id != null && var.backend_vpc_id != "")
|
||||
error_message = "backend_vpc_id is required when backend_network_mode is 'VPC'."
|
||||
}
|
||||
precondition {
|
||||
condition = var.backend_network_mode != "VPC" || length(var.backend_vpc_subnet_ids) > 0
|
||||
error_message = "backend_vpc_subnet_ids must contain at least one subnet ID when backend_network_mode is 'VPC'."
|
||||
}
|
||||
replace_triggered_by = [
|
||||
terraform_data.agent_code_hash,
|
||||
terraform_data.docker_image_hash,
|
||||
]
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
aws_iam_role_policy.runtime,
|
||||
null_resource.invoke_zip_packager,
|
||||
null_resource.docker_build_push,
|
||||
null_resource.invoke_oauth2_provider # Ensure provider is registered before Runtime starts
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# SSM Parameters & Secrets Manager
|
||||
# Maps to: backend-stack.ts createRuntimeSSMParameters() + createCognitoSSMParameters()
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SSM Parameters
|
||||
# Store configuration values for cross-stack references and frontend access
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "aws_ssm_parameter" "runtime_arn" {
|
||||
name = "${local.ssm_parameter_prefix}/runtime-arn"
|
||||
description = "AgentCore Runtime ARN"
|
||||
type = "String"
|
||||
value = aws_bedrockagentcore_agent_runtime.main.agent_runtime_arn
|
||||
|
||||
}
|
||||
|
||||
resource "aws_ssm_parameter" "cognito_user_pool_id" {
|
||||
name = "${local.ssm_parameter_prefix}/cognito-user-pool-id"
|
||||
description = "Cognito User Pool ID"
|
||||
type = "String"
|
||||
value = var.user_pool_id
|
||||
|
||||
}
|
||||
|
||||
resource "aws_ssm_parameter" "cognito_user_pool_client_id" {
|
||||
name = "${local.ssm_parameter_prefix}/cognito-user-pool-client-id"
|
||||
description = "Cognito User Pool Client ID"
|
||||
type = "String"
|
||||
value = var.web_client_id
|
||||
|
||||
}
|
||||
|
||||
resource "aws_ssm_parameter" "machine_client_id" {
|
||||
name = "${local.ssm_parameter_prefix}/machine_client_id"
|
||||
description = "Machine Client ID for M2M authentication"
|
||||
type = "String"
|
||||
value = aws_cognito_user_pool_client.machine.id
|
||||
|
||||
}
|
||||
|
||||
resource "aws_ssm_parameter" "cognito_provider" {
|
||||
name = "${local.ssm_parameter_prefix}/cognito_provider"
|
||||
description = "Cognito domain URL for token endpoint"
|
||||
type = "String"
|
||||
value = var.cognito_domain_url
|
||||
|
||||
}
|
||||
|
||||
resource "aws_ssm_parameter" "feedback_api_url" {
|
||||
name = "${local.ssm_parameter_prefix}/feedback-api-url"
|
||||
description = "Feedback API Gateway URL"
|
||||
type = "String"
|
||||
value = "${aws_api_gateway_stage.prod.invoke_url}/feedback"
|
||||
|
||||
}
|
||||
|
||||
resource "aws_ssm_parameter" "copilotkit_runtime_url" {
|
||||
name = "${local.ssm_parameter_prefix}/copilotkit-runtime-url"
|
||||
description = "CopilotKit runtime API URL"
|
||||
type = "String"
|
||||
value = "${aws_apigatewayv2_stage.copilotkit_runtime.invoke_url}/copilotkit"
|
||||
|
||||
}
|
||||
|
||||
resource "aws_ssm_parameter" "gateway_url" {
|
||||
name = "${local.ssm_parameter_prefix}/gateway_url"
|
||||
description = "AgentCore Gateway URL"
|
||||
type = "String"
|
||||
value = aws_bedrockagentcore_gateway.main.gateway_url
|
||||
|
||||
}
|
||||
|
||||
# Agent Code Bucket (zip mode only) - matches CDK's AgentCodeBucketNameParam
|
||||
resource "aws_ssm_parameter" "agent_code_bucket" {
|
||||
count = local.is_zip ? 1 : 0
|
||||
|
||||
name = "${local.ssm_parameter_prefix}/agent-code-bucket"
|
||||
description = "S3 bucket for agent code deployment packages"
|
||||
type = "String"
|
||||
value = aws_s3_bucket.agent_code[0].id
|
||||
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Secrets Manager - Machine Client Secret
|
||||
# Store the machine client secret securely
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "aws_secretsmanager_secret" "machine_client_secret" {
|
||||
name = "${local.ssm_parameter_prefix}/machine_client_secret"
|
||||
description = "Machine Client Secret for M2M authentication"
|
||||
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret_version" "machine_client_secret" {
|
||||
secret_id = aws_secretsmanager_secret.machine_client_secret.id
|
||||
secret_string = aws_cognito_user_pool_client.machine.client_secret
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Core Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "stack_name_base" {
|
||||
description = "Base name for all resources."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "backend_pattern" {
|
||||
description = "Agent pattern to deploy."
|
||||
type = string
|
||||
default = "strands-single-agent"
|
||||
}
|
||||
|
||||
variable "backend_deployment_type" {
|
||||
description = "Deployment type: 'docker' (container via ECR) or 'zip' (Python package via S3). Note: claude-agent-sdk patterns require 'docker'."
|
||||
type = string
|
||||
default = "docker"
|
||||
}
|
||||
|
||||
variable "backend_network_mode" {
|
||||
description = "Network mode for AgentCore Runtime (PUBLIC or VPC)."
|
||||
type = string
|
||||
default = "PUBLIC"
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VPC Configuration (Required if backend_network_mode = VPC)
|
||||
# =============================================================================
|
||||
|
||||
variable "backend_vpc_id" {
|
||||
description = "VPC ID for VPC network mode. Required when backend_network_mode is 'VPC'."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "backend_vpc_subnet_ids" {
|
||||
description = "List of subnet IDs for VPC network mode. Required when backend_network_mode is 'VPC'."
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "backend_vpc_security_group_ids" {
|
||||
description = "List of security group IDs for VPC network mode. Optional when backend_network_mode is 'VPC'. If omitted, a default security group is created."
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Cognito Configuration (passed from cognito module)
|
||||
# =============================================================================
|
||||
|
||||
variable "user_pool_id" {
|
||||
description = "Cognito User Pool ID."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "user_pool_arn" {
|
||||
description = "Cognito User Pool ARN."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "web_client_id" {
|
||||
description = "Cognito Web Client ID (for frontend OAuth)."
|
||||
type = string
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Amplify Configuration (passed from amplify module)
|
||||
# =============================================================================
|
||||
|
||||
variable "frontend_url" {
|
||||
description = "Frontend URL for CORS and callback configuration."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "cognito_domain_url" {
|
||||
description = "Cognito domain URL for OAuth token endpoint."
|
||||
type = string
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Optional Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "container_uri" {
|
||||
description = "Container image URI. If not provided, ECR repository will be created."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "log_retention_days" {
|
||||
description = "CloudWatch log retention in days."
|
||||
type = number
|
||||
default = 7
|
||||
}
|
||||
|
||||
variable "throttling_rate_limit" {
|
||||
description = "API Gateway throttling rate limit."
|
||||
type = number
|
||||
default = 100
|
||||
}
|
||||
|
||||
variable "throttling_burst_limit" {
|
||||
description = "API Gateway throttling burst limit."
|
||||
type = number
|
||||
default = 200
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.82.0"
|
||||
}
|
||||
archive = {
|
||||
source = "hashicorp/archive"
|
||||
version = ">= 2.4.0"
|
||||
}
|
||||
null = {
|
||||
source = "hashicorp/null"
|
||||
version = ">= 3.2.0"
|
||||
}
|
||||
time = {
|
||||
source = "hashicorp/time"
|
||||
version = ">= 0.9.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Data Sources
|
||||
# =============================================================================
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
|
||||
# =============================================================================
|
||||
# Local Values
|
||||
# =============================================================================
|
||||
|
||||
locals {
|
||||
account_id = data.aws_caller_identity.current.account_id
|
||||
region = data.aws_region.current.id
|
||||
|
||||
# Cognito domain prefix (must be globally unique and lowercase)
|
||||
domain_prefix = "${lower(replace(var.stack_name_base, "_", "-"))}-${local.account_id}-${local.region}"
|
||||
|
||||
# Callback URLs (hardcoded to match CDK cognito-stack.ts defaults)
|
||||
default_callback_urls = ["http://localhost:3000", "https://localhost:3000"]
|
||||
|
||||
# Combine callback URLs with Amplify URL if provided
|
||||
all_callback_urls = var.amplify_url != null ? concat(local.default_callback_urls, [var.amplify_url]) : local.default_callback_urls
|
||||
|
||||
# Password minimum length (hardcoded to match CDK cognito-stack.ts)
|
||||
password_minimum_length = 8
|
||||
|
||||
# User invitation email template
|
||||
invitation_email_subject = "Welcome to ${var.stack_name_base}!"
|
||||
invitation_email_body = <<-EOF
|
||||
<p>Hello {username},</p>
|
||||
<p>Welcome to ${var.stack_name_base}! Your username is <strong>{username}</strong> and your temporary password is: <strong>{####}</strong></p>
|
||||
<p>Please use this temporary password to log in and set your permanent password.</p>
|
||||
<p>The CloudFront URL to your application is stored as an output in the "${var.stack_name_base}" stack, and will be printed to your terminal once the deployment process completes.</p>
|
||||
<p>Thanks,</p>
|
||||
<p>Fullstack AgentCore Solution Template Team</p>
|
||||
EOF
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Cognito User Pool
|
||||
# =============================================================================
|
||||
# Main user pool for authentication with password policy and invitation templates
|
||||
|
||||
resource "aws_cognito_user_pool" "main" {
|
||||
name = "${var.stack_name_base}-user-pool"
|
||||
|
||||
# Admin-only user creation (self sign-up disabled)
|
||||
admin_create_user_config {
|
||||
allow_admin_create_user_only = true
|
||||
|
||||
# User invitation email template
|
||||
invite_message_template {
|
||||
email_subject = local.invitation_email_subject
|
||||
email_message = local.invitation_email_body
|
||||
sms_message = "Your username is {username} and temporary password is {####}"
|
||||
}
|
||||
}
|
||||
|
||||
# Sign-in with email
|
||||
username_attributes = ["email"]
|
||||
|
||||
# Auto-verify email
|
||||
auto_verified_attributes = ["email"]
|
||||
|
||||
# Account recovery via email only
|
||||
account_recovery_setting {
|
||||
recovery_mechanism {
|
||||
name = "verified_email"
|
||||
priority = 1
|
||||
}
|
||||
}
|
||||
|
||||
# Password policy
|
||||
password_policy {
|
||||
minimum_length = local.password_minimum_length
|
||||
require_lowercase = true
|
||||
require_uppercase = true
|
||||
require_numbers = true
|
||||
require_symbols = true
|
||||
temporary_password_validity_days = 7
|
||||
}
|
||||
|
||||
# Email attribute (required and immutable)
|
||||
schema {
|
||||
name = "email"
|
||||
attribute_data_type = "String"
|
||||
required = true
|
||||
mutable = false
|
||||
developer_only_attribute = false
|
||||
|
||||
string_attribute_constraints {
|
||||
min_length = 0
|
||||
max_length = 2048
|
||||
}
|
||||
}
|
||||
|
||||
# Email configuration
|
||||
email_configuration {
|
||||
email_sending_account = "COGNITO_DEFAULT"
|
||||
}
|
||||
|
||||
# Allow deletion (no protection)
|
||||
deletion_protection = "INACTIVE"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Cognito User Pool Domain
|
||||
# =============================================================================
|
||||
# Domain for hosted UI with managed login V2
|
||||
|
||||
resource "aws_cognito_user_pool_domain" "main" {
|
||||
domain = local.domain_prefix
|
||||
user_pool_id = aws_cognito_user_pool.main.id
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Cognito Managed Login Branding (V2)
|
||||
# =============================================================================
|
||||
# Required for the v2 managed login to display properly
|
||||
|
||||
resource "aws_cognito_managed_login_branding" "main" {
|
||||
user_pool_id = aws_cognito_user_pool.main.id
|
||||
client_id = aws_cognito_user_pool_client.web.id
|
||||
|
||||
# Use Cognito's default styles
|
||||
use_cognito_provided_values = true
|
||||
|
||||
depends_on = [aws_cognito_user_pool_domain.main]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Cognito User Pool Client - Web (Frontend)
|
||||
# =============================================================================
|
||||
# OAuth client for frontend application
|
||||
|
||||
resource "aws_cognito_user_pool_client" "web" {
|
||||
name = "${var.stack_name_base}-client"
|
||||
user_pool_id = aws_cognito_user_pool.main.id
|
||||
|
||||
# No secret for public client
|
||||
generate_secret = false
|
||||
|
||||
# Auth flows
|
||||
explicit_auth_flows = [
|
||||
"ALLOW_USER_PASSWORD_AUTH",
|
||||
"ALLOW_USER_SRP_AUTH",
|
||||
"ALLOW_REFRESH_TOKEN_AUTH"
|
||||
]
|
||||
|
||||
# OAuth configuration
|
||||
allowed_oauth_flows = ["code"]
|
||||
allowed_oauth_flows_user_pool_client = true
|
||||
allowed_oauth_scopes = ["openid", "email", "profile"]
|
||||
|
||||
# Callback and logout URLs
|
||||
callback_urls = local.all_callback_urls
|
||||
logout_urls = local.all_callback_urls
|
||||
|
||||
# Supported identity providers
|
||||
supported_identity_providers = ["COGNITO"]
|
||||
|
||||
# Prevent user existence errors
|
||||
prevent_user_existence_errors = "ENABLED"
|
||||
|
||||
# Token validity
|
||||
access_token_validity = 1
|
||||
id_token_validity = 1
|
||||
refresh_token_validity = 30
|
||||
|
||||
token_validity_units {
|
||||
access_token = "hours"
|
||||
id_token = "hours"
|
||||
refresh_token = "days"
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Cognito Admin User (Conditional)
|
||||
# =============================================================================
|
||||
# Creates admin user if email is provided
|
||||
|
||||
resource "aws_cognito_user" "admin" {
|
||||
count = var.admin_user_email != null ? 1 : 0
|
||||
|
||||
user_pool_id = aws_cognito_user_pool.main.id
|
||||
username = var.admin_user_email
|
||||
|
||||
attributes = {
|
||||
email = var.admin_user_email
|
||||
email_verified = true
|
||||
}
|
||||
|
||||
# Send invitation email with temporary password
|
||||
desired_delivery_mediums = ["EMAIL"]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# User Pool Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "user_pool_id" {
|
||||
description = "Cognito User Pool ID"
|
||||
value = aws_cognito_user_pool.main.id
|
||||
}
|
||||
|
||||
output "user_pool_arn" {
|
||||
description = "Cognito User Pool ARN"
|
||||
value = aws_cognito_user_pool.main.arn
|
||||
}
|
||||
|
||||
output "user_pool_endpoint" {
|
||||
description = "Cognito User Pool endpoint"
|
||||
value = aws_cognito_user_pool.main.endpoint
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Web Client Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "web_client_id" {
|
||||
description = "Cognito Web Client ID (for frontend)"
|
||||
value = aws_cognito_user_pool_client.web.id
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Domain Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "domain_name" {
|
||||
description = "Cognito domain name"
|
||||
value = aws_cognito_user_pool_domain.main.domain
|
||||
}
|
||||
|
||||
output "cognito_domain_url" {
|
||||
description = "Full Cognito domain URL for OAuth token endpoint"
|
||||
value = "${aws_cognito_user_pool_domain.main.domain}.auth.${local.region}.amazoncognito.com"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# OIDC Configuration Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "oidc_issuer_url" {
|
||||
description = "OIDC issuer URL for JWT validation"
|
||||
value = "https://cognito-idp.${local.region}.amazonaws.com/${aws_cognito_user_pool.main.id}"
|
||||
}
|
||||
|
||||
output "oidc_discovery_url" {
|
||||
description = "OIDC discovery URL (well-known configuration)"
|
||||
value = "https://cognito-idp.${local.region}.amazonaws.com/${aws_cognito_user_pool.main.id}/.well-known/openid-configuration"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Admin User Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "admin_user_created" {
|
||||
description = "Whether admin user was created"
|
||||
value = var.admin_user_email != null
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
variable "stack_name_base" {
|
||||
description = "Base name for all resources."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "admin_user_email" {
|
||||
description = "Email address for the admin user. If provided, creates an admin user."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "amplify_url" {
|
||||
description = "Amplify app URL to add to callback URLs."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.82.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Cognito Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "cognito_user_pool_id" {
|
||||
description = "Cognito User Pool ID"
|
||||
value = module.cognito.user_pool_id
|
||||
}
|
||||
|
||||
output "cognito_web_client_id" {
|
||||
description = "Cognito Web Client ID (for frontend)"
|
||||
value = module.cognito.web_client_id
|
||||
}
|
||||
|
||||
output "cognito_machine_client_id" {
|
||||
description = "Cognito Machine Client ID (for M2M authentication)"
|
||||
value = module.backend.machine_client_id
|
||||
}
|
||||
|
||||
output "cognito_domain_url" {
|
||||
description = "Cognito domain URL for OAuth"
|
||||
value = module.cognito.cognito_domain_url
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Amplify Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "amplify_app_id" {
|
||||
description = "Amplify App ID"
|
||||
value = module.amplify_hosting.app_id
|
||||
}
|
||||
|
||||
output "amplify_app_url" {
|
||||
description = "Amplify App URL (frontend)"
|
||||
value = module.amplify_hosting.app_url
|
||||
}
|
||||
|
||||
output "amplify_staging_bucket" {
|
||||
description = "S3 bucket for frontend staging deployments"
|
||||
value = module.amplify_hosting.staging_bucket_name
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# AgentCore Memory Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "memory_arn" {
|
||||
description = "AgentCore Memory ARN"
|
||||
value = module.backend.memory_arn
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# AgentCore Gateway Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "gateway_id" {
|
||||
description = "AgentCore Gateway ID"
|
||||
value = module.backend.gateway_id
|
||||
}
|
||||
|
||||
output "gateway_arn" {
|
||||
description = "AgentCore Gateway ARN"
|
||||
value = module.backend.gateway_arn
|
||||
}
|
||||
|
||||
output "gateway_url" {
|
||||
description = "AgentCore Gateway URL"
|
||||
value = module.backend.gateway_url
|
||||
}
|
||||
|
||||
output "gateway_target_id" {
|
||||
description = "AgentCore Gateway Target ID"
|
||||
value = module.backend.gateway_target_id
|
||||
}
|
||||
|
||||
output "tool_lambda_arn" {
|
||||
description = "Sample tool Lambda function ARN"
|
||||
value = module.backend.tool_lambda_arn
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# AgentCore Runtime Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "runtime_id" {
|
||||
description = "AgentCore Runtime ID"
|
||||
value = module.backend.runtime_id
|
||||
}
|
||||
|
||||
output "runtime_arn" {
|
||||
description = "AgentCore Runtime ARN"
|
||||
value = module.backend.runtime_arn
|
||||
}
|
||||
|
||||
output "runtime_role_arn" {
|
||||
description = "AgentCore Runtime execution role ARN"
|
||||
value = module.backend.runtime_role_arn
|
||||
}
|
||||
|
||||
output "copilotkit_runtime_url" {
|
||||
description = "CopilotKit runtime API URL"
|
||||
value = module.backend.copilotkit_runtime_url
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SSM Parameter Paths (for reference)
|
||||
# =============================================================================
|
||||
|
||||
output "ssm_parameter_prefix" {
|
||||
description = "SSM parameter prefix for this deployment"
|
||||
value = local.ssm_parameter_prefix
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Summary Output
|
||||
# =============================================================================
|
||||
|
||||
output "deployment_summary" {
|
||||
description = "Summary of deployed resources"
|
||||
value = {
|
||||
stack_name = var.stack_name_base
|
||||
region = local.region
|
||||
account_id = local.account_id
|
||||
deployment_type = var.backend_deployment_type
|
||||
frontend_url = module.amplify_hosting.app_url
|
||||
gateway_url = module.backend.gateway_url
|
||||
copilotkit_url = module.backend.copilotkit_runtime_url
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
#!/bin/bash
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Build and Push Docker Image to ECR for AgentCore Runtime
|
||||
# =============================================================================
|
||||
#
|
||||
# NOTE: This script is OPTIONAL. Running `terraform apply` with docker mode
|
||||
# automatically builds and pushes the image. Use this script only if you
|
||||
# prefer to build separately (e.g., in CI/CD pipelines) or need to rebuild
|
||||
# the image without a full terraform apply.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-and-push-image.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# -p, --pattern Agent pattern to build (default: strands-single-agent)
|
||||
# -r, --region AWS region (default: from terraform.tfvars or us-east-1)
|
||||
# -s, --stack Stack name (default: from terraform.tfvars)
|
||||
# -h, --help Show this help message
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default values
|
||||
PATTERN="strands-single-agent"
|
||||
REGION=""
|
||||
STACK_NAME=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TERRAFORM_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
PROJECT_ROOT="$(dirname "$TERRAFORM_DIR")"
|
||||
|
||||
# Print usage
|
||||
usage() {
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -p, --pattern Agent pattern to build (default: strands-single-agent)"
|
||||
echo " -r, --region AWS region (default: from terraform.tfvars or us-east-1)"
|
||||
echo " -s, --stack Stack name (default: from terraform.tfvars)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Use defaults from terraform.tfvars"
|
||||
echo " $0 -p langgraph-single-agent # Build LangGraph agent"
|
||||
echo " $0 -s my-stack -r us-west-2 # Custom stack and region"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-p|--pattern)
|
||||
PATTERN="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--region)
|
||||
REGION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-s|--stack)
|
||||
STACK_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unknown option $1${NC}"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Read values from terraform.tfvars if not provided
|
||||
if [[ -z "$STACK_NAME" || -z "$REGION" ]]; then
|
||||
TFVARS_FILE="$TERRAFORM_DIR/terraform.tfvars"
|
||||
if [[ -f "$TFVARS_FILE" ]]; then
|
||||
echo -e "${BLUE}Reading configuration from terraform.tfvars...${NC}"
|
||||
|
||||
if [[ -z "$STACK_NAME" ]]; then
|
||||
STACK_NAME=$(grep -E '^stack_name_base\s*=' "$TFVARS_FILE" | awk -F'"' '{print $2}')
|
||||
fi
|
||||
|
||||
# Region is resolved from AWS_REGION env var or AWS CLI profile (not in tfvars)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check deployment type - this script is for docker mode only
|
||||
TFVARS_FILE="$TERRAFORM_DIR/terraform.tfvars"
|
||||
if [[ -f "$TFVARS_FILE" ]]; then
|
||||
DEPLOYMENT_TYPE=$(grep -E '^backend_deployment_type\s*=' "$TFVARS_FILE" | awk -F'"' '{print $2}')
|
||||
if [[ "$DEPLOYMENT_TYPE" == "zip" ]]; then
|
||||
echo -e "${YELLOW}===========================================${NC}"
|
||||
echo -e "${YELLOW} backend_deployment_type is set to 'zip' ${NC}"
|
||||
echo -e "${YELLOW}===========================================${NC}"
|
||||
echo ""
|
||||
echo -e "This script is only needed for ${GREEN}docker${NC} deployment mode."
|
||||
echo -e "With ${GREEN}zip${NC} mode, agent code is packaged automatically during ${BLUE}terraform apply${NC}."
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Resolve region: CLI flag > AWS_REGION env > AWS_DEFAULT_REGION env > AWS CLI config
|
||||
if [[ -z "$REGION" ]]; then
|
||||
REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-$(aws configure get region 2>/dev/null || echo "")}}"
|
||||
fi
|
||||
|
||||
# Validate required values
|
||||
if [[ -z "$STACK_NAME" ]]; then
|
||||
echo -e "${RED}Error: Stack name not found. Please specify with -s or set in terraform.tfvars${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$REGION" ]]; then
|
||||
echo -e "${RED}Error: AWS region not found. Set AWS_REGION environment variable or configure via 'aws configure'.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get AWS account ID
|
||||
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text 2>/dev/null)
|
||||
if [[ -z "$AWS_ACCOUNT_ID" ]]; then
|
||||
echo -e "${RED}Error: Could not get AWS account ID. Check your AWS credentials.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Construct ECR repository URL
|
||||
ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${STACK_NAME}-agent-runtime"
|
||||
DOCKERFILE="patterns/${PATTERN}/Dockerfile"
|
||||
|
||||
# Print configuration
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} Docker Image Build & Push for ECR ${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo -e " Stack Name: ${GREEN}${STACK_NAME}${NC}"
|
||||
echo -e " AWS Account: ${GREEN}${AWS_ACCOUNT_ID}${NC}"
|
||||
echo -e " Region: ${GREEN}${REGION}${NC}"
|
||||
echo -e " Pattern: ${GREEN}${PATTERN}${NC}"
|
||||
echo -e " ECR Repo: ${GREEN}${ECR_REPO}${NC}"
|
||||
echo ""
|
||||
|
||||
# Verify Dockerfile exists
|
||||
if [[ ! -f "$PROJECT_ROOT/$DOCKERFILE" ]]; then
|
||||
echo -e "${RED}Error: Dockerfile not found at $PROJECT_ROOT/$DOCKERFILE${NC}"
|
||||
echo -e "${YELLOW}Available patterns:${NC}"
|
||||
ls -1 "$PROJECT_ROOT/patterns/" 2>/dev/null || echo " No patterns found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: Docker is not running. Please start Docker Desktop.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Login to ECR
|
||||
echo -e "${BLUE}Step 1/3: Logging into ECR...${NC}"
|
||||
aws ecr get-login-password --region "$REGION" | docker login --username AWS --password-stdin "${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${RED}Error: ECR login failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ ECR login successful${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 2: Build Docker image with ARM64 architecture (required by AgentCore Runtime)
|
||||
echo -e "${BLUE}Step 2/3: Building Docker image (ARM64 architecture)...${NC}"
|
||||
cd "$PROJECT_ROOT"
|
||||
docker build \
|
||||
--platform linux/arm64 \
|
||||
-f "$DOCKERFILE" \
|
||||
-t "${ECR_REPO}:latest" \
|
||||
.
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${RED}Error: Docker build failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Docker build successful${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 3: Push to ECR
|
||||
echo -e "${BLUE}Step 3/3: Pushing image to ECR...${NC}"
|
||||
docker push "${ECR_REPO}:latest"
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${RED}Error: Docker push failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Docker push successful${NC}"
|
||||
echo ""
|
||||
|
||||
# Success message
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} Image successfully pushed to ECR! ${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "Image URI: ${BLUE}${ECR_REPO}:latest${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next step:${NC} Run 'terraform apply' again to create the AgentCore Runtime"
|
||||
echo ""
|
||||
@@ -0,0 +1,614 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
Cross-platform frontend deployment script for Terraform deployments.
|
||||
|
||||
Deploys the Next.js frontend to AWS Amplify by:
|
||||
1. Fetching configuration from Terraform outputs
|
||||
2. Generating aws-exports.json
|
||||
3. Building the frontend
|
||||
4. Packaging and uploading to S3
|
||||
5. Triggering Amplify deployment
|
||||
|
||||
Requires: Python 3.8+, AWS CLI, npm, Node.js, Terraform
|
||||
No external Python dependencies - uses standard library only.
|
||||
|
||||
Usage:
|
||||
cd infra-terraform
|
||||
python scripts/deploy-frontend.py
|
||||
|
||||
# Or with pattern override
|
||||
python scripts/deploy-frontend.py --pattern langgraph-single-agent
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess # nosec B404 - subprocess used securely with explicit parameters
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
# Minimum Python version check
|
||||
if sys.version_info < (3, 8):
|
||||
print("Error: Python 3.8 or higher is required")
|
||||
sys.exit(1)
|
||||
|
||||
# Constants
|
||||
BRANCH_NAME = "main"
|
||||
NEXT_BUILD_DIR = "build"
|
||||
CLEANUP_FILES: list = []
|
||||
|
||||
|
||||
# --- Logging helpers ---
|
||||
|
||||
|
||||
def log_info(message: str) -> None:
|
||||
"""Print an info message."""
|
||||
print(f"ℹ {message}")
|
||||
|
||||
|
||||
def log_success(message: str) -> None:
|
||||
"""Print a success message."""
|
||||
print(f"✓ {message}")
|
||||
|
||||
|
||||
def log_error(message: str) -> None:
|
||||
"""Print an error message to stderr."""
|
||||
print(f"✗ {message}", file=sys.stderr)
|
||||
|
||||
|
||||
def log_warning(message: str) -> None:
|
||||
"""Print a warning message."""
|
||||
print(f"⚠ {message}")
|
||||
|
||||
|
||||
# --- Utility functions ---
|
||||
|
||||
|
||||
def cleanup() -> None:
|
||||
"""Remove temporary files created during deployment."""
|
||||
for filepath in CLEANUP_FILES:
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
log_info(f"Cleaned up {filepath}")
|
||||
|
||||
|
||||
def run_command(
|
||||
command: list,
|
||||
capture_output: bool = True,
|
||||
check: bool = True,
|
||||
cwd: Optional[str] = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Execute a command securely via subprocess.
|
||||
|
||||
Args:
|
||||
command: List of command arguments
|
||||
capture_output: Whether to capture stdout/stderr
|
||||
check: Whether to raise on non-zero exit
|
||||
cwd: Working directory for the command
|
||||
|
||||
Returns:
|
||||
CompletedProcess instance with command results
|
||||
"""
|
||||
return subprocess.run( # nosec B603 - command constructed from safe list
|
||||
command,
|
||||
capture_output=capture_output,
|
||||
text=True,
|
||||
check=check,
|
||||
shell=False,
|
||||
timeout=300,
|
||||
cwd=cwd,
|
||||
)
|
||||
|
||||
|
||||
def check_prerequisite(command: str) -> bool:
|
||||
"""
|
||||
Check if a command is available in PATH.
|
||||
|
||||
Args:
|
||||
command: Name of the command to check
|
||||
|
||||
Returns:
|
||||
True if command exists, False otherwise
|
||||
"""
|
||||
return shutil.which(command) is not None
|
||||
|
||||
|
||||
def parse_tfvars(tfvars_path: Path) -> Dict[str, str]:
|
||||
"""
|
||||
Parse terraform.tfvars using regex (no HCL parser dependency).
|
||||
|
||||
Args:
|
||||
tfvars_path: Path to terraform.tfvars file
|
||||
|
||||
Returns:
|
||||
Dictionary with parsed values
|
||||
"""
|
||||
config = {"backend_pattern": "strands-single-agent"}
|
||||
|
||||
if not tfvars_path.exists():
|
||||
return config
|
||||
|
||||
content = tfvars_path.read_text()
|
||||
|
||||
# Extract backend_pattern
|
||||
match = re.search(r'^backend_pattern\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
if match:
|
||||
config["backend_pattern"] = match.group(1)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_file_size_human(filepath: str) -> str:
|
||||
"""
|
||||
Get human-readable file size.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file
|
||||
|
||||
Returns:
|
||||
Human-readable size string (e.g., "1.5MB")
|
||||
"""
|
||||
size = os.path.getsize(filepath)
|
||||
for unit in ["B", "KB", "MB", "GB"]:
|
||||
if size < 1024:
|
||||
return f"{size:.1f}{unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f}TB"
|
||||
|
||||
|
||||
# --- Terraform output functions ---
|
||||
|
||||
|
||||
def get_terraform_outputs(terraform_dir: Path) -> Dict[str, str]:
|
||||
"""
|
||||
Fetch Terraform outputs via terraform output command.
|
||||
|
||||
Args:
|
||||
terraform_dir: Path to the Terraform directory
|
||||
|
||||
Returns:
|
||||
Dictionary mapping output keys to values
|
||||
"""
|
||||
result = run_command(["terraform", "output", "-json"], cwd=str(terraform_dir))
|
||||
|
||||
raw_outputs = json.loads(result.stdout)
|
||||
|
||||
# Flatten the Terraform output format (each value has a "value" key)
|
||||
outputs = {}
|
||||
for key, data in raw_outputs.items():
|
||||
if isinstance(data, dict) and "value" in data:
|
||||
outputs[key] = data["value"]
|
||||
else:
|
||||
outputs[key] = data
|
||||
|
||||
return outputs
|
||||
|
||||
|
||||
# --- AWS CLI wrappers ---
|
||||
|
||||
|
||||
def upload_to_s3(local_path: str, bucket: str, key: str) -> None:
|
||||
"""
|
||||
Upload a file to S3 via AWS CLI.
|
||||
|
||||
Args:
|
||||
local_path: Path to local file
|
||||
bucket: S3 bucket name
|
||||
key: S3 object key
|
||||
"""
|
||||
run_command(
|
||||
["aws", "s3", "cp", local_path, f"s3://{bucket}/{key}", "--no-progress"]
|
||||
)
|
||||
|
||||
|
||||
def start_amplify_deployment(app_id: str, branch: str, source_url: str) -> Dict:
|
||||
"""
|
||||
Start an Amplify deployment via AWS CLI.
|
||||
|
||||
Args:
|
||||
app_id: Amplify application ID
|
||||
branch: Branch name to deploy
|
||||
source_url: S3 URL of deployment package
|
||||
|
||||
Returns:
|
||||
Deployment response as dictionary
|
||||
"""
|
||||
result = run_command(
|
||||
[
|
||||
"aws",
|
||||
"amplify",
|
||||
"start-deployment",
|
||||
"--app-id",
|
||||
app_id,
|
||||
"--branch-name",
|
||||
branch,
|
||||
"--source-url",
|
||||
source_url,
|
||||
"--output",
|
||||
"json",
|
||||
]
|
||||
)
|
||||
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def get_amplify_job_status(app_id: str, branch: str, job_id: str) -> str:
|
||||
"""
|
||||
Get the status of an Amplify deployment job.
|
||||
|
||||
Args:
|
||||
app_id: Amplify application ID
|
||||
branch: Branch name
|
||||
job_id: Deployment job ID
|
||||
|
||||
Returns:
|
||||
Job status string
|
||||
"""
|
||||
result = run_command(
|
||||
[
|
||||
"aws",
|
||||
"amplify",
|
||||
"get-job",
|
||||
"--app-id",
|
||||
app_id,
|
||||
"--branch-name",
|
||||
branch,
|
||||
"--job-id",
|
||||
job_id,
|
||||
"--output",
|
||||
"json",
|
||||
]
|
||||
)
|
||||
|
||||
return json.loads(result.stdout)["job"]["summary"]["status"]
|
||||
|
||||
|
||||
def get_amplify_app_domain(app_id: str) -> str:
|
||||
"""
|
||||
Get the default domain for an Amplify app.
|
||||
|
||||
Args:
|
||||
app_id: Amplify application ID
|
||||
|
||||
Returns:
|
||||
Default domain string
|
||||
"""
|
||||
result = run_command(
|
||||
[
|
||||
"aws",
|
||||
"amplify",
|
||||
"get-app",
|
||||
"--app-id",
|
||||
app_id,
|
||||
"--query",
|
||||
"app.defaultDomain",
|
||||
"--output",
|
||||
"text",
|
||||
]
|
||||
)
|
||||
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
# --- Main deployment logic ---
|
||||
|
||||
|
||||
def generate_aws_exports(
|
||||
outputs: Dict[str, str], pattern: str, frontend_dir: Path
|
||||
) -> None:
|
||||
"""
|
||||
Generate aws-exports.json configuration file.
|
||||
|
||||
Args:
|
||||
outputs: Terraform outputs dictionary
|
||||
pattern: Agent pattern name
|
||||
frontend_dir: Path to frontend directory
|
||||
"""
|
||||
# Map Terraform output names to required values
|
||||
required_mappings = {
|
||||
"cognito_web_client_id": "client_id",
|
||||
"cognito_user_pool_id": "user_pool_id",
|
||||
"amplify_app_url": "app_url",
|
||||
"runtime_arn": "runtime_arn",
|
||||
"feedback_api_url": "feedback_api_url",
|
||||
"copilotkit_runtime_url": "copilotkit_runtime_url",
|
||||
}
|
||||
|
||||
missing = [k for k in required_mappings.keys() if k not in outputs]
|
||||
if missing:
|
||||
raise ValueError(f"Missing required Terraform outputs: {', '.join(missing)}")
|
||||
|
||||
# Get region from deployment_summary or default
|
||||
region = "us-east-1"
|
||||
if "deployment_summary" in outputs and isinstance(
|
||||
outputs["deployment_summary"], dict
|
||||
):
|
||||
region = outputs["deployment_summary"].get("region", region)
|
||||
|
||||
aws_exports = {
|
||||
"authority": f"https://cognito-idp.{region}.amazonaws.com/{outputs['cognito_user_pool_id']}",
|
||||
"client_id": outputs["cognito_web_client_id"],
|
||||
"redirect_uri": outputs["amplify_app_url"],
|
||||
"post_logout_redirect_uri": outputs["amplify_app_url"],
|
||||
"response_type": "code",
|
||||
"scope": "email openid profile",
|
||||
"automaticSilentRenew": True,
|
||||
"agentRuntimeArn": outputs["runtime_arn"],
|
||||
"awsRegion": region,
|
||||
"feedbackApiUrl": outputs["feedback_api_url"],
|
||||
"copilotKitRuntimeUrl": outputs["copilotkit_runtime_url"],
|
||||
"agentPattern": pattern,
|
||||
}
|
||||
|
||||
public_dir = frontend_dir / "public"
|
||||
public_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
output_path = public_dir / "aws-exports.json"
|
||||
output_path.write_text(json.dumps(aws_exports, indent=2))
|
||||
|
||||
log_success(f"Generated aws-exports.json at {output_path}")
|
||||
|
||||
|
||||
def create_deployment_zip(build_dir: Path, output_path: Path) -> None:
|
||||
"""
|
||||
Create a zip archive of the build directory.
|
||||
|
||||
Args:
|
||||
build_dir: Path to the build directory
|
||||
output_path: Path for the output zip file (without .zip extension)
|
||||
"""
|
||||
# shutil.make_archive adds .zip automatically
|
||||
shutil.make_archive(
|
||||
str(output_path.with_suffix("")), "zip", root_dir=str(build_dir)
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Deploy frontend to AWS Amplify using Terraform outputs",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python scripts/deploy-frontend.py
|
||||
python scripts/deploy-frontend.py --pattern langgraph-single-agent
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--pattern",
|
||||
"-p",
|
||||
type=str,
|
||||
help="Override agent pattern (default: from terraform.tfvars)",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
Main deployment function.
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
atexit.register(cleanup)
|
||||
args = parse_args()
|
||||
|
||||
# Determine paths
|
||||
script_dir = Path(__file__).parent.resolve()
|
||||
terraform_dir = script_dir.parent
|
||||
project_root = terraform_dir.parent
|
||||
frontend_dir = project_root / "frontend"
|
||||
tfvars_path = terraform_dir / "terraform.tfvars"
|
||||
|
||||
print()
|
||||
print("========================================")
|
||||
print(" Frontend Deployment (Terraform) ")
|
||||
print("========================================")
|
||||
print()
|
||||
|
||||
log_info("🚀 Starting frontend deployment process...")
|
||||
print()
|
||||
|
||||
# Validate prerequisites
|
||||
log_info("Validating prerequisites...")
|
||||
prerequisites = ["npm", "aws", "node", "terraform"]
|
||||
for prereq in prerequisites:
|
||||
if not check_prerequisite(prereq):
|
||||
log_error(f"{prereq} is not installed")
|
||||
return 1
|
||||
log_success("All prerequisites found")
|
||||
|
||||
# Verify AWS credentials are configured
|
||||
log_info("Verifying AWS credentials...")
|
||||
try:
|
||||
run_command(["aws", "sts", "get-caller-identity"], capture_output=True)
|
||||
log_success("AWS credentials configured")
|
||||
except subprocess.CalledProcessError:
|
||||
log_error("AWS credentials not configured or invalid")
|
||||
log_info("Run 'aws configure' to set up your AWS credentials")
|
||||
return 1
|
||||
|
||||
# Verify Terraform state exists
|
||||
if not (terraform_dir / "terraform.tfstate").exists():
|
||||
log_error("Terraform state not found. Run 'terraform apply' first.")
|
||||
return 1
|
||||
|
||||
# Fetch Terraform outputs
|
||||
log_info("Fetching configuration from Terraform outputs...")
|
||||
try:
|
||||
outputs = get_terraform_outputs(terraform_dir)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"Failed to fetch Terraform outputs: {e.stderr}")
|
||||
return 1
|
||||
except json.JSONDecodeError as e:
|
||||
log_error(f"Failed to parse Terraform outputs: {e}")
|
||||
return 1
|
||||
|
||||
# Validate required outputs
|
||||
app_id = outputs.get("amplify_app_id")
|
||||
deployment_bucket = outputs.get("amplify_staging_bucket")
|
||||
region = "us-east-1"
|
||||
|
||||
if "deployment_summary" in outputs and isinstance(
|
||||
outputs["deployment_summary"], dict
|
||||
):
|
||||
region = outputs["deployment_summary"].get("region", region)
|
||||
|
||||
if not app_id:
|
||||
log_error("Could not find amplify_app_id in Terraform outputs")
|
||||
return 1
|
||||
if not deployment_bucket:
|
||||
log_error("Could not find amplify_staging_bucket in Terraform outputs")
|
||||
return 1
|
||||
|
||||
log_success(f"App ID: {app_id}")
|
||||
log_success(f"Staging Bucket: {deployment_bucket}")
|
||||
log_success(f"Region: {region}")
|
||||
|
||||
# Get agent pattern
|
||||
if args.pattern:
|
||||
pattern = args.pattern
|
||||
else:
|
||||
tfvars = parse_tfvars(tfvars_path)
|
||||
pattern = tfvars.get("backend_pattern", "strands-single-agent")
|
||||
|
||||
log_info(f"Agent pattern: {pattern}")
|
||||
|
||||
# Generate aws-exports.json
|
||||
log_info("Generating aws-exports.json...")
|
||||
try:
|
||||
generate_aws_exports(outputs, pattern, frontend_dir)
|
||||
except ValueError as e:
|
||||
log_error(str(e))
|
||||
return 1
|
||||
|
||||
# Change to frontend directory
|
||||
os.chdir(frontend_dir)
|
||||
log_info(f"Working directory: {frontend_dir}")
|
||||
|
||||
# Install dependencies if needed
|
||||
node_modules = frontend_dir / "node_modules"
|
||||
package_json = frontend_dir / "package.json"
|
||||
|
||||
if not node_modules.exists() or (
|
||||
node_modules.exists()
|
||||
and package_json.exists()
|
||||
and package_json.stat().st_mtime > node_modules.stat().st_mtime
|
||||
):
|
||||
log_info("Installing dependencies...")
|
||||
try:
|
||||
run_command(["npm", "install"], capture_output=False)
|
||||
log_success("Dependencies installed")
|
||||
except subprocess.CalledProcessError:
|
||||
log_error("Failed to install dependencies")
|
||||
return 1
|
||||
else:
|
||||
log_success("Dependencies are up to date")
|
||||
|
||||
# Build frontend
|
||||
log_info("Building Next.js app...")
|
||||
try:
|
||||
run_command(["npm", "run", "build"], capture_output=False)
|
||||
log_success("Build completed")
|
||||
except subprocess.CalledProcessError:
|
||||
log_error("Build failed")
|
||||
return 1
|
||||
|
||||
# Verify build directory
|
||||
build_dir = frontend_dir / NEXT_BUILD_DIR
|
||||
if not build_dir.exists():
|
||||
log_error(f"Build directory '{NEXT_BUILD_DIR}' not found")
|
||||
return 1
|
||||
|
||||
# Copy aws-exports.json to build
|
||||
aws_exports_src = frontend_dir / "public" / "aws-exports.json"
|
||||
aws_exports_dst = build_dir / "aws-exports.json"
|
||||
shutil.copy2(aws_exports_src, aws_exports_dst)
|
||||
log_success("Added aws-exports.json to build directory")
|
||||
|
||||
# Create deployment zip
|
||||
log_info("Creating deployment package...")
|
||||
zip_path = frontend_dir / "amplify-deploy.zip"
|
||||
CLEANUP_FILES.append(str(zip_path))
|
||||
|
||||
create_deployment_zip(build_dir, zip_path)
|
||||
zip_size = get_file_size_human(str(zip_path))
|
||||
log_success(f"Package created ({zip_size})")
|
||||
|
||||
# Upload to S3
|
||||
s3_key = f"amplify-deploy-{int(time.time())}.zip"
|
||||
log_info(f"Uploading to S3 (s3://{deployment_bucket}/{s3_key})...")
|
||||
try:
|
||||
upload_to_s3(str(zip_path), deployment_bucket, s3_key)
|
||||
log_success("Upload completed")
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"S3 upload failed: {e.stderr}")
|
||||
return 1
|
||||
|
||||
# Start Amplify deployment
|
||||
log_info("Starting Amplify deployment...")
|
||||
source_url = f"s3://{deployment_bucket}/{s3_key}"
|
||||
|
||||
try:
|
||||
deployment = start_amplify_deployment(app_id, BRANCH_NAME, source_url)
|
||||
job_id = deployment["jobSummary"]["jobId"]
|
||||
log_success(f"Deployment initiated (Job ID: {job_id})")
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"Amplify deployment failed: {e.stderr}")
|
||||
return 1
|
||||
|
||||
# Poll deployment status
|
||||
log_info("Monitoring deployment status...")
|
||||
while True:
|
||||
try:
|
||||
status = get_amplify_job_status(app_id, BRANCH_NAME, job_id)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"Failed to get deployment status: {e.stderr}")
|
||||
return 1
|
||||
|
||||
print(f" Status: {status}")
|
||||
|
||||
if status == "SUCCEED":
|
||||
log_success("Deployment completed successfully!")
|
||||
break
|
||||
elif status in ("FAILED", "CANCELLED"):
|
||||
log_error(f"Deployment {status.lower()}")
|
||||
return 1
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
# Cleanup
|
||||
log_info("Cleaned up temporary files")
|
||||
|
||||
# Print final info
|
||||
print()
|
||||
log_info(f"S3 Package: s3://{deployment_bucket}/{s3_key}")
|
||||
log_info("Console: https://console.aws.amazon.com/amplify/apps")
|
||||
try:
|
||||
app_domain = get_amplify_app_domain(app_id)
|
||||
log_info(f"App URL: https://{BRANCH_NAME}.{app_domain}")
|
||||
except subprocess.CalledProcessError:
|
||||
log_warning("Could not retrieve app URL - check Amplify console")
|
||||
|
||||
print()
|
||||
print("========================================")
|
||||
print(" Frontend Deployment Complete! ")
|
||||
print("========================================")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
#!/bin/bash
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Deploy Frontend to AWS Amplify
|
||||
# =============================================================================
|
||||
#
|
||||
# This script deploys the Next.js frontend to Amplify using Terraform outputs.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/deploy-frontend.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# -p, --pattern Agent pattern (default: from terraform.tfvars or strands-single-agent)
|
||||
# -h, --help Show this help message
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default values
|
||||
PATTERN=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TERRAFORM_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
PROJECT_ROOT="$(dirname "$TERRAFORM_DIR")"
|
||||
FRONTEND_DIR="$PROJECT_ROOT/frontend"
|
||||
BRANCH_NAME="main"
|
||||
BUILD_DIR="build"
|
||||
|
||||
# Print usage
|
||||
usage() {
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -p, --pattern Agent pattern (default: from terraform.tfvars or strands-single-agent)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Use defaults from Terraform"
|
||||
echo " $0 -p langgraph-single-agent # Use LangGraph pattern"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-p|--pattern)
|
||||
PATTERN="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unknown option $1${NC}"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Helper functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}✗${NC} $1" >&2
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
# Get human-readable file size
|
||||
get_file_size() {
|
||||
local size=$(stat -f%z "$1" 2>/dev/null || stat --printf="%s" "$1" 2>/dev/null)
|
||||
if [[ $size -lt 1024 ]]; then
|
||||
echo "${size}B"
|
||||
elif [[ $size -lt 1048576 ]]; then
|
||||
echo "$(echo "scale=1; $size/1024" | bc)KB"
|
||||
else
|
||||
echo "$(echo "scale=1; $size/1048576" | bc)MB"
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} Frontend Deployment (Terraform) ${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
log_info "🚀 Starting frontend deployment process..."
|
||||
echo ""
|
||||
|
||||
# Validate prerequisites
|
||||
log_info "Validating prerequisites..."
|
||||
for cmd in npm aws node terraform jq; do
|
||||
if ! command -v $cmd &> /dev/null; then
|
||||
log_error "$cmd is not installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
log_success "All prerequisites found"
|
||||
|
||||
# Verify AWS credentials
|
||||
log_info "Verifying AWS credentials..."
|
||||
if ! aws sts get-caller-identity &> /dev/null; then
|
||||
log_error "AWS credentials not configured or invalid"
|
||||
log_info "Run 'aws configure' to set up your AWS credentials"
|
||||
exit 1
|
||||
fi
|
||||
log_success "AWS credentials configured"
|
||||
|
||||
# Change to Terraform directory to get outputs
|
||||
cd "$TERRAFORM_DIR"
|
||||
|
||||
# Verify Terraform state exists
|
||||
if [[ ! -f "terraform.tfstate" ]]; then
|
||||
log_error "Terraform state not found. Run 'terraform apply' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get Terraform outputs
|
||||
log_info "Fetching configuration from Terraform outputs..."
|
||||
|
||||
# Get all required outputs
|
||||
AMPLIFY_APP_ID=$(terraform output -raw amplify_app_id 2>/dev/null)
|
||||
STAGING_BUCKET=$(terraform output -raw amplify_staging_bucket 2>/dev/null)
|
||||
COGNITO_CLIENT_ID=$(terraform output -raw cognito_web_client_id 2>/dev/null)
|
||||
COGNITO_USER_POOL_ID=$(terraform output -raw cognito_user_pool_id 2>/dev/null)
|
||||
AMPLIFY_URL=$(terraform output -raw amplify_app_url 2>/dev/null)
|
||||
RUNTIME_ARN=$(terraform output -raw runtime_arn 2>/dev/null)
|
||||
FEEDBACK_API_URL=$(terraform output -raw feedback_api_url 2>/dev/null)
|
||||
COPILOTKIT_RUNTIME_URL=$(terraform output -raw copilotkit_runtime_url 2>/dev/null)
|
||||
AWS_REGION=$(terraform output -json deployment_summary 2>/dev/null | jq -r '.region')
|
||||
|
||||
# Validate required outputs
|
||||
if [[ -z "$AMPLIFY_APP_ID" ]]; then
|
||||
log_error "Could not find Amplify App ID in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$STAGING_BUCKET" ]]; then
|
||||
log_error "Could not find Staging Bucket in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$COGNITO_CLIENT_ID" ]]; then
|
||||
log_error "Could not find Cognito Client ID in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$RUNTIME_ARN" ]]; then
|
||||
log_error "Could not find Runtime ARN in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$COPILOTKIT_RUNTIME_URL" ]]; then
|
||||
log_error "Could not find CopilotKit runtime URL in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "App ID: $AMPLIFY_APP_ID"
|
||||
log_success "Staging Bucket: $STAGING_BUCKET"
|
||||
log_success "Region: $AWS_REGION"
|
||||
|
||||
# Get pattern from terraform.tfvars if not provided
|
||||
if [[ -z "$PATTERN" ]]; then
|
||||
TFVARS_FILE="$TERRAFORM_DIR/terraform.tfvars"
|
||||
if [[ -f "$TFVARS_FILE" ]]; then
|
||||
PATTERN=$(grep -E '^backend_pattern\s*=' "$TFVARS_FILE" | awk -F'"' '{print $2}')
|
||||
fi
|
||||
fi
|
||||
PATTERN="${PATTERN:-strands-single-agent}"
|
||||
log_info "Agent pattern: $PATTERN"
|
||||
|
||||
# Generate aws-exports.json
|
||||
log_info "Generating aws-exports.json..."
|
||||
|
||||
AWS_EXPORTS=$(cat <<EOF
|
||||
{
|
||||
"authority": "https://cognito-idp.${AWS_REGION}.amazonaws.com/${COGNITO_USER_POOL_ID}",
|
||||
"client_id": "${COGNITO_CLIENT_ID}",
|
||||
"redirect_uri": "${AMPLIFY_URL}",
|
||||
"post_logout_redirect_uri": "${AMPLIFY_URL}",
|
||||
"response_type": "code",
|
||||
"scope": "email openid profile",
|
||||
"automaticSilentRenew": true,
|
||||
"agentRuntimeArn": "${RUNTIME_ARN}",
|
||||
"awsRegion": "${AWS_REGION}",
|
||||
"feedbackApiUrl": "${FEEDBACK_API_URL}",
|
||||
"copilotKitRuntimeUrl": "${COPILOTKIT_RUNTIME_URL}",
|
||||
"agentPattern": "${PATTERN}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Write aws-exports.json to frontend/public
|
||||
mkdir -p "$FRONTEND_DIR/public"
|
||||
echo "$AWS_EXPORTS" > "$FRONTEND_DIR/public/aws-exports.json"
|
||||
log_success "Generated aws-exports.json at $FRONTEND_DIR/public/aws-exports.json"
|
||||
|
||||
# Change to frontend directory
|
||||
cd "$FRONTEND_DIR"
|
||||
log_info "Working directory: $FRONTEND_DIR"
|
||||
|
||||
# Install dependencies if needed
|
||||
if [[ ! -d "node_modules" ]] || [[ "package.json" -nt "node_modules" ]]; then
|
||||
log_info "Installing dependencies..."
|
||||
npm install
|
||||
log_success "Dependencies installed"
|
||||
else
|
||||
log_success "Dependencies are up to date"
|
||||
fi
|
||||
|
||||
# Build frontend
|
||||
log_info "Building Next.js app..."
|
||||
npm run build
|
||||
log_success "Build completed"
|
||||
|
||||
# Verify build directory exists
|
||||
if [[ ! -d "$BUILD_DIR" ]]; then
|
||||
log_error "Build directory '$BUILD_DIR' not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy aws-exports.json to build directory
|
||||
cp "$FRONTEND_DIR/public/aws-exports.json" "$FRONTEND_DIR/$BUILD_DIR/aws-exports.json"
|
||||
log_success "Added aws-exports.json to build directory"
|
||||
|
||||
# Create deployment zip
|
||||
log_info "Creating deployment package..."
|
||||
ZIP_FILE="$FRONTEND_DIR/amplify-deploy.zip"
|
||||
cd "$FRONTEND_DIR/$BUILD_DIR"
|
||||
zip -r "$ZIP_FILE" . -x "*.DS_Store" > /dev/null
|
||||
cd "$FRONTEND_DIR"
|
||||
|
||||
ZIP_SIZE=$(get_file_size "$ZIP_FILE")
|
||||
log_success "Package created ($ZIP_SIZE)"
|
||||
|
||||
# Upload to S3
|
||||
S3_KEY="amplify-deploy-$(date +%s).zip"
|
||||
log_info "Uploading to S3 (s3://${STAGING_BUCKET}/${S3_KEY})..."
|
||||
aws s3 cp "$ZIP_FILE" "s3://${STAGING_BUCKET}/${S3_KEY}" --no-progress
|
||||
log_success "Upload completed"
|
||||
|
||||
# Start Amplify deployment
|
||||
log_info "Starting Amplify deployment..."
|
||||
DEPLOYMENT=$(aws amplify start-deployment \
|
||||
--app-id "$AMPLIFY_APP_ID" \
|
||||
--branch-name "$BRANCH_NAME" \
|
||||
--source-url "s3://${STAGING_BUCKET}/${S3_KEY}" \
|
||||
--output json)
|
||||
|
||||
JOB_ID=$(echo "$DEPLOYMENT" | jq -r '.jobSummary.jobId')
|
||||
log_success "Deployment initiated (Job ID: $JOB_ID)"
|
||||
|
||||
# Monitor deployment status
|
||||
log_info "Monitoring deployment status..."
|
||||
while true; do
|
||||
STATUS=$(aws amplify get-job \
|
||||
--app-id "$AMPLIFY_APP_ID" \
|
||||
--branch-name "$BRANCH_NAME" \
|
||||
--job-id "$JOB_ID" \
|
||||
--query 'job.summary.status' \
|
||||
--output text)
|
||||
|
||||
echo " Status: $STATUS"
|
||||
|
||||
if [[ "$STATUS" == "SUCCEED" ]]; then
|
||||
log_success "Deployment completed successfully!"
|
||||
break
|
||||
elif [[ "$STATUS" == "FAILED" ]] || [[ "$STATUS" == "CANCELLED" ]]; then
|
||||
log_error "Deployment ${STATUS,,}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Cleanup
|
||||
rm -f "$ZIP_FILE"
|
||||
log_info "Cleaned up temporary files"
|
||||
|
||||
# Print final info
|
||||
echo ""
|
||||
log_info "S3 Package: s3://${STAGING_BUCKET}/${S3_KEY}"
|
||||
log_info "Console: https://console.aws.amazon.com/amplify/apps"
|
||||
|
||||
# Get app domain
|
||||
APP_DOMAIN=$(aws amplify get-app --app-id "$AMPLIFY_APP_ID" --query 'app.defaultDomain' --output text 2>/dev/null || echo "")
|
||||
if [[ -n "$APP_DOMAIN" ]]; then
|
||||
log_info "App URL: https://${BRANCH_NAME}.${APP_DOMAIN}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} Frontend Deployment Complete! ${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
@@ -0,0 +1,366 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
Test Agent - AgentCore Runtime CLI Tester (Terraform)
|
||||
|
||||
Tests the deployed agent using Terraform outputs. Authenticates via Cognito
|
||||
and invokes the agent with streaming response display.
|
||||
|
||||
Prerequisites:
|
||||
- Terraform infrastructure deployed (terraform apply)
|
||||
- AgentCore Runtime created
|
||||
- Dependencies: pip install boto3 requests colorama
|
||||
|
||||
Usage:
|
||||
cd infra-terraform
|
||||
python scripts/test-agent.py [message]
|
||||
|
||||
Examples:
|
||||
python scripts/test-agent.py 'Hello' # Test with message
|
||||
python scripts/test-agent.py # Uses default message
|
||||
"""
|
||||
|
||||
import getpass
|
||||
import json
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple
|
||||
from urllib.parse import quote
|
||||
|
||||
import boto3
|
||||
import requests
|
||||
from colorama import Fore, Style, init
|
||||
|
||||
# Initialize colorama for cross-platform colored output
|
||||
init()
|
||||
|
||||
|
||||
def log_info(msg: str) -> None:
|
||||
"""Print info message."""
|
||||
print(f"{Fore.BLUE}ℹ{Style.RESET_ALL} {msg}")
|
||||
|
||||
|
||||
def log_success(msg: str) -> None:
|
||||
"""Print success message."""
|
||||
print(f"{Fore.GREEN}✓{Style.RESET_ALL} {msg}")
|
||||
|
||||
|
||||
def log_error(msg: str) -> None:
|
||||
"""Print error message."""
|
||||
print(f"{Fore.RED}✗{Style.RESET_ALL} {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def get_terraform_outputs() -> Dict[str, str]:
|
||||
"""
|
||||
Get outputs from Terraform state.
|
||||
|
||||
Returns:
|
||||
Dict with runtime_arn, cognito_user_pool_id, cognito_web_client_id, region
|
||||
"""
|
||||
terraform_dir = Path(__file__).parent.parent
|
||||
|
||||
try:
|
||||
# Get individual outputs
|
||||
result = subprocess.run( # nosec B603, B607
|
||||
["terraform", "output", "-json"],
|
||||
cwd=terraform_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
outputs = json.loads(result.stdout)
|
||||
|
||||
return {
|
||||
"runtime_arn": outputs.get("runtime_arn", {}).get("value", ""),
|
||||
"cognito_user_pool_id": outputs.get("cognito_user_pool_id", {}).get(
|
||||
"value", ""
|
||||
),
|
||||
"cognito_web_client_id": outputs.get("cognito_web_client_id", {}).get(
|
||||
"value", ""
|
||||
),
|
||||
"region": outputs.get("deployment_summary", {})
|
||||
.get("value", {})
|
||||
.get("region", "us-east-1"),
|
||||
}
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"Failed to get Terraform outputs: {e.stderr}")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
log_error(f"Failed to parse Terraform outputs: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def authenticate_cognito(
|
||||
user_pool_id: str, client_id: str, username: str, password: str, region: str
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Authenticate with Cognito and return ID token.
|
||||
|
||||
Args:
|
||||
user_pool_id: Cognito User Pool ID
|
||||
client_id: Cognito Client ID
|
||||
username: User's email/username
|
||||
password: User's password
|
||||
region: AWS region
|
||||
|
||||
Returns:
|
||||
Tuple of (id_token, access_token)
|
||||
"""
|
||||
client = boto3.client("cognito-idp", region_name=region)
|
||||
|
||||
try:
|
||||
response = client.initiate_auth(
|
||||
AuthFlow="USER_PASSWORD_AUTH",
|
||||
ClientId=client_id,
|
||||
AuthParameters={
|
||||
"USERNAME": username,
|
||||
"PASSWORD": password,
|
||||
},
|
||||
)
|
||||
|
||||
# Handle NEW_PASSWORD_REQUIRED challenge
|
||||
if response.get("ChallengeName") == "NEW_PASSWORD_REQUIRED":
|
||||
log_info("New password required for first-time login")
|
||||
new_password = getpass.getpass("Set New Password: ")
|
||||
|
||||
response = client.respond_to_auth_challenge(
|
||||
ClientId=client_id,
|
||||
ChallengeName="NEW_PASSWORD_REQUIRED",
|
||||
ChallengeResponses={
|
||||
"USERNAME": username,
|
||||
"NEW_PASSWORD": new_password,
|
||||
},
|
||||
Session=response["Session"],
|
||||
)
|
||||
|
||||
auth_result = response.get("AuthenticationResult", {})
|
||||
id_token = auth_result.get("IdToken")
|
||||
access_token = auth_result.get("AccessToken")
|
||||
|
||||
if not id_token:
|
||||
log_error("Failed to get ID token from authentication response")
|
||||
sys.exit(1)
|
||||
|
||||
return id_token, access_token
|
||||
|
||||
except client.exceptions.NotAuthorizedException as e:
|
||||
log_error(f"Authentication failed: {e}")
|
||||
sys.exit(1)
|
||||
except client.exceptions.UserNotFoundException as e:
|
||||
log_error(f"User not found: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
log_error(f"Authentication error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def generate_session_id() -> str:
|
||||
"""Generate a session ID (must be >= 33 characters)."""
|
||||
return f"test-session-{int(time.time())}-{uuid.uuid4().hex[:16]}"
|
||||
|
||||
|
||||
def generate_trace_id() -> str:
|
||||
"""Generate X-Amzn-Trace-Id header value."""
|
||||
timestamp_hex = format(int(time.time()), "x")
|
||||
return f"1-{timestamp_hex}-{uuid.uuid4().hex[:24]}"
|
||||
|
||||
|
||||
def sanitize_user_id(email: str) -> str:
|
||||
"""
|
||||
Sanitize user ID for Memory API (replace @ and . with allowed characters).
|
||||
|
||||
Args:
|
||||
email: User's email address
|
||||
|
||||
Returns:
|
||||
Sanitized user ID matching regex [a-zA-Z0-9][a-zA-Z0-9-_/]*
|
||||
"""
|
||||
return email.replace("@", "-at-").replace(".", "-")
|
||||
|
||||
|
||||
def invoke_agent(
|
||||
runtime_arn: str,
|
||||
region: str,
|
||||
id_token: str,
|
||||
prompt: str,
|
||||
session_id: str,
|
||||
user_id: str,
|
||||
) -> None:
|
||||
"""
|
||||
Invoke the agent and stream the response.
|
||||
|
||||
Args:
|
||||
runtime_arn: AgentCore Runtime ARN
|
||||
region: AWS region
|
||||
id_token: Cognito ID token
|
||||
prompt: User's message
|
||||
session_id: Session ID for conversation
|
||||
user_id: Sanitized user ID
|
||||
"""
|
||||
# Build URL
|
||||
endpoint = f"https://bedrock-agentcore.{region}.amazonaws.com"
|
||||
encoded_arn = quote(runtime_arn, safe="")
|
||||
url = f"{endpoint}/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT"
|
||||
|
||||
# Generate trace ID
|
||||
trace_id = generate_trace_id()
|
||||
|
||||
# Headers
|
||||
headers = {
|
||||
"Authorization": f"Bearer {id_token}",
|
||||
"Content-Type": "application/json",
|
||||
"X-Amzn-Trace-Id": trace_id,
|
||||
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": session_id,
|
||||
}
|
||||
|
||||
# Payload
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"runtimeSessionId": session_id,
|
||||
"userId": user_id,
|
||||
}
|
||||
|
||||
log_info(f"Invoking agent at: {url}")
|
||||
print()
|
||||
print(f"{Fore.GREEN}Agent Response:{Style.RESET_ALL}")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Stream the response
|
||||
response = requests.post(
|
||||
url, headers=headers, json=payload, stream=True, timeout=120
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
log_error(f"HTTP {response.status_code}: {response.text}")
|
||||
return
|
||||
|
||||
# Process streaming response
|
||||
for line in response.iter_lines(decode_unicode=True):
|
||||
if line:
|
||||
# Parse SSE format
|
||||
if line.startswith("data: "):
|
||||
data = line[6:] # Remove "data: " prefix
|
||||
try:
|
||||
parsed = json.loads(data)
|
||||
# Check if parsed is a dict (not a string)
|
||||
if isinstance(parsed, dict):
|
||||
# Extract text from contentBlockDelta events
|
||||
if "event" in parsed:
|
||||
event = parsed["event"]
|
||||
if (
|
||||
isinstance(event, dict)
|
||||
and "contentBlockDelta" in event
|
||||
):
|
||||
delta = event["contentBlockDelta"].get("delta", {})
|
||||
if isinstance(delta, dict):
|
||||
text = delta.get("text", "")
|
||||
if text:
|
||||
print(text, end="", flush=True)
|
||||
# Check for final message
|
||||
if "message" in parsed:
|
||||
message = parsed["message"]
|
||||
if isinstance(message, dict):
|
||||
content = message.get("content", [])
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and "text" in block
|
||||
):
|
||||
# Don't print final message - we already streamed it
|
||||
pass
|
||||
except json.JSONDecodeError:
|
||||
# Not JSON, skip internal debug strings
|
||||
pass
|
||||
|
||||
print()
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log_error(f"Connection error: {e}")
|
||||
except requests.exceptions.Timeout:
|
||||
log_error("Request timed out")
|
||||
except Exception as e:
|
||||
log_error(f"Error: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
print()
|
||||
print(f"{Fore.BLUE}========================================{Style.RESET_ALL}")
|
||||
print(f"{Fore.BLUE} Agent Test (Terraform - Python) {Style.RESET_ALL}")
|
||||
print(f"{Fore.BLUE}========================================{Style.RESET_ALL}")
|
||||
print()
|
||||
|
||||
# Get message from args or use default
|
||||
message = sys.argv[1] if len(sys.argv) > 1 else "Hello! What are you capable of?"
|
||||
|
||||
# Get Terraform outputs
|
||||
log_info("Fetching configuration from Terraform outputs...")
|
||||
outputs = get_terraform_outputs()
|
||||
|
||||
runtime_arn = outputs["runtime_arn"]
|
||||
if not runtime_arn:
|
||||
log_error("Could not find Runtime ARN in Terraform outputs")
|
||||
sys.exit(1)
|
||||
|
||||
log_success(f"Runtime ARN: {runtime_arn}")
|
||||
log_success(f"Region: {outputs['region']}")
|
||||
|
||||
# Get credentials
|
||||
print()
|
||||
log_info("Enter your Cognito credentials (admin user created during deployment)")
|
||||
email = input("Email: ").strip()
|
||||
password = getpass.getpass("Password: ")
|
||||
|
||||
# Authenticate
|
||||
log_info("Authenticating with Cognito...")
|
||||
id_token, _ = authenticate_cognito(
|
||||
user_pool_id=outputs["cognito_user_pool_id"],
|
||||
client_id=outputs["cognito_web_client_id"],
|
||||
username=email,
|
||||
password=password,
|
||||
region=outputs["region"],
|
||||
)
|
||||
log_success("Authentication successful!")
|
||||
|
||||
# Generate session ID
|
||||
session_id = generate_session_id()
|
||||
|
||||
# Sanitize user ID
|
||||
user_id = sanitize_user_id(email)
|
||||
|
||||
# Invoke agent
|
||||
print()
|
||||
log_info("Sending message to agent...")
|
||||
print(f"{Fore.CYAN}You:{Style.RESET_ALL} {message}")
|
||||
print()
|
||||
|
||||
start_time = time.time()
|
||||
invoke_agent(
|
||||
runtime_arn=runtime_arn,
|
||||
region=outputs["region"],
|
||||
id_token=id_token,
|
||||
prompt=message,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
print()
|
||||
print(f"{Fore.CYAN}[Completed in {elapsed:.2f}s]{Style.RESET_ALL}")
|
||||
print()
|
||||
print(f"{Fore.GREEN}========================================{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN} Agent Test Complete! {Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}========================================{Style.RESET_ALL}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,64 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Terraform Configuration Example
|
||||
# =============================================================================
|
||||
# Copy this file to terraform.tfvars and customize the values.
|
||||
#
|
||||
# Region: Set via AWS_REGION environment variable or AWS CLI profile.
|
||||
# Tags: Add custom tags via the provider's default_tags block in main.tf.
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Required Variables
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Base name for all resources
|
||||
stack_name_base = "fast-demo-stack"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Optional: Admin User
|
||||
# -----------------------------------------------------------------------------
|
||||
# Set to automatically create an admin user and email credentials
|
||||
# If not provided (null), you'll need to manually create users via AWS Console
|
||||
|
||||
admin_user_email = null # Example: "admin@example.com"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Backend Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Agent pattern to deploy
|
||||
# Available patterns: strands-single-agent, langgraph-single-agent, claude-agent-sdk-single-agent, claude-agent-sdk-multi-agent
|
||||
backend_pattern = "strands-single-agent"
|
||||
|
||||
# Deployment type for AgentCore Runtime
|
||||
# "docker" - Container image via ECR (requires Docker + separate build step)
|
||||
# Deployment: terraform apply -> ./scripts/build-and-push-image.sh -> terraform apply
|
||||
# "zip" - Python package via S3 (no Docker required, single-step deploy)
|
||||
# Deployment: terraform apply (packages and deploys automatically)
|
||||
backend_deployment_type = "docker"
|
||||
|
||||
# Network mode for AgentCore Runtime
|
||||
# PUBLIC: Runtime is accessible over the public internet (default)
|
||||
# VPC: Runtime is deployed into a user-provided VPC for private network isolation
|
||||
backend_network_mode = "PUBLIC"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# VPC Configuration (Required only if backend_network_mode = "VPC")
|
||||
# -----------------------------------------------------------------------------
|
||||
# Uncomment and configure if using VPC network mode.
|
||||
# Your VPC must have the necessary VPC endpoints for AWS services.
|
||||
# See README.md VPC Deployment Mode section for full requirements.
|
||||
#
|
||||
# Security groups are optional. If omitted, a default security group is created
|
||||
# with HTTPS (443) self-referencing ingress and all-traffic egress.
|
||||
#
|
||||
# IMPORTANT: AgentCore Runtime is only available in specific Availability Zones
|
||||
# per region. Ensure your subnets are in supported AZs. See:
|
||||
# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-vpc.html#agentcore-supported-azs
|
||||
|
||||
# backend_vpc_id = "vpc-xxxxxxxxxxxxxxxxx"
|
||||
# backend_vpc_subnet_ids = ["subnet-xxxxxxxxxxxxxxxxx", "subnet-yyyyyyyyyyyyyyyyy"]
|
||||
# backend_vpc_security_group_ids = ["sg-xxxxxxxxxxxxxxxxx"] # Optional
|
||||
@@ -0,0 +1,91 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Required Variables
|
||||
# =============================================================================
|
||||
|
||||
variable "stack_name_base" {
|
||||
description = "Base name for all resources. Used as prefix for resource naming."
|
||||
type = string
|
||||
|
||||
validation {
|
||||
condition = can(regex("^[a-z][a-z0-9-]{2,34}$", var.stack_name_base))
|
||||
error_message = "Stack name must start with a lowercase letter, be 3-35 characters, and contain only lowercase alphanumeric characters and hyphens."
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Optional Variables - Admin User
|
||||
# =============================================================================
|
||||
|
||||
variable "admin_user_email" {
|
||||
description = "Email address for the admin user. If provided, creates an admin user and sends credentials via email. Set to null to skip admin user creation."
|
||||
type = string
|
||||
default = null
|
||||
|
||||
validation {
|
||||
condition = var.admin_user_email == null || can(regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", var.admin_user_email))
|
||||
error_message = "Must be a valid email address or null."
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Backend Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "backend_pattern" {
|
||||
description = "Agent pattern to deploy. Available patterns: strands-single-agent, langgraph-single-agent, claude-agent-sdk-single-agent, claude-agent-sdk-multi-agent"
|
||||
type = string
|
||||
default = "strands-single-agent"
|
||||
|
||||
validation {
|
||||
condition = contains(["strands-single-agent", "langgraph-single-agent", "claude-agent-sdk-single-agent", "claude-agent-sdk-multi-agent"], var.backend_pattern)
|
||||
error_message = "Backend pattern must be one of: strands-single-agent, langgraph-single-agent, claude-agent-sdk-single-agent, claude-agent-sdk-multi-agent."
|
||||
}
|
||||
}
|
||||
|
||||
variable "backend_deployment_type" {
|
||||
description = "Deployment type for AgentCore Runtime. 'docker' uses ECR container image (requires Docker + separate build step). 'zip' uses S3 Python package (no Docker required, single-step deploy)."
|
||||
type = string
|
||||
default = "docker"
|
||||
|
||||
validation {
|
||||
condition = contains(["docker", "zip"], var.backend_deployment_type)
|
||||
error_message = "Deployment type must be 'docker' or 'zip'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "backend_network_mode" {
|
||||
description = "Network mode for AgentCore Runtime. PUBLIC (default) uses public internet. VPC deploys into a user-provided VPC for private network isolation."
|
||||
type = string
|
||||
default = "PUBLIC"
|
||||
|
||||
validation {
|
||||
condition = contains(["PUBLIC", "VPC"], var.backend_network_mode)
|
||||
error_message = "Network mode must be 'PUBLIC' or 'VPC'."
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# VPC Configuration (Required if backend_network_mode = VPC)
|
||||
# =============================================================================
|
||||
|
||||
variable "backend_vpc_id" {
|
||||
description = "VPC ID for VPC network mode. Required when backend_network_mode is 'VPC'."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "backend_vpc_subnet_ids" {
|
||||
description = "List of subnet IDs for VPC network mode. Required when backend_network_mode is 'VPC'. Subnets should be in at least two Availability Zones."
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "backend_vpc_security_group_ids" {
|
||||
description = "List of security group IDs for VPC network mode. Optional when backend_network_mode is 'VPC'. If omitted, a default security group is created with HTTPS self-referencing ingress and all-traffic egress."
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 6.35.1"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.5.0"
|
||||
}
|
||||
archive = {
|
||||
source = "hashicorp/archive"
|
||||
version = ">= 2.4.0"
|
||||
}
|
||||
null = {
|
||||
source = "hashicorp/null"
|
||||
version = ">= 3.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user