Course 2 · after Fountain: The IAM repo · lesson 4 (I4)
Deploy to Floci
- Properties
- XI The live system is the truth, I Honor the lower layer
- Goal
- Apply the estate you have been declaring for three lessons against a local Floci, with no AWS account and no credentials anywhere, prove it converged with a plan that exits 0, read one role back out of the cloud and match it against the file that declared it, then add that same credential-free plan to the check stack a PR job runs.
- Done when
terraform -chdir=access/envs/prod plan -detailed-exitcodeexits 0 straight after the apply, with no AWS credential in the environment and no account behind it, andaws iam get-role --role-name site-publisherreturns theRoleName,Descriptionandownertag thataccess/envs/prod/iam_role.site_publisher.tfdeclared.- Restart from
- checkpoint/i3 (the lesson whose checkpoint to reload if this one breaks)
- Mode
- self-paced or live · about 30 min
Run with an agent
Paste this to your agent.
curl -fsSL https://raw.githubusercontent.com/INTENTIUS/waterpark/main/skills/i4-deploy-to-floci/SKILL.md and follow that skill with me, step by step. Confirm with me before you install or write anything.
The skill is skills/i4-deploy-to-floci/SKILL.md (on GitHub). Installable too, with npx skills add INTENTIUS/waterpark, or by copying skills/i4-deploy-to-floci into ~/.claude/skills/, or as a Fountain agent skill from INTENTIUS/waterpark.
Do · 30 min
You need: a water park checkout with the tags fetched, Docker running, the patched Floci image ghcr.io/lex00/floci:iam-boundary, terraform 1.9 or newer, tflint from terraform-linters/tap/tflint, just access-init run once per clone.
Context
- One variable picks the target.
flocidefaults to true, which points the provider’siam,stsands3endpoints athttp://localhost:4566, hands it the throwawaytestkey pair and skips every call that would resolve a real account. The live path passes-var floci=falseand the same code talks towaterpark-prod. - The backend that ships checked in is the local one,
access/envs/prod/backend.local.tf, so a fresh clone runsterraform initwith no account, no credentials and no bucket to create first.access/scripts/backend s3 envs/prodswaps in the S3 backend for the live path, and exactly one backend file is present at a time. - Terraform hosts a state file, which is the thing Accessible Ops XI warns about. This lesson names the cost out loud rather than hiding it (decision 32). On the live path state lives in
waterpark-securitywith locking, it is never the system of record, and every read of the estate in these lessons goes to the cloud instead. plan -detailed-exitcodeexits 0 for no changes, 2 for a diff and 1 for an error. A green apply that has not converged gets caught rather than believed.aws iam get-rolereads a role back out of the cloud rather than out of state. The file predicted it.- Floci runs no Organizations, no Identity Center and no Access Analyzer, and the verdicts are recorded in upstream .
- A failed apply stops where it failed and leaves behind what it already made, with no rollback. The answer is a small change planned first, not a bigger apply.
- The provider lock file is not committed, because it records provider hashes for the platforms it was generated on and students run this on three of them.
Do
Lessons 1 to 3 declared an estate and checked it without ever talking to a cloud. This is where it runs.
Start from the checkpoint lesson 3 left, in a worktree of its own, so your own branch stays where it is.
git fetch origin --tags git worktree add ../waterpark-i4 checkpoint/i3 cd ../waterpark-i4 just access-initjust access-initiscd access && tflint --init, which installs the OPA ruleset the rule pack runs on. It is once per clone and says “All plugins are already installed” if you have done it before.Start Floci with IAM enforcement on, and prove it answers.
docker run -d --name wp-i4-floci -p 4566:4566 \ -e FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED=true \ ghcr.io/lex00/floci:iam-boundary curl -s --retry 15 --retry-all-errors --retry-delay 1 \ -o /dev/null -w '%{http_code}\n' http://localhost:4566/The curl prints
200. The retry flags are there because the container binds the port before it answers on it, so the same curl without them prints000the first time. The image is a patched fork build rather than the upstream release, and lesson 5 is where that matters. compose/README.md says which two bugs it fixes, andjust uppulls the same image.Initialize with the local backend, and read what it tells you.
terraform -chdir=access/envs/prod initIt prints
Successfully configured the backend "local", two lines in. That is the cost this lesson names. Terraform is about to writeaccess/envs/prod/terraform.tfstate, a JSON file that lists every resource it manages, and Accessible Ops XI says the live system is the truth. Water park does not pretend the file is not there. It is gitignored, it is bookkeeping and never the system of record, and on the live pathaccess/scripts/backend s3 envs/prodputs it in thewaterpark-terraform-statebucket inwaterpark-security, encrypted and locked. Every read in the rest of this lesson goes to the cloud instead of to that file.Apply.
terraform -chdir=access/envs/prod apply -auto-approveNineteen resources land. Three workload roles, the grant policies
modules/personaexpands from each leaf file’s access levels, an attachment per grant, and the two buckets. No credential was asked for and no account exists.Prove it converged, and make the tool say so rather than saying it yourself.
terraform -chdir=access/envs/prod plan -detailed-exitcode echo $?“No changes. Your infrastructure matches the configuration.” and
0. A run that left something unapplied would print2here even though the apply was green, which is the whole reason this flag is in the command.Read a role back out of the cloud and hold it against the file.
AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 \ aws --endpoint-url http://localhost:4566 iam get-role --role-name site-publisherOpen
access/envs/prod/iam_role.site_publisher.tfbeside it.RoleNameis thename.Descriptionis thedescription, word for word. Theownerandpersonaandteamstags are whatmodules/personabuilds fromowner,personaandteams, andmanaged_by,repoandenvcome from the provider’sdefault_tags.AssumeRolePolicyDocumenttrustscodebuild.amazonaws.com, which is the module’strusted_servicesdefault, not anything the leaf file said.PermissionsBoundaryis absent, and that is lesson 5.The three
testvariables are the throwaway pair the provider already uses. They are not a credential, and nothing in this lesson has one.Put that plan in the check stack, so a PR job runs it too. Open
access/scripts/checkand add the stage below afterrun_fixtures.floci_endpoint="${FLOCI_ENDPOINT:-http://localhost:4566}" # The plan a PR job runs. It reaches Floci and no cloud, so it holds no # credential, which is the half of prescription 6 lesson 4 closes. Skipped # with a line rather than a failure when Floci is not up, because the rest of # the stack is still worth running. run_plan() { say "== terraform plan against Floci" if [ "$(curl -s -o /dev/null -m 3 -w '%{http_code}' "$floci_endpoint/" 2>/dev/null)" = "000" ]; then say "skip Floci is not answering on $floci_endpoint. Start it with:" say " docker run -d --name wp-access-floci -p 4566:4566 \\" say " -e FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED=true \\" say " ghcr.io/lex00/floci:iam-boundary" return fi terraform -chdir=envs/prod init -input=false -no-color >/dev/null 2>&1 terraform -chdir=envs/prod plan -detailed-exitcode -input=false -no-color >/dev/null 2>&1 case $? in 0) ok "envs/prod matches the account, exit 0" ;; 2) fail "envs/prod has an unapplied diff. Run terraform -chdir=access/envs/prod plan" ;; *) fail "terraform plan against Floci errored" ;; esac }Then wire it in, in four places. Add
plan) run_plan ;;to thecaseat the bottom, underfixtures) run_fixtures ;;. Addrun_planas the last line of theallbranch. Change the usage string tousage: access/scripts/check [all|fmt|validate|lint|fixtures|plan]. And in the comment header at the top of the file, add, planto the end of the first line and this line under thefixturesone, spaced to line up with it.# access/scripts/check plan just the credential-free plan against FlociThe skip is the point of the shape. Floci not being up is a reason to say so and carry on, not a reason to fail a PR.
Run the one stage, then the whole stack.
just access-check plan just access-checkThe first prints
ok envs/prod matches the account, exit 0. The second runs fmt, validate, tflint, the rule fixtures and now the plan, and ends oncheck passed. One warning survives it,aws_iam_role.this carries no permissions_boundary, onmodules/persona. It is a warning rather than an error because the boundary it asks for does not exist yet. Lesson 5 builds it and promotes the rule.Nothing in that command holds a credential. That is the half of prescription 6 this lesson closes. The fork-PR test that proves no credentialed job is reachable lands in lesson 6.
Compare with the reference repo.
git diff checkpoint/i4 -- accessThe only file left is
access/README.md, which is the prose the reference repo carries for the same steps you just ran.Tear down. Floci keeps everything in memory, so removing the container removes the account, but destroy first so state and cloud agree at the end rather than only at the start.
terraform -chdir=access/envs/prod destroy -auto-approve docker rm -f wp-i4-flociLeave the worktree. Lesson 5 starts a fresh one from
checkpoint/i4.
Self-paced
Everything in this lesson is self-paced. Floci runs the AWS APIs in process, so the Terraform is real, the IAM is real and the account is not.
Three things it does not run, and each one is a lesson that says so. There is no Organizations, so the multi-account layer is live only. There is no Identity Center, so the human principals in access/identity/ are validated on every check and applied only against a real account. There is no Access Analyzer, so the validate-policy proofs in lesson 6 are live only.
Two more limits worth knowing before you trust a green run. The provider overrides three endpoints, iam and sts and s3, so anything outside those three is not declared in envs/prod yet. And the test key pair resolves to arn:aws:iam::000000000000:root, which acts unrestricted even with enforcement on, so what you proved here is that the estate converges, not that a caller is refused. Refusal gets proven in lesson 8, where Floci does answer the question.
Live
Fifteen minutes, and the room watches one terminal.
Run the apply, then the plan -detailed-exitcode, and say the number out loud. Zero means the account matches the file. Then get-role beside iam_role.site_publisher.tf on the other half of the screen, line by line, until somebody says it before you do.
Two honesty lines belong in this room. The first is that the Floci image is a patched fork build, ghcr.io/lex00/floci:iam-boundary, not the upstream release. Upstream 2.0.1 never returns a role’s permission boundary on read, so the clean plan you are about to show would not be clean on it, and lesson 5 would have nothing to demonstrate. The fix is ours and it is linked from compose/README.md.
The second is the state file. Point at access/envs/prod/terraform.tfstate and say that Terraform hosts state, that Accessible Ops XI counts that against it, and that a backend with no state file scores better on this property. The mitigations are real and they are not the same as the cost not existing. State goes to waterpark-security with locking, it is never read as truth, and lesson 7 makes the drift watch compare what the repo declares against what the cloud holds.
Live, the same code runs against a real sandbox account with -var floci=false and the S3 backend from access/scripts/backend s3 envs/prod.
Further reading
- access/README.md , “Deploy to Floci” and “State and the two backends”
- Upstream , both “Floci as a Terraform target” runs
- compose/README.md , the Floci image note
- The estate , accounts and resources
- Decisions , 32
- Multi-account , item 3
- Issues A13