Article
Archviz: Draw Your AWS Architecture, Get Real Terraform
Archviz: Draw Your AWS Architecture, Get Real Terraform
Architecture diagrams and the infrastructure they describe drift apart almost immediately. The diagram in Confluence says there are two availability zones; the Terraform says one. Nobody notices until an outage.
Archviz is my attempt at collapsing that gap: an open-source visual infrastructure builder where the diagram is the source of truth. Drag AWS resources onto a canvas, connect them, and export idiomatic Terraform HCL that actually plans.
The problem with both halves
Diagramming tools produce pictures. Lucidchart doesn't know that an aws_lambda_function requires a role, so it will happily let you draw a Lambda floating in space. The picture looks fine and tells you nothing.
Writing Terraform by hand goes the other way: it's precise, but the mental model lives in your head. You're translating a spatial idea — this subnet sits inside that VPC, this service spans those two AZs — into flat resource blocks and string references.
Archviz tries to keep the spatial model and the precision at the same time.
How it's put together
It's a pnpm monorepo of focused packages:
| Package | Description |
| --- | --- |
| @archviz/schema | Meta-schema types, defineResource(), ResourceRegistry |
| @archviz/core | Document model, constraint engine, validator |
| @archviz/provider-aws | AWS resource definitions (22 common resources) |
| @archviz/codegen | Terraform HCL generator + materializers |
| @archviz/runner | Local companion CLI that runs terraform plan for the studio |
| @archviz/studio | Visual editor (React Flow + XState) |
The document model uses @xstate/store to hold the graph, and both React Flow and the generated HCL are derived projections of it. An XState v5 statechart tracks editor gestures — connecting, dragging, editing — which turned out to be the right call the moment interactions started overlapping.
The one design decision that matters
Resource constraints live in TypeScript definitions via defineResource(), and both the UI constraint engine and the Terraform generator read the same registry.
That single choice is what makes the tool honest. The palette can gray out a Subnet until you've placed a VPC, and the generator can refuse to emit a Lambda without an execution role, because neither is guessing — they're reading the same declaration.
If the UI and the code generator had separate notions of what's valid, every new resource would be two implementations that slowly disagree. One registry, two consumers.
A worked example: the Lambda that can't exist yet
Terraform requires role on aws_lambda_function, and that ARN can only come from a connection to an IAM Role. Drop a Lambda on the canvas by itself and Archviz tells you before you ever run Terraform:
- the node gets an error badge
- Plan and Export are disabled behind an
ERRORSbadge - Diagnostics names the exact gap: "Lambda Function needs a 'Execution Role' connection"
Draw the assumes edge to an IAM Role and it clears — role = aws_iam_role.<name>.arn appears in the HCL, and Plan and Export re-enable.
The alternative would have been to emit the Lambda anyway and let you discover the problem at plan time:
The argument "role" is required, but no definition was found.
Failing in the diagram, where you can see the missing edge, beats failing in a terminal thirty seconds later. So some connections are validated as required rather than silently omitted.
Secrets that never touch your source
Instead of typing a password into a property field, you connect a Secrets Manager Secret or SSM Parameter to a database's "Password from Secret" relationship. With the secret's source set to generated-password, the export contains:
resource "random_password" "db_secret_password" {
length = 20
special = true
}
resource "aws_secretsmanager_secret" "db_secret" {
name = "db-secret"
}
resource "aws_secretsmanager_secret_version" "db_secret_version" {
secret_id = aws_secretsmanager_secret.db_secret.id
secret_string = random_password.db_secret_password.result
}
resource "aws_db_instance" "rds_instance" {
# ...
password = random_password.db_secret_password.result
}
Switch the source to variable and you get a sensitive input variable instead, wired to both the secret version and the consumer.
ECS Task Definitions use the same uses-secret connection but materialize it differently — a secrets entry with valueFrom = <arn> inside container_definitions, plus an IAM policy granting the connected execution role read access to exactly those ARNs. The value never appears in the task definition, the plan output, or the ECS console.
That's the payoff of modeling relationships instead of property values: the generator can see the whole triangle (task, secret, role) and emit all three pieces.
Running terraform plan from the UI
A browser can't execute binaries or hold AWS credentials, so the studio talks to a small local companion:
pnpm runner # uses ./terraform-out (gitignored)
pnpm runner --dir <some-folder> # or any folder you like
It binds to 127.0.0.1:4180 and uses your local terraform binary and AWS credentials — nothing sensitive reaches the browser. Once it's up, the Plan button lights up and:
- each diagram plans in its own workspace (
<root>/<diagram-slug>/), so state and provider caches never cross-contaminate - required variables get seeded with a
CHANGEMEplaceholder in that workspace'sterraform.tfvars, and placeholders it stamped are cleaned up when a variable stops being declared — so renaming a resource doesn't leave a line behind that Terraform flags asValue for undeclared variableon every later plan - it runs
terraform plan -detailed-exitcodeand streams output live with a summary badge
The runner tracks what it wrote in a per-workspace .archviz-manifest.json, so it cleans up its own stale files and never touches yours — your backend.tf and tfvars are left alone.
Export layouts
"Export .tf" always writes the current live-preview HCL, in one of three shapes:
- Single file — everything in
main.tf - By category —
versions.tf,providers.tf,variables.tf,network.tf,compute.tf,database.tf,storage.tf,security.tf,outputs.tf - Multi-service directories — partitioned by a "Service / Directory" field into independent root modules, where cross-boundary references become
data "terraform_remote_state"lookups with a generated README documenting the layout
Guardrails
The thing I'd most expect to rot is a resource definition quietly omitting an argument Terraform requires. So there's a fixture that builds a diagram using every resource type and validates it against real Terraform, in CI:
node scripts/validate-fixture.mjs tmp/tf-fixture
It runs terraform init -backend=false, terraform validate, and terraform fmt -check against the generated output. Add a resource, add it to the fixture.
What it deliberately doesn't do
Archviz does not run terraform apply against real AWS. That stays a review step in your terminal.
A few other non-goals, left out on purpose rather than missing by accident:
- No remote state or locking. A backend has to exist before it can hold state, and teams bootstrap that differently. Add your own
backend.tf— the runner won't touch files it didn't write. - No secret storage. Promoted variables are emitted without defaults so values never get baked into committed HCL.
- LocalStack is the one automated apply path. Start / Apply / Destroy against a local container, emulated APIs only.
Applying to real infrastructure deserves an approval flow and an audit trail. Generating reviewable Terraform is the job; deciding to run it isn't.
Getting started
git clone https://github.com/codeupllc/archviz
cd archviz
pnpm install
pnpm build
pnpm --filter @archviz/studio dev
Open http://localhost:5173, drag resources from the palette onto the canvas, nest Subnets inside VPCs and EC2 inside Subnets, connect the handles, and export main.tf.
Current AWS coverage is 22 resources: VPC, Subnet, Internet Gateway, EC2, Security Group, RDS, Aurora, ElastiCache, S3, ALB, NLB, Target Group, Lambda, DynamoDB, IAM Role, ECR, ECS (Cluster / Task Definition / Service), Secrets Manager, SSM Parameter, SQS, SNS, API Gateway HTTP API, and CloudWatch Log Group.
Contributing
There's an AWS coverage board with open items — P0 and good first issue are the best entry points. CONTRIBUTING.md has the workflow, and there's a skill file at .cursor/skills/add-aws-resource/SKILL.md plus docs/aws-coverage.md for adding AWS nodes.
Archviz is MIT licensed and on GitHub. If you try it and the generated HCL doesn't match what you'd have written by hand, that's a bug worth filing — idiomatic output is the whole point.