AWS architecture¶
The containers from solution-architecture.md mapped to AWS. Network details are in networking.md, the Terraform code layout in terraform.md.
Everything runs in one region: ap-northeast-2 (Seoul). Users are close to it, and one region keeps the network simple.
1. Overview¶
2. Services and why¶
| Service | Used for | Why |
|---|---|---|
| ECS on Fargate | Runs the frontend, backend, docs site and (develop only) mock as ECS services in one cluster, with Container Insights | Required by the brief. Fargate removes host management. One task definition per service lets frontend and backend deploy independently |
| Application Load Balancer | Public entry. Routes the docs host (dev.docs. / docs.) to the docs site (public) and everything else to the frontend. With a domain: HTTPS (TLS 1.2/1.3 policy), HTTP → HTTPS redirect, and Cognito login on /agent, /agent/*, /api/agent/*. Without a domain: plain HTTP on port 80 |
Built-in Cognito authentication, health checks, and SSE support with the idle timeout raised to 300 s |
| Route 53 + ACM | the zone onboardassist.click (registered in Route 53, which created the hosted zone) holds a customer, an agent and a docs host per environment: develop dev.app., dev.agent. and dev.docs.onboardassist.click, prod app., agent. and docs.onboardassist.click; one certificate covers all three. The apex serves nothing. Terraform issues a DNS-validated ACM certificate and an alias record to the ALB |
HTTPS and Cognito both need a domain. Public ACM certificates are free |
| RDS for PostgreSQL | One PostgreSQL 16 instance, three schemas: checkpoint, domain, catalog. gp3 storage, rds.force_ssl = 1 |
Entities are relational. PostgresSaver takes an encrypting serializer as a normal argument. One instance is the cheapest option |
| Amazon Bedrock | LLM calls through the Converse API. Model global.anthropic.claude-sonnet-4-6 with global cross-region inference from Seoul |
The account has quota and accepted terms for this model, and it handles Korean extraction and summaries well. See tradeoffs.md |
| Secrets Manager | The RDS master password (managed by RDS), the checkpoint AES key, and the HMAC key for session tokens and ID numbers. Terraform generates the two application keys | ECS injects them into the backend container as environment variables; nothing sensitive is in images or task definitions |
| KMS | One customer-managed key per environment for RDS storage and the secrets | Encryption at rest with a key the account controls |
| Cognito | User pool for support agents, one per environment, created only when a domain is set. Admin-created accounts only, email as username, optional TOTP MFA | Agents are staff with accounts. ALB integrates with it directly |
| ECR | One repository per image: onboarding/backend, onboarding/frontend, onboarding/mock. Tag = commit SHA, tags immutable, scan on push, newest 50 kept. Shared by develop and prod |
The same image moves from develop to prod without a rebuild |
| CloudWatch Logs | One log group per service, 30-day retention | Standard ECS logging. Message bodies are not logged |
| VPC endpoints | Private access to Bedrock, Secrets Manager, ECR, CloudWatch Logs, S3 | Customer conversations sent to Bedrock never cross the public internet |
| S3 + DynamoDB (Terraform state) | One state file per environment in an S3 bucket, locked through a DynamoDB table. Both created by infra/bootstrap |
Standard remote backend. Terraform 1.5 needs DynamoDB for S3 state locking |
Customers do not use Cognito. They have no account; the backend issues a random session token and the customer
opens /s/{token}. See networking.md.
3. Bedrock¶
- Model: Claude Sonnet 4.6, called as the global inference profile
global.anthropic.claude-sonnet-4-6fromap-northeast-2. Requests may be served in another region. Logs, quota and billing stay in Seoul. - Client:
langchain-awsChatBedrockConverse,with_structured_outputinfunction_callingmode, temperature 0. - Auth: the backend ECS task role. No API keys.
- IAM:
bedrock:InvokeModelandbedrock:InvokeModelWithResponseStreamon the inference profile (arn:aws:bedrock:ap-northeast-2:<account>:inference-profile/global.<model>) and on the foundation models it points to (the region-lessarn:aws:bedrock:::foundation-model/<model>andarn:aws:bedrock:ap-northeast-2::foundation-model/<model>). Terraform grants this for both Sonnet 4.6 and Haiku 4.5 (anthropic.claude-haiku-4-5-20251001-v1:0). - Endpoint:
BEDROCK_ENDPOINT_URLpoints to the mock only indocker compose. In both AWS environments it is unset, so the SDK uses the regional endpoint through thebedrock-runtimeVPC endpoint. Develop calls real Bedrock. - Per-node model: the agent config bundle (an S3 bucket per environment, read by the backend at startup) names
a model profile for each LLM node, so extraction nodes can move to Claude Haiku 4.5
(
global.anthropic.claude-haiku-4-5-20251001-v1:0) to save cost by publishing a new bundle version. The backend refuses a bundle naming a model outsidebedrock_foundation_models, which is also what the IAM policy allows. See langgraph-design.md.
4. IAM roles¶
| Role | Allowed |
|---|---|
| Backend task role | Invoke Bedrock (above); read the DB master secret, checkpoint key and HMAC key; KMS decrypt with the project key |
| Frontend task role | Nothing beyond the defaults. The frontend holds no secrets |
| Mock task role | Nothing beyond the defaults |
| Task execution role (one per service) | Pull the image from ECR, write logs; for the backend also read the three secrets and decrypt them, so ECS can inject PGPASSWORD, CHECKPOINT_AES_KEY and SESSION_HMAC_KEY |
| Backend task role, agent config | List and read the agent config bundle prefix in the environment's config bucket (the bundle is read once, at startup) |
| Agent config operator role (one per environment) | Assumed by the operator: the principals in agent_config_operator_arns, or any principal of the account signed in with MFA when the list is empty. List, read and create objects under the bundle prefix: every write must carry If-None-Match: *, so a published version is never replaced, and there is no delete. KMS encrypt/decrypt through S3 only. Restart the backend service (UpdateService --force-new-deployment) so it loads the new version |
| GitHub deploy role (one per environment) | Assumed through GitHub OIDC. PowerUserAccess, plus IAM limited to onboarding-* roles and policies, the Terraform state bucket and lock table, and ECR push. See cicd.md |
5. Storage¶
| Schema | Holds | Written by | Lifetime |
|---|---|---|---|
checkpoint |
LangGraph checkpoint tables (AsyncPostgresSaver) |
The LangGraph runtime | Meant to be 30 days without activity (cleanup not built) |
domain |
Customer and transaction entities, session links | Graph nodes and the API | The customer relationship |
catalog |
Products, eligibility rules, target markets | The backend at startup: Alembic migrations plus an idempotent seed of the eight products | While a product is on sale |
- Encrypted at rest with the KMS key; the parameter group forces TLS and the backend connects with
sslmode=require. The password comes from the RDS-managed secret asPGPASSWORD. - develop:
db.t4g.micro, single-AZ, no deletion protection. prod:db.t4g.small, Multi-AZ, deletion protection and a final snapshot. - Checkpoint contents are also compressed and AES-encrypted by the application (state-management.md).
- 30-day cleanup (designed, not built). PostgreSQL has no TTL. The plan is an EventBridge Scheduler rule that
starts a daily ECS task (backend image, cleanup command). It finds threads whose session has been inactive for
30 days in the
domainschema and deletes them through the checkpointer's delete-thread API. ATODOininfra/modules/data/main.tfrecords this.
6. Environments¶
One AWS account, two environments. Each has its own VPC and its own Terraform state. The Terraform code is the
same; only terraform.tfvars differs.
| develop | prod | |
|---|---|---|
| External systems | Mock service (one ECS service) | Real endpoints. Partner, identity and contract admin do not exist yet, so they are https://*.invalid placeholders |
| Bedrock | Real, through the VPC endpoint | Real, through the VPC endpoint |
| Domain | dev.app. (customers), dev.agent. (agents, Cognito on every path) and dev.docs.onboardassist.click: HTTPS, one ACM certificate |
app., agent. and docs.onboardassist.click, set up the same way when prod is first applied |
| NAT gateways | 1 | 1 per AZ |
| Interface endpoints | One AZ (2a) only | Both AZs |
| ECS tasks | 1 per service | 2 per service, spread across AZs |
| RDS | db.t4g.micro, single-AZ |
db.t4g.small, Multi-AZ |
| GitHub OIDC provider, ECR repositories | Created here | Looked up from develop |
| Deploy | Automatic on push to develop, once the deploy role variable is set in GitHub |
On push to main (a fast-forward of develop), after approval, same image SHA |
Scope for this submission: envs/develop is the environment the deploy workflow targets; envs/prod uses the
same code with different variables, and applying it is deferred. Two things must change before prod: the
backend's in-process session lock assumes one backend task (prod asks for two; SSE events already cross tasks over Postgres LISTEN/NOTIFY), and the frontend
must verify the ALB's signed agent header before AGENT_DEV_AUTH can be turned off. See
future-improvements.md.