AWS Deployment · Kiro-Generated

Advisory AI — AWS Deployment
From Spec to Cloud in One Shot

The same Claude-powered governance platform — assessment workflows, artifact generation, code scanning, compliance gates — deployed to AWS. Kiro generated the entire cloud layer from a design spec: CloudFormation nested stacks, Lambda handler, DynamoDB tables with GSIs, CloudFront CDN, CI/CD pipeline, seed scripts, and smoke tests. Zero lines of IaC written manually.

5
CFN Nested Stacks
7
DynamoDB Tables
17
API Routes
6
PBT Properties
0
Manual IaC Lines
3
Environments

Kiro Generated the Entire AWS Layer

The Advisory platform already ran on Azure Functions + Cosmos DB. The challenge: deploy the same product to AWS without forking 7,500 lines of business logic. Kiro read the design spec and generated everything — adapter abstractions, CloudFormation templates, CI/CD, seed scripts, property-based tests, and runbooks.

What "spec-driven generation" means here

A design document described the architecture decisions, adapter interfaces, CloudFormation module boundaries, DynamoDB GSI design rationale, CI/CD trigger strategy, and 6 correctness properties to verify. Kiro (AI-powered IDE built on Claude) read that spec and generated production-ready artifacts — not stubs, not scaffolding. The CloudFormation templates include precise IAM policies scoped to individual table ARNs. The adapter factory handles CLOUD_PROVIDER routing with module-level secret caching. The property tests cover error mapping, URL generation, and cache behavior with Hypothesis strategies.

Spec-driven Zero forked business logic ABCs + factory pattern Hypothesis PBT CloudFormation nested stacks actionlint CI validation
Generated 01

Cloud Adapter Layer

4 abstract base classes (DatabaseAdapter, StorageAdapter, AIModelAdapter, SecretsAdapter) with full AWS boto3 implementations and an adapter factory that switches on CLOUD_PROVIDER.

Generated 02

Lambda Handler

Single-function API Gateway proxy handler routing all 17 API paths to existing business logic — no route forking, full CORS header injection, top-level error catch returning 500 with traceback to CloudWatch.

Generated 03

CloudFormation Nested Stacks

5-module architecture: main.yaml orchestrator, plus compute, data, CDN, monitoring, and security nested stacks. Deploy order encoded as CloudFormation dependencies.

Generated 04

7 DynamoDB Tables + GSIs

Schema-matched to Cosmos DB items. TTL on stock-data (5 min) and news-articles (1 hr). docType-tenantId-index and docType-projectId-index GSIs covering all query patterns.

Generated 05

S3 + CloudFront CDN

OAI-gated S3 origin, custom 404→200 SPA routing, per-extension content-type metadata, CloudFront cache invalidation script, and same-bucket generated-documents/ prefix for artifact storage.

Generated 06

GitHub Actions CI/CD

5-job pipeline: build → deploy-infra → deploy-functions → deploy-web → verify. Push-to-main deploys dev; aws-v* tags deploy staging → manual approval → prod.

Generated 07

Bedrock Claude Integration

AwsAIModelAdapter wraps AnthropicBedrock client — same Anthropic SDK, endpoint-swapped. Model ID parameterized in CloudFormation (anthropic.claude-sonnet-4-5 default). No business logic change required.

Generated 08

DynamoDB Seed Runner

AwsSeedRunner reuses shared frameworks.py and patterns.py data modules — same deterministic IDs as Azure. Idempotent via unconditional put_item. CLI with env-var fallback.

Generated 09

Smoke Tests + Validation

smoke-test-aws.sh checks health, CloudFront root, governance API, and DynamoDB connectivity with retry logic. validate-env-aws.sh pre-checks all 7 required env vars and AWS CLI auth.

Generated 10

6 Property-Based Tests

Hypothesis tests covering: Lambda response format, DynamoDB query translation, pre-signed URL generation, secret caching behavior, error status mapping, and seed ID determinism. 100+ iterations each.

AWS Service Layers

Every layer mirrors the Azure deployment's structure — DNS/edge, networking, compute, databases, AI, and observability — using native AWS services throughout.

