After the migration, the repository knew exactly where every stack ran, and I no longer did. The same stack name now lives in several accounts and regions, five directory levels down. Finding all of them is a search rather than a directory listing. So I built a small terminal tool that lists every stack, shows where each one runs, and puts me in its directory.
What the deep tree costs
The first post
made the path carry the facts: a stack lives at
stacks/aws/<account>/<region>/<stack>, and its directory tells it which
account, region and environment it belongs to.
The migration
moved every legacy app/<stack>/<env>/ directory into that shape. Both are
right for the machine, and together they cost the person something:
With a dozen stacks the cost is nothing. The repository this comes from has
roughly five hundred stack directories and about two hundred distinct stack
names. Its most common name exists in around twenty places at once. At that
size, “where does foundation run” is no longer ls app/. It is a find
over the tree, or a mental model of it. What a person wants is the tree turned
inside out: one row per stack name, with every place it runs underneath:
Building the inventory
I built a small Go program for this and called it terramate-stacks-explorer.
It has two faces. Run plainly, it prints a table: one row per stack, with the
account, region and environment it runs in. Run interactively, it is a
terminal UI over the same list, with filters, a detail pane and a way to jump
into a stack’s directory. This section is about where the list comes from. The
next one is about the UI.
The difficult part is not walking directories. Account, region and environment
are not written in a stack’s path. They are globals, declared once in
account.tm.hcl and region.tm.hcl and inherited downward, and a stack can
override any of them. The only thing that knows their final value for a given
stack is Terramate’s own evaluation, the one code generation runs.
Terramate is written in Go, and the terramate command is built from packages
a Go program can import. Running terramate list and parsing its output would
give the paths but not the values. So the tool imports three of those
packages, config, stack and globals, and asks them directly. Three calls
do the work:
// The repository, from the directory that holds terramate.tm.hcl.root, err := config.LoadRoot(rootdir, false)
// Every stack Terramate can see, the set `terramate list` prints.entries, err := stack.List(root, root.Tree())
// One stack's globals, fully evaluated: inherited and overridden.for _, e := range entries { g := globals.ForStack(root, e.Stack) fmt.Println(e.Stack.Dir, str(g.Globals, "account_name"), str(g.Globals, "region_name"), str(g.Globals, "environment"))}The values come from the same evaluation code generation uses. So a stack
whose environment global says production while its path says otherwise
reports production. A global comes back as a typed value, so str turns it
into text and prints - for a key the stack does not define. With a header
and aligned columns on top, that is the whole non-interactive inventory, in
under a hundred lines:
STACK ACCOUNT REGION ENV/stacks/aws/acme-development/eu-west-1/acm acme-development eu-west-1 development/stacks/aws/acme-production/eu-west-1/dns acme-production eu-west-1 production/stacks/saas/github - - operations/stacks/teams/team-payments acme-operations eu-west-1 operations/stacks/teams/team-platform acme-operations eu-west-1 operationsThe terminal UI
The table answers the question once. The interactive version is for the way the question is actually asked: several times in a row, narrowing as it goes. It opens on a count of what the inventory contains: environments, regions, accounts. The left pane is one row per stack name with a deployment count. The right pane is every deployment of the selected one, grouped by environment, with development before staging before production. The header carries the active filters. Fuzzy search narrows the list as you type. Selecting a deployment opens a shell in its directory, which turns “where does this run” into “put me there”. It is built with Bubble Tea, the usual toolkit for this in Go, over the same list the table prints.
Recorded against a mock repository of 22 deployments:
The same views exist without the UI, for scripts and for a quick look. By stack name:
terramate-stacks-explorer --format table# STACK COUNT ENVIRONMENTS# acm 4 development, staging, production# eks-cluster 4 development, staging, production# foundation 4 development, staging, production# networking 2 development, production# ... 8 more ...# 12 distinct stacks across 22 deploymentsBy deployment, one row per place a stack actually runs, with the same --env
and --region filters the UI has:
terramate-stacks-explorer --view deployments --env production \ --format table# STACK ACCOUNT REGION ENVIRONMENT# acm acme-production eu-west-1 production# acm acme-production us-east-1 production# foundation acme-production eu-west-1 production# ... 5 more ...# 8 deployments--format json prints every field the loader resolved, including the account
id, AWS profile and stack id. It is for anything that wants the inventory as
data rather than as a find command with conventions baked in.
The tool is thin because the repository does the work. That holds on three
conditions. It runs from anywhere inside a Terramate project and finds the
root by climbing to terramate.tm.hcl. It sees the stacks Terramate itself
sees, .tmskip and all. And it expects the four globals the layout in part
one names:
account_name, region_name, environment and aws_config_profile. Those
are its entire contract with the repository. A stack that omits one is not an
error. It just shows -, as the saas/github row above does for account and
region.
Limitations
Importing Terramate’s packages ties the tool to them. They are not a public
API and move between releases, so the tool pins the Terramate version it was
built against and upgrades on purpose, not by accident. Shelling out to
terramate list is the slower, more stable alternative for the paths, though
not for the values.
Evaluating globals for every stack is real work per stack, not a directory walk. It is fast enough at a few hundred stacks when spread across goroutines.
The inventory reports what the globals say, which is what code generation acts on. Neither knows what is actually deployed. A stack applied this morning and one last applied six months ago look identical here. The git history of the directory is the closest thing to an answer, and it only reports on the code.
What’s next?
Every post in this series so far assumes the declarations are right: a stack
names its providers, a descriptor names its platforms, a global names its
account. A later post covers what happens when one of them is wrong, and why a
generator is the right place to refuse. It is about assert blocks, and the
version contracts they let a shared generator enforce before Terraform ever
runs.
Comments