# cast01–cast03 lab architecture

## Goals

Simulate production patterns on three isolated Alpine VMs with **shared config only** on NFS (`/shared/cluster/`). No database files on NFS — each node owns `/var/lib/mysql` locally.

## Topology

```text
                    ┌─────────────────────────────────────┐
                    │  FE 10.7.0.10 (NFS export /shared)  │
                    └──────────────────┬──────────────────┘
                                       │ NFS (configs/scripts only)
         ┌─────────────────────────────┼─────────────────────────────┐
         ▼                             ▼                             ▼
   ┌───────────┐               ┌───────────┐               ┌───────────┐
   │  cast01   │  async GTID   │  cast02   │  async GTID   │  cast03   │
   │ .16.236   │◄──────────────│ .16.237   │◄──────────────│ .16.238   │
   │ PRIMARY   │   replica     │ REPLICA   │   replica     │ REPLICA   │
   │ MariaDB   │               │ MariaDB   │               │ MariaDB   │
   │ nginx/php │               │ nginx/php │               │ nginx/php │
   └───────────┘               └───────────┘               └───────────┘
         ▲                             │                             │
         └──────── writes + default reads ────────────────────────────┘
                    (replicas: read-only, failover drill)
```

| Node | IP | MariaDB | App role (planned) |
|------|-----|---------|-------------------|
| cast01 | 10.7.16.236 | **Primary** (read/write) | Crashes console primary target |
| cast02 | 10.7.16.237 | Replica (read_only) | App worker / read replica |
| cast03 | 10.7.16.238 | Replica (read_only) | App worker / read replica |

## MariaDB

- **Engine:** MariaDB 11.8 (Alpine 3.24 packages — no edge required)
- **Replication:** GTID async primary → two replicas (`server-id` 1/2/3)
- **Bootstrap:** app DB dump over TCP (`--skip-ssl`); GTID position copied from primary; `replicate-do-db` filters ongoing binlog to app schemas only (avoids `mysql.help_topic` dump failures)
- **Databases:** `androidcast_crashes`, `url_shortener` (schemas from project SQL in `/shared/cluster/sql/`)
- **Credentials:** see `credentials.txt` (same lab password on all nodes)

## PHP

- **Version:** 8.3 (`php83-fpm`) on all nodes — matches modern Alpine; prod BE remains 8.1 until upgraded separately
- **Socket:** `127.0.0.1:9000`

## DNS

Pending external records. Until then: `/etc/hosts` block maintained by `baseline.sh`.

## Service split (selected for lab)

| Service | Write DB host | Read DB host (lab default) | Future app path (local disk) |
|---------|---------------|----------------------------|------------------------------|
| Crashes / tickets / RA | cast01 | cast01 (cast02/03 for drill) | `/var/www/androidcast/` per node |
| URL shortener API | cast01 | cast01 | `/var/www/url-shortener/` on cast03 |
| Graphs ingest | cast01 | cast01 | shared crashes backend |

App deploys stay on **local VM disk**, not NFS.

## Operations

```sh
sudo sh /shared/cluster/scripts/baseline.sh          # per-node baseline
sudo sh /shared/cluster/scripts/mariadb-primary.sh   # cast01 only
sudo sh /shared/cluster/scripts/load-schemas.sh      # cast01 only
sudo sh /shared/cluster/scripts/mariadb-replica.sh # cast02 + cast03
sudo sh /shared/cluster/scripts/verify-cluster.sh  # any node
```

NFS mount may be `noexec` — always invoke scripts with `sh`.

## Not in scope yet

- ProxySQL / MaxScale (add if read splitting becomes necessary)
- Galera multi-primary (overkill for 3-node lab; async matches prod BE migration path)
- TLS between nodes (intra VLAN)
