Implement reversible PII de-identification round trip (T1–T7)
Build the AWS side of the HNCB demo end to end (region ap-southeast-1): - T1 /tokenize + T2 /restore: Lambdas behind a public API Gateway (shared-secret auth), Presidio detection, random per-request tokens, DynamoDB vault; overlap resolution so a ROC ID stays TW_ROC_ID. - T3: Presidio made private (SG-locked to the tokenize Lambda in-VPC; DynamoDB gateway endpoint); only /tokenize + /restore are public. - T4: RAG Lambda registered as an MCP tool on an AgentCore Gateway (AWS_IAM/SigV4); agentcore_setup.sh + a SigV4 MCP invoke test. - T5: Strands agent deployed to AgentCore Runtime; SigV4 gateway auth, apac inference profile, pinned deps. - T6: advisor UI on S3+CloudFront with a Fusion-less demo orchestrator (/demo) chaining tokenize -> runtime -> restore. - T7: README runbook + trace check; teardown deletes gateway/runtime/memory/ECR. Verified live: the cloud AgentCore/Bedrock trace shows only tokens, never the real name. Secrets stay in gitignored local.auto.tfvars. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
46
terraform/.terraform.lock.hcl
generated
Normal file
46
terraform/.terraform.lock.hcl
generated
Normal file
@@ -0,0 +1,46 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/archive" {
|
||||
version = "2.8.0"
|
||||
constraints = "~> 2.0"
|
||||
hashes = [
|
||||
"h1:WB6H5ksIZiyq1lQlD/PWeh+tn4FLsbSjVnRW3+4xe2Y=",
|
||||
"zh:0d14713fdc259fb377d0b899ad3c650a34194bd52194c863303ef22a65a580e2",
|
||||
"zh:369b56040c7a8085d04e7e8ffac1e2b321a3170e502f788819bc34b868ec016f",
|
||||
"zh:4d1a3b983ed6af5a52bfe12794674ae55cbadfa6021b37106ade68b433ad216a",
|
||||
"zh:5c547549e26e083573c78a966ca68ce6d7df6bb8f3948f66a575f07da46b74ea",
|
||||
"zh:6de093e62a975eb19a5e3017ce38e6e3cb639c17b79648d2000e0a8348f0e997",
|
||||
"zh:7267936c2cdbc448efeb594d73e6b56a53d6a7ae14fe88cdd2a4133adc3302f0",
|
||||
"zh:7482f023050ed426b4b45116e1761643bc33b1fd4ce4a6fab207ae2571f35940",
|
||||
"zh:76bbd93b234e5a2927d98b511d86565700f549b570871a194c35f944b96cefb7",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:c6afc4bc1f002bac9c173007dd4da05fde788cd14c2916089f958c33fedb0dfa",
|
||||
"zh:d3ba40bd806a3a08e9237dece679193c99afb2085de6b45d7f5d1f673cfcd368",
|
||||
"zh:e1ad7ded53ecd6f0e5b473a3b44eae2b2e885653a56050ab583d387332be02e4",
|
||||
"zh:e93e78575ce82be6084cc153c24ba8f385dc8d6880888ee66e918460c870953d",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "5.100.0"
|
||||
constraints = "~> 5.0"
|
||||
hashes = [
|
||||
"h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=",
|
||||
"zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644",
|
||||
"zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2",
|
||||
"zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274",
|
||||
"zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b",
|
||||
"zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862",
|
||||
"zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342",
|
||||
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||
"zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93",
|
||||
"zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2",
|
||||
"zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e",
|
||||
"zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421",
|
||||
"zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4",
|
||||
"zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9",
|
||||
"zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9",
|
||||
"zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70",
|
||||
]
|
||||
}
|
||||
41
terraform/agentcore.tf
Normal file
41
terraform/agentcore.tf
Normal file
@@ -0,0 +1,41 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# AgentCore Gateway support (task T4). Terraform's AgentCore coverage lags, so the
|
||||
# Gateway + target themselves are created by scripts/agentcore_setup.sh via
|
||||
# `aws bedrock-agentcore-control`. What IS declarative here is the IAM role the
|
||||
# Gateway assumes to invoke the RAG tool Lambda -- kept in Terraform so it is
|
||||
# tagged, auditable, and torn down with the rest of the stack.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_iam_role" "agentcore_gateway" {
|
||||
name = "${local.name}-gateway-role"
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Principal = { Service = "bedrock-agentcore.amazonaws.com" }
|
||||
Action = "sts:AssumeRole"
|
||||
Condition = {
|
||||
StringEquals = { "aws:SourceAccount" = data.aws_caller_identity.current.account_id }
|
||||
}
|
||||
}]
|
||||
})
|
||||
tags = local.cloud_tag # the Gateway is a cloud-zone component
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "agentcore_gateway" {
|
||||
name = "${local.name}-gateway-policy"
|
||||
role = aws_iam_role.agentcore_gateway.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
# The Gateway only needs to invoke the one RAG tool Lambda.
|
||||
Effect = "Allow"
|
||||
Action = ["lambda:InvokeFunction"]
|
||||
Resource = [aws_lambda_function.rag.arn]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
output "gateway_role_arn" { value = aws_iam_role.agentcore_gateway.arn }
|
||||
@@ -1,9 +1,10 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hosting for the Presidio detector only. Fusion is a shared SaaS instance and is
|
||||
# NOT deployed here. Because Fusion SaaS calls the detector over the internet, the
|
||||
# analyzer port is public in this demo (lock it down / add auth for anything real;
|
||||
# better: fold detection behind the /tokenize endpoint so only that is public --
|
||||
# see CLAUDE.md task T3).
|
||||
# NOT deployed here. The detector is PRIVATE (task T3): its port 5001 is reachable
|
||||
# only from the /tokenize Lambda's security group, not the internet. The only
|
||||
# public surface is the API Gateway (/tokenize + /restore). The task keeps a public
|
||||
# IP purely so Fargate can pull the image from ECR (there's no NAT); inbound is
|
||||
# still SG-locked, so nothing is reachable from outside the VPC.
|
||||
#
|
||||
# For a laptop-only rehearsal you can skip this file and run Presidio via docker.
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -17,21 +18,42 @@ data "aws_subnets" "default" {
|
||||
}
|
||||
}
|
||||
|
||||
# Route tables of the default VPC -- needed to attach the DynamoDB gateway endpoint
|
||||
# so the (now VPC-attached) tokenize Lambda can still reach the vault/customers tables.
|
||||
data "aws_route_tables" "default" {
|
||||
vpc_id = data.aws_vpc.default.id
|
||||
}
|
||||
|
||||
resource "aws_ecs_cluster" "this" {
|
||||
name = "${local.name}-cluster"
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
# SG the tokenize Lambda runs in. No ingress; egress open so it can reach Presidio
|
||||
# (5001) and the DynamoDB endpoint. Presidio trusts this SG (below).
|
||||
resource "aws_security_group" "tokenize_lambda" {
|
||||
name = "${local.name}-tokenize-lambda-sg"
|
||||
description = "tokenize Lambda ENIs"
|
||||
vpc_id = data.aws_vpc.default.id
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_security_group" "svc" {
|
||||
name = "${local.name}-svc-sg"
|
||||
description = "Demo services SG"
|
||||
vpc_id = data.aws_vpc.default.id
|
||||
ingress {
|
||||
description = "Presidio analyzer (public for SaaS Fusion demo - restrict for real use)"
|
||||
from_port = 5001
|
||||
to_port = 5001
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
description = "Presidio analyzer -- only the tokenize Lambda may call it"
|
||||
from_port = 5001
|
||||
to_port = 5001
|
||||
protocol = "tcp"
|
||||
security_groups = [aws_security_group.tokenize_lambda.id]
|
||||
}
|
||||
egress {
|
||||
from_port = 0
|
||||
@@ -42,6 +64,15 @@ resource "aws_security_group" "svc" {
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
# Gateway endpoint so VPC-attached Lambdas reach DynamoDB without a NAT / public route.
|
||||
resource "aws_vpc_endpoint" "dynamodb" {
|
||||
vpc_id = data.aws_vpc.default.id
|
||||
service_name = "com.amazonaws.${var.region}.dynamodb"
|
||||
vpc_endpoint_type = "Gateway"
|
||||
route_table_ids = data.aws_route_tables.default.ids
|
||||
tags = merge(local.onprem_tag, { Name = "${local.name}-dynamodb" })
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "ecs_exec" {
|
||||
name = "${local.name}-ecs-exec"
|
||||
assume_role_policy = jsonencode({
|
||||
@@ -64,17 +95,37 @@ resource "aws_ecs_task_definition" "presidio" {
|
||||
requires_compatibilities = ["FARGATE"]
|
||||
network_mode = "awsvpc"
|
||||
cpu = 512
|
||||
memory = 1024
|
||||
execution_role_arn = aws_iam_role.ecs_exec.arn
|
||||
# zh_core_web_lg (~600MB) plus presidio/spacy needs headroom; 1GB OOMs on load.
|
||||
memory = 2048
|
||||
execution_role_arn = aws_iam_role.ecs_exec.arn
|
||||
# Built natively on Apple Silicon (podman) -> run on Fargate Graviton.
|
||||
runtime_platform {
|
||||
cpu_architecture = "ARM64"
|
||||
operating_system_family = "LINUX"
|
||||
}
|
||||
container_definitions = jsonencode([{
|
||||
name = "presidio"
|
||||
image = var.presidio_image_uri
|
||||
essential = true
|
||||
portMappings = [{ containerPort = 5001 }]
|
||||
logConfiguration = {
|
||||
logDriver = "awslogs"
|
||||
options = {
|
||||
"awslogs-group" = aws_cloudwatch_log_group.presidio.name
|
||||
"awslogs-region" = var.region
|
||||
"awslogs-stream-prefix" = "presidio"
|
||||
}
|
||||
}
|
||||
}])
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_log_group" "presidio" {
|
||||
name = "/ecs/${local.name}-presidio"
|
||||
retention_in_days = 7
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_ecs_service" "presidio" {
|
||||
name = "${local.name}-presidio"
|
||||
cluster = aws_ecs_cluster.this.id
|
||||
|
||||
296
terraform/gateway.tf
Normal file
296
terraform/gateway.tf
Normal file
@@ -0,0 +1,296 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public HTTPS endpoints that Fusion SaaS calls: POST /tokenize (T1) and
|
||||
# POST /restore (T2), both on one API Gateway HTTP API. Fusion is shared SaaS, so
|
||||
# it can only reach PUBLIC HTTPS with auth -- each handler checks a shared secret
|
||||
# (tokenize_api_key, sent as x-api-key / bearer). These live in the "on-prem" zone
|
||||
# (tags): they own the reversible token<->PII map, which must never leave it.
|
||||
# (API Gateway, not a Lambda Function URL: this org's SCP blocks unauthenticated
|
||||
# Function URLs.)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
data "archive_file" "tokenize" {
|
||||
type = "zip"
|
||||
source_dir = "${path.module}/../gateway_api/tokenize"
|
||||
output_path = "${path.module}/tokenize_lambda.zip"
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "tokenize" {
|
||||
name = "${local.name}-tokenize-role"
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Principal = { Service = "lambda.amazonaws.com" }
|
||||
Action = "sts:AssumeRole"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "tokenize" {
|
||||
name = "${local.name}-tokenize-policy"
|
||||
role = aws_iam_role.tokenize.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
# Mint into the vault; scan customers for best-effort PERSON -> customer_id.
|
||||
Effect = "Allow"
|
||||
Action = ["dynamodb:PutItem"]
|
||||
Resource = [aws_dynamodb_table.vault.arn]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = ["dynamodb:Scan"]
|
||||
Resource = [aws_dynamodb_table.customers.arn]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
|
||||
Resource = "arn:aws:logs:*:*:*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_lambda_function" "tokenize" {
|
||||
function_name = "${local.name}-tokenize"
|
||||
role = aws_iam_role.tokenize.arn
|
||||
runtime = "python3.12"
|
||||
handler = "handler.lambda_handler"
|
||||
filename = data.archive_file.tokenize.output_path
|
||||
source_code_hash = data.archive_file.tokenize.output_base64sha256
|
||||
timeout = 15
|
||||
environment {
|
||||
variables = {
|
||||
VAULT_TABLE = aws_dynamodb_table.vault.name
|
||||
CUSTOMERS_TABLE = aws_dynamodb_table.customers.name
|
||||
PRESIDIO_URL = var.presidio_url
|
||||
TOKENIZE_API_KEY = var.tokenize_api_key
|
||||
VAULT_TTL_SECONDS = "3600"
|
||||
}
|
||||
}
|
||||
# In-VPC so it can reach the now-private Presidio over 5001. DynamoDB is reached
|
||||
# via the gateway endpoint (see ecs.tf); no NAT needed.
|
||||
vpc_config {
|
||||
subnet_ids = data.aws_subnets.default.ids
|
||||
security_group_ids = [aws_security_group.tokenize_lambda.id]
|
||||
}
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
# VPC-attached Lambdas need ENI management permissions.
|
||||
resource "aws_iam_role_policy_attachment" "tokenize_vpc" {
|
||||
role = aws_iam_role.tokenize.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
|
||||
}
|
||||
|
||||
# Public HTTPS front door. We use an API Gateway HTTP API rather than a Lambda
|
||||
# Function URL because this org's SCP blocks unauthenticated (auth_type=NONE)
|
||||
# Function URLs. The API is public; auth is the shared secret checked in-handler
|
||||
# (Fusion sends it as x-api-key / bearer). T2 adds a POST /restore route here.
|
||||
resource "aws_apigatewayv2_api" "gateway" {
|
||||
name = "${local.name}-gateway"
|
||||
protocol_type = "HTTP"
|
||||
# The static UI (CloudFront origin) calls /demo cross-origin -> allow CORS.
|
||||
cors_configuration {
|
||||
allow_origins = ["*"]
|
||||
allow_methods = ["POST", "OPTIONS"]
|
||||
allow_headers = ["content-type", "x-api-key"]
|
||||
}
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_integration" "tokenize" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
integration_type = "AWS_PROXY"
|
||||
integration_uri = aws_lambda_function.tokenize.invoke_arn
|
||||
payload_format_version = "2.0"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "tokenize" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
route_key = "POST /tokenize"
|
||||
target = "integrations/${aws_apigatewayv2_integration.tokenize.id}"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_stage" "default" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
name = "$default"
|
||||
auto_deploy = true
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_lambda_permission" "tokenize_apigw" {
|
||||
statement_id = "AllowApiGatewayInvoke"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.tokenize.function_name
|
||||
principal = "apigateway.amazonaws.com"
|
||||
source_arn = "${aws_apigatewayv2_api.gateway.execution_arn}/*/*"
|
||||
}
|
||||
|
||||
########################################
|
||||
# T2: /restore -- egress re-identification (vault lookup by session_id)
|
||||
########################################
|
||||
data "archive_file" "restore" {
|
||||
type = "zip"
|
||||
source_dir = "${path.module}/../gateway_api/restore"
|
||||
output_path = "${path.module}/restore_lambda.zip"
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "restore" {
|
||||
name = "${local.name}-restore-role"
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Principal = { Service = "lambda.amazonaws.com" }
|
||||
Action = "sts:AssumeRole"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "restore" {
|
||||
name = "${local.name}-restore-policy"
|
||||
role = aws_iam_role.restore.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
# Read-only: look up this session's tokens to re-attach identity.
|
||||
Effect = "Allow"
|
||||
Action = ["dynamodb:Scan"]
|
||||
Resource = [aws_dynamodb_table.vault.arn]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
|
||||
Resource = "arn:aws:logs:*:*:*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_lambda_function" "restore" {
|
||||
function_name = "${local.name}-restore"
|
||||
role = aws_iam_role.restore.arn
|
||||
runtime = "python3.12"
|
||||
handler = "handler.lambda_handler"
|
||||
filename = data.archive_file.restore.output_path
|
||||
source_code_hash = data.archive_file.restore.output_base64sha256
|
||||
timeout = 15
|
||||
environment {
|
||||
variables = {
|
||||
VAULT_TABLE = aws_dynamodb_table.vault.name
|
||||
TOKENIZE_API_KEY = var.tokenize_api_key
|
||||
}
|
||||
}
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_integration" "restore" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
integration_type = "AWS_PROXY"
|
||||
integration_uri = aws_lambda_function.restore.invoke_arn
|
||||
payload_format_version = "2.0"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "restore" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
route_key = "POST /restore"
|
||||
target = "integrations/${aws_apigatewayv2_integration.restore.id}"
|
||||
}
|
||||
|
||||
resource "aws_lambda_permission" "restore_apigw" {
|
||||
statement_id = "AllowApiGatewayInvoke"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.restore.function_name
|
||||
principal = "apigateway.amazonaws.com"
|
||||
source_arn = "${aws_apigatewayv2_api.gateway.execution_arn}/*/*"
|
||||
}
|
||||
|
||||
########################################
|
||||
# T6: /demo -- Fusion-less orchestrator the static UI calls (tokenize->agent->restore)
|
||||
########################################
|
||||
data "archive_file" "orchestrator" {
|
||||
type = "zip"
|
||||
source_dir = "${path.module}/../gateway_api/orchestrator"
|
||||
output_path = "${path.module}/orchestrator_lambda.zip"
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "orchestrator" {
|
||||
name = "${local.name}-orchestrator-role"
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Principal = { Service = "lambda.amazonaws.com" }
|
||||
Action = "sts:AssumeRole"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "orchestrator" {
|
||||
name = "${local.name}-orchestrator-policy"
|
||||
role = aws_iam_role.orchestrator.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
# Invoke the AgentCore Runtime. Scoped to the configured runtime ARN when set.
|
||||
Effect = "Allow"
|
||||
Action = ["bedrock-agentcore:InvokeAgentRuntime"]
|
||||
Resource = var.agent_runtime_arn != "" ? [var.agent_runtime_arn, "${var.agent_runtime_arn}/*"] : ["*"]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
|
||||
Resource = "arn:aws:logs:*:*:*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_lambda_function" "orchestrator" {
|
||||
function_name = "${local.name}-orchestrator"
|
||||
role = aws_iam_role.orchestrator.arn
|
||||
runtime = "python3.12"
|
||||
handler = "handler.lambda_handler"
|
||||
filename = data.archive_file.orchestrator.output_path
|
||||
source_code_hash = data.archive_file.orchestrator.output_base64sha256
|
||||
timeout = 120
|
||||
environment {
|
||||
variables = {
|
||||
REGION = var.region
|
||||
TOKENIZE_URL = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/tokenize"
|
||||
RESTORE_URL = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/restore"
|
||||
TOKENIZE_API_KEY = var.tokenize_api_key
|
||||
AGENT_RUNTIME_ARN = var.agent_runtime_arn
|
||||
}
|
||||
}
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_integration" "orchestrator" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
integration_type = "AWS_PROXY"
|
||||
integration_uri = aws_lambda_function.orchestrator.invoke_arn
|
||||
payload_format_version = "2.0"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "orchestrator" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
route_key = "POST /demo"
|
||||
target = "integrations/${aws_apigatewayv2_integration.orchestrator.id}"
|
||||
}
|
||||
|
||||
resource "aws_lambda_permission" "orchestrator_apigw" {
|
||||
statement_id = "AllowApiGatewayInvoke"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.orchestrator.function_name
|
||||
principal = "apigateway.amazonaws.com"
|
||||
source_arn = "${aws_apigatewayv2_api.gateway.execution_arn}/*/*"
|
||||
}
|
||||
|
||||
output "tokenize_url" { value = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/tokenize" }
|
||||
output "restore_url" { value = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/restore" }
|
||||
output "demo_url" { value = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/demo" }
|
||||
@@ -8,6 +8,14 @@ terraform {
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
|
||||
# Every taggable resource created by this stack carries this tag, on top of any
|
||||
# per-resource tags (e.g. the on-prem/cloud Zone tags).
|
||||
default_tags {
|
||||
tags = {
|
||||
Owner = "conan hncb demo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
|
||||
85
terraform/ui.tf
Normal file
85
terraform/ui.tf
Normal file
@@ -0,0 +1,85 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# T6: host the advisor UI on S3 + CloudFront. The bucket is private; CloudFront
|
||||
# reaches it via Origin Access Control. index.html is static and committed; the
|
||||
# live endpoint is injected via a generated config.js so we never bake an
|
||||
# ephemeral URL into the repo.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "aws_s3_bucket" "ui" {
|
||||
bucket_prefix = "${local.name}-ui-"
|
||||
force_destroy = true
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_cloudfront_origin_access_control" "ui" {
|
||||
name = "${local.name}-ui-oac"
|
||||
origin_access_control_origin_type = "s3"
|
||||
signing_behavior = "always"
|
||||
signing_protocol = "sigv4"
|
||||
}
|
||||
|
||||
resource "aws_cloudfront_distribution" "ui" {
|
||||
enabled = true
|
||||
default_root_object = "index.html"
|
||||
comment = "${local.name} advisor UI"
|
||||
|
||||
origin {
|
||||
domain_name = aws_s3_bucket.ui.bucket_regional_domain_name
|
||||
origin_id = "ui-s3"
|
||||
origin_access_control_id = aws_cloudfront_origin_access_control.ui.id
|
||||
}
|
||||
|
||||
default_cache_behavior {
|
||||
allowed_methods = ["GET", "HEAD"]
|
||||
cached_methods = ["GET", "HEAD"]
|
||||
target_origin_id = "ui-s3"
|
||||
viewer_protocol_policy = "redirect-to-https"
|
||||
forwarded_values {
|
||||
query_string = false
|
||||
cookies { forward = "none" }
|
||||
}
|
||||
}
|
||||
|
||||
restrictions {
|
||||
geo_restriction { restriction_type = "none" }
|
||||
}
|
||||
|
||||
viewer_certificate {
|
||||
cloudfront_default_certificate = true
|
||||
}
|
||||
|
||||
tags = local.cloud_tag
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_policy" "ui" {
|
||||
bucket = aws_s3_bucket.ui.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Principal = { Service = "cloudfront.amazonaws.com" }
|
||||
Action = "s3:GetObject"
|
||||
Resource = "${aws_s3_bucket.ui.arn}/*"
|
||||
Condition = { StringEquals = { "AWS:SourceArn" = aws_cloudfront_distribution.ui.arn } }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_s3_object" "index" {
|
||||
bucket = aws_s3_bucket.ui.id
|
||||
key = "index.html"
|
||||
source = "${path.module}/../ui/index.html"
|
||||
etag = filemd5("${path.module}/../ui/index.html")
|
||||
content_type = "text/html"
|
||||
}
|
||||
|
||||
# Injected config: the demo orchestrator endpoint the UI calls.
|
||||
resource "aws_s3_object" "config" {
|
||||
bucket = aws_s3_bucket.ui.id
|
||||
key = "config.js"
|
||||
content = "window.DEMO_ENDPOINT = \"${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/demo\";\n"
|
||||
content_type = "application/javascript"
|
||||
etag = md5("${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/demo")
|
||||
}
|
||||
|
||||
output "ui_url" { value = "https://${aws_cloudfront_distribution.ui.domain_name}" }
|
||||
@@ -23,3 +23,30 @@ variable "presidio_image_uri" {
|
||||
type = string
|
||||
default = "REPLACE_ME_PRESIDIO_IMAGE_URI"
|
||||
}
|
||||
|
||||
# Where the /tokenize Lambda reaches the Presidio detector. After deploy this is
|
||||
# the Presidio Fargate task's public endpoint (http://<public-ip>:5001). Kept a
|
||||
# variable so a laptop rehearsal can point at a local/ngrok detector.
|
||||
variable "presidio_url" {
|
||||
description = "Base URL of the Presidio detector /analyze service."
|
||||
type = string
|
||||
default = "http://localhost:5001"
|
||||
}
|
||||
|
||||
# Shared secret Fusion SaaS presents to the public endpoints (bearer / x-api-key).
|
||||
# NEVER commit a real value -- pass via TF_VAR_tokenize_api_key or a .tfvars file
|
||||
# that is gitignored. Empty default leaves the endpoint open (dev only).
|
||||
variable "tokenize_api_key" {
|
||||
description = "Shared secret required on /tokenize (and /restore) requests."
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# AgentCore Runtime ARN (created out-of-band by `agentcore launch`, see T5). The
|
||||
# demo orchestrator (T6) invokes it. Empty -> orchestrator skips the agent step.
|
||||
variable "agent_runtime_arn" {
|
||||
description = "Bedrock AgentCore Runtime ARN the demo orchestrator invokes."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user