Skip to content

See every project in one page

llms.txtlists every page for an agent
Optional: hand this page to your coding agentThe steps work by hand too.
Show the whole prompt
Read https://intentius.io/terragucci/guides/see-every-project/.
In this control repo, add the scheduled estate job the page gives for my forge,
and write the IAM policy for bucket <bucket> as a file for me to review. Open a pull request.
Do not create the role, the bucket or any secret.
Never apply, approve (a pull request review or `terragucci approve`), override a policy denial (`terragucci override`), use `--mode apply`, or merge; never touch `.chant/allowed_signers` or `chant/lifecycle`.

A scheduled job rebuilds estate.html and estate.json at the top of your reports prefix. Nothing is hosted, since the page is an object in your bucket or Blob container; a signed link to it (an S3 presigned URL, a GCS V4 signed URL or an Azure user delegation SAS) ends the job’s log.

The page shows From
projects, waves waiting, projects drifted, roots failed four counts at the top
each waiting wave and how long it has waited the newest commit that ran tf-apply
roots applied under a policy override, when there are any a fifth count, and the apply wave that applied them
per project: latest plan, latest drift check, apply waves each project’s index.json
each project’s roots by wave, an arrow from each root to the roots that read its state, dashed between projects, and a list of the reads between projects the run view of each project’s newest applied commit, runs/<commit>/run.json
the 20 newest runs every project’s index.json
the resources each root holds, by project, root and type, with a filter box each project’s inventory.json
each resource’s change history, on history.html each project’s changes.json, and audit.jsonl for the approvers
each root’s state versions, newest first, or that its bucket keeps none each project’s states.json
each root that reads another root’s state, with its last plan against the producer’s last apply each project’s edges.json
a link to the audit trail, when the job writes one audit.json beside the page
the four DORA metrics per project and for the estate, week by week, also in dora.json audit.jsonl and each project’s index.json

The page reads only the files above and never a report or a plan.

In a Terragrunt repo the graph groups units into the waves their apply cut from terragrunt find; an edge links a unit to each unit whose dependency or dependencies block names it. A read between projects is a terraform_remote_state block in one project whose bucket and key name the state another project’s root holds in its backend block. Under choudoufu, a root’s state is its estate’s records, and a terraform_estate_outputs read of an estate another project owns is such a read. A project shows on the graph once one of its applies wrote a run view, which needs reports.bucket in its own config. The graph and the run view are inline SVG, so the page loads no script.

