See every project in one page
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`.Result
Section titled “Result”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.
Prerequisites
Section titled “Prerequisites”| 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 |
-
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 reportsdefaults.reportsone repo every project in the top index.jsonthat repo’s reportsthe same bucket For projects in other accounts:
Option Set up one central bucket every project’s reports.bucketnames it, each writing with areports.roleof its owna bucket per account the project’s reportsin the control repo names its bucket and arolethat can read it -
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"] }]}A service account with
roles/storage.objectUseron the bucket androles/iam.serviceAccountTokenCreatoron itself.A client with Storage Blob Data Contributor on the container and Storage Blob Delegator on the account.
Credentials are read as for reports: Keep reports in a bucket, step 3.
-
Add the scheduled job.
.github/workflows/estate.yml:name: estateon:schedule:- cron: "23 * * * *"workflow_dispatch: {}permissions:contents: readid-token: writejobs:estate:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: 24- uses: aws-actions/configure-aws-credentials@v4with:role-to-assume: arn:aws:iam::123456789012:role/terragucci-estateaws-region: us-east-1- run: npx --yes @intentius/terragucci estateA job in
.gitlab-ci.yml, and a schedule under Build, Pipeline schedules:estate:image: node:24rules:- if: $CI_PIPELINE_SOURCE == "schedule"id_tokens:AWS_TOKEN:aud: sts.amazonaws.comvariables:AWS_ROLE_ARN: arn:aws:iam::123456789012:role/terragucci-estateAWS_REGION: us-east-1script:- echo "$AWS_TOKEN" > "$CI_BUILDS_DIR/aws-token"- export AWS_WEB_IDENTITY_TOKEN_FILE="$CI_BUILDS_DIR/aws-token"- npx --yes @intentius/terragucci estate.forgejo/workflows/estate.yml, withAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYas repository secrets:name: estateon:schedule:- cron: "23 * * * *"workflow_dispatch: {}jobs:estate:runs-on: dockercontainer: node:24steps:- uses: actions/checkout@v4- run: npx --yes @intentius/terragucci estateenv:AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}AWS_REGION: us-east-1The 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 anexternal_accountfile for the service account of step 2, or aservice_accountkey filegoogle-github-actions/auth@v2withworkload_identity_providerandservice_accountwrites the file and sets the variableAzure ARM_TENANT_ID,ARM_CLIENT_ID, and the job’s OIDC token (audienceapi://AzureADTokenExchange) in the fileARM_OIDC_TOKEN_FILE_PATHnames; or the account key asAZURE_STORAGE_KEYpermissions: id-token: write, and a step that requests the token and writes the file -
Run it once. The log ends with the link:
estate: 3 projects, 1 wave waiting, 1 project drifted, 0 roots failedgitlab.example.com/platform/network: wave 2 waits, for 3h 0mgithub.com/acme/data: 2 roots drifteddora: 2.5 deployments per week, lead time 3h 10m, change failure rate 5%, time to restore 1h 20mwrote terragucci-estate/estate.json, terragucci-estate/estate.html, terragucci-estate/dora.json, terragucci-estate/history.json and terragucci-estate/history.htmlcopied to s3://acme-terragucci/reports/estate.json, reports/estate.html, reports/dora.json, reports/history.json and reports/history.htmllink, 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.
-
Add a
terragucci auditstep 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 -
Share the link in the drift issue or a chat channel.
Link lifetime Set by 24 hours the default up to 168 hours --link-hoursshorter 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).
The resources
Section titled “The resources”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.
Change history
Section titled “Change history”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.
State versions
Section titled “State versions”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.
Cross-state edges
Section titled “Cross-state edges”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.
Delivery metrics
Section titled “Delivery metrics”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.
A resource graph beside the page
Section titled “A resource graph beside the page”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.
- CLI commands lists the flags.
- The audit trail describes every entry of
audit.jsonl. - Delivery metrics defines each DORA metric and the fields of
dora.json. - Report JSON schema describes
estate.json, the index rows,inventory.json,changes.jsonandhistory.json.
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.