DEV Community

Cover image for Terraform to Architecture Diagram Without a Plugin
Adnan H. Çelik
Adnan H. Çelik

Posted on

Terraform to Architecture Diagram Without a Plugin

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 graph needs 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
}
Enter fullscreen mode Exit fullscreen mode

Paste it into the dialog (or use Load file on a .tf), switch on Add icons, and insert:

The Terraform example as an editable diagram with AWS icons

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, variable and output blocks each become a box. provider, locals and the terraform {} 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 a depends_on list.
  • 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, _name and _arn are dropped, so labels read like English.
  • Cards show what matters. The title is a friendly type name (aws_db_instance reads "DB Instance", azurerm_virtual_network reads "Virtual Network"). Under it you get the literal name if there is one, plus up to two key attributes such as cidr_block, engine, instance_class, location or sku. 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 same diagram with the vpc relationships 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:

Drift view: the new worker service and Redis cluster in green, the removed S3 bucket dashed in red

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:

  • count and for_each are not expanded. A resource with count = 3 is one box, which is usually what you want in an architecture view anyway.
  • A module is 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 app in aws_lb.app) is not shown on the card; the card uses the type, the name attribute 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

  1. Open letdraw.com and choose Generate from Code.
  2. Paste a .tf file (or a few concatenated) and look at the live preview.
  3. Turn on Add icons, hide the relationships that add noise, and insert.
  4. 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)