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.
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.
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.
4 abstract base classes (DatabaseAdapter, StorageAdapter, AIModelAdapter, SecretsAdapter) with full AWS boto3 implementations and an adapter factory that switches on CLOUD_PROVIDER.
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.
5-module architecture: main.yaml orchestrator, plus compute, data, CDN, monitoring, and security nested stacks. Deploy order encoded as CloudFormation dependencies.
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.
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.
5-job pipeline: build → deploy-infra → deploy-functions → deploy-web → verify. Push-to-main deploys dev; aws-v* tags deploy staging → manual approval → prod.
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.
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.
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.
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.
Every layer mirrors the Azure deployment's structure — DNS/edge, networking, compute, databases, AI, and observability — using native AWS services throughout.
From browser request to DynamoDB and back — tracing a governance API call through every hop.
Static dashboard assets served from CloudFront + S3. API calls hit API Gateway REST endpoint with {proxy+} resource routing all methods to the Lambda integration.
lambda_handler(event, context) extracts httpMethod + path from the API Gateway proxy event and dispatches to the matching handler function from governance_routes.py.
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.
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.
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.
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 |
| Adapter | Azure Implementation | AWS Implementation | Key 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 |
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.
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.
CloudFormation parameter files drive per-environment behavior. Prod adds provisioned DynamoDB capacity with auto-scaling, cross-region S3 replication, and 90-day CloudWatch retention.
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.
advisory-{env}-api-keys_secrets_cache dict — one Secrets Manager call per Lambda cold startSECRETS_ARNgenerated-documents/ prefix — pre-signed URL access onlyAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYvalidate-env-aws.sh verifies AWS CLI auth before any deployment stepThe 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.
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.1For 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.2For 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.3For 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.5For any DynamoDB error type — ResourceNotFoundException → 404, ConditionalCheckFailedException → 409, AccessDeniedException → 403, all other ClientError → 500 with error message preserved in body.
Validates: 1.8For 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.4aws cloudformation validate-template runs on all 5 YAML files in aws/infra/ as a pre-deploy stepactionlint validates deploy-aws.yml syntax and expression referencespy_compile on all .py files in the Lambda package catches syntax errors before packagingA 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.
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.
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.
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.
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.
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.