Every infrastructure repo I have joined had the same two artefacts side by side: a folder of .tf files that described the system exactly, and a diagram in the wiki that described it roughly, as of some quarter in the past.
Nobody wants it that way. The problem is that turning Terraform into a picture has always come with a toll:
-
terraform graphneeds an initialised working directory, downloads providers, and produces a DOT dependency graph that is accurate and nearly unreadable once you pass a dozen resources. - Most "Terraform visualiser" tools want a plan file, a state file, or cloud credentials, and some want a plugin in your pipeline before they show you anything.
All I usually want is to look at the code I am reviewing and see its shape. So this post is about doing exactly that, with nothing installed: paste HCL, get a diagram you can edit.
I am using LetDraw for this. It is a whiteboard with a Generate from Code dialog that reads Terraform (and OpenTofu, which shares the syntax) as text.
The example
A small but realistic AWS stack: a VPC with a public and a private subnet, an ALB in front of an ECS service, a Postgres RDS instance, an S3 bucket, a monitoring module, a variable and an output.
variable "env" {
default = "prod"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
}
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id
}
resource "aws_lb" "app" {
name = "app-${var.env}"
subnets = [aws_subnet.public.id]
security_groups = [aws_security_group.web.id]
}
resource "aws_lb_target_group" "app" {
name = "app-tg"
vpc_id = aws_vpc.main.id
}
resource "aws_ecs_cluster" "main" {
name = "checkout"
}
resource "aws_ecs_service" "api" {
name = "api"
cluster = aws_ecs_cluster.main.id
subnets = [aws_subnet.private.id]
load_balancer_target_group_arn = aws_lb_target_group.app.arn
}
resource "aws_db_instance" "orders" {
identifier = "orders"
engine = "postgres"
instance_class = "db.t4g.medium"
db_subnet_group_name = aws_subnet.private.id
}
resource "aws_s3_bucket" "assets" {
bucket = "checkout-assets"
}
module "monitoring" {
source = "./modules/monitoring"
cluster = aws_ecs_cluster.main.name
}
output "lb_dns" {
value = aws_lb.app.dns_name
}
Paste it into the dialog (or use Load file on a .tf), switch on Add icons, and insert:
That is 13 blocks and 13 relationships, grouped into Variables, AWS, Modules and Outputs, with a product icon on each AWS resource. Every box and arrow is a normal shape on a whiteboard, so the next step is the part a generator can never do for you: drag things into the layout your team actually talks about, add the third-party payment API that Terraform knows nothing about, and circle the part you are worried about.
What it actually reads
It helps to know what the parser does, so you know what to expect from your own code. It is a static reading of the HCL text:
-
Blocks become nodes.
resource,data,module,variableandoutputblocks each become a box.provider,localsand theterraform {}block are configuration, so they are skipped to keep the picture clean. -
References become arrows. Any interpolation that points at another declared block draws an arrow from the dependency to the dependent:
aws_subnet.public.id,data.aws_ami.ubuntu.id,module.network.vpc_id,var.env, and the entries of adepends_onlist. -
Arrows are labelled with the attribute that created them.
subnet_ids = [aws_subnet.private.id]becomes an arrow labelled "subnet";security_groups = [...]becomes "security groups". The suffixes_id,_ids,_nameand_arnare dropped, so labels read like English. -
Cards show what matters. The title is a friendly type name (
aws_db_instancereads "DB Instance",azurerm_virtual_networkreads "Virtual Network"). Under it you get the literalnameif there is one, plus up to two key attributes such ascidr_block,engine,instance_class,locationorsku. Interpolated values are left out rather than shown half-resolved. - Groups follow the provider. AWS, Azure, Google Cloud, Kubernetes, Cloudflare, GitHub and other providers each get their own group, so a multi-cloud repo separates itself visually.
Arrows only connect blocks that exist in what you pasted. If module.network is not in the text, a reference to it is simply ignored instead of inventing a phantom box.
Make it readable: hide the noisy relationships
The honest first result of any infrastructure graph is too many arrows. In almost every AWS stack, everything points at the VPC, and those arrows add little.
The dialog lists the relationship types it found (here: vpc, subnets, security groups, cluster, db subnet group, and so on) and lets you switch them off. The layout still uses every edge, so boxes stay where they are while you toggle. With vpc hidden:
The request path from the load balancer to the service and the database is now the thing you see first. You can also switch between a top-down and a left-to-right layout.
The feature I use most: drift against the last import
Diagrams are most useful during review, when you want to see what a change does to the architecture, not what the architecture is.
Each time you insert a diagram, the dialog remembers it in your browser as the baseline for that format. Paste the new version of the code, turn on Drift vs last, and the preview colours the difference: added blocks and relationships in green, removed ones in red with a dashed outline.
In the next version of our example, the S3 bucket is gone and an ElastiCache Redis cluster and a second ECS service for background workers have arrived:
For a PR, this is the picture I actually want in the description. It answers "what does this change touch?" faster than scrolling a terraform plan, and it works before anyone has run a plan at all.
Why "without a plugin" matters
This approach has a few properties that are easy to overlook until you need them:
- No credentials, no state. The parser reads text. It never talks to AWS, never needs a backend, and does not care whether the code has been applied.
-
No
terraform init. No providers are downloaded and no working directory is needed, so it works on a branch you just checked out, on a snippet from a code review, or on a module you are only evaluating. - It runs in your browser. The parsing and layout happen client-side, so your resource names and CIDR ranges are not sent to a server to build the picture.
- Licensing-neutral. Nothing is run or redistributed; the HCL is only parsed as text, so the same flow works for Terraform and OpenTofu code alike.
Limits worth knowing
A static parser is honest about what it cannot know:
-
countandfor_eachare not expanded. A resource withcount = 3is one box, which is usually what you want in an architecture view anyway. - A
moduleis one box. Its internals are not followed; if you want them, paste the module's own files and import them as a separate diagram. - Values that only exist after apply (IDs, ARNs, DNS names) are not resolved.
- The local name of a resource (the
appinaws_lb.app) is not shown on the card; the card uses the type, thenameattribute and key attributes instead. Variables, modules and outputs therefore show their kind rather than their name. -
Load file takes one file at a time. For a whole folder, concatenate first (
cat *.tf) and paste the result. - Generate from Code is part of the Pro plan.
Automating it
The same converter is available outside the editor. With a personal API token, POST https://api.letdraw.com/api-v1/documents/from-code with the Terraform text in the body creates the diagram in a workspace, and the diagram_from_code tool does the same for AI coding assistants over MCP. That makes it possible to regenerate the diagram from CI whenever the infrastructure folder changes. (Icons and the drift view are editor features; the API returns the plain diagram.)
Try it
- Open letdraw.com and choose Generate from Code.
- Paste a
.tffile (or a few concatenated) and look at the live preview. - Turn on Add icons, hide the relationships that add noise, and insert.
- Next time the code changes, paste the new version and turn on Drift vs last.
The diagram you can generate in thirty seconds from the code under review will always beat the beautiful one in the wiki that nobody trusts. Give it a try on your own repo, and tell me which provider or block type it should understand better.



Top comments (0)