← All essays · 2026-08-11
Okta org as code

I didn’t start writing Terraform for Okta because I enjoy HCL. Nobody enjoys HCL. I started because of a question that sounds easy and isn’t: this sign-on policy rule, the one that exempts a group from MFA, who approved it, and when?
In a console-managed org, the honest answer is a shrug. Most orgs are configured by hand, by whoever held Super Admin that quarter. Nobody wrote down why. Three years later the org is an archaeology site, and the person who could read the strata has left.
What the code actually buys you
Terraform flips the direction of truth: the Git repository becomes the declared intent, the org an artifact reconciled against it. Every change arrives as a diff with an author and an approver attached. That alone justifies the effort, but the interesting part is drift.
State records what Terraform believes it created; the live org records what exists after admins touched it. Run terraform plan every night, not just at deploy time, and it becomes a detective control. -detailed-exitcode returns 0 when the org matches the code, 2 when something drifted, 1 on error. A nightly job alerting on exit code 2 answers a question most security teams cannot: did anyone change production identity policy outside change management yesterday?
The trap that makes teams quit
The most instructive failure mode never throws an error. Okta keeps policy rule priorities as a dense sequence: 1, 2, 3, no gaps. Declare priorities that collide or leave holes and Okta silently renumbers them on write; the next refresh reads back numbers that don’t match your code, and plan shows a diff. You apply, Okta renumbers, the diff returns. Forever.
The conclusion writes itself: Terraform doesn’t work with Okta. The team drifts back to the console, and the org ends up half-managed, worse than before. The fix: declare priorities explicitly and contiguously, and leave no unmanaged rule inside a managed policy.
Blast radius is the real design problem
A wrong terraform apply against a production org can lock out your entire workforce. Delete an okta_app_oauth block and Terraform destroys the app, including its client_id; everything that hard-coded that ID breaks, and re-creation does not give it back.
So design for containment: one state file per tenant, a break-glass Super Admin left outside Terraform so no apply can orphan you out of your own org, prevent_destroy on production apps, and production applying only a plan computed moments ago against live state, after a human read it.
One last question: who can merge to the branch that owns production sign-on policy, and is that set different from who holds Super Admin? If “same people”, you moved the console into GitHub without gaining a control.
Start smaller than you think
Code answers my opening question as a side effect: the diff is the approval, the merge is the timestamp, the nightly plan keeps the answer true at 3 a.m.
This week, in a preview org: terraform import one sign-on policy, change one rule by hand in the console, run the plan with -detailed-exitcode, and watch it come back 2. One imported resource and a twelve-line script: your org’s most sensitive configuration, telling on itself.