# mcp-server-infra

Terraform for the TiDB Cloud MCP server's AWS deployment. Separate from the product repository on
purpose; see below.

The design is settled in `reference/aws-architecture.md` in the private insight repo, after three
rounds of adversarial review. This repository implements it and does not re-decide it. Where a
choice looks surprising, the reason is in a comment next to the resource.

## Why this is not in the product repository

Not general practice. For a team this size, with one service, infrastructure in the product repo
under `infra/` would normally be the better call: fewer repositories, and a change that touches
both the app and its deployment stays one commit.

The reason here is specific. The product repository is going to move to the `tidbcloud`
organisation, and after that its visibility is not ours to control. Someone could reasonably decide
to open source it, and that decision would take the AWS account layout, VPC structure, and security
group rules with it. None of that is secret, and all of it is a map.

If the organisation later settles that the product repo stays private for good, merging this back is easy.
The reverse, extracting infrastructure history out of a repository that has already been public, is
not.

**Consequence to keep in mind:** changes here and in the product repo are no longer atomic. That is
mitigated by where the boundary is drawn rather than by discipline: Terraform owns secret
references, ports, log groups, and task sizing, and the application owns its own configuration.
Adding an application setting should not require a change in this repository. If you find yourself
editing both for a routine change, the boundary has slipped.

## Layout

| File               | What it holds                                                              |
| ------------------ | -------------------------------------------------------------------------- |
| `versions.tf`      | Provider versions, S3 backend, and the wrong-account guard                 |
| `variables.tf`     | Inputs. `account_id` and `repo_slug` are required                          |
| `network.tf`       | VPC, two public subnets, no NAT, security groups                           |
| `ecr.tf`           | Two image repositories, immutable tags, scan on push, lifecycle expiry     |
| `iam.tf`           | Task and execution roles per service, and the GitHub OIDC deploy role      |
| `secrets.tf`       | Secret containers with placeholder versions. No values                     |
| `alb.tf`           | Two load balancers, target groups, listeners, the interim certificate      |
| `ecs.tf`           | Cluster, two task definitions, three services (staging idles at zero)      |
| `waf.tf`           | Managed rule sets and a rate limit. Read the SQLi exclusion note           |
| `observability.tf` | Log-group retention lives in `ecs.tf`; alarms, SNS, and the budget are here |
| `outputs.tf`       | The handoff to the runbook and to CI                                       |

## Before your first apply

The state backend and everything else in the admin-only list must exist first. Follow
`runbooks/aws-admin-window.md` in the insight repo; it is ordered, and it ends with a test that
proves the deployment no longer depends on admin credentials.

```bash
export AWS_PROFILE=mcp        # NOT the default profile, which is a different account
export AWS_REGION=us-west-2

terraform init -backend-config="bucket=mcp-tfstate-<account>"

terraform plan -var="account_id=<account>" -var="repo_slug=<owner>/<repo>"
```

The account id is passed rather than committed. `versions.tf` checks it against whoever the
credentials resolve to and fails the plan on a mismatch, because the default profile on the
operator's machine points at an unrelated production account and an apply there would have
succeeded.

Put the values in a local `terraform.tfvars` (git-ignored) once, rather than retyping them.

### State of this stack

Applied and running in the project's dedicated AWS account (us-west-2). `terraform validate`, `fmt -check`, and
`plan` are clean, and `plan` reports no drift.

What is deployed: the VPC and both load balancers, two ECR repositories, all IAM roles, eight secret
containers holding real values, the ECS cluster and three services, WAF on both ALBs, alarms, and
the budget. The staging service has run a task end to end through CI; the official and POC services
point at a placeholder image tag on purpose and run nothing until someone deploys to them
deliberately.

Neither endpoint has TLS. Both serve a fixed 503 on port 80 for everything except `/health`, and
they stay that way until `interim_domain` and `poc_interim_domain` are set. Do not point a real
credential at either one before then.

Alarm notifications are switched off through `alarm_notifications = false` in `terraform.tfvars`,
because the two `unhealthy-targets` alarms describe an empty cluster until the first real deploy.
Remove that line once official is deployed and green, or the alarms stay silent and nobody notices.

The runbook's exit criterion has been run: `mcp-operator`, with no admin credentials present, can
plan across every resource, apply a real change, and read CloudWatch Logs. Use the `mcp-operator`
profile for day-to-day work.

## What this repository never contains

- **Secret values.** Terraform creates secret CONTAINERS; values go in by CLI or console so that
  nothing sensitive enters Terraform state. State is not an encrypted vault, it is a JSON file that
  records what was applied.
- **The account number**, outside a git-ignored tfvars file.
