Skip to content

Deployment Plan

EnvironmentTriggerVPCAurora pauseNotes
LocalDeveloper-runDocker ComposeN/ANo AWS resources; local DB replaces Aurora
QAAuto on merge to mainDedicated VPC, us-east-1Aggressive (0-ACU floor)Full AWS stack; validates before Prod
ProdManual approval gateDedicated VPC, us-east-1Standard (0-ACU floor)GitHub Environment protection rule (#28)

What differs per environment: Aurora cluster ARN, Secrets Manager secret paths, API Gateway base URL, CloudWatch log group names, budget alarm thresholds, and Terraform remote state key prefix. All configuration is injected at deploy time via per-env Terraform roots; no application code branches on environment name.


LayerArtifactTooling
Backend Lambda (API + authorizer + ingestion)Zip package produced from src/ using Hatchling; one zip per Lambdahatch build or equivalent CI step
SPA bundleStatic assets (dist/) produced by Vite or equivalent React buildnpm run build
TerraformNo artifact; plan output is ephemeral. Modules under infra/modules/; per-env roots under infra/roots/qa/ and infra/roots/prod/terraform plan, terraform apply
OpenAPI specbackend/openapi.json committed to the app repo; regenerated in CI and diff-checked (RAG005)make export-openapi

Lambda zip files are uploaded to S3 (or passed directly to the Lambda update API) as part of the Terraform apply step. The SPA bundle is synced to the environment-specific S3 bucket and a CloudFront invalidation is issued (#30).


The QA/Prod corpus is neural-bridge/rag-dataset-12000, whose Parquet files (~23 MB total, discovered at runtime per RAG003) hold 12,000 documents and expand to 52,620 chunks. Chunking is single-threaded and CPU-cheap (seconds for the whole corpus). The dominant cost is embedding: every chunk is a separate Bedrock Titan call.

A single-worker local rag-ingest all against QA Aurora (embed batch size 16) took 6 hours 45 minutes to embed all 52,620 chunks. The sequential embedding calls, not the download or chunking, accounted for nearly all of that wall-clock time. Treat the single-worker path as local and QA-verification only; it does not scale to a production re-ingest window.

Production-scale ingestion therefore runs as parallel out-of-VPC Lambda workers (RAG011 amendment). Each invocation is time-boxed (about 7 minutes) and claims work with FOR UPDATE SKIP LOCKED plus a lease: the ingest_pages control table for the load phase, lease columns on chunks for the embed phase. Many workers ingest concurrently, and a crashed or timed-out worker’s claim is recovered on lease expiry (600 s). Re-runs are idempotent via ON CONFLICT upserts, so an interrupted or partial load is safe to resume rather than restart.


merge to main
|
v
[CI pipeline — QA branch]
|
v
QA deploy (auto, no gate)
|
v
Manual approval (GitHub Environment — Prod)
|
v
Prod deploy

Per RAG001 and issue #28: QA deploys on every merge to main with no human step. Prod is gated by a GitHub Environment protection rule requiring explicit operator approval. The operator is the sole approver. There is no automatic Prod promote on QA health; approval is always manual.


The CI pipeline (#29) runs as a fan-out per environment after the shared test stage. This section describes the target under RAG016; #302 is outstanding, so today qa-deploy.yml also runs a post-merge Infracost job in the QA branch, which plan depends on, and no ceiling assertion exists yet.

[test]
run pytest (90% aggregate + 70% per-file hard-fail, [RAG006](/adr/rag006/))
run ruff lint
run openapi-check diff ([RAG005](/adr/rag005/))
|
v
[fan-out: QA branch] [fan-out: Prod branch]
terraform plan --env=qa (holds at approval gate)
terraform apply --env=qa manual approval required
Lambda zip upload |
SPA sync + CF invalidation v
terraform plan --env=prod
terraform apply --env=prod
Lambda zip upload
SPA sync + CF invalidation

Infracost posts a per-env cost breakdown as a PR comment (#32) on the Terraform pull request, and asserts each environment’s monthly total against a fixed-price ceiling in the same job (RAG016). That job is gated to pull_request, so neither runs in a fan-out branch; both happen before the merge that triggers any terraform apply. The apply does not block on the comment, which is informational, but a breached ceiling fails the PR, so the change never reaches a fan-out branch at all. The comment is always present before the operator approves Prod. A failed terraform plan in either branch blocks that branch’s apply; QA failure does not block the Prod gate from opening, but the operator should not approve Prod against a failing QA state.


Managed per issue #25, per RAG009.

  • Secrets (DB credentials, API Gateway details, key-store secrets) live in AWS Secrets Manager with separate secret ARNs per environment. No secret is shared across QA and Prod.
  • Each Lambda function (API, authorizer, ingestion) has a dedicated IAM role scoped to least-privilege: only the specific rds-data:ExecuteStatement, bedrock:InvokeModel, secretsmanager:GetSecretValue, xray:PutTraceSegments, and cloudwatch:PutMetricData actions needed for that function, and only against the ARNs in its environment.
  • CI/CD uses a dedicated IAM role assumed by GitHub Actions via OIDC (no long-lived keys in GitHub Secrets). The role has deploy-scoped permissions only.
  • The operator-side key issuance CLI (python -m rag_sample.tools.issue_key) runs with operator credentials, not CI credentials.
  • Authorization headers are scrubbed from CloudWatch logs at the Lambda Powertools Logger layer (RAG009 consequence).

Per RAG001 and issue #20: one S3 bucket with versioning enabled, using S3-native state locking (use_lockfile = true, no DynamoDB table; see the RAG001 amendment). State files are keyed by environment:

  • rag-sample/qa/terraform.tfstate
  • rag-sample/prod/terraform.tfstate

A one-time bootstrap script provisions the S3 bucket before any environment can be deployed. State is not shared between environments. S3 bucket versioning provides a point-in-time history for manual state recovery.


LayerRollback mechanism
Lambda (API, authorizer, ingestion)Publish versioned Lambda on every deploy. If the new version fails, re-point the alias to the previous version (single CLI call). No Terraform involvement required for a quick rollback.
SPAS3 versioning retains previous bundle. Re-sync the previous dist/ artifact and invalidate CloudFront.
Terraform infrastructureRevert the offending commit in the Terraform root, re-run terraform apply. For state corruption, restore from S3 version history and re-apply.
Database schemaMigrations follow the plain-SQL pattern (RAG002). Reversibility: each migration that cannot be made backwards-compatible must ship a paired down-migration script. The operator runs the down-migration manually before rolling back the Lambda version. Schema changes that are non-reversible (destructive column drops) require a two-phase deploy (add-column, migrate data, drop-column in a separate story).

There is no automated rollback trigger. The operator monitors the CloudWatch alarm on API Lambda error rate (> 5% over 5 minutes, RAG008) and initiates rollback manually.


EventChannel
QA deploy started / completed / failedGitHub Actions status check on the PR / commit
Prod approval requiredGitHub Environment protection rule sends email to the operator
Prod deploy completed / failedGitHub Actions status check
API Lambda error rate > 5% for 5 minutesCloudWatch Alarm to SNS, email subscription (RAG008)
AWS Budget threshold reached (per env)AWS Budgets email alert (#33)
Infracost cost diff on PRPR comment (#32)