Reading a Terraform plan is not the same as seeing it

A plan is a thousand lines of diff in a terminal. The questions you actually want answered — what does this cost, what does it expose, what breaks if it fails — are not in that format. Here is how we made Skyline answer them from the code alone.

Every team that runs Terraform has the same ritual. Someone opens a pull request, CI posts the plan, and a reviewer scrolls through a thousand lines of +, ~ and - looking for something alarming. Then they approve it, because the alarming thing, if it is there, does not look alarming in that format.

The plan is an excellent machine artifact and a poor human one. It tells you exactly which attributes change on which addresses. It does not tell you what the result will cost, which of those resources ends up reachable from the internet, or what else depends on the thing being replaced. Those are the three questions that actually come up in review, and answering them means reading the plan and holding the whole architecture in your head at the same time.

Skyline reads .tf files, whole project folders and terraform plan JSON, and answers those three questions directly — with no cloud access at all.

What the parser has to get right

Parsing HCL to draw a map is easy until it meets a real repository. A few things we had to handle before it was useful on anything but a demo:

Directory structure matters. Teams do not upload one file, they upload a tree: modules/network/main.tf, envs/prod/main.tf, envs/staging/main.tf. The parser keeps the paths, because the same resource name in two environment folders is two different resources, and flattening them produces a map that is wrong in a confusing way.

Local modules have to be expanded. A source = "./modules/database" block is where most of the actual infrastructure lives. If you do not follow it, you draw a map of the wrapper and miss the estate. Skyline expands local module sources recursively, with a depth limit so a circular reference cannot hang the parse.

References are the dependency graph. ${aws_vpc.main.id} inside a subnet is not just a string, it is an edge. Pulling resource references out of attribute values and out of depends_on gives you the graph for free, and that graph is what makes a replacement legible: this RDS instance is being replaced, and here are the four things that point at it.

Variables need resolving where they can be. instance_type = var.db_size is useless for a cost estimate. Resolving against the variable defaults in the same project turns it into db.r6g.xlarge, which is a number. Where a variable has no default, the resource is reported with lower confidence rather than silently guessed.

The three answers

Once the code is a resource model, the three views fall out of the same data, which is the part that makes them useful together.

What it will cost

Every resource goes through a per-provider price catalog — AWS, Google Cloud, Azure and Oracle Cloud each have one — that turns a type and its attributes into an estimated monthly cost, adjusted by a region factor. The output is a monthly figure per region, per service and per resource.

Two things about that number. It comes from public price lists, so it is an estimate for prioritising, not an invoice; commitments, negotiated rates and usage-based lines will move it. And resources that are billed purely by usage — API calls, data processing — are marked as such rather than given a fake number, because a confident wrong figure is worse than an honest gap.

What it is genuinely good at is the comparison nobody does by hand: this pull request adds €1,400 a month, and €1,100 of that is one NAT gateway per availability zone in a module someone copied.

What it exposes

The same rules engine that runs over a live account runs over the parsed code, because both produce the same flat resource list. So a security group with 0.0.0.0/0 on 3306, a bucket with a public ACL, a policy with Action: "*", an unencrypted database, a Lambda on a deprecated runtime — these are findings before the apply, not after.

The ones that come up most in plan review, in our experience, are wildcard IAM policies and sensitive ports open to the world, and both are almost always accidental: a permissive rule written for a debugging session that got committed.

There is also a check that only makes sense on code: secrets in attributes. password, client_secret, private_key, access_token and their relatives, hardcoded in HCL or in a Lambda environment block. That finding does not exist in a live scan because the value is already in the provider's API; it exists in the parser because the value is also in your git history.

What breaks

The dependency graph, drawn from the references, turns "this resource is being replaced" into "this resource is being replaced and these six things point at it". For a plan that contains a -/+ on anything stateful, that is the entire review.

Where this fits in the workflow

We do not think this replaces plan review in CI. It sits next to it, and it is most useful at two moments:

  • Before the pull request, on the branch, while the cost of changing the design is still zero. Upload the folder, look at the number and the findings, adjust the module.
  • In the review, on the plan JSON, when the diff is too large for anyone to hold in their head — a first landing zone, a new environment, a migration.

For a plan, Skyline shows what will be created, changed, replaced or destroyed, so the estimate is a delta and not just a total.

Try it on something real

The honest test is a project you already know well. Upload the folder, look at the cost breakdown by service, and see whether the top three lines are where you would have guessed. In our experience they are not, about half the time, and the gap is where the interesting conversation starts.

Skyline is free and the Terraform path needs no cloud access whatsoever. If the result raises questions bigger than a pull request, that is what our cloud work is for.

ConsultorIA

Want this done on your cloud?

A ten-day read-only assessment is free, and Skyline lets you see your estate on a map before you write to us.

Related articles

Modules people reuse instead of copying

The two failures are a module that wraps one resource and adds nothing, and a module that does everything and nobody dares change. A minimal interface, safe defaults and honest versioning are what separate them.