Automation IP calculations in Microsoft Desired State Configuration (DSC)

How to calculate IPs in Desired State Configuration using ARM functions

Automation IP calculations in Microsoft Desired State Configuration (DSC)

Network configurations have always been that exhausting task. One wrong digit means hours of debugging. You grab your spreadsheet again and start calculating the subnet masks. You double-check the CIDRS and hope you didn't accidentally overlap two network segments.

What if I told you there's a better way?

Not just better tooling, but a different approach that eliminates the spreadsheet entirely when you're working with network configurations and Desired State Configuration (DSC).

Here's how you can automate network configurations with PowerShell DSC and Microsoft DSC.

The problem with calculating

When you're managing infrastructure, IP address allocation becomes that silent, productive killer. You need to:

  • Divide a /16 network into multiple /24 subnets for different tiers.
  • Ensure nothing overlaps in the address spaces across regions.
  • Assign gateway addresses, DNS servers, and load balancers consistently.
  • Document everything

And the tools? They're either complex (enterprise IPAM solutions) or too simple (good old Excel). There's no middle ground for teams that want automation without the overhead.

Enter class-based DSC resources

If you're coming from the PowerShell landscape, developing script-based DSC resources is old school. Class-based DSC resources are the latest pattern that supports additional features when using Microsoft DSC.

Let's look at a simple example of a DSC resource that manages IP access lists:

[DscResource()]
class IpAccessList
{
    [DscProperty(Key)]
    [System.String]
    $Name

    [DscProperty()]
    [System.String[]]
    $IpAddresses

    [IpAccessList] Get()
    {
        $currentState = [IpAccessList]::new()
        $currentState.Name = $this.Name
        $currentState.IpAddresses = $this.IpAddresses

        Write-Verbose "Get: Name=$($this.Name), IpAddresses=$($this.IpAddresses -join ', ')"

        return $currentState
    }

    [System.Boolean] Test()
    {
        Write-Verbose "Test: Checking IP addresses for '$($this.Name)'"

        if ($null -eq $this.IpAddresses -or $this.IpAddresses.Count -eq 0)
        {
            Write-Verbose "Test: No IP addresses defined"
            return $false
        }

        Write-Verbose "Test: Found $($this.IpAddresses.Count) IP address(es)"
        return $true
    }

    [void] Set()
    {
        Write-Verbose "Set: Configuring IP addresses for '$($this.Name)'"

        foreach ($ip in $this.IpAddresses)
        {
            Write-Verbose "Set: Processing IP address: $ip"
        }

        Write-Verbose "Set: Completed"
    }
}

This class defines three methods:

  • Get() - Returns the current state.
  • Test() - Checks if configuration matches desired state.
  • Set() - Enforces the desired state.

The class itself has a IpAddress property to illustrate the example. An array of strings can be passed in. This means you can pass multiple IP addresses in CIDR notation:

Invoke-DscResource -Name IPAccessList -ModuleName MyModule -Method Set -Property @{
  Name = 'WebServers'
  IpAddresses = @('10.0.1.0/24', '10.0.2.0/24', '10.0.3.0/24')
}

But here's where it gets interesting. How do you actually calculate those subnets systematically? How do you ensure nothing overlaps? How do you assign gateway addresses without manual calculation?

And then there's another problem: when you pass IP addresses as strings, you're responsible for validation. If you type in 10.0.1.0/33 or 10.0.1.256/24, and the error happens later, you're scratching your head, thinking what's going on. The feedback loop is painful.

Enter Microsoft DSC

Microsoft DSC follows the Azure ARM functions implementation, allowing configuration functions that solve these problems.

Let's look at each function separately, starting of with cidrSubnet(). This function divides a large network block into smaller subnets automatically:

$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
  baseNetwork:
    type: string
    defaultValue: 10.0.0.0/16
resources:
  - name: Network segmentation
    type: Microsoft.DSC.Debug/Echo
    properties:
      output:
        webTier: "[cidrSubnet(parameters('baseNetwork'), 24, 0)]"
        appTier: "[cidrSubnet(parameters('baseNetwork'), 24, 1)]"
        dataTier: "[cidrSubnet(parameters('baseNetwork'), 24, 2)]"

This produces:

output:
  webTier: 10.0.0.0/24
  appTier: 10.0.1.0/24
  dataTier: 10.0.2.0/24

There you have it already. No spreadsheet, no calculator, no mistakes.

The cidrSubnet() function takes three parameters:

  1. Base CIDR block
  2. New prefix length
  3. Subnet index

Now, imagine you want to calculate specific host IP addresses within a subnet. Grab the cidrHost() function:

resources:
  - name: Infrastructure IPs
    type: Microsoft.DSC.Debug/Echo
    properties:
      output:
        gateway: "[cidrHost('192.168.100.0/24', 1)]"
        primaryDNS: "[cidrHost('192.168.100.0/24', 2)]"
        secondaryDNS: "[cidrHost('192.168.100.0/24', 3)]"
        loadBalancer: "[cidrHost('192.168.100.0/24', 10)]"

The result:

output:
  gateway: 192.168.100.1
  primaryDNS: 192.168.100.2
  secondaryDNS: 192.168.100.3
  loadBalancer: 192.168.100.10

