Choreo.Infrastructure: Cloud Network Topology Walkthrough
Mix.install([
# {:choreo, "~> 0.9"},
{:choreo, path: Path.expand("~/repos/elixir/choreo"), force: true},
{:kino_vizjs, "~> 0.8.0"}
])
Section
> Rendering diagrams: This livebook uses Kino.VizJS to render DOT diagrams inline.
> Mermaid output can be rendered with Kino.Mermaid.new/1, which is supported natively in Livebook.
What is Choreo.Infrastructure?
Choreo.Infrastructure is a cloud network topology preset built on top of Choreo’s
existing graph and rendering stack. It provides a domain vocabulary for modelling VPCs,
subnets, compute instances, databases, and load balancers — with structural validation
rules to catch common networking mistakes before they reach production.
It is not a separate rendering engine: node shapes and colors are resolved through
Choreo.Theme (just like Choreo), and the rendering pipeline is the same Yog-based
stack. What it adds is:
- Typed cluster boundaries — VPCs and subnets carry security semantics (public vs. private).
-
Choreo.Infrastructure.Analysis— audit rules that operate on those semantics. -
Network vocabulary — intent-revealing builders (
add_vpc,add_compute, etc.).
Node Types
| Builder | Node Type | DOT Shape | Mermaid Shape | Purpose |
|---|---|---|---|---|
add_internet/3 |
:internet |
☁️ cloud | Circle | Public internet gateway |
add_load_balancer/3 |
:load_balancer |
▽ invhouse | Hexagon | ALB, NLB, Nginx, HAProxy |
add_compute/3 |
:compute |
📦 box3d | Subroutine | EC2, ECS task, Kubernetes pod |
add_managed_db/3 |
:managed_db |
🛢️ cylinder | Cylinder | RDS, Aurora, Cloud SQL |
add_storage/3 |
:storage |
📁 folder | Rounded rect | S3, EFS, GCS bucket |
Cluster (Boundary) Types
| Builder | Cluster Type | Style | Meaning |
|---|---|---|---|
add_vpc/3 |
:vpc |
Dashed | Virtual Private Cloud — the outer envelope |
add_subnet_public/3 |
:subnet_public |
Rounded | Internet-facing zone (DMZ, load balancers) |
add_subnet_private/3 |
:subnet_private |
Rounded | Isolated zone (app servers, databases) |
alias Choreo.Infrastructure
alias Choreo.Infrastructure.Analysis
Step 1: Minimal topology — a node type legend
legend =
Infrastructure.new()
|> Infrastructure.add_internet(:gw, label: "Internet")
|> Infrastructure.add_load_balancer(:lb, label: "Load Balancer")
|> Infrastructure.add_compute(:app, label: "Compute")
|> Infrastructure.add_managed_db(:db, label: "Managed DB")
|> Infrastructure.add_storage(:store, label: "Storage")
|> Infrastructure.connect(:gw, :lb)
|> Infrastructure.connect(:lb, :app)
|> Infrastructure.connect(:app, :db)
|> Infrastructure.connect(:app, :store)
Kino.VizJS.render(Infrastructure.to_dot(legend))
# Mermaid output — rendered natively in Livebook
Kino.Mermaid.new(Infrastructure.to_mermaid(legend))
Step 2: A realistic three-tier web application
We model a production VPC with:
- A public subnet (DMZ) holding the load balancer
- A private subnet (App) holding compute and the database
- A storage bucket outside the VPC (managed S3-like service)
prod =
Infrastructure.new()
|> Infrastructure.add_internet(:internet, label: "Internet")
|> Infrastructure.add_vpc("vpc_prod", label: "Production VPC")
|> Infrastructure.add_subnet_public("subnet_dmz",
label: "Public Subnet (DMZ)",
parent: "vpc_prod"
)
|> Infrastructure.add_subnet_private("subnet_app",
label: "Private Subnet (App)",
parent: "vpc_prod"
)
|> Infrastructure.add_load_balancer(:alb,
label: "Application LB",
cluster: "subnet_dmz"
)
|> Infrastructure.add_compute(:api,
label: "API Service",
cluster: "subnet_app"
)
|> Infrastructure.add_compute(:worker,
label: "Background Worker",
cluster: "subnet_app"
)
|> Infrastructure.add_managed_db(:rds,
label: "Postgres RDS",
cluster: "subnet_app"
)
|> Infrastructure.add_storage(:s3, label: "Object Store (S3)")
|> Infrastructure.connect(:internet, :alb, protocol: :https, label: "HTTPS")
|> Infrastructure.connect(:alb, :api, protocol: :http)
|> Infrastructure.connect(:api, :rds, protocol: :tcp)
|> Infrastructure.connect(:api, :worker, protocol: :amqp, label: "Jobs")
|> Infrastructure.connect(:api, :s3, protocol: :https)
|> Infrastructure.connect(:worker, :rds, protocol: :tcp)
|> Infrastructure.connect(:worker, :s3, protocol: :https)
Kino.VizJS.render(Infrastructure.to_dot(prod))
Kino.Mermaid.new(Infrastructure.to_mermaid(prod))
Step 3: Security analysis — Choreo.Infrastructure.Analysis
The analysis module runs structural audit rules against your topology.
Each warning carries a :rule, :message, and :nodes list.
warnings = Analysis.warnings(prod)
if warnings == [] do
IO.puts("✅ No security warnings — topology looks clean.")
else
Enum.each(warnings, fn message ->
IO.puts("⚠️ #{message}")
end)
end
Intentionally broken topology — triggering all three rules
Now let’s build a deliberately misconfigured topology to see every rule fire:
broken =
Infrastructure.new()
|> Infrastructure.add_internet(:internet, label: "Internet")
|> Infrastructure.add_vpc("vpc", label: "VPC")
|> Infrastructure.add_subnet_public("pub", label: "Public Subnet", parent: "vpc")
|> Infrastructure.add_subnet_private("priv", label: "Private Subnet", parent: "vpc")
# ❌ Load balancer placed in the private subnet (should be in public/DMZ)
|> Infrastructure.add_load_balancer(:lb, label: "LB", cluster: "priv")
# ❌ Database placed in the public subnet (should be private)
|> Infrastructure.add_managed_db(:db, label: "DB", cluster: "pub")
|> Infrastructure.add_compute(:app, label: "App", cluster: "priv")
# ❌ Direct internet → private subnet connection (bypasses DMZ)
|> Infrastructure.connect(:internet, :app, protocol: :https)
|> Infrastructure.connect(:lb, :app)
|> Infrastructure.connect(:app, :db)
Analysis.warnings(broken)
|> Enum.each(fn message ->
IO.puts("⚠️ #{message}")
end)
The three audit rules explained
Analysis.warnings/1 returns a list of plain strings. Each message describes the violation:
| Violation detected | Example message |
|---|---|
| Direct internet → private subnet |
"Private resource 'app' is connected directly to public internet boundary 'internet'." |
| Database not in private subnet |
"Managed database 'db' should be located in a private subnet, but it is in subnet 'pub'." |
| Load balancer not in public subnet |
"Load balancer 'lb' should be in a public-facing subnet, but it is in subnet 'priv'." |
Step 4: Themes
All six Choreo.Theme presets work out of the box with Choreo.Infrastructure because
:internet, :compute, and :managed_db are now first-class types in the theme system.
# Build a compact topology for comparing themes
demo =
Infrastructure.new()
|> Infrastructure.add_internet(:gw, label: "Internet")
|> Infrastructure.add_vpc("vpc", label: "VPC")
|> Infrastructure.add_subnet_public("pub", parent: "vpc")
|> Infrastructure.add_subnet_private("priv", parent: "vpc")
|> Infrastructure.add_load_balancer(:lb, label: "ALB", cluster: "pub")
|> Infrastructure.add_compute(:app, label: "API", cluster: "priv")
|> Infrastructure.add_managed_db(:db, label: "RDS", cluster: "priv")
|> Infrastructure.connect(:gw, :lb, protocol: :https)
|> Infrastructure.connect(:lb, :app, protocol: :http)
|> Infrastructure.connect(:app, :db, protocol: :tcp)
# Default (light)
Kino.VizJS.render(Infrastructure.to_dot(demo, theme: :default))
# Dark theme
Kino.VizJS.render(Infrastructure.to_dot(demo, theme: :dark))
# Ocean theme
Kino.VizJS.render(Infrastructure.to_dot(demo, theme: :ocean))
# Forest theme
Kino.VizJS.render(Infrastructure.to_dot(demo, theme: :forest))
Step 5: Multi-region topology
A more complex deployment: two availability zones with shared storage and a global load balancer.
multi_region =
Infrastructure.new()
|> Infrastructure.add_internet(:internet, label: "Internet")
# Global load balancer (outside VPC)
|> Infrastructure.add_load_balancer(:global_lb, label: "Global LB / CDN")
# VPC
|> Infrastructure.add_vpc("vpc", label: "AWS VPC (us-east-1)")
# AZ-1
|> Infrastructure.add_subnet_public("az1_pub",
label: "AZ-1 Public",
parent: "vpc"
)
|> Infrastructure.add_subnet_private("az1_priv",
label: "AZ-1 Private",
parent: "vpc"
)
|> Infrastructure.add_load_balancer(:alb1,
label: "ALB (AZ-1)",
cluster: "az1_pub"
)
|> Infrastructure.add_compute(:api1,
label: "API (AZ-1)",
cluster: "az1_priv"
)
|> Infrastructure.add_managed_db(:primary_db,
label: "Primary RDS",
cluster: "az1_priv"
)
# AZ-2
|> Infrastructure.add_subnet_public("az2_pub",
label: "AZ-2 Public",
parent: "vpc"
)
|> Infrastructure.add_subnet_private("az2_priv",
label: "AZ-2 Private",
parent: "vpc"
)
|> Infrastructure.add_load_balancer(:alb2,
label: "ALB (AZ-2)",
cluster: "az2_pub"
)
|> Infrastructure.add_compute(:api2,
label: "API (AZ-2)",
cluster: "az2_priv"
)
|> Infrastructure.add_managed_db(:replica_db,
label: "Read Replica RDS",
cluster: "az2_priv"
)
# Shared storage (outside any subnet — managed service)
|> Infrastructure.add_storage(:s3, label: "S3 (shared)")
# Connections
|> Infrastructure.connect(:internet, :global_lb, protocol: :https)
|> Infrastructure.connect(:global_lb, :alb1, protocol: :https)
|> Infrastructure.connect(:global_lb, :alb2, protocol: :https)
|> Infrastructure.connect(:alb1, :api1, protocol: :http)
|> Infrastructure.connect(:alb2, :api2, protocol: :http)
|> Infrastructure.connect(:api1, :primary_db, protocol: :tcp)
|> Infrastructure.connect(:api2, :replica_db, protocol: :tcp)
# Replication stream
|> Infrastructure.connect(:primary_db, :replica_db,
protocol: :tcp,
label: "Replication"
)
|> Infrastructure.connect(:api1, :s3, protocol: :https)
|> Infrastructure.connect(:api2, :s3, protocol: :https)
Kino.VizJS.render(Infrastructure.to_dot(multi_region, theme: :dark))
# Confirm no security warnings on the multi-region design
Analysis.warnings(multi_region)
|> case do
[] -> IO.puts("✅ Clean topology")
ws -> Enum.each(ws, &IO.puts("⚠️ #{&1}"))
end
Step 6: Protocol-aware edge styling
connect/3 accepts a :protocol key that drives edge color in the DOT output:
-
:https/:ssl→ green (#10b981) — secure traffic - everything else → grey (#64748b) — internal traffic
proto_demo =
Infrastructure.new()
|> Infrastructure.add_internet(:inet, label: "Internet")
|> Infrastructure.add_load_balancer(:lb, label: "LB")
|> Infrastructure.add_compute(:api, label: "API")
|> Infrastructure.add_managed_db(:db, label: "DB")
|> Infrastructure.add_storage(:store, label: "Store")
|> Infrastructure.connect(:inet, :lb, protocol: :https, label: "TLS")
|> Infrastructure.connect(:lb, :api, protocol: :http, label: "Plaintext")
|> Infrastructure.connect(:api, :db, protocol: :tcp, label: "TCP/5432")
|> Infrastructure.connect(:api, :store, protocol: :https, label: "TLS")
Kino.VizJS.render(Infrastructure.to_dot(proto_demo))
Step 7: Choreo.View lens operations
Choreo.Infrastructure implements Choreo.Viewable, so the full View API works — focus,
zoom, filter, and collapse.
alias Choreo.View
# Build a full topology to slice through
full =
Infrastructure.new()
|> Infrastructure.add_internet(:inet, label: "Internet")
|> Infrastructure.add_load_balancer(:lb, label: "Load Balancer")
|> Infrastructure.add_compute(:api, label: "API")
|> Infrastructure.add_compute(:worker, label: "Worker")
|> Infrastructure.add_managed_db(:db, label: "DB")
|> Infrastructure.add_storage(:store, label: "Store")
|> Infrastructure.connect(:inet, :lb, protocol: :https)
|> Infrastructure.connect(:lb, :api, protocol: :http)
|> Infrastructure.connect(:api, :worker)
|> Infrastructure.connect(:api, :db, protocol: :tcp)
|> Infrastructure.connect(:api, :store, protocol: :https)
|> Infrastructure.connect(:worker, :db, protocol: :tcp)
# Focus: show API and its immediate 1-hop neighbourhood
focused = View.focus(full, :api, radius: 1)
Kino.VizJS.render(Infrastructure.to_dot(focused))
# Filter: show only compute nodes (and edges between them)
compute_only = View.filter(full, fn _id, data -> data[:node_type] == :compute end)
Kino.VizJS.render(Infrastructure.to_dot(compute_only))
Summary
| Feature | API |
|---|---|
| Create topology |
Infrastructure.new/0 |
| Add boundary |
add_vpc/3, add_subnet_public/3, add_subnet_private/3 |
| Add nodes |
add_internet/3, add_load_balancer/3, add_compute/3, add_managed_db/3, add_storage/3 |
| Add edge |
connect/3 — options: :protocol, :label |
| Audit |
Analysis.warnings/1 |
| Render DOT |
Infrastructure.to_dot/2 — option: theme: |
| Render Mermaid |
Infrastructure.to_mermaid/2 — option: theme:, direction: |
| Lens ops |
Choreo.View.focus/3, filter/3, zoom/2, collapse/4 |