D2 in action: Building professional architecture diagrams step-by-step
Learn how to easily create architectural diagrams with D2
A reader asked me in my previous article: "What does that D2 code actually produce?"
Fair question. I showed the syntax. Explained the benefits. But never showcased through how a diagram comes together piece by piece with images.
Let's fix that.
Today, we're going to build a complete architecture diagram for Azure DevOps Managed Pools from scratch. Each section adds one concept. By the end, you'll have a professional diagram and understand precisely how every line of D2 contributes to it.
What we're building
Azure DevOps Managed Pools is Microsoft's managed build agent solution. No more maintaining VMs or patch agent software. You define a pool, and Azure handles the rest.
The diagram will show:
- The Azure DevOps organization structure.
- Managed Pool configuration.
- Agent provisioning flow.
- Connection to Azure resources.
If you want to follow along, make sure you have D2 installed on your local machine. If you've installed D2, let's start with the basics.
Step 1: Defining shapes
Every D2 diagram starts with shapes. These are your building blocks. It includes boxes, cylinders, and clouds that represent the components of your architecture.
The syntax is simple. Just name it:
azdo: Azure DevOps
pool: Managed Pool
agents: Build Agents
That's three shapes generated by D2. They automatically render as rectangles with your labels. But rectangles are boring for architectural diagrams. Let's add a tint to it.
D2 supports multiple shape types. Here are the ones we'll use:
azdo: Azure DevOps {
shape: rectangle
}
pool: Managed Pool {
shape: hexagon
}
agents: Build Agents {
shape: person
}
azure: Azure Subscription {
shape: cloud
}
The shape property changes how D2 renders each element. To see a list of available shapes, check out the official documentation.
For our Azure DevOps Managed Pools diagram, we're using:
rectangle– for the Azure DevOps organization (it's a boundary).hexagon– for the Managed Pool (it's a service/process).cylinder- for agents (they do the work).cloud– for Azure (because well, it's Azure).
Step 2: Creating connections
The shapes are now loosely defined. There aren't any connections, meaning they're just floating around. Connections show the relationships among shapes, data flow, and their dependencies.
D2 uses arrows to define connections between boxes:
azdo -> pool: manages
pool -> agents: provisions
agents -> azure: deploys toFour connection types are supported in D2:
--– Undirected line.->– Arrow pointing right.<-– Arrow pointing left.<->– Bidirectional arrow.
The text after the colon becomes the connection label. This is where you describe what flows between components.
Let's expand our diagram:
azdo: Azure DevOps {
shape: rectangle
}
pool: Managed Pool {
shape: hexagon
}
agents: Build Agents {
shape: cylinder
}
azure: Azure Subscription {
shape: cloud
}
azdo -> pool: configures
pool -> agents: spins up
agents -> azure: deploys
azure -> azdo: reports status
You now have a cycle showing the complete flow. It starts by configuring the Managed Pools, then spins up (provisions), deploys the agents in Azure, and finally reports back the status to Azure DevOps.
Step 3: Using containers for grouping
An architectural diagram without hierarchy isn't a real diagram. Services live inside subscriptions. The pool contains agents, and Azure DevOps Services has projects.
D2 can handle this with containers, meaning shapes that contain other shapes:
organization: MyProject DevOps {
project: MyProject {
pipeline: Build Pipeline
pipeline2: Release Pipeline
}
project2: Portal {
pipeline3: CI Pipeline
}
}Nesting creates visual grouping and allows the outer shape to become a container with a label. Inner shapes appear inside it.
For our Azure DevOps Managed Pools diagram, this is compelling:
organization: Azure DevOps organization {
pool: Managed Pool {
config: Pool Configuration
scaling: Auto-scaling rules
}
pipelines: Pipelines {
build: Build
test: Test
deploy: Deploy
}
}
azure: Azure Subscription {
agents: Provisioned agents {
agent1: Agent 1
agent2: Agent 2
agent3: Agent N
}
resources: Target resources {
aks: AKS Cluster
webapp: App Service
}
}
In the above example, the target resources refer to the Azure services where your pipelines actually deploy your applications or infrastructure. You can now see the structure clearly: an organization containing the pool and pipelines, while Azure contains the agents and target resources.
Step 4: Adding icons
Text labels are fine, but icons make diagrams come to life. D2 supports icons via URLs. There's a known side for hosting various useful icons, which is Terrastruct:
azdo: Azure DevOps {
icon: https://icons.terrastruct.com/azure%2FDevOps%20Service%20Color%2FAzure%20DevOps.svg
}
pool: Managed Pool {
icon: https://icons.terrastruct.com/azure%2FCompute%20Service%20Color%2FVM%2FVM%20Scale%20Sets.svg
}The icon property accepts any image URL. Terrastruct hosts a free icon library with hundreds of icons for cloud providers. It's also possible to use local files:
myservice: My service {
icon: ./icons/custom-service.png
}Or you can even use it as standalone icons (no box, just the image) by using shape: image:
github: {
shape: image
icon: https://icons.terrastruct.com/dev%2Fgithub.svg
}
Step 5: Styling your diagram
By now, we've only used the default D2 styling. D2's style property lets you customize colors, borders, and effects:
pool: Managed Pool {
style: {
fill: "#0078D4"
stroke: "#005A9E"
font-color: white
border-radius: 8
shadow: true
}
}The following list represents the available style properties you can use:
fill— Background color (CSS color or hex).stroke— Border color.stroke-width— Border thickness (1-15).stroke-dash— Dashed border (0-10).font-color— Text color.font-size— Text size (8-100).opacity— Transparency (0-1).shadow— Drop shadow (true/false).3d— 3D effect for rectangles (true/false).border-radius— Rounded corners (0-20)
There's one more style you can use for connections that lets you animate. This is perfect for indicating a data flow, for example:
agents -> azure: deploys {
style: {
animated: true
stroke: "#00AA00"
}
}
The complete diagram
Time to put everything together. Here's a complete Azure DevOps Managed Pools diagram:
direction: right
title: Azure DevOps Managed Pools architecture {
shape: text
near: top-center
style.font-size: 24
style.bold: true
}
organization: Azure DevOps organization {
icon: https://icons.terrastruct.com/azure%2FDevOps%20Service%20Color%2FAzure%20DevOps.svg
style.fill: "#f0f0f0"
pool: Managed pool {
shape: hexagon
style.fill: "#0078D4"
style.font-color: white
config: Configuration {
shape: document
style.fill: "#E6F2FF"
}
scaling: Auto-scaling {
shape: stored_data
style.fill: "#E6F2FF"
}
}
pipelines: Build pipelines {
style.fill: "#FFF4E6"
yaml: YAML Definition {
shape: page
}
}
}
azure: Azure subscription {
shape: cloud
icon: https://icons.terrastruct.com/azure%2FGeneral%20Service%20Icons%2FSubscriptions.svg
style.fill: "#E6F7FF"
vmss: Agent Scale Set {
shape: cylinder
style.fill: "#0078D4"
style.font-color: white
style.multiple: true
}
vnet: Virtual network {
style.stroke-dash: 3
agent1: Agent 1 {
shape: cylinder
}
agent2: Agent 2 {
shape: cylinder
}
agentN: Agent N {
shape: cylinder
}
}
targets: Deployment Targets {
style.fill: "#E6FFE6"
aks: AKS {
shape: hexagon
}
webapp: App Service {
shape: rectangle
}
}
}
# Connections
organization.pool -> azure.vmss: provisions {
style.animated: true
}
azure.vmss -> azure.vnet.agent1: creates
azure.vmss -> azure.vnet.agent2: creates
azure.vmss -> azure.vnet.agentN: creates
organization.pipelines -> organization.pool: requests agent
organization.pool -> organization.pipelines: assigns agent
azure.vnet.agent1 -> azure.targets: deploys {
style.stroke: "#00AA00"
}
azure.vnet.agent2 -> azure.targets: deploys {
style.stroke: "#00AA00"
}
Save this file as azure-devops-managed-pool.d2 and run:
d2 azure-devops-managed-pool.d2 azure-devops-managed-pool.pngSummary
As you can see, in just about 100 lines of D2 code, we built a complete architectural diagram showing:
- Shapes – The building blocks.
- Connections – Adds relationships and data flow.
- Containers – Hierarchy and grouping.
- Icons – Visual recognitions.
- Styling – Professional polishing.
You can safely store this in version control and update it with a single command. Storing it in version control makes it reviewable. And it never lies about your architecture because you update the code when you update the architecture.
It's now your turn. Pick one of your existing architectural diagrams, especially an outdated one. Try creating it in D2 by starting simple. Define shapes, start making the connections, and in the end, add the containers for logical grouping.
References
Here are the references I used for this blog post: