An organisation from scratch

One organisation, from an empty directory to an estate that runs as its own IaC service account, carries the pack map, the security groups and the CIS baseline, and publishes what the projects beside it read. The customer is Acme Corp. of docs/examples.md: acme, example.com, directory C0example, organisation 123456789012, billing account 012345-6789AB-CDEF01. None of the values exists; replace them with yours. The result is the estate under examples/acme/, written by the same command.

What each step does in full is workflows: from nothing to applied; this page is the order in which you type it.

What you need

1. Write the estate

In an empty directory — the repository of this organisation from now on — satz init writes the estate from the day-0 values and asks for what they leave out:

mkdir acme && cd acme
satz init \
  --customer-id C0example \
  --customer-shortname acme \
  --customer-domain example.com \
  --customer-organization-id 123456789012 \
  --billing-account-infra 012345-6789AB-CDEF01 \
  --iac-user admin@example.com \
  --default-region europe-west3 \
  --workload-folder-name Workloads \
  --interview

A value you leave out is derived from the login where it can be — the domain and the first admin from the identity, the organisation and the directory id from organizations:search, the billing account when exactly one is open. --interview fetches the preset library before the file is written, so the estate carries the pack menu, and then asks the day-0 questions neither the flags nor the login answer: here one, the customer's full name, customer_longname — answer Acme Corp.. It ends with every question answered and names the next command.

It writes:

file what
config.toml where the estate, the generated HCL, the interfaces, the schemas and the presets live
satz/C0example.satz the estate: its params { }; use "presets/estate-core.satz", the pack that declares the day-0 params with their questions; the providers, the IaC group and service account, the infrastructure folder with the management project and the state bucket in it, the Workloads folder and what the estate publishes (export "workload_folder", export "infra_folder"); and the pack menu — one commented use "presets/…" when <gate> line per pack of the library, under the phase it can be adopted in
presets/ the library, with its pack graph
schemas/ the provider schemas every resource key is checked against, read from the provider through tofu init
hcl/ empty: where the HCL goes
.gitignore what is generated and what is private

Open satz/C0example.satz. Two things to read: the params { } block, which binds every day-0 value by name and is the one place a value is changed; and the resources below it, which are the Terraform provider's resource types to the underscore. The pack menu is commented: the day-0 scaffold deploys alone, and every pack comes after the identity it runs as exists.

satz questions C0example.satz --format text --out -

lists each question of the core pack with its answer. Without --interview, init writes the core line commented and no menu: satz get-presets then satz merge-presets write the menu, and satz interview offers to switch the core line on.

Commit the directory. From here every change to the organisation is a change to this file, and tofu's state is the only other thing that moves.

2. Bootstrap the organisation

satz bootstrap C0example.satz --dry-run
satz bootstrap C0example.satz

The dry run prints what the real run would create and runs its pre-flight: the identity against first_admin, the organisation resolved and matched to the directory id, the permissions tested one by one. The real run creates the day-0 infrastructure — the infrastructure folder, the management project acme-infra-001, its billing link, the foundation APIs, the state bucket acme-infra-001-state — then transpiles, initialises tofu and imports what it made, so the estate manages it from the first plan on. It creates no service account and grants nothing beyond the roles it may have to give the caller: the identity layer is declared in the estate and comes with the first apply.

Bootstrap ends by naming the next commands with the estate's own values. They are the next two sections.

3. Apply as yourself

satz update-prerequisites C0example.satz
satz transpile C0example.satz --plan
satz transpile C0example.satz --apply

update-prerequisites writes into the estate the roles its resource types need on the IaC service account and the APIs they need on the project the calls are billed to. transpile compiles the estate to hcl/ — main.tf, providers.tf, variables.tf, terraform.tfvars, imports.tf, and outputs.tf with what the estate exports — and --plan runs tofu plan on it after enabling the APIs the plan would otherwise trip over. Read the plan: the Cloud Identity group svc-iac-users, the service account svc-iac-001, its roles on the organisation and the billing account, the bucket's settings. --apply creates them. This run authenticates as you: the estate is in deployment_mode = "local", the state is a file in hcl/.