You need Why
Reports in a bucket for every project you want on the page the page reads each project’s report index there
A repo to run the job in: your control repo, or any repo whose terragucci.yml names the bucket the job writes the page to that bucket
  1. Pick where the projects come from.

    Run from Projects on the page Read from Page written to
    a control repo its projects: each project’s own reports defaults.reports
    one repo every project in the top index.json that repo’s reports the same bucket

    For projects in other accounts:

    Option Set up
    one central bucket every project’s reports.bucket names it, each writing with a reports.role of its own
    a bucket per account the project’s reports in the control repo names its bucket and a role that can read it
  2. Give the job an identity.

    The job What
    reads the indexes, each apply wave’s report, the audit trail
    writes the page and its files, the audit trail (step 5)
    signs the link
    {
    "Version": "2012-10-17",
    "Statement": [
    { "Effect": "Allow", "Action": "s3:GetObject", "Resource": ["arn:aws:s3:::acme-terragucci/reports/*index.json", "arn:aws:s3:::acme-terragucci/reports/*inventory.json", "arn:aws:s3:::acme-terragucci/reports/*changes.json", "arn:aws:s3:::acme-terragucci/reports/*states.json", "arn:aws:s3:::acme-terragucci/reports/*edges.json", "arn:aws:s3:::acme-terragucci/reports/*/runs/*/run.json", "arn:aws:s3:::acme-terragucci/reports/estate.html", "arn:aws:s3:::acme-terragucci/reports/history.html", "arn:aws:s3:::acme-terragucci/reports/audit.json", "arn:aws:s3:::acme-terragucci/reports/audit.jsonl", "arn:aws:s3:::acme-terragucci/reports/*/tf-apply-wave-*/report.json"] },
    { "Effect": "Allow", "Action": "s3:PutObject", "Resource": ["arn:aws:s3:::acme-terragucci/reports/estate.html", "arn:aws:s3:::acme-terragucci/reports/estate.json", "arn:aws:s3:::acme-terragucci/reports/history.html", "arn:aws:s3:::acme-terragucci/reports/history.json", "arn:aws:s3:::acme-terragucci/reports/dora.json", "arn:aws:s3:::acme-terragucci/reports/audit.jsonl", "arn:aws:s3:::acme-terragucci/reports/audit.html", "arn:aws:s3:::acme-terragucci/reports/audit.json"] }
    ]
    }

    Credentials are read as for reports: Keep reports in a bucket, step 3.

  3. Add the scheduled job.

    .github/workflows/estate.yml:

    name: estate
    on:
    schedule:
    - cron: "23 * * * *"
    workflow_dispatch: {}
    permissions:
    contents: read
    id-token: write
    jobs:
    estate:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    with:
    node-version: 24
    - uses: aws-actions/configure-aws-credentials@v4
    with:
    role-to-assume: arn:aws:iam::123456789012:role/terragucci-estate
    aws-region: us-east-1
    - run: npx --yes @intentius/terragucci estate

    The tabs show AWS. On GCP or Azure, replace the AWS lines with the cloud’s own:

    Cloud The job sets On GitHub
    GCP GOOGLE_APPLICATION_CREDENTIALS, pointing at an external_account file for the service account of step 2, or a service_account key file google-github-actions/auth@v2 with workload_identity_provider and service_account writes the file and sets the variable
    Azure ARM_TENANT_ID, ARM_CLIENT_ID, and the job’s OIDC token (audience api://AzureADTokenExchange) in the file ARM_OIDC_TOKEN_FILE_PATH names; or the account key as AZURE_STORAGE_KEY permissions: id-token: write, and a step that requests the token and writes the file
  4. Run it once. The log ends with the link:

    estate: 3 projects, 1 wave waiting, 1 project drifted, 0 roots failed
    gitlab.example.com/platform/network: wave 2 waits, for 3h 0m
    github.com/acme/data: 2 roots drifted
    dora: 2.5 deployments per week, lead time 3h 10m, change failure rate 5%, time to restore 1h 20m
    wrote terragucci-estate/estate.json, terragucci-estate/estate.html, terragucci-estate/dora.json, terragucci-estate/history.json and terragucci-estate/history.html
    copied to s3://acme-terragucci/reports/estate.json, reports/estate.html, reports/dora.json, reports/history.json and reports/history.html
    link, until 2026-10-08T12:00:00.000Z:
    https://acme-terragucci.s3.us-east-1.amazonaws.com/reports/estate.html?X-Amz-Algorithm=AWS4-HMAC-SHA256&...

    The job fails (exit 1) when a project’s index could not be read; the page still goes up and names that project.

  5. Add a terragucci audit step before the estate step. It writes the audit trail (audit.jsonl, audit.html, audit.json) and the page links it. The approvers in the change history and the applies in the delivery metrics come from it. The audit trail lists what that step reads.

    - run: npx --yes @intentius/terragucci audit
    - run: npx --yes @intentius/terragucci estate
  6. Share the link in the drift issue or a chat channel.

    Link lifetime Set by
    24 hours the default
    up to 168 hours --link-hours
    shorter the job’s AWS role session, when that ends first; the log prints the real end

The page’s numbers need nothing else; its links to each run’s report open where the bucket is served (reports.url, or your own front door).

A tf-apply wave records the resources held by every root it applied or found nothing to apply in (address, type and provider, from the applied plan). The upload keeps each root’s newest list in inventory.json.

On the page What it holds
a count at the top resources across every project
per project how many of each type
per root each resource’s address, type and provider, and the wave that recorded them

No attribute value is ever recorded, sensitive or not. A root is listed from the first wave that applies it, and keeps its last list after you remove it from the repo.

Each tf-apply wave also records what it did to each resource of the roots it applied, and the upload adds one row per resource to the project’s changes.json. From these the estate job writes history.html and history.json beside the page, listing every resource address with each apply that changed it, oldest first. Each resource on the page links to its history.

Action Attributes named
create, delete, import, move, forget none
update, replace the top-level attributes whose value changed
Each apply shows From
when, and what it did the wave’s plan
the attributes it changed, by name the wave’s plan
who approved the wave the wave’s apply entry in the audit trail
the commit, the wave, its report and the root’s plan digest the wave’s report

Without the audit trail (step 5) the history lists every apply but names nobody. A wave no gate held has no approver.

For a choudoufu estate, each wave also lists the past versions of each record it changed with choudoufu live-history: version id, time, and whether it is current or deleted, never a value. The page shows the count beside the resource. A local or kubernetes record store keeps none, and the page says so. Listing takes s3:ListBucketVersions on the record store bucket; when the root’s identity lacks it, the history shows choudoufu’s error.

A tf-apply wave records the version id of each root’s state from the bucket’s versioning. The page lists them per root, newest first. When the backend keeps no history (a bucket with versioning off, or a local, pg, kubernetes, consul or plain http backend), it says versions are off. When the backend may keep versions terragucci does not read, such as remote, it says versions not read, and why. Find the state version an apply left sets this up and says how to go back to a version.

A root that reads another root’s state (terraform_remote_state or a Terragrunt dependency block) is marked stale when the producer applied a change after the consumer’s last plan. Track the roots that read other roots’ state covers the edge sources and fixing a stale one.

For each project and the estate, the Delivery section shows the four DORA metrics week by week over the newest eight weeks. The same numbers go to dora.json beside the page.

Metric What the page counts
Deployment frequency applied waves per week
Lead time the median time from a change’s first plan to its wave applied, and that time before the gate, at the gate and after the approval
Change failure rate failed applies, plus applied waves whose roots drifted within 7 days, over all applies; a refused wave does not count
Time to restore the median time from a failed apply to the next apply of those roots, and from drift found to the next drift check that found none

The applies come from the audit trail (step 5). With an OTLP endpoint set, the job also sends the four as gauges for the Estate dashboard.

The estate page counts per project. To see which resources in which roots drifted or will change, the job can publish a behold view next to it. It is optional, and terragucci does not run or need it. To look at one repo on your machine instead, see See your estate in behold.

Add one step after the estate step, in a repo whose roots the view should draw:

- run: >-
npx --yes -p @intentius/behold@0.25.0 -p @intentius/chant-lexicon-terraform@^0.121.0 -p @cdktn/hcl2json@^0.24.0
behold export . --terragucci s3://acme-terragucci/reports --no-source
--publish s3://acme-terragucci/reports/views/behold
The view shows From
every root of the checkout and the resources it declares the checkout
on each resource, what the newest drift check and plan found, and when each root’s newest tf-drift and tf-plan report
each waiting wave, with the terragucci approve line to run estate.json
the counts, as the estate page shows them, with a link to it estate.json

Each run publishes the view for the checkout’s commit under reports/views/behold/<commit>/ and keeps the 30 newest (--keep <n> changes it). reports/views/behold/index.html opens the newest, and a picker in the view opens an older commit.

It writes only under reports/views/behold/, which terragucci never writes, with the job’s credentials. Add s3:GetObject on reports/*/report.json, and s3:PutObject, s3:GetObject and s3:DeleteObject on reports/views/behold/*, to the policy of step 2. A commit past the newest 30 is deleted. Its links open each run’s report through the same presigned link or front door as the page. --no-source leaves each root’s HCL out of the view.

Give the step continue-on-error: true on GitHub and Forgejo, or allow_failure: true on GitLab: a view that cannot be built leaves the estate page as it is.

terragucci

These docs count page views and clicks with PostHog. They set no cookies, store nothing in your browser, and send nothing when your browser asks not to be tracked.