Edge / CDN
CloudFront Distribution S3 Static Hosting Origin Access Identity Custom 404 → 200 SPA
API Layer
API Gateway REST {proxy+} ANY Lambda Python 3.11 256 MB · 300s timeout CORS headers
Business Logic
governance_routes.py code_scanner.py function_app.py Unchanged from Azure
Cloud Adapters
DatabaseAdapter StorageAdapter AIModelAdapter SecretsAdapter CLOUD_PROVIDER factory
Data / Storage
DynamoDB (7 tables) docType-tenantId GSI docType-projectId GSI S3 (artifacts) TTL on cache tables
AI / LLM
Amazon Bedrock Claude Sonnet 4.5 AnthropicBedrock SDK NewsAPI · Alpha Vantage GitHub API (scanning)
Security / Ops
Secrets Manager IAM Execution Role CloudWatch Logs CloudWatch Alarms 30-day log retention

End-to-End Data Path

From browser request to DynamoDB and back — tracing a governance API call through every hop.

1

Browser → CloudFront → API Gateway

Static dashboard assets served from CloudFront + S3. API calls hit API Gateway REST endpoint with {proxy+} resource routing all methods to the Lambda integration.

CloudFront API Gateway HTTPS
2

Lambda Handler — Route Matching

lambda_handler(event, context) extracts httpMethod + path from the API Gateway proxy event and dispatches to the matching handler function from governance_routes.py.

lambda_handler.py Route table Event → request
3

Business Logic — Cloud-Agnostic

Handler calls into governance_routes.py — the exact same file as the Azure deployment. All DB, storage, and AI calls go through adapter interfaces; no cloud SDK imports in business logic.

governance_routes.py DatabaseAdapter AIModelAdapter
4

AWS Adapter — DynamoDB Query Translation

AwsDatabaseAdapter translates Cosmos DB SQL-style queries into DynamoDB operations: docType + tenantId filter → GSI KeyConditionExpression; cross-partition → scan with FilterExpression. TTL epoch conversion on write.

AwsDatabaseAdapter GSI routing TTL conversion
5

Response — CORS + API Gateway Proxy Format

Lambda returns {"statusCode": N, "headers": {...CORS}, "body": "..."}. API Gateway unwraps the proxy response. CloudWatch captures all logs with 30-day retention. Alarms fire on Lambda errors > 5 or p99 latency > 10s.

Proxy response CORS headers CloudWatch

Cosmos DB → DynamoDB Translation

The AwsDatabaseAdapter is the most complex component — translating Cosmos DB's SQL-like query API into DynamoDB's expression-based model without touching business logic.

Cosmos DB Operation DynamoDB Equivalent Notes
container.read_item(item=id, partition_key=pk) table.get_item(Key={'id': id}) Direct key lookup
container.upsert_item(doc) table.put_item(Item=doc) Unconditional overwrite
WHERE docType='X' AND tenantId=@t query(IndexName='docType-tenantId-index', KeyConditionExpression=...) Most common pattern; uses dedicated GSI
WHERE docType='artifact' AND data.projectId=@p query(IndexName='docType-projectId-index', ...) projectId promoted to top-level on write
enable_cross_partition_query=True table.scan(FilterExpression=...) Only for admin/rare queries
CosmosResourceNotFoundError Empty get_item response or ResourceNotFoundException Mapped to custom ItemNotFoundError → 404
ttl: 300 (seconds from now) ttl: int(time.time()) + 300 (epoch) Adapter converts on every write

Other Adapter Implementations

AdapterAzure ImplementationAWS ImplementationKey Difference
StorageAdapter azure.storage.blob boto3 S3 Pre-signed URL vs SAS token expiry model
AIModelAdapter Azure AI Foundry / Claude AnthropicBedrock SDK Same Anthropic API, different endpoint config
SecretsAdapter Azure Key Vault Secrets Manager (JSON blob) AWS stores all keys in one JSON secret; module-level cache per Lambda lifetime
DatabaseAdapter Cosmos DB SDK boto3 DynamoDB SQL query → expression translation; GSI routing; projectId promotion

17 Routes, Zero Forked Handlers

All routes are served by the existing governance_routes.py handlers. The Lambda handler implements the same routing table as the Azure function_app.py decorators.

