AWS Deployment (Turnkey Cloud Ranges)¶
The lab runs on AWS as a full 6-node mirror of the Proxmox estate, so you can stand a range up
in the cloud with no on-prem hypervisor — and stand up N isolated ranges in parallel (the
AWS analog of Proxmox multi-estate GROUP_ID). This is a validated turnkey path:
terraform apply → one provisioning command per range → green (verify-lab 62/0 +
verify-chain 13/13 + verify-aipostex 304/0), with a between-wave reset-wave and proven
cross-range isolation.
Proxmox remains the primary path for multi-estate workshop hosting; AWS gives you portable, self-contained ranges (CI, a remote demo box, workshop overflow, or resilience).
Topology (per range K → subnet 10.0.(K+1).0/24)¶
| Instance | Role | Type | Private IP (range K) |
|---|---|---|---|
| ailab-dev | Developer Workstation (Ollama) | c6i.large | 10.0.(K+1).10 |
| ailab-ml | ML Platform (13 mocks + Ray) | t3.large | 10.0.(K+1).20 |
| ailab-ds | Data Science (Ollama) | c6i.large | 10.0.(K+1).30 |
| ailab-app | Shared AI Apps / agents | t3.small | 10.0.(K+1).40 |
| ailab-k8s | single-node k3s "ACME cluster" | t3.medium | 10.0.(K+1).50 |
| ailab-attack | Attack Box (deploy origin) | t3.small | 10.0.(K+1).99 |
All ranges share one VPC (10.0.0.0/16). Range 0 uses subnet 10.0.1.0/24 and unsuffixed
Name tags (aipostex-lab-<role>) for lab-ready-AMI back-compat; range K>0 uses 10.0.(K+1).0/24
and aipostex-lab-r<K>-<role>. LAB_SUBNET=10.0.(K+1) threads through inventory.sh, so every
host IP and peer config resolves within the range with no lab-script changes — the same
native provision.sh / seed.sh as on Proxmox.
Only Ollama (dev + ds) is real CPU inference → non-burstable c6i.large; everything else is
light. A 6-node range is 12 vCPU; running cost ≈ $0.34/hr (us-east-1 on-demand).
Prerequisites¶
- AWS profile with EC2 + VPC permissions. Configure locally (
aws configure --profile aipostex) andexport AWS_PROFILE=aipostex. - SSH keypair whose public key becomes the operator +
labadminkey. Default~/.ssh/aipostex_rec(reused from the on-prem attack box). - terraform ≥ 1.5, aws-cli v2, jq.
- A linux/amd64
aipostexbinary for the attack box (cross-compile from the tool repo):
vCPU quota (the multi-range ceiling)¶
Each 6-node range is 12 vCPU, and every node is already the 2-vCPU floor (6 × 2). The AWS
account's Running On-Demand Standard quota (L-1216C47, us-east-1) is what caps how many
ranges you can run: at the default 32 vCPU you get N=2 (24 vCPU); N=3 needs 36. To run
N=5, raise L-1216C47 to 64 via the Service Quotas console (the lab IAM user typically
lacks servicequotas:*, so do it as an account admin). See also
subset ranges — attendee ranges that drop the presenter-only k8s node
fit more per quota.
1. Provision the infrastructure¶
cd terraform
export AWS_PROFILE=aipostex
terraform init
# Single range (range 0) — the default:
terraform apply -var="ssh_public_key_path=$HOME/.ssh/aipostex_rec.pub"
# N isolated ranges (e.g. a 2-estate workshop under the default quota):
terraform apply -var="ssh_public_key_path=$HOME/.ssh/aipostex_rec.pub" -var='range_ids=[0,1]'
allowed_ssh_cidr defaults to 0.0.0.0/0 — override it to your operator IP during provisioning
(-var="allowed_ssh_cidr=$(curl -s https://checkip.amazonaws.com)/32"), or move to a VPN entrance
(see Access model). cloud-init creates labadmin with your key and a
range-scoped /etc/hosts (a range never name-resolves another estate's peers).
New-account note: a brand-new account may return
PendingVerification/VcpuLimitExceededon first launch. Re-runterraform applyonce the account clears — it creates only the missing instances.
2. Provision each range¶
One command per range from the dev machine (the range's attack box is the deploy origin, reaching the role hosts by private IP inside the range's subnet). Instances are discovered by EC2 Name tag, so this is independent of the terraform output shape:
AIPOSTEX_BIN=/tmp/aipostex-linux-amd64 RANGE=0 bash scripts/aws-provision.sh
AIPOSTEX_BIN=/tmp/aipostex-linux-amd64 RANGE=1 bash scripts/aws-provision.sh # ... per range
Each run waits for labadmin SSH, stages lab-scripts + the aipostex_rec key + the binary onto
that range's attack box, runs attack-box/provision.sh (lab-listener :9000, mcp-configs), then runs
LAB_SUBNET=10.0.(K+1) deploy-all.sh (in a detached tmux, tolerant of SSH drops) to bring the role
hosts up and seed them. Ranges are independent — provision them in parallel.
3. Verify green¶
# on each range's attack box (RANGE 0 shown):
cd ~/lab && LAB_SUBNET=10.0.1 bash verify-lab.sh # expect 62 passed / 0 failed
cd ~/lab && LAB_SUBNET=10.0.1 bash ctf/verify-chain.sh # expect 13/13
LAB_SUBNET=10.0.1 bash ~/lab/attack-box/verify-aipostex.sh --layer all # expect 304/0
Cross-range isolation — prove estates can't reach each other's service ports (each range's SG admits only its own /24):
RANGES="0 1" bash scripts/aws-verify-isolation.sh
# control: each range reaches its own services (rc 0); isolation: a probe to another
# range's service port times out (rc 28 = SG drop, not "refused"). Expect all PASS.
4. Bake the lab-ready AMIs + between-wave reset¶
Snapshot the green instances so a between-wave reset is a fast disk-restore (the cloud equivalent of
the lab-ready Proxmox snapshot + reset-wave):
RANGE=0 STAMP=$(date +%Y%m%d%H%M) bash scripts/aws-bake-ami.sh # tags LabReady=r0,Role=<role>
RANGE=1 STAMP=$(date +%Y%m%d%H%M) bash scripts/aws-bake-ami.sh # ... per range
# reset any/all ranges in parallel from their baked AMIs (restore root volume -> re-arm -> verify):
RANGES="0 1" bash scripts/aws-reset-wave.sh # expect each PASS (62/0 + 13/13)
aws-reset-wave.sh uses create-replace-root-volume-task (the direct analog of qm rollback),
keeping the instance, ENI, private IP, and type, then re-arms the state a disk restore can't hold
(seed ml, restart acme-mcp, warm Ollama) and re-verifies.
Access model¶
Provisioning uses a public IP + SSH (lock allowed_ssh_cidr to your operator IP). For the
attendee-facing / standing deployment, move to a VPN entrance and drop the public-SSH rule:
- Per-range WireGuard (workshop model, planned): each range's attack box terminates its own
WireGuard endpoint; each attendee gets a per-range
.confthat reaches only their range. Drop the SG port-22 rule so the only public inbound is WG UDP. This mirrors the Ludus/RTV per-range model (docs/deployment/ludus.md,rtv-capacity.md). - Tailscale standing entrance (operator): install Tailscale on the attack box so it joins your tailnet outbound, before baking the AMI, then drop the SG SSH rule:
Isolation & hardening¶
Each range is a network island:
- Its own subnet + security group; the SG admits only that range's own /24 (all ports) plus
operator SSH, so estates are isolated on every service port (proven by
aws-verify-isolation.sh). - The EC2 instances carry no IAM instance profile — a popped box has zero AWS-account reach.
- Every resource is tagged
Project=aipostex-labfor clean teardown.
Subset (tactic) ranges¶
The hands-on attendee tactic is the credential chain, which needs only dev, ml, ds, app, attack
(5 VMs, no k8s — the k8s coda is a presenter/sandbox beat). Deploying attendee ranges without the
k8s node saves 2 vCPU each, fitting 3 chain ranges under the default 32-vCPU quota vs 2 full.
Profile-based subset deployment (var.ranges profiles + deploy-all.sh --only) is a documented
follow-on — see the multi-range/profiles plan.
Teardown¶
cd terraform && terraform destroy \
-var="ssh_public_key_path=$HOME/.ssh/aipostex_rec.pub" -var='range_ids=[0,1]'
Deregister any transient baked AMIs + their snapshots you no longer need (keep the r0 baseline for
fast redeploy):