The pattern here is consistency. Always use .1 for gateways, .2 and .3 for DNS, and .10 for load balancers. Your team starts learning the conventions once and applies them everywhere.

Lastly, there's one more function you can use to extract detailed network information from CIDR notation. This is the parseCidr() function:

resources:
  - name: Network details
    type: Microsoft.DSC.Debug/Echo
    properties:
      output: "[parseCidr('192.168.1.0/24')]"

Returns:

output:
  network: 192.168.1.0
  netmask: 255.255.255.0
  broadcast: 192.168.1.255
  firstUsable: 192.168.1.1
  lastUsable: 192.168.1.254
  cidr: 24

This is perfect for documentation, validation, or configuring systems that require subnet masks rather than CIDR notation.

Bringing it together

Here's where the power starts by combining in. You can combine PSDSC class-based resources with CIDR functions to automate entire network configurations:

$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
  vnetCidr:
    type: string
    defaultValue: 172.16.0.0/12
  
resources:
  - name: Web tier network
    type: MyModule/IpAccessList
    properties:
      Name: WebTier
      IpAddresses:
        - "[cidrSubnet(parameters('vnetCidr'), 24, 0)]"
        - "[cidrSubnet(parameters('vnetCidr'), 24, 1)]"
        - "[cidrSubnet(parameters('vnetCidr'), 24, 2)]"
  
  - name: App tier network
    type: DatabricksDsc/IpAccessList
    properties:
      Name: AppTier
      IpAddresses:
        - "[cidrSubnet(parameters('vnetCidr'), 24, 10)]"
        - "[cidrSubnet(parameters('vnetCidr'), 24, 11)]"
  
  - name: Infrastructure details
    type: Microsoft.DSC.Debug/Echo
    properties:
      output:
        webTierSubnet: "[cidrSubnet(parameters('vnetCidr'), 24, 0)]"
        webTierGateway: "[cidrHost(cidrSubnet(parameters('vnetCidr'), 24, 0), 1)]"
        webServer1: "[cidrHost(cidrSubnet(parameters('vnetCidr'), 24, 0), 10)]"
        webServer2: "[cidrHost(cidrSubnet(parameters('vnetCidr'), 24, 0), 11)]"
        loadBalancer: "[cidrHost(cidrSubnet(parameters('vnetCidr'), 24, 0), 20)]"

Run this configuration:

dsc config get --file network-config.dsc.config.yaml

The output:

results:
- metadata:
    Microsoft.DSC:
      duration: PT2.9537957S
  name: Web tier network
  type: MyModule/IpAccessList
  result:
    actualState:
      Name: WebTier
      IpAddresses:
      - 172.16.0.0/24
      - 172.16.1.0/24
      - 172.16.2.0/24
- metadata:
    Microsoft.DSC:
      duration: PT2.5088117S
  name: App tier network
  type: MyModule/IpAccessList
  result:
    actualState:
      IpAddresses:
      - 172.16.10.0/24
      - 172.16.11.0/24
      Name: AppTier
- metadata:
    Microsoft.DSC:
      duration: PT0.3531996S
  name: Infrastructure details
  type: Microsoft.DSC.Debug/Echo
  result:
    actualState:
      output:
        webTierSubnet: 172.16.0.0/24
        webTierGateway: 172.16.0.2
        webServer1: 172.16.0.11
        webServer2: 172.16.0.12
        loadBalancer: 172.16.0.21
messages: []
hadErrors: false

What problem does this actually solve

Let's be honest about what this eliminates. First, the spreadsheet problem. No more Excel files needed to calculate subnets. No more "check the documentation" when someone asks what IP range web servers use, as it's all declared in documents.

Then you have the consistency. Every environment eventually follows the same pattern. Western Europe uses the subnet index 0-9, whereas the north uses 10-19.

Lastly, the overlapping problem. You can't accidentally overlap subnets when the function calculates them sequentially. Math doesn't make mistakes; humans do.

When you should use this

This approach shines when you're managing your network using DSC and when:

  • You're managing multiple environments.
  • You need consistent IP allocation across regions.
  • You're systematically dividing large address spaces.
  • You want Infrastructure-as-Code (IaC) for networking.

It's not necessary if:

  • You have three servers to maintain (even though you still can).
  • Your network topology changes yearly.
  • You prefer GUI-based network tools

Getting started

If the above examples inspired you to get started, there are two things you need:

  • PowerShell 7+
  • Microsoft DSC for CIDR configuration functions.

That's it. Just PowerShell and YAML together.

Where this goes next

The future here is reusability. Imagine the following:

  • A DSC resource that configures Databricks IP access lists using these functions.
  • A resource that configures firewall rules based on CIDR calculations.
  • A resource that generates network diagrams from your DSC config.

The building blocks already exist. The CIDR functions handle the math. Class-based DSC resources handle the state management.

What you need is a problem worth solving.

For me, that's always been network automation. Because it helps me explore the systematic deployment of infrastructure, it helps me ensure production environments are consistent. It helps me document complex networks in code, even though that's my Achilles heel.

That's the fastest way to build reliable systems.