D2 in action: Building professional architecture diagrams step-by-step

Learn how to easily create architectural diagrams with D2

D2 in action: Building professional architecture diagrams step-by-step

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
Figure 1: Defining shapes

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
}
Figure 2: Different shape types

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 to

Four 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
Figure 3: Creating connections between boxes

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
  }
}
Figure 4: Containers and grouping

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
}
Figure 5: Adding icons

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"
  }
}
Figure 6: Animated with style

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"
}
Figure 6: Complete diagram

Save this file as azure-devops-managed-pool.d2 and run:

d2 azure-devops-managed-pool.d2 azure-devops-managed-pool.png

Summary

As you can see, in just about 100 lines of D2 code, we built a complete architectural diagram showing:

  1. Shapes The building blocks.
  2. Connections Adds relationships and data flow.
  3. Containers Hierarchy and grouping.
  4. Icons – Visual recognitions.
  5. 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: