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:

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

shell
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

shell
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

shell
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

shell
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

shell
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:

shell
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

shell
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=true is never touched. Resources tagged cin90:role=guardrail survive too, unless --final is 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:protect unchecked.

--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:

shell
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:

shell
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.

shell
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.