# cube-mcp — `prod` environment

The release stack. Every merge to `main` ships here. Serves the live
`cube-cloud` FastAPI backend that the `cube-mcp` / `cube-dev` MCP servers proxy
to.

> ⚠️ **Destructive-plan warning.** This is production and it shares an AWS
> account with [`dev`](../dev/README.md). A `terraform apply` run with the wrong
> `-var-file` or the wrong backend `key` will *not* be stopped by an
> account/credential boundary — the guardrail the org convention relies on does
> not exist here (see [divergence 3](#divergences-from-convention)). Always
> confirm `key=cube-cloud/terraform.tfstate` +
> `-var-file=environments/prod.tfvars` and **read the plan** before applying.
> Prod applies normally go through `deploy.yml`, not a laptop.

- **AWS account / region:** `992382448282` / `us-west-2` (shared with dev).
- **Domain:** `cube.edgescaleai-cube.com`
- **Deployed by:** `deploy.yml` on every push to `main` (images tagged `:latest`); publish path also runs `publish.yml`.
- **State:** `s3://cube-mcp-terraform-state/cube-cloud/terraform.tfstate`
- **Inputs:** [`environments/prod.tfvars`](../../environments/prod.tfvars) (mostly defaults from [`variables.tf`](../../variables.tf))

> This directory is documentation only. The deployable Terraform is the flat
> root module in [`infra/terraform/`](../..); `prod` is produced by
> `terraform init -backend-config="key=cube-cloud/terraform.tfstate"` +
> `terraform apply -var-file=environments/prod.tfvars`.

## What prod sets

Prod carries the variable defaults; [`environments/prod.tfvars`](../../environments/prod.tfvars)
only sets `environment = "prod"`, `domain = "cube.edgescaleai-cube.com"`, and
`desired_count = 2`. The tbot secret/token fall back to the unprefixed
`cube-mcp/tbot-config` / `cube-mcp-bot-iam` defaults. See the
[dev override table](../dev/README.md#what-dev-overrides) for the full dev↔prod diff.

## Resource registry (per-env, prefixed `cube-mcp-prod-`)

| Resource | Name | File |
|---|---|---|
| ECS cluster / service | `cube-mcp-prod-cluster` / `cube-mcp-prod-service` | [`ecs.tf`](../../ecs.tf) |
| ALB + HTTPS listener | `cube-mcp-prod-alb` (serves `cube.edgescaleai-cube.com`) | [`alb.tf`](../../alb.tf) |
| DynamoDB | `cube-mcp-prod-api-keys`, `cube-mcp-prod-login-sessions` | [`dynamodb.tf`](../../dynamodb.tf) |
| Cognito user pool | `cube-mcp-prod-users` + one group per profile (see [`cognito.tf`](../../cognito.tf)) | [`cognito.tf`](../../cognito.tf) |
| ECS task IAM role | `cube-mcp-prod-ecs-task` | [`iam.tf`](../../iam.tf) |
| CloudWatch Logs | `/ecs/cube-mcp-prod` | [`ecs.tf`](../../ecs.tf) |
| Route53 records | `edgescaleai-cube.com` zone (`Z0571857327BEMX2EHNZU`) | [`route53.tf`](../../route53.tf) |

The account-level SES sender identity (`edgescaleai-cube.com`, `noreply@…`,
wired into the Cognito pool in [`cognito.tf`](../../cognito.tf)) is not
env-prefixed and is shared like the resources below.

## Shared-resource hazards

Some resources are declared with fixed `cube-mcp/…` names (no environment
segment), so **dev and prod share one physical copy** in the account:

> **Dual-state ownership — the sharper hazard.** These fixed-name resources are
> declared in the flat root module, so **both** the dev and prod state files
> claim ownership of the *same* physical resource. That is worse than tag churn:
> a dev-side `terraform destroy`, a taint, or a name/attribute change that forces
> a replace can delete or recreate prod's copy directly. Both ECR repos set
> `force_delete = true` ([`ecr.tf`](../../ecr.tf), [`tbot.tf`](../../tbot.tf)),
> so a dev-side destroy of the repo would take the prod images with it, not just
> evict a tag. Treat any dev apply that touches a fixed-name resource as a prod
> change.

- **ECR** — `cube-mcp/cube-cloud`, `cube-mcp/tbot`. The prod `latest` image and
  the dev `:dev` image live in the *same* repository. A June 2026 prod outage
  traced to dev/test build churn evicting the prod `latest` image behind a
  keep-last-N lifecycle rule; [`ecr.tf`](../../ecr.tf) now expires only
  *untagged* images so in-use tags stay safe. Do not change that lifecycle rule
  back to a count-based reap across all tags. Both repos also carry
  `force_delete = true`, so a dev-side destroy/replace deletes the shared
  repository (and prod's images) outright — see the dual-state note above.
- **Profile secrets** — `cube-mcp/profiles/*`, one per Cognito group, in
  [`secrets.tf`](../../secrets.tf).
- **Groundcover API key** — `cube-mcp/groundcover/api-key`, org-wide read-only.
- **tbot config secret** — `cube-mcp/tbot-config`, in
  [`tbot.tf`](../../tbot.tf). The resource name is hardcoded (no env segment),
  so both envs share one copy. Prod's `tbot_config_secret` variable matches this
  name; dev's overrides to `cube-mcp-dev/tbot-config`, so the dev ECS task's IAM
  grant references a name this stack never creates.

## Divergences from convention

The full list (with the convention each breaks) is the
[`envs/` summary table](../README.md#divergences-from-the-org-conventions--summary).
The ones that matter most for prod safety:

1. **No dedicated prod account (convention 3).** The org convention runs prod in
   `esai-prod`, reachable only with prod credentials, precisely so a stray
   `apply` fails at auth instead of mutating prod. cube-mcp prod shares account
   `992382448282` with dev — that safety net is absent, which is why the
   destructive-plan warning above matters.
2. **Repo-private state bucket, legacy key (conventions 4–5).** State is
   `s3://cube-mcp-terraform-state/cube-cloud/terraform.tfstate`, not the org
   `edgescaleai-terraform-state` bucket, and the key uses the legacy
   `cube-cloud/` prefix rather than `cube-mcp/prod.tfstate`.
3. **No baked `profile`, no `encrypt`/`use_lockfile`, `required_version >= 1.5`
   (conventions 6–7).** The backend in [`main.tf`](../../main.tf) omits the
   fail-fast `profile`, S3-native locking, and at-rest encryption the
   convention requires; concurrent applies are not lock-protected.

Reconciliation: move prod to `esai-prod` + the org backend, split
`versions.tf`/`providers.tf` with a baked `profile`, and raise
`required_version` to `>= 1.10.0` with `use_lockfile`/`encrypt`. Track under the
naming/backend standardization work (SCRUM-967 family).