GET/api/v1/healthhealth_check
GET/api/v1/chatchat_handler
POST/api/v1/chatchat_handler
GET/api/v1/stock/quoteget_stock_quote
GET/api/v1/stock/historyget_stock_history
GET/api/v1/newsget_company_news
POST/api/v1/governance/aimscreate_aims
GET/api/v1/governance/aims/{aimsId}get_aims
POST/api/v1/governance/projectscreate_project
GET/api/v1/governance/projects/listlist_projects
GET/api/v1/governance/projects/{projectId}get_project
PUT/api/v1/governance/projects/{projectId}update_project
GET/api/v1/governance/projects/{id}/complianceget_compliance_status
POST/api/v1/governance/artifacts/*Artifact generators
GET/api/v1/governance/gates/*Gate workflows
POST/api/v1/governance/scan/githubscan_github_repo
GET/api/v1/governance/frameworksget_frameworks

7 DynamoDB Tables

All tables use id (String) as partition key — matching Cosmos DB's /id scheme. Documents are stored as-is; only the projectId promotion and TTL epoch conversion are applied on write.

{prefix}-governance-projects

Partition keyid (String)
BillingPAY_PER_REQUEST (dev/staging) · Provisioned (prod)
GSI: docType-tenantId

{prefix}-governance-artifacts

Partition keyid (String)
SpecialprojectId promoted to top-level on write
GSI: docType-tenantId + docType-projectId

{prefix}-governance-frameworks

Partition keyid (String)
Seededframeworks.py (shared with Azure)
GSI: docType-tenantId

{prefix}-governance-patterns

Partition keyid (String)
Seededpatterns.py (shared with Azure)
GSI: docType-tenantId

{prefix}-governance-audit

Partition keyid (String)
PurposeCompliance event trail
GSI: docType-tenantId

{prefix}-stock-data

Partition keyid (String)
TTL attrttl (epoch) · auto-delete after 5 min
TTL Enabled

{prefix}-news-articles

Partition keyid (String)
TTL attrttl (epoch) · auto-delete after 1 hr
TTL Enabled

Dev · Staging · Prod Configurations

CloudFormation parameter files drive per-environment behavior. Prod adds provisioned DynamoDB capacity with auto-scaling, cross-region S3 replication, and 90-day CloudWatch retention.

Dev
Lambda memory128 MB
DynamoDB billingPAY_PER_REQUEST
CloudFrontPriceClass_100
Log retention7 days
Deploy triggerPush to main
Staging
Lambda memory256 MB
DynamoDB billingPAY_PER_REQUEST
CloudFrontPriceClass_100
Log retention14 days
Deploy triggeraws-v* tag
Prod
Lambda memory512 MB
DynamoDB billingPROVISIONED + autoscale
CloudFrontPriceClass_200
Log retention90 days
S3 replicationus-west-2 CRR
Deploy triggerManual approval

IAM, Secrets, and Access Control

All permissions are scoped to specific resource ARNs. No wildcard S3 or DynamoDB policies. Secrets Manager stores all third-party API keys in a single encrypted JSON blob with module-level Lambda caching.

IAM Execution Role

  • DynamoDB: GetItem, PutItem, Query, Scan, UpdateItem, DeleteItem on all 7 tables + GSI ARNs explicitly listed
  • S3: GetObject, PutObject on web/artifacts bucket only
  • Bedrock: InvokeModel on configured model ARN only
  • Secrets Manager: GetSecretValue on specific secret ARN
  • CloudWatch Logs: CreateLogGroup, CreateLogStream, PutLogEvents

Secrets Manager Design

  • Single JSON secret: advisory-{env}-api-keys
  • Contains: NewsAPI, GNews, MediaStack, Alpha Vantage, GitHub Token
  • Module-level cache in _secrets_cache dict — one Secrets Manager call per Lambda cold start
  • Secret ARN injected as Lambda env var SECRETS_ARN

CloudFront + S3 Access

  • S3 static website hosting disabled — CloudFront is the only entry point
  • Origin Access Identity (OAI) grants CloudFront read-only access to bucket
  • Direct S3 URL access blocked by bucket policy
  • Generated documents stored under generated-documents/ prefix — pre-signed URL access only

CI/CD Secrets Handling

  • AWS credentials from GitHub repository secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
  • No secrets in CloudFormation templates or config JSON files
  • validate-env-aws.sh verifies AWS CLI auth before any deployment step
  • Lambda ZIP packaging never includes secrets — injected at runtime via Secrets Manager

6 Correctness Properties — Hypothesis PBT

The adapter layer is pure Python translation logic — an ideal target for property-based testing. Each property captures a behavioral guarantee that must hold across all valid inputs, not just examples. 100+ random iterations per property via the Hypothesis library.

Property 1

Lambda Response Format

For any HTTP status code (100–599), body string, and content type — the response builder always produces a dict with correct statusCode, all CORS headers present, and body matching input.

Validates: 0.1
Property 2

DatabaseAdapter Query Translation

For any combination of docType, tenantId, projectId — query_items() selects the correct GSI and builds the KeyConditionExpression matching those parameters, returning the same items as the equivalent Cosmos SQL query.

Validates: 0.4, 1.2
Property 3

Pre-Signed URL Generation

For any non-empty object name, content type, and positive expiry duration — generate_presigned_url() produces a URL containing the bucket name, object key, and expiry, and URLs differ when any parameter differs.

Validates: 0.7, 1.3
Property 4

Secrets Caching Behavior

For any sequence of get_secret(name) calls — AwsSecretsAdapter returns the same value every time and makes at most one boto3 get_secret_value call regardless of call count within a Lambda execution lifetime.

Validates: 0.8, 1.5
Property 5

Cloud Adapter Error Mapping

For any DynamoDB error type — ResourceNotFoundException → 404, ConditionalCheckFailedException → 409, AccessDeniedException → 403, all other ClientError → 500 with error message preserved in body.

Validates: 1.8
Property 6

Seed ID Determinism

For any (type, name) pair — generate_document_id() returns the same ID on every call, and two different (type, name) pairs never produce the same ID. Tested with arbitrary text() strategies.

Validates: 5.4
Additional Testing — CloudFormation, Pipeline YAML, Integration+
  • CloudFormation validation: aws cloudformation validate-template runs on all 5 YAML files in aws/infra/ as a pre-deploy step
  • Pipeline YAML lint: actionlint validates deploy-aws.yml syntax and expression references
  • Seed idempotency: seed_all_aws.py runs twice against DynamoDB Local; item counts are identical after both runs
  • Python compile check: py_compile on all .py files in the Lambda package catches syntax errors before packaging
  • Smoke test suite: Post-deploy verification hits health, CloudFront root, governance API, and DynamoDB connectivity with 3-retry logic

Key Engineering Choices

Why a single Lambda function instead of per-route functions?+

A single Lambda with API Gateway {proxy+} integration mirrors the Azure Function App's monolithic handler pattern exactly. This keeps the routing table in one place, simplifies cold-start management (one provisioned concurrency allocation, not 17), and means the existing governance_routes.py dispatch logic requires zero changes. The trade-off — larger ZIP, slightly more memory — is acceptable for this workload profile.

Why CloudFormation instead of CDK or Terraform?+

CloudFormation is AWS-native, requires no additional tooling beyond the AWS CLI, and aligns structurally with the Azure Bicep approach used in the parallel deployment. Nested stacks mirror Bicep module decomposition — each stack has a single responsibility and clear input/output interface. CDK would add a TypeScript compile step; Terraform would introduce state file management. For a spec-generated deployment, CloudFormation's declarative YAML is the cleanest fit.

The projectId GSI promotion problem — why it matters+

DynamoDB GSI sort keys cannot reference nested attributes. The query WHERE c.data.projectId = @id works natively in Cosmos DB. In DynamoDB, the docType-projectId-index GSI requires projectId as a top-level attribute. The AwsDatabaseAdapter promotes data.projectId to projectId on every write to the artifacts table — transparently, without any change to the write callers. This is the one structural difference between the Azure and AWS data schemas.

Why one JSON secret vs individual Secrets Manager secrets?+

Storing all API keys (NewsAPI, GNews, MediaStack, Alpha Vantage, GitHub Token) in a single JSON-encoded secret reduces Secrets Manager API calls to one per Lambda cold start. The AwsSecretsAdapter loads the full JSON blob into a module-level dict and serves subsequent get_secret(name) calls from cache. This avoids the 5 separate API calls that would be required with individual secrets — important given that Secrets Manager has a default throttle of 10,000 requests/second per region and Lambda cold starts can be bursty.

Shared seed data — how Azure and AWS stay in sync+

scripts/seed/frameworks.py and scripts/seed/patterns.py are the single source of truth for reference data. Both the Azure seed runner (scripts/seed/seed_all.py) and the AWS seed runner (aws/scripts/seed/seed_all_aws.py) import these modules directly. Document IDs are generated by the same deterministic generate_document_id() function. Running either seed runner twice produces identical state — no manual synchronization required between clouds.

DynamoDB on-demand vs provisioned — the cost/performance split+

Dev and staging use PAY_PER_REQUEST (on-demand): zero capacity planning, zero cost when idle, scales automatically up to burst limits. Production switches to PROVISIONED with auto-scaling (5–100 RCU/WCU): lower per-request cost at sustained load, predictable latency, and no throttling surprises at traffic peaks. The CloudFormation parameter file controls the billing mode — the templates include conditional auto-scaling resources that only apply in provisioned mode.

Azure Version ↗ Governance Platform ↗ EARE on AWS ↗ EARE on Azure ↗ All Projects ↗