AWS Deployment Proposal
Infrastructure Proposal — Draft for Review. Moving the tithe/stewardship knowledge-graph viewer and its research-assistant chatbot from a local Python process to ECS on Fargate, with the chatbot re-platformed onto Bedrock.
Scope: high-level architecture only. Audience: small shared team, low concurrency.
Decisions locked in (2026-08-15)
This is explicitly a pilot — proving out the research-assistant model on one real domain before any wider rollout or rebrand decision.
- Source control: GitHub.
- AWS account: the church's existing AWS account, not a new one — see the note in §7 on what that implies for IAM/tagging boundaries.
- Domain:
steward.tillplains.churchfor this pilot.tillplains.appis also owned and floated as possible future branding, but out of scope until the model is proven out here. "Steward" was chosen over more generic options (ra,research-assistant) as the tool's persona/name — it ties directly to the project's actual subject (tithe = stewardship) rather than reading as a generic tech label, and reads naturally as "ask the Steward." - IaC tool: AWS CDK.
These replace the corresponding rows in §9's open-questions list, which is now shorter.
1. Current state
The viewer is a dependency-light Python http.server process (no framework) that renders ~60
Markdown knowledge-graph nodes as linked HTML, serves a handful of source PDFs, renders timestamped
video transcripts from saved caption files, and hosts a chatbot backed directly by the Anthropic API
with a hand-built, read-only tool set (the knowledge graph, a local EGW Writings database, and a
third-party Bible/lexicon MCP server). It runs on one machine, reads one SQLite file from local disk,
and authenticates the chatbot with an API key in a .env file. None of that survives a move to a
shared, always-on service as-is.
The number that shapes everything below: data/egw-writings.db is 4.7 GB. Every sizing and
persistence decision in this document is downstream of that one fact — it's too large to treat
casually, and too large to bake into a container image without consequences.
Also true today, worth stating plainly since it affects the pipeline section: this project is not yet in a git repository. That's step zero, not an afterthought.
2. Proposed architecture
Two Fargate tasks, split across two Availability Zones, behind an Application Load Balancer — the standard shape for a low-traffic internal service that still shouldn't go dark if one AZ has a bad day. Both tasks mount the same read-only data volume; that only works because the content is idempotent (a batch-synced archive, not live user data), which is exactly the case here.
Figure: two Fargate tasks in separate AZs behind one ALB, both mounting the same EFS volume
read-only. A separate scheduled task (not the app) mounts EFS read-write to run the existing
catalog-sync.js / zip-sync.js content updates — the running app never writes.
Components
- ALB — public subnets, terminates HTTPS (ACM certificate), forwards to the Fargate service's target group. Also the natural home for a health-check path.
- ECS Fargate service, desired count 2 — private subnets, one task per AZ. The container is the existing Python viewer, containerized as-is.
- EFS — the 4.7 GB dataset lives here, not in the image. Mounted read-only into both app tasks; mounted read-write only by the separate sync task. See §4.
- Bedrock — reached via a VPC interface endpoint, so chatbot traffic never leaves AWS's network. See §5.
- NAT gateway (or a public-subnet egress path) — the only thing that needs real internet egress
is the third-party
study-bibleMCP server (studybible-mcp.fly.dev). Everything else (Bedrock, EFS, ECR pulls) can go through VPC endpoints instead, which is worth doing to keep the NAT gateway's data-transfer bill near zero. - ECR — holds the application image. Small, since the data doesn't live in it (see §4) — expect tens of MB, not gigabytes.
- CloudWatch Logs / Container Insights — stdout/stderr from both tasks, plus the standard Fargate CPU/memory/network metrics used to validate the sizing guess in §3.
3. Compute sizing
This is a starting point to validate against real CloudWatch numbers after the first deploy, not a final answer — but it needs to be some concrete number to review, so:
| Task | vCPU | Memory | Why |
|---|---|---|---|
| Starting size (recommended) | 0.5 vCPU | 1 GB | The app is I/O-bound, not CPU-bound — it's mostly waiting on SQLite reads over EFS and on Bedrock/MCP round-trips, not computing. 1 GB covers the Python process, request handling, and SQLite's page cache for the working set of a given query; it does not need to hold the 4.7 GB file in memory. |
| If it's sluggish | 1 vCPU | 2 GB | Next step up if Container Insights shows CPU throttling or memory pressure under real use. |
Two tasks at the starting size is a deliberately small, cheap baseline for "a small team, low traffic" — see §8 for what that costs. Scale-out policy: keep it simple to start (a fixed desired count of 2), add ECS service auto-scaling on CPU or ALB request count only if usage patterns actually call for it later.
4. Data & persistence
The single architectural decision everything else here hangs on: the 4.7 GB SQLite database does not go into the Docker image.
Figure: baking the database in bloats every image build and every task start with a 4.7 GB payload that rarely changes. Mounting it from EFS keeps the two concerns — "deploy new code" and "update the archive" — independent, which matches how this project already operates (content synced by separate Node scripts, on its own schedule).
| Approach | Deploy speed | Update story | Verdict |
|---|---|---|---|
| Bake into image | Slow — multi-GB image every build | Every content update requires a full rebuild + redeploy | Avoid |
| EFS mount | Fast — image stays small | Separate scheduled task re-syncs EFS directly; no redeploy needed | Recommended |
| Download from S3 on boot | Slow task start (downloads 4.7 GB per task launch) | Straightforward, but pays the download cost on every scale-out/restart | Fallback only |
Updating content
A scheduled task (EventBridge Scheduler → one-off Fargate task, or just run manually) mounts the
same EFS volume read-write and runs the existing catalog-sync.js / zip-sync.js Node scripts
against it. The two always-on app tasks mount read-only and never touch the sync process — clean
separation, and consistent with "content is idempotent enough" as stated: readers never race a
writer.
4a. Source PDFs — decided 2026-08-15
The four PDFs at the repo root exist locally purely to let the research assistant analyze them without traversing the internet on every query — not because they need to be served by this deployment. Checked each one for a legitimate public source instead of hosting local copies:
| Public source | Verdict | |
|---|---|---|
| Rodriguez, Tithing in the Writings of Ellen White | Biblical Research Institute — the publisher's own site | Link out, don't host |
| Ferrell, The Broken Blueprint | archive.org (PDF/EPUB/full-text) | Link out, don't host |
| Hawley, The Second Advent Doctrine Vindicated (1843) | archive.org — public domain | Link out, don't host |
| Standish & Standish, Tithes and Offerings: Trampling the Conscience | None found — commercially sold (Hartland Publications, 1997, ISBN 9780923309534), still in print via Amazon/AbeBooks | Keep local-only; do not serve or link publicly |
Three of four have a legitimate free public source and don't need to live on EFS at all in this
deployment — the viewer's PDF-listing feature (/pdf/<filename> in viewer/server.py) should point
those three at their public URLs instead. The fourth is still commercially sold with no free public
edition; serving it from a public-facing app would be a real copyright problem, not just an access-
control nicety. It stays exactly what it always was — a private local research aid for the agent,
excluded from whatever goes on EFS/the public site.
5. Chatbot on Bedrock
The research-assistant's tool-use loop (assistant.py) stays structurally the same — the change is
entirely in how it talks to the model.
| Today | Proposed | |
|---|---|---|
| Client | anthropic.Anthropic(api_key=...) |
anthropic.AnthropicBedrock(...) — same SDK, same .messages.create / .messages.stream surface used today, so the tool-use loop and streaming logic don't need a rewrite. |
| Auth | API key in .env |
ECS task IAM role — no secret to store or rotate at all. This removes the one credential this whole system currently has. |
| Model selection | Model name string | Bedrock model ID (region-qualified) — confirm the exact current ID for the intended Claude model in the Bedrock console at implementation time rather than assuming one here. |
IAM: the task role needs bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream, scoped
to the specific model ARN(s) in use — not a wildcard.
Doesn't change: the study-bible MCP dependency stays external either way — it's a third-party
hosted service, not an AWS resource, so moving the chatbot to Bedrock doesn't bring it in-house. See
the open questions in §9 for whether that's worth revisiting later.
6. CI/CD pipeline
Shape of the pipeline, not a finished implementation:
- Step zero: push this repository to GitHub (decided — see Decisions above).
- Build: on push, build the application image (small — see §4) and push to ECR via GitHub Actions with OIDC-based AWS auth — no long-lived AWS credentials stored in GitHub, the workflow assumes an IAM role scoped to this pipeline for the duration of the run.
- Deploy: update the ECS service to the new image, rolling deployment across both AZs (one task
at a time, so the service never drops to zero capacity). The VPC, ECS service, ALB, and EFS
volume themselves are defined in AWS CDK (decided) — the same GitHub Actions workflow can run
cdk deployfor infrastructure changes, separately from the plain image build/deploy for code changes. - Content updates run on their own, separate track (§4's scheduled sync task) — decoupled from the code pipeline entirely, since data lives on EFS, not in the image.
Not yet started: the CDK app itself, and the Dockerfile.
7. Security posture
- No public compute. Both Fargate tasks sit in private subnets; only the ALB is internet-facing.
- No API keys after the Bedrock migration. IAM roles replace the one secret this system has today.
- Read-only by construction, carried over as-is. The chatbot's tool set (
assistant.py) already has no filesystem, bash, or write access of any kind — that property doesn't change by moving to AWS, it just now also runs behind a private subnet boundary. - EFS encryption at rest, and encryption in transit for the NFS mount.
- VPC endpoints for Bedrock and ECR keep that traffic off the public internet entirely; the NAT gateway's job shrinks to just the one external MCP dependency.
- HTTPS only, ACM-issued certificate on the ALB.
- Shared AWS account. This deploys into the church's existing AWS account, not a fresh one — the
CDK app should put everything in its own resource-tagged, IAM-scoped footprint (a dedicated VPC,
its own IAM roles/policies, consistent tags like
project: research-viewer) so it's clearly separable from whatever else lives in that account, easy to cost-attribute, and easy to tear down cleanly if the pilot doesn't move forward. Don't assume account-wide permissions are fine just because it's a shared account — scope IAM the same as if this were a fresh account.
8. Rough cost shape
Order-of-magnitude only — confirm against current AWS pricing for the target region before this becomes a real budget line:
- Fargate — two tasks at 0.5 vCPU / 1 GB, running continuously: the smallest meaningful line item here, roughly the price of a couple of streaming subscriptions per month.
- ALB — a fixed hourly charge plus load-balancer-capacity-unit usage; at this traffic level, dominated by the fixed charge.
- EFS — priced per GB stored, not provisioned; roughly the same order of magnitude as the Fargate line for 4.7 GB on EFS Standard.
- NAT gateway — a fixed hourly charge plus per-GB data processing; kept low by routing
Bedrock/ECR through VPC endpoints instead (§2), leaving only the
study-biblecalls to actually cross it. - Bedrock — separate, usage-based, per-token — scales with chatbot usage, not a fixed infrastructure cost.
Total shape: a small, mostly-fixed monthly infrastructure floor, plus Bedrock usage that scales with how much the chatbot actually gets used.
9. Open questions
Source control, AWS account, domain, IaC tool, and the source-PDF handling below are now decided (see Decisions above and §4a). Still open:
- Is the
study-biblethird-party MCP dependency (no SLA, outside AWS) acceptable long-term, or should it eventually be self-hosted? Out of scope for this first pass, but worth flagging now. - EFS performance mode: General Purpose is the reasonable default to start; only worth revisiting if query latency actually becomes a problem under real load.
10. Next steps
- Push this repository to GitHub.
- Write a
Dockerfilefor the viewer; confirm it runs locally exactly as it does today, reading a local EFS-shaped mount point. - Stand up the VPC, ECS service, ALB, and EFS volume as a CDK app.
- Migrate
assistant.pytoAnthropicBedrock; drop the.env/API-key path entirely. - Wire the CI/CD pipeline (§6) and the separate scheduled content-sync task (§4).
- Deploy, smoke-test both AZs independently (kill one task, confirm the ALB routes around it), and watch Container Insights for a week before revisiting the sizing guess in §3.