
From Plain English to AWS Architecture Diagrams with Amazon Bedrock and diagram-as-code
AWS Architecture Diagrams with Amazon Bedrock and diagram-as-code
Architecture diagrams are essential, but drawing them by hand is slow. You align icons, fix arrows, and redo it all when the design changes. I wanted to describe an architecture in plain English — or paste a screenshot of an existing one — and get a clean, AWS-style diagram back.
So I built Diagram AI. It uses Amazon Bedrock (Claude Haiku 4.5) to understand the request and AWS Lambda running the open-source
diagram-as-code CLI to render the actual diagram. This post focuses on how the AWS pieces fit together — and the non-obvious gotchas I hit, in case they save you time.
Architecture
1
2
3
4
5
6
7
8
9
[User input: text or a diagram screenshot]
│
▼
[Frontend on Vercel · Next.js]
├─ /api/generate-yaml → Amazon Bedrock (Claude Haiku 4.5) → diagram-as-code YAML
└─ /api/render → AWS Lambda (Function URL) → awsdac CLI → PNG
│
▼
[Browser: live preview + PNG download]The key design decision: I don't ask the LLM to "draw" anything. I ask it to emit YAML in the
diagram-as-code schema, and a deterministic CLI renders that YAML into a real, convention-compliant AWS diagram. Bedrock does the understanding; a proven AWS tool does the drawing. This separation keeps output reliable — the model can't produce a malformed image, only structured text that either validates or doesn't.Why Amazon Bedrock + Claude Haiku 4.5
Two reasons drove the model choice:
- Cost and latency. Generating structured YAML is a well-bounded task. Haiku 4.5 is fast and inexpensive, and end-to-end generation lands in a few seconds.
- Vision, included. Claude Haiku 4.5 on Bedrock is multimodal, which later let me add "read an existing diagram from a screenshot" with the same model and the same API call.
I used the cross-region inference profile in
us-east-1:1
us.anthropic.claude-haiku-4-5-20251001-v1:0The Bedrock call itself is a standard
InvokeModel with the Anthropic messages format. The system prompt teaches the model the diagram-as-code schema and layout rules; the user message carries the request (and optionally an image).Gotcha — lazy-init your Bedrock client. I first constructed theBedrockRuntimeClientat module load. During the frontend's production build (which runs without AWS environment variables), that threwRegion is missing. Moving client creation into a request-time factory fixed it and is a good pattern regardless of framework.
Rendering on AWS Lambda
diagram-as-code is a Go CLI (awsdac) that turns YAML into diagrams using official AWS icons. My first instinct was to import it as a Go library inside a serverless function — but almost all of its logic lives under Go's internal/ directory, which cannot be imported from outside the module. So I run it as a CLI in Lambda instead.I also skipped container images (no Docker on my build machine) in favor of a zip-based Lambda on the Python 3.12 runtime:
- Download the official prebuilt
awsdacLinux binary from the project's GitHub releases. - Add a small Python handler that shells out to it.
- Zip both and deploy. No Docker, no Go toolchain.
The handler receives YAML, writes it to
/tmp, runs the binary, and returns the PNG as base64:1
2
3
4
5
proc = subprocess.run(
[AWSDAC_PATH, "--allow-untrusted-definitions", in_path, "-o", out_path],
capture_output=True, text=True,
env={**os.environ, "HOME": "/tmp"}, timeout=25,
)Two things to note for Lambda specifically:
/var/taskis read-only. Don't try tochmodthe bundled binary at runtime; set the executable bit when you zip it instead (the Unixzipcommand preserves it).awsdaccaches fetched definition files under$HOME/.cache. On warm invocations that meant stale icon definitions. Clearing the cache at the start of each request (withHOME=/tmp) keeps results fresh.
The Function URL 403 that wasn't in my policy
I exposed the function via a Lambda Function URL with
AuthType: NONE and a resource policy granting lambda:InvokeFunctionUrl to *. Direct aws lambda invoke worked. The Function URL returned 403 AccessDeniedException.The config looked correct. No SCPs, no RCPs. The real cause was a platform change:
Since October 2025, public Lambda Function URLs require two permissions:lambda:InvokeFunctionUrlandlambda:InvokeFunction.
Adding the second statement fixed it immediately:
1
2
3
4
aws lambda add-permission --function-name diagram-ai-render \
--statement-id FunctionURLAllowInvoke \
--action lambda:InvokeFunction --principal "*" \
--invoked-via-function-url --region us-east-1If a Lambda 403 makes no sense, check whether the platform's rules changed recently — not just your own config.
Teaching the model to draw clean diagrams
Valid YAML was easy. Good-looking diagrams took iteration, each round driven by an ugly result:
- Arrows piercing containers. The model drew
User → ALBstraight through the "AWS Cloud" and "VPC" boxes. Fix: require external→internal traffic to route through an Internet Gateway placed on the VPC border (BorderChildren) — exactly like AWS reference architectures. - Arrows crossing label text.
diagram-as-codepins labels under icons and routes arrows through icon centers, so vertical flows always crossed text. After testing four layouts, horizontal flow (Direction: horizontal) won: horizontal arrows pass at icon mid-height and clear the labels. - Invented resource types. Asked for a "box," the model produced
AWS::Diagram::Container, which doesn't exist. Fix: an explicit allowlist of safe AWS types plus a rule never to invent types.
Every fix went into the system prompt as a concrete good/bad example. The prompt got long; the diagrams got clean.

Hybrid diagrams: beyond AWS icons
diagram-as-code is AWS-first but extensible via definition files. To draw "GitHub → Vercel → AWS Lambda" properly, I generated 19 external-service icons (Vercel, Cloudflare, Supabase, OpenAI, Stripe, and more): pull SVGs from simple-icons, tint with brand colors, convert to PNG with sharp, and map them to types like External::Vercel in a definition file.Note that
awsdac rejects definition files from non-official URLs unless you pass --allow-untrusted-definitions. I serve the definition file and icon zip from the app's own origin so they're versioned with each deploy.Reading existing diagrams
Because Claude Haiku 4.5 is multimodal, I extended the Bedrock call to accept an image, giving three modes for free:
- Image only → reproduce an equivalent diagram.
- Image + text → transform it ("swap Cloudflare for CloudFront, EC2 for ECS").
- Text only → the original flow.
The same
InvokeModel call carries an image content block plus a text instruction — no separate vision pipeline needed.Takeaways
- Let the LLM produce structured text, not pixels. Bedrock writes schema-valid YAML; a deterministic CLI renders it. Reliability comes from that split.
- Read the source before choosing an integration path. Go's
internal/rule ruled out the library approach on day one — I just found out on day two. - When auth fails inexplicably, suspect recent platform changes. The dual-permission Function URL rule was invisible in my own config.
- Lazy-init AWS SDK clients. It avoids build-time "Region is missing" and is cleaner anyway.
Stack
Amazon Bedrock (Claude Haiku 4.5, text + vision) · AWS Lambda (zip, Python 3.12) running the
awsdac CLI via a Function URL · Next.js frontend on Vercel · simple-icons + sharp for non-AWS icons.Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article