chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -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]
}
@@ -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"
}
}
}