Just getting started?
For students — Run the labs in your own AWS account, inside a budget cin90 enforces.
Teaching the class?
For instructors — Preview labs, rehearse, and read what your students filed.
AWS cost guardrails
Students run everything in For students, in their own AWS account. Instructors have extra capabilities, in For instructors. Enforcement differs between them — where the server holds the line and where it does not yet is set out in Who can do what.
The aws-de labs run in your own AWS account. cin90 aws puts a ceiling on
what that can cost, and tears the labs down afterwards.
These commands talk to AWS with AWS credentials — the same ones aws sts get-caller-identity uses. Only cin90 aws prove also reads your coderin90.com
token, because it submits to the server; setup, guard, status and nuke
never do.
Configuration
$XDG_CONFIG_HOME/cin90/aws.json, or ~/.config/cin90/aws.json:
{
"alert_email": "you@example.com",
"regions": ["eu-west-1"],
"budget_amount": 100,
"course_start": "2026-09-01",
"protected_accounts": ["999988887777"],
"allowed_accounts": []
}
Only alert_email has no default — a budget with no subscriber is a budget
nobody hears about, and cin90 aws setup refuses without it.
protected_accounts is never operated on, whatever else is set. A non-empty
allowed_accounts is the only set of accounts that can be. An account named in
both is refused: the lists disagreeing is a misconfiguration, and the one that
means "never touch this" wins.
Tagging defaults to course=aws-de. Nothing outside that tag is ever
considered for teardown.
Who can do what
Enforcement is per capability, not one blanket rule. Check the row you care about.
| Student | Instructor | Enforced by | |
|---|---|---|---|
cin90 aws setup / guard / status / nuke |
yes | yes, in their own account | your AWS credentials |
cin90 aws prove |
yes | yes | cohort roster |
Filing evidence as a rehearsal (cin90 aws selftest) |
no | yes | server, not-instructor (cin90-rewrite#1087) |
cin90 kickoff preview / run |
no | yes | server, not-instructor |
cin90 kickoff is genuinely server-enforced: a non-instructor is refused with
a not-instructor problem response whatever their local config says.
The rehearsal flag is meant to be as well. cin90 aws selftest is the only
command that sets it, since cin90 aws prove always sends rehearsal: false
and has no flag for it. The server check that refuses it from anyone who does
not teach the cohort is cin90-rewrite#1087. See Rehearsals.
Every AWS command operates on whichever AWS account the caller's own
credentials resolve to, and nothing in the CLI accepts another account as a
target. Be precise about what that guarantees: the boundary is your AWS
credential chain, not the CLI. An instructor holding a cross-account role in a
student's account — through AWS_PROFILE, an assumed role, or AWS_* in the
environment — resolves to that account like any other, and nuke would act on
it. The protected_accounts and allowed_accounts config exists to stop you
operating on the wrong account of your own, not to grant or withhold reach
into somebody else's.
Evidence is filed against the caller's own identity. A student cannot file evidence for another student, and the server refuses a caller who is not on the cohort roster at all.
For students
Day one
cin90 aws setup
Creates a $100 annual cost budget with alerts at 25%, 50% and 75%, plus a
forecast alert at 100%; a cin90-budget-deny-create policy; a budget action
that attaches that policy automatically at 75%; and a cin90 Athena workgroup
capped at 5 GB scanned per query.
The budget is created with credits and refunds excluded. With credits counted, net cost sits near zero however much you spend, so nothing ever alerts and the first you hear of it is when the credits run out.
Run it again any time — everything it creates, it also recognises.
Two cases it will tell you about rather than paper over:
- Signed in as root. Root cannot be restricted by any policy, so the budget action is skipped. Create an IAM user and run setup again as that user.
- An IAM Identity Center role (
AWSReservedSSO_…). The policy attaches, but Identity Center rewrites those roles on every provisioning run and drops it.
After the Redshift and Glue labs
cin90 aws guard
Caps Redshift Serverless base capacity at 8 RPUs with a 5 RPU-hour daily limit, and Glue jobs at 30 minutes and 2 workers. Glue's own default timeout is 48 hours.
Run it after the labs that create those resources — a cap cannot be applied to something that does not exist yet. Run it before you reach 75% of budget: past that the deny policy is attached, and it blocks Redshift's update actions, which is how capacity comes down as well as up.
guard writes, so it honours cin90:protect=true the same way teardown does: a
protected workgroup or Glue job is left at whatever size you set it. An existing
usage limit that does not enforce the daily cap is repaired; one that is
tighter than the course default is left alone.
Checking spend
cin90 aws status
cin90 aws status --no-cost # skip Cost Explorer; AWS bills $0.01 per query
Shows spend by service, credits applied, what is left of the budget, and everything currently running — including untagged resources that bill by the hour. Elastic IPs are listed whether or not they are attached: associated public IPv4 bills too.
The window matches the budget's own period. With course_start set, the budget
is created to start on that date and spend is reported from it; without one,
both run over the calendar year. They have to agree, or "remaining" is a month
of spending subtracted from a year's allowance.
Cost Explorer takes about 24 hours to populate after it is first enabled. On
day one status says so and still reports what is running.
Proving you built the lab
cin90 aws prove --lab day4 <cohort>/<assignment>
Captures what the lab asked you to build: every resource carrying the course tag, plus a shape check per service saying the lab ran rather than that a resource was created — the Glue job has a script, a run history and a last run that succeeded, the bucket has objects, the workgroup is available.
Each check reports one of three things, and the third is not a failure:
- pass — the check found what it expected.
- fail — it did not.
- unavailable — nobody could look. A missing permission, a service that answered an error. It is recorded with the reason and is never graded as a fail: the two are different facts.
The capture is written to .cin90/aws-evidence.json before anything is
uploaded. Run it before cin90 aws nuke, which destroys exactly what the
evidence describes.
The capture is submitted to coderin90.com. If the upload fails for any reason,
the command still exits zero and tells you where the file is: the capture is
complete on disk and prove can be run again to retry, but the resources it
describes may not survive a teardown in between.
Working through a lab
cin90 aws lab --plain <cohort>/<assignment>
Fetches the checklist your instructor published for the assignment and checks each item against what carries the course tag and the lab's tag in your account. Each item is one of:
- done: enough resources of that kind exist and every listed check passed on each of them.
- waiting: none found yet, or fewer than the item asks for.
- failing: found, but a check failed. The line names the check.
- unknown: nobody could look. A check could not run, or discovery could
not read part of the account (listed under
coverage:). This is never graded as failing.
The checklist is fetched before anything touches AWS. If you are not on the cohort, or the assignment has no checklist, the command stops without making any AWS call.
--plain is one read-only evaluation for scripts and CI. It writes nothing,
not even .cin90/, and uploads nothing. The exit code carries the verdict:
| Code | Meaning |
|---|---|
| 0 | every item is done |
| 3 | some items are waiting or failing, none unknown |
| 4 | at least one item is unknown |
| 5 | the assignment has no checklist yet |
| 1 | anything else: not signed in, not enrolled, assignment not found, a checklist the CLI cannot use, an account outside allowed_accounts, discovery failed |
Without --plain, the command needs a terminal, and opens an interactive
view of the same checklist:
| Key | What it does |
|---|---|
r |
check again (writes nothing) |
f |
save the capture on screen under .cin90/aws-lab/captures, then file every pending capture with the server |
F |
first return captures the server rejected to pending (after cin90 login, or once you are on the roster), then as f |
t |
save the capture on screen, then plan a teardown of this lab |
q |
quit; the first press warns if captures are still unfiled |
A saved capture is never overwritten. One the server did not answer for stays
pending and goes with the next f. When the view closes, the command prints
the last report and how many captures are still unfiled.
Tearing the lab down from the view
t saves the capture on screen first, because once the lab is gone it cannot
be captured again, and the plan screen says whether that capture is filed yet.
It then lists what carries the course tag and this lab's tag, exactly what
cin90 aws nuke --lab <lab> would find: no student filter, no untagged
burners, and the guardrails kept. Each resource is marked delete, skip
(with the tag that protects it) or manual (with the reason). A long plan
scrolls with the arrow keys; the counts line always shows the totals.
To confirm, type the lab name exactly and press Enter. Every printable key is
typed into the name, including q, r, f and t, so only Esc or Ctrl-C
go back. Nothing is trimmed or case-folded. Before anything is deleted, Enter
checks the account and the plan again: if the credentials now resolve to
another account, or anything that would change what gets deleted has changed
(a resource retagged, protected, or newly created), nothing is deleted, the
new plan is shown, and you type the name again. Otherwise it runs the plan
that is on screen and nothing else.
While it runs, each resource shows deleting, deleted, retry (pass N) or
failed. q or Ctrl-C stops it: the delete in flight is cancelled and shown
pending with everything not reached, never failed. Further presses do
nothing until the outcome is on screen.
Afterwards the view looks again and shows what is still present, separately
from what is pending and what failed. A resource reported deleted that is
still listed is usually the tagging API catching up; press t again in a
minute. Skipped and manual items are listed as the second look found them.
Until that look shows nothing left and no place it could not read, the
screen shows the command that finishes the job, with every region the view
covered as its own --region flag:
cin90 aws nuke --lab day4 --region eu-west-1 --execute --confirm-account 123456789012
The command prints the same line on stderr when the view closes, including when the terminal is closed during a teardown or while it is being checked.
The checklist endpoint is not live on coderin90.com yet. Until it is, the command refuses with "not implemented yet on the server".
Tearing down
cin90 aws nuke # prints a plan, deletes nothing
cin90 aws nuke --lab day4 # narrow it
cin90 aws nuke --execute --confirm-account 123456789012
nuke prints a plan and stops. --execute requires --confirm-account with
the account id typed out in full, compared exactly — no trimming, no prefix
match. That one step is what makes you look at which account you are about to
empty.
The plan has three parts, and reads all three out:
- delete — ordered so nothing restarts what is being deleted. Schedulers first, then jobs, then warehouses, then network, then buckets.
- skip —
cin90:protect=trueis never touched. Resources taggedcin90:role=guardrailsurvive too, unless--finalis passed, so a mid-course teardown leaves the caps standing. - manual — VPCs, subnets, security groups, IAM. Nothing deletes these:
their blast radius is your whole account rather than one lab. A resource
whose tags could not be read lands here too, rather than being deleted with
cin90:protectunchecked.
--include-untagged-burners extends teardown to resources nobody tagged.
status always shows them; nuke only touches them when asked. A burner that
is tagged still has to agree with --lab and --student: the flag widens
teardown to untagged resources, not to other people's labs.
Deletion runs up to five passes, 45 seconds apart, because dependencies clear
in order — an Elastic IP cannot be released until its NAT gateway is gone.
Anything still blocked at the end is reported pending, and running nuke
again clears it. failed is different: another pass will not help.
For instructors
Everything above applies to your own AWS account too — the guardrails are the same guardrails. This section is the part students do not have.
Confirming the tooling works before a class
You want to know the whole path works on your machine before thirty people depend on it:
cin90 aws selftest <cohort>/<assignment>
It creates one throwaway S3 bucket, tagged lab=selftest by the same call that
creates it, and puts one object in it. Then it runs the real capture — the same
discovery and probes prove uses — checks that the capture found the bucket and
that its probe passed, and files it through the real evidence endpoint as a
rehearsal. Last, it tears the bucket down with nuke's own teardown, whatever
failed before that.
selftest passed means the whole path works: credentials, tagging, discovery,
probes, auth, and the server. Anything else exits non-zero and names what
failed. Unlike prove, a refused upload fails the command: the upload is part
of what is being tested. So does a row the server stored without the
rehearsal flag. The failure names that row's id, because it is gradeable work
and has to be removed.
Your AWS credentials need s3:CreateBucket, s3:TagResource (the tags go on
at creation), s3:PutObject and the usual discovery and teardown permissions.
Do not run two selftests in the same account at once. Each one's teardown
deletes everything tagged lab=selftest, including the other run's bucket in
the middle of its capture.
The bucket bills for as long as it exists. Teardown still runs after Ctrl-C, and after a teardown pass it deletes the bucket by name in case discovery did not see it. If teardown does not finish, the command prints this line, which finds what was left:
cin90 aws nuke --lab selftest --execute --confirm-account <your account id>
The teardown deletes everything carrying the course tag and lab=selftest, not
only the bucket this run made, so an earlier interrupted run is cleaned up too.
Do not use lab=selftest for anything you want to keep.
Rehearsals
Evidence carrying the rehearsal flag is stored and shown to you, and is never counted toward anyone's work. It is how a self-test result is told apart from a student's real submission.
cin90 aws selftest is the only command that sets it. cin90 aws prove always
sends rehearsal: false and has no flag for it.
The flag is instructor-only. cin90-rewrite#1087 makes the evidence endpoint
refuse a rehearsal from anyone who does not teach the cohort, with a
not-instructor problem. A rehearsal row a student could forge would make the
self-test signal unreadable, and a student who set the flag by accident would
file work that is silently never graded.
selftest does not take the server's word on the other half either. If the row
comes back without rehearsal: true, the command fails and names it rather
than passing over a gradeable row.
Reading what a student filed
Each resource in a capture carries probes, and there are three outcomes. The third is not a worse version of the second:
- pass — the check found what it expected.
- fail — it did not. The lab did not produce what it should have.
- unavailable — nobody could look. A missing IAM permission, a service that errored. This is not a failing lab and must not be read as one. It is an infrastructure fact about the student's account, and treating it as evidence of missing work turns a permissions problem into an accusation.
An empty capture is also a real answer: it means the command ran and found nothing tagged, which is different from the student never having run it.
End of the course
This one is for whoever owns the account — student or instructor, in their own.
cin90 aws nuke --execute --confirm-account <id> --final
--final removes the guardrails themselves along with everything else.
Read this page in your terminal: install the cin90 CLI and run
cin90 docs. The same page is served as
raw markdown.