4. Switch to the service account

satz migrate C0example.satz --mode cloud
satz whoami C0example.satz
satz plan

migrate sets deployment_mode = "cloud" in the estate, moves the state into the bucket, re-initialises tofu and checks that the service account holds Groups Admin, the Workspace role the group resources need; from here every provider and the backend impersonate svc-iac-001@acme-infra-001.iam.gserviceaccount.com, and so does every live satz command. whoami prints that: runs as: svc-iac-001@… — impersonated by admin@…. The plan must read No changes — the estate, the state and the organisation agree — and that is the condition for the first pack.

5. The packs, one at a time

The first line of the menu is the map, and it goes on by its path:

satz add-pack C0example.satz presets/estate-map.satz

The map declares the questions every other pack's line is gated on — which security group model, which packs on — and add-pack says how many it opened and names the command that asks them:

satz interview C0example.satz
satz packs C0example.satz --format text --out -

Answer the model (S1, the five admin groups) and the recommended packs with their defaults, or run satz interview C0example.satz --accept-defaults and read packs for what is on: the pack, its gate's answer, its line, what it needs and what needs it. A yes switches the pack's line on, so the estate's file is the record of every choice.

Each pack is its own plan and apply, in the order the menu writes them — the security groups before anything that grants to them:

satz transpile C0example.satz --plan
satz transpile C0example.satz --apply

The CIS baseline is the one to read before applying: its organisation policies land in one apply. Switch it on and read what it asks for first:

satz add-pack C0example.satz use_cis_baseline
satz adopt C0example.satz --execute --import
satz transpile C0example.satz --plan
satz transpile C0example.satz --apply

add-pack binds the gate and activates the line; the pack's notice says why adopt comes first — Google sets some of these policies on every new organisation, and an apply that creates a policy that exists stops on 409 POLICY_ALREADY_EXISTS. adopt resolves what is already there, writes its ids into the estate as "import-id", so the plan imports instead of creating, and binds cis_baseline_adopted = true, which every command that writes to the organisation refuses to run without. A pack that needs a role the service account does not hold yet is named by the compile; satz update-prerequisites C0example.satz writes the grant, and the apply creates the grant and what needs it.

What the estate claims against the frameworks it is held to, and what it has in fact:

satz require cis-gcp-5.0 C0example.satz --format text --out -
satz report-compliance cis-gcp-5.0 C0example.satz --format markdown --out evidence/cis-5.0.md

require judges the declared estate — which controls the packs claim and whether every witness they name is emitted; report-compliance verifies the witnesses live, compares each organisation policy by value, and writes the report with its append-only evidence history into evidence/, beside the estate and git-ignored.

6. What the organisation publishes

Every transpile writes interfaces/ beside hcl/:

satz interfaces C0example.satz --format text --out -

interfaces/common/core/ holds the core exports — organization_id, customer_domain, customer_shortname, default_region, infra_project_id, iac_service_account from estate-core, infra_folder and workload_folder from the estate — as an HCL module and as a Satz file, with a README that names each value, how it is obtained and what may be attached to it. That folder is what a project beside the organisation reads, and the next tutorial starts from it: projects beside the organisation.

Where things stand

the estate satz/C0example.satz, the one file every change goes through, in your repository
the state gs://acme-infra-001-state, read and written as the IaC service account
the identity svc-iac-001@acme-infra-001.iam.gserviceaccount.com, impersonated by the members of svc-iac-users@example.com
the organisation the infrastructure folder with its project and bucket, the Workloads folder, the five S1 groups and their roles, the CIS baseline's policies
what it publishes interfaces/common/, taken whole by every project beside it
the next change edit the file, satz transpile C0example.satz --plan, read, --apply

A pack the library gains later is one satz merge-presets C0example.satz away, written into the menu commented; satz self-update brings the binary that knows it.