Automation IP calculations in Microsoft Desired State Configuration (DSC)
How to calculate IPs in Desired State Configuration using ARM functions
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
/16network into multiple/24subnets 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/24There you have it already. No spreadsheet, no calculator, no mistakes.
The cidrSubnet() function takes three parameters:
- Base CIDR block
- New prefix length
- 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.10The 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: 24This 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.yamlThe 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: falseWhat 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.