How to Configure PowerShell DSC for Configuration Management on Windows Server 2025
PowerShell Desired State Configuration (DSC) is a declarative management platform built into Windows that lets you define the desired state of servers and continuously enforce it. Rather than writing procedural scripts that execute step-by-step, you declare what a server should look like — which Windows features are installed, which services run, what files exist — and the DSC engine enforces that state, correcting drift automatically. On Windows Server 2025, DSC remains a foundational tool for configuration management, integrating with Azure Automation DSC for fleet-scale management and with pull servers for on-premises environments. This tutorial covers writing reusable DSC configurations, using community resource modules from the PowerShell Gallery, configuring the Local Configuration Manager (LCM) in both Push and Pull modes, and monitoring configuration drift.
Prerequisites
- Windows Server 2025 with PowerShell 5.1 (DSC v2 is built in) or PowerShell 7.x with PSDesiredStateConfiguration module
- Administrator access to the target nodes
- Internet access to download modules from the PowerShell Gallery (or an internal NuGet feed)
- Optional: A second Windows Server to act as a pull server or managed node
- Optional: An Azure Automation account for cloud-hosted pull mode
Step 1: Install Community DSC Resource Modules from PSGallery
The built-in DSC resources cover basic Windows management, but community modules from the PowerShell Gallery provide resources for networking, file management, SQL Server, IIS, and much more. Install the most commonly needed modules on both the authoring machine and target nodes.
# Ensure NuGet provider and PSGallery are trusted
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
# Core community resource modules
$DscModules = @(
"ComputerManagementDsc", # Computer name, time zone, power plan, scheduled tasks
"NetworkingDsc", # IP addresses, DNS, firewall rules, host file entries
"PSDesiredStateConfiguration", # DSC core module for PS7+
"xWebAdministration", # IIS websites and app pools
"SqlServerDsc", # SQL Server configuration
"FileSystemDsc" # Advanced file system operations
)
foreach ($Module in $DscModules) {
Write-Host "Installing $Module..."
Install-Module -Name $Module -Force -AllowClobber -Scope AllUsers
}
# List installed DSC resources
Get-DscResource | Select-Object Name, Module, Properties | Format-Table -AutoSize
Step 2: Write a Reusable DSC Configuration with ConfigurationData
A DSC configuration is a PowerShell function decorated with the Configuration keyword. ConfigurationData separates environment-specific values (node names, IP addresses, feature flags) from the configuration logic, making the same configuration file reusable across Dev, Staging, and Production environments.
New-Item -ItemType Directory -Path "C:DSCConfigs" -Force | Out-Null
Set-Location "C:DSCConfigs"
@'
# ConfigurationData separates node-specific data from configuration logic.
$ConfigurationData = @{
AllNodes = @(
@{
NodeName = "*"
# Skip certificate encryption for lab; use certificates in production
PSDscAllowPlainTextPassword = $true
PSDscAllowDomainUser = $true
TimeZone = "UTC"
NtpServer = "time.windows.com"
},
@{
NodeName = "WebServer01"
Role = "WebServer"
Features = @("Web-Server", "Web-Asp-Net45", "Web-Mgmt-Console")
},
@{
NodeName = "AppServer01"
Role = "AppServer"
Features = @("NET-Framework-45-Core", "NET-WCF-Services45")
}
)
# Non-node data shared across all nodes
NonNodeData = @{
LogPath = "C:LogsDSC"
AppPoolVersion = "v4.0"
}
}
# ── DSC Configuration ─────────────────────────────────────────────────────────
Configuration WebServerBaseline {
param (
[Parameter(Mandatory)]
[string[]] $NodeName
)
Import-DscResource -ModuleName PSDesiredStateConfiguration
Import-DscResource -ModuleName ComputerManagementDsc
Import-DscResource -ModuleName NetworkingDsc
Node $NodeName {
# ── Time zone ──────────────────────────────────────────────────────
TimeZone SystemTimeZone {
IsSingleInstance = "Yes"
TimeZone = $Node.TimeZone
}
# ── NTP server ─────────────────────────────────────────────────────
# (Uses Script resource when ComputerManagementDsc NTP resource unavailable)
Script SetNTP {
SetScript = {
w32tm /config /manualpeerlist:"$using:Node.NtpServer" /syncfromflags:manual /reliable:YES /update
Restart-Service w32tm -Force
}
TestScript = {
$cfg = w32tm /query /configuration
$cfg -match [regex]::Escape($using:Node.NtpServer)
}
GetScript = { @{ Result = (w32tm /query /configuration) } }
}
# ── Windows Features (role-specific) ───────────────────────────────
foreach ($Feature in $Node.Features) {
WindowsFeature "Feature_$Feature" {
Name = $Feature
Ensure = "Present"
}
}
# ── Firewall rule: allow RDP only from management subnet ───────────
Firewall AllowRDPFromMgmt {
Name = "Allow-RDP-Management"
DisplayName = "Allow RDP from Management Network"
Ensure = "Present"
Enabled = "True"
Direction = "Inbound"
LocalPort = "3389"
Protocol = "TCP"
RemoteAddress = "10.0.0.0/24"
}
# ── Ensure log directory exists ────────────────────────────────────
File DSCLogDirectory {
DestinationPath = $ConfigurationData.NonNodeData.LogPath
Type = "Directory"
Ensure = "Present"
}
}
}
# Compile the configuration — produces MOF files in .WebServerBaseline
WebServerBaseline -NodeName "WebServer01", "AppServer01" `
-ConfigurationData $ConfigurationData `
-OutputPath "C:DSCMOF"
Write-Host "MOF files generated:"
Get-ChildItem "C:DSCMOF" | Select-Object Name, Length
'@ | Set-Content -Path "WebServerBaseline.ps1" -Encoding UTF8
# Source the configuration script and compile MOF files
. "C:DSCConfigsWebServerBaseline.ps1"
Step 3: Apply Configuration in Push Mode
Push mode sends the MOF file directly to a target node and immediately applies it. This is the simplest deployment model, ideal for small environments or initial testing.
# Push to local machine (use -ComputerName for remote nodes)
Start-DscConfiguration -Path "C:DSCMOF" -Wait -Verbose -Force
# Push to a remote node (requires WinRM)
Start-DscConfiguration -Path "C:DSCMOF" `
-ComputerName "WebServer01" `
-Credential (Get-Credential) `
-Wait -Verbose -Force
Step 4: Configure the LCM for Pull Mode
In Pull mode, nodes periodically contact a pull server to retrieve their configuration MOF and apply it. The Local Configuration Manager (LCM) on each node is configured with the pull server URL, a configuration ID or configuration name, and a refresh interval.
@'
# LCM meta-configuration — apply with Set-DscLocalConfigurationManager
[DSCLocalConfigurationManager()]
Configuration LCMPullConfig {
Node "localhost" {
Settings {
RefreshMode = "Pull"
ConfigurationMode = "ApplyAndAutoCorrect" # Enforces state continuously
RefreshFrequencyMins = 30 # Check pull server every 30 min
RebootNodeIfNeeded = $true
ActionAfterReboot = "ContinueConfiguration"
AllowModuleOverwrite = $true
}
# Pull server endpoint (SMB share or HTTP pull server)
ConfigurationRepositoryWeb DSCPullServer {
ServerURL = "https://dscpull.contoso.local:8080/PSDSCPullServer.svc"
RegistrationKey = "a8f6b2e0-4c91-4d3e-bf12-0e9a7c5d82f4"
ConfigurationNames = @("WebServerBaseline")
AllowUnsecureConnection = $false
}
# Resource module pull — nodes download needed DSC resource modules
ResourceRepositoryWeb DSCResourceServer {
ServerURL = "https://dscpull.contoso.local:8080/PSDSCPullServer.svc"
RegistrationKey = "a8f6b2e0-4c91-4d3e-bf12-0e9a7c5d82f4"
}
# Send reports to a report server (optional)
ReportServerWeb DSCReportServer {
ServerURL = "https://dscpull.contoso.local:8080/PSDSCPullServer.svc"
RegistrationKey = "a8f6b2e0-4c91-4d3e-bf12-0e9a7c5d82f4"
}
}
}
# Compile the meta-MOF
LCMPullConfig -OutputPath "C:DSCLCM"
'@ | Set-Content -Path "C:DSCConfigsLCMPullConfig.ps1" -Encoding UTF8
# Source and compile
. "C:DSCConfigsLCMPullConfig.ps1"
# Apply LCM settings to the local node
Set-DscLocalConfigurationManager -Path "C:DSCLCM" -Verbose
# View current LCM settings
Get-DscLocalConfigurationManager | Select-Object RefreshMode, ConfigurationMode, RefreshFrequencyMins
Step 5: Partial Configurations
Partial configurations allow different teams to own different sections of a node’s configuration. For example, the security team might manage firewall rules while the application team manages IIS. Each partial configuration is independently authored, compiled, and published.
@'
[DSCLocalConfigurationManager()]
Configuration PartialConfigLCM {
Node "localhost" {
Settings {
RefreshMode = "Push"
ConfigurationMode = "ApplyAndAutoCorrect"
}
PartialConfiguration BaseOS {
Description = "Handles OS-level configuration: features, time zone, NTP"
RefreshMode = "Push"
}
PartialConfiguration WebRole {
Description = "Handles IIS and web role configuration"
RefreshMode = "Push"
DependsOn = "[PartialConfiguration]BaseOS"
}
}
}
PartialConfigLCM -OutputPath "C:DSCLCMPartial"
Set-DscLocalConfigurationManager -Path "C:DSCLCMPartial" -Verbose
'@ | Set-Content -Path "C:DSCConfigsPartialLCM.ps1" -Encoding UTF8
# After applying partial configs individually, publish both simultaneously
Publish-DscConfiguration -Path "C:DSCMOFBaseOS" -ComputerName localhost
Publish-DscConfiguration -Path "C:DSCMOFWebRole" -ComputerName localhost
Start-DscConfiguration -UseExisting -Wait -Verbose
Step 6: Monitor Drift with Test-DscConfiguration
Configuration drift occurs when the actual state of a server diverges from its desired state — for example, someone manually stops a required service or uninstalls a Windows feature. Test-DscConfiguration compares the current system state to the applied MOF without making changes, and returns $true only if everything is in compliance.
# Test local node compliance — returns True/False
$Result = Test-DscConfiguration -Detailed
Write-Host "Overall compliance: $($Result.InDesiredState)"
Write-Host ""
Write-Host "Non-compliant resources:"
$Result.ResourcesNotInDesiredState | Format-Table ResourceId, InstanceName -AutoSize
Write-Host "Compliant resources:"
$Result.ResourcesInDesiredState | Format-Table ResourceId -AutoSize
# Test a remote node
Test-DscConfiguration -ComputerName "WebServer01" -Credential (Get-Credential) -Detailed
# Force immediate re-application of configuration on pull clients
Update-DscConfiguration -Wait -Verbose
# View the DSC event log for recent activity
Get-WinEvent -LogName "Microsoft-Windows-Dsc/Operational" |
Select-Object TimeCreated, LevelDisplayName, Message |
Sort-Object TimeCreated -Descending |
Select-Object -First 20 |
Format-Table -Wrap
Conclusion
PowerShell DSC on Windows Server 2025 provides a robust, built-in configuration management platform that scales from a handful of manually pushed servers to hundreds of nodes managed through a pull server or Azure Automation DSC. By separating ConfigurationData from configuration logic, you create reusable, environment-aware configurations that are easy to version-control and peer-review. Combining community modules from the PowerShell Gallery, partial configurations for multi-team ownership, and continuous drift monitoring via Test-DscConfiguration, you have the building blocks for a mature, policy-driven server management practice. As a next step, explore Azure Automation DSC, which provides a fully managed pull server in the cloud with a dashboard for fleet-wide compliance reporting without the overhead of managing a dedicated DSC pull server on-premises.