Two tools dominate declarative Windows Server configuration, and most teams pick one for reasons that don’t survive contact with an incident. PowerShell DSC gives you an on-box agent — the Local Configuration Manager (LCM) — that re-applies a compiled desired state on a schedule with no controller required, so a fleet self-heals even when the orchestrator is unreachable. Ansible gives you agentless push from a control node, an enormous module library, and orchestration across mixed Linux/Windows estates from a single inventory. Neither is strictly “better”; they solve different halves of the problem. The mature pattern in a real datacenter is Ansible as the orchestration plane that ships and triggers DSC exactly where DSC’s always-on remediation earns its keep, while Ansible owns breadth, sequencing, and everything that needs cross-host coordination.
This is the architect’s walkthrough of both, side by side, at Advanced depth. You will build the same Windows Server baseline — an IIS role, a guaranteed service, a hardened registry value — three ways: as a DSC configuration compiled to MOF and enforced by the LCM, as an idempotent Ansible role using the ansible.windows collection, and as the combined model where Ansible drives DSC resources through win_dsc. Along the way we enumerate every moving part that bites in production: the Get/Test/Set resource contract, MOF compilation, the three ConfigurationMode values, push versus pull versus Azure Machine Configuration (the artist formerly known as guest configuration), the split between Windows PowerShell 5.1 DSC and the cross-platform DSC v3 runtime, WinRM versus PSRP versus SSH transport, the win_ versus ansible.windows module naming migration, what idempotency actually costs to guarantee, how each tool handles secrets, and how each scales to hundreds of nodes.
By the end you will stop treating “DSC or Ansible” as a religious war and start treating it as a routing decision: which settings must never drift even when the network is down (DSC’s LCM), which need orchestrated, sequenced, cross-host changes (Ansible), and which niche resource is simply better in DSC and worth invoking from an Ansible play. You will also have a Pester-and-ansible-lint-gated CI pipeline so neither a broken MOF nor a non-idempotent task ever reaches a node.
What problem this solves
Windows Server drifts. Someone RDPs into a box during an incident and flips a registry key, disables a service “just to test,” or installs a feature by hand — and it stays that way, silently, until an audit or an outage finds it. Multiply that across 50, 200, or 800 hosts across regions and “what is the actual configuration of production right now?” becomes unanswerable. Imperative scripting doesn’t fix this: a .ps1 that installs IIS is not idempotent, is not safe to re-run, and tells you nothing about whether the box is currently correct. Click-ops via Server Manager doesn’t scale and leaves no reviewable artifact. Group Policy handles a slice (see Group Policy as Code) but can’t install roles, manage arbitrary services, or lay down application config, and it’s domain-bound.
What breaks without declarative config management: a security baseline that silently regresses (SMBv1 quietly re-enabled by a manual “hotfix”), a fleet where no two “identical” web servers are actually identical (snowflakes), patch and hardening state that nobody can prove for a compliance auditor, and a rebuild-a-lost-node process that lives in one senior engineer’s head. When a host dies you want to stamp out its replacement from a reviewed, version-controlled definition in minutes, not reverse-engineer it.
Who hits this: anyone running more than a handful of Windows Servers — IIS/middleware fleets, file and print, AD infrastructure, SQL hosts, RDS/AVD session hosts. It bites hardest on teams with mixed Linux/Windows estates (they want one control plane), regulated shops (they must prove state, not assert it), and anyone whose control node or network is not guaranteed reachable 24/7 (a partition must not mean a compliance gap). The fix is to define the desired state as code, enforce it continuously, and detect and remediate drift automatically — and to know precisely which tool does which job.
Learning objectives
By the end of this article you can:
- Explain the DSC model end to end — the
Get/Test/Setresource contract, a configuration as a PowerShell function, MOF as the compiled artifact, and the LCM as the on-box enforcement agent — and where idempotency actually lives. - Configure the LCM deliberately:
RefreshMode(Push vs Pull), all threeConfigurationModevalues,ConfigurationModeFrequencyMins, reboot behaviour, and partial configurations, and explain what each unlocks and its floor/limit. - Distinguish Windows PowerShell 5.1 DSC (WMI/CIM-based LCM, MOF, pull server) from DSC v3 (a standalone cross-platform executable with no LCM, no MOF pull server) and know which docs apply to which runtime — never mixing them.
- Deliver desired state at scale three ways — LCM push, a pull endpoint, and Azure Machine Configuration (Arc-enabled guest config) — and pick the right one.
- Manage Windows from Ansible over WinRM, PSRP, and SSH; distinguish the modern
ansible.windows/community.windowscollections from legacywin_FQCNs; and write roles that are provably idempotent with--check. - Detect and remediate drift in both worlds — the LCM’s
ApplyAndAutoCorrectloop and Ansible’s scheduled idempotent converge — and wire the change signal into alerting. - Handle secrets safely in each tool (certificate-encrypted MOF,
ansible-vault, external brokers), reason about scaling limits, and choose DSC vs Ansible vs both per requirement with a defensible decision matrix.
Prerequisites & where this fits
You should be comfortable on Windows Server 2019/2022/2025: installing roles and features, reading and setting registry values, Get-Service/Set-Service, and the basics of PowerShell (functions, parameters, modules, Install-Module from the PowerShell Gallery). You should know what WinRM is (the WS-Management listener on TCP 5985 HTTP / 5986 HTTPS) even if you’ve only ever used Enable-PSRemoting. For the Ansible half you need a Linux (or WSL) control node with a recent ansible-core, and familiarity with YAML, inventories, and playbooks. For the Azure section, an Azure subscription and a passing familiarity with Azure Arc helps but isn’t required to follow the concepts.
This sits in the Servers → Automation track and is upstream of most other Windows automation you’ll do. It pairs directly with Group Policy as Code (the GPO slice of the same drift problem), Windows Server hardening and the WSUS patch pipeline (what you’re often enforcing is a hardening baseline), and gMSA, Windows LAPS and service-account credential hardening (how the automation authenticates without plaintext passwords). On the Ansible side, the deeper collection and testing mechanics live in idempotent Ansible collections and Molecule testing, inventory and secrets in Ansible dynamic inventory for AWS/Azure with secrets, and controller-scale operations in Ansible Automation Platform at enterprise scale. If you manage headless boxes, managing Server Core with Windows Admin Center is the interactive counterpart to this code-driven approach.
A quick map of which tool owns which layer, so you route work correctly from the start:
| Layer / concern | PowerShell DSC (5.1 + LCM) | Ansible | Best owner |
|---|---|---|---|
| On-box self-healing when control node is down | Native (ApplyAndAutoCorrect) |
None — converges only when it runs | DSC |
| Cross-host orchestration & sequencing | Weak (per-node MOF) | Native (plays, serial, handlers) |
Ansible |
| Mixed Linux + Windows from one inventory | Windows-only (5.1) | Native | Ansible |
| Agentless (no software on target) | No (LCM is always present) | Yes (WinRM/SSH only) | Ansible |
Rich niche Windows resources (xWebAdministration) |
Mature module ecosystem | Can invoke via win_dsc |
DSC |
| Ad-hoc “run this now everywhere” | No | Native (ansible -m) |
Ansible |
| Continuous drift correction with no controller | Native | No (needs a scheduler) | DSC |
| Fleet reporting from Azure | Azure Machine Configuration | AAP + callbacks | Either |
Core concepts
Six mental models make every later decision obvious. Read them once; the tables that follow enumerate the specifics.
DSC is compile-then-enforce; Ansible is push-and-converge. In DSC you author a configuration (a PowerShell function), compile it to a MOF document per node (a flat, logic-free declarative artifact), and hand that MOF to the LCM, an agent baked into every Windows box that applies it and — critically — re-applies it on a timer. In Ansible there is no compile step and no resident agent: a control node opens a remote session, runs each task’s check-then-change logic in place, and disconnects. DSC’s enforcement is continuous and autonomous; Ansible’s is episodic and orchestrated. That single difference explains most of the trade-offs below.
Idempotency lives in the Test-before-Set gate. A DSC resource is a module exposing Get (read current state), Test (does current state match desired? return a boolean), and Set (make it match). The contract is that Set runs only when Test reports drift. Ansible’s win_ modules implement the same discipline internally: they inspect current state and report changed only when they actually mutate something. Idempotency is not a property you get for free by “using a config tool” — it’s a property of each resource/module honouring that gate, and it’s your job to preserve it (especially the moment you drop to raw shell).
The MOF is a compiled binary; the configuration is the source. Treat the .ps1 configuration like source code and the <node>.mof like a build artifact. The MOF is what the LCM stores and enforces; the script that produced it is build-time only. This matters for secrets (the MOF sits on disk in clear text unless you encrypt credentials) and for CI (you test that the configuration compiles to the expected MOF).
The LCM’s ConfigurationMode decides whether drift is corrected, merely reported, or ignored. ApplyOnly applies once and never touches the box again. ApplyAndMonitor applies, then logs drift to the event log but does not fix it. ApplyAndAutoCorrect applies and re-applies every consistency check — the self-healing mode. This one meta-setting is the entire reason to reach for DSC over Ansible for security-critical state.
There are two DSCs and they are different runtimes. Everything the LCM understands — MOF, Start-DscConfiguration, the pull server, Get-DscLocalConfigurationManager — belongs to Windows PowerShell 5.1 DSC (WMI/CIM-based). The cross-platform rewrite, DSC v3, is a standalone Rust executable (dsc) with no LCM, no MOF pull server, and a JSON/YAML resource model; it’s what powers Azure Machine Configuration under the hood. Do not mix their documentation: a 5.1 deployment does not gain DSC v3 features, and DSC v3 concepts (resource manifests, dsc config set) do not map onto the LCM.
Ansible reaches Windows over a transport, and the transport is where auth pain lives. Ansible does not run an agent on Windows; it connects over WinRM (default winrm plugin, or the faster psrp plugin, both riding the WS-Man listener) or, on modern Windows, over SSH (ansible.windows supports the Win32-OpenSSH server). Which transport and which auth (Kerberos, NTLM, CredSSP, certificate, Basic) you choose determines your credential-exposure posture and whether double-hop scenarios work at all.
The vocabulary in one table
Before the deep sections, pin down every term. The glossary repeats these for lookup; this is the mental model side by side.
| Term | One-line definition | World | Why it matters |
|---|---|---|---|
| Resource | Module with Get/Test/Set for one state kind |
DSC | Idempotency unit; Test gates Set |
| Configuration | PowerShell function declaring desired state | DSC | Your source of truth |
| MOF | Compiled, logic-free desired-state document per node | DSC | The artifact the LCM enforces |
| LCM | On-box agent that applies + re-applies MOF | DSC | Autonomous drift correction |
ConfigurationMode |
Whether the LCM auto-corrects, monitors, or applies once | DSC | The self-heal switch |
RefreshMode |
Push (sent) vs Pull (fetched) delivery | DSC | Operational model |
| Meta-configuration | Config that configures the LCM itself | DSC | Where LCM settings are set |
| DSC v3 | Standalone cross-platform dsc executable, no LCM/MOF |
DSC | Different runtime; powers machine config |
| Machine Configuration | Azure/Arc guest-config that audits/enforces via DSC v3 | Azure | Fleet compliance from the cloud |
| Module (Ansible) | Unit of work run on the target | Ansible | Implements idempotency internally |
| Collection | Namespaced bundle of modules (ansible.windows) |
Ansible | Modern packaging of win_* |
| Connection plugin | How Ansible reaches the host (winrm/psrp/ssh) |
Ansible | Transport + auth choice |
win_dsc |
Ansible module that invokes any installed DSC resource | Both | The bridge between the two |
| Idempotency | Re-running produces no further change | Both | changed=0 on second pass |
| Drift | Live state diverging from desired | Both | What both tools exist to kill |
DSC concepts: resources, MOF, and the LCM
Three pieces make DSC. Resources know how to test and set one kind of state — a Windows feature, a service, a registry value, a file. Each exposes Get, Test, and Set. Confirm what’s installed before you author against it, and prefer the maintained PSDscResources module over the ancient in-box PSDesiredStateConfiguration resources:
# What resources does this box have, and at what version?
Get-DscResource | Select-Object Name, ModuleName, Version | Sort-Object ModuleName
# Install the supported successor resource module + a networking one, machine-wide
Install-Module -Name PSDscResources, NetworkingDsc -Scope AllUsers -Force
A configuration is a PowerShell function (keyword Configuration) that declares resources and their target values. Compiling it emits one MOF per node. The LCM ingests a MOF, calls each resource’s Test, applies Set where drift exists, and re-runs on its consistency timer. Inspect the LCM’s current settings any time:
Get-DscLocalConfigurationManager
# Key fields: RefreshMode (Push|Pull), ConfigurationMode,
# ConfigurationModeFrequencyMins, RefreshFrequencyMins, RebootNodeIfNeeded, LCMState
The resource categories you’ll use constantly, and the module each ships in — pin to the maintained module, not the in-box one:
| State to manage | Resource | Ships in | Key parameters |
|---|---|---|---|
| Windows role/feature | WindowsFeature |
PSDscResources |
Name, Ensure, IncludeAllSubFeature |
| Service state/startup | Service |
PSDscResources |
Name, State, StartupType, Credential |
| Registry value | Registry |
PSDscResources |
Key, ValueName, ValueData, ValueType, Ensure |
| File / directory | File |
PSDesiredStateConfiguration (in-box) |
DestinationPath, Contents, Ensure |
| Local group membership | Group |
PSDscResources |
GroupName, MembersToInclude |
| Environment variable | Environment |
PSDscResources |
Name, Value, Ensure |
| Scheduled task | ScheduledTask |
ComputerManagementDsc |
TaskName, ActionExecutable, ScheduleType |
| IIS website/app pool | xWebsite, xWebAppPool |
xWebAdministration |
Name, PhysicalPath, BindingInfo |
| Firewall rule | Firewall |
NetworkingDsc |
Name, DisplayName, Direction, Action |
The Ensure property is the DSC idiom you’ll type most: Present means “make it exist as specified,” Absent means “make sure it’s gone.” The three-method contract, and what each returns, is worth memorising because custom resources must honour it exactly:
| Method | Purpose | Returns | Runs when |
|---|---|---|---|
Get |
Report the current state | A hashtable of current values | On Get-DscConfiguration; for reporting |
Test |
Is current == desired? | $true (compliant) / $false (drift) |
Every consistency check, before Set |
Set |
Make current match desired | Nothing (side effects only) | Only when Test returned $false |
Mental model: the configuration is your source of truth, the MOF is the compiled binary, and the LCM is the runtime that enforces it. When you find yourself writing imperative “check if X then do Y” logic in a configuration, stop — that logic belongs inside a resource’s
Test/Set, not in the declarative document.
Authoring a configuration: roles, services, and registry
Here is a real baseline: install IIS, guarantee W3SVC is running and automatic, and pin a registry value that disables the legacy SMBv1 server. Parameterise the node list so the same configuration compiles for every host.
Configuration WebBaseline {
param([string[]]$NodeName = 'localhost')
Import-DscResource -ModuleName PSDscResources -ModuleVersion 2.12.0.0
Node $NodeName {
WindowsFeature IIS {
Name = 'Web-Server'
Ensure = 'Present'
}
Service W3SVC {
Name = 'W3SVC'
State = 'Running'
StartupType = 'Automatic'
DependsOn = '[WindowsFeature]IIS' # ordering: feature before service
}
Registry DisableSmb1Server {
Key = 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters'
ValueName = 'SMB1'
ValueData = '0'
ValueType = 'Dword'
Ensure = 'Present'
}
}
}
# Compile -> emits .\WebBaseline\<NodeName>.mof, one file per node
WebBaseline -NodeName 'WEB01','WEB02' -OutputPath .\WebBaseline
DependsOn is the only ordering primitive you get inside a node block: the service resource will not run until the feature is present. Everything without an explicit DependsOn may run in any order — DSC is declarative, not sequential. Apply the compiled MOF locally to validate before wiring any delivery model:
Start-DscConfiguration -Path .\WebBaseline -Wait -Verbose -Force
Test-DscConfiguration -Detailed # InDesiredState : True means no drift
Get-DscConfiguration # what the LCM believes is currently applied
The verbs you’ll use to author, apply, inspect, and unwind a configuration — memorise this table, it is the whole DSC operational surface:
| Cmdlet | What it does | Common flags |
|---|---|---|
Start-DscConfiguration |
Apply a compiled MOF to node(s) | -Wait -Verbose -Force, -UseExisting |
Test-DscConfiguration |
Report compliance vs the applied MOF | -Detailed (per-resource result) |
Get-DscConfiguration |
Return the current applied state | (none) |
Get-DscLocalConfigurationManager |
Show LCM settings/state | (none) |
Set-DscLocalConfigurationManager |
Apply an LCM meta-configuration | -Path -Verbose |
Update-DscConfiguration |
Pull nodes: fetch + apply now | -Wait |
Remove-DscConfigurationDocument |
Delete a pending/current/previous MOF | -Stage Current |
Publish-DscConfiguration |
Stage a MOF without applying it | -Path |
The Registry resource’s ValueType must match the actual registry type or you get a perpetual “not in desired state” — this table is the mapping you’ll reach for, and it’s the same set of types win_regedit’s type: parameter accepts:
DSC ValueType |
Registry type | win_regedit type: |
Typical use | Value format |
|---|---|---|---|---|
String |
REG_SZ |
string |
Plain text setting | 'Enabled' |
ExpandString |
REG_EXPAND_SZ |
expandstring |
Path with %VAR% |
'%SystemRoot%\logs' |
Dword |
REG_DWORD |
dword |
Flags, on/off, small ints | '0' / '1' |
Qword |
REG_QWORD |
qword |
64-bit numeric | '4294967296' |
Binary |
REG_BINARY |
binary |
Raw bytes | 'hex:00,ff' |
MultiString |
REG_MULTI_SZ |
multistring |
List of strings | @('a','b') |
A PSCredential embedded in a MOF is stored on disk in clear text unless you encrypt it. DSC supports certificate-encrypted credentials via a configuration-data block referencing a CertificateFile (public key at compile time) and a Thumbprint the LCM uses to decrypt at apply time. We cover the full pattern in Security notes — for now, the rule is absolute: never ship a plaintext credential in a MOF.
Push versus pull, and configuring the LCM
The LCM runs in one of two RefreshMode values, and that choice drives your whole operational model.
| Dimension | Push | Pull |
|---|---|---|
| Delivery | Start-DscConfiguration sends the MOF |
LCM fetches MOF + modules from a server on a timer |
| Direction | Controller → node (imperative) | Node → server (self-service) |
| Scale model | One-to-many from a controller | Fleet polls independently; no fan-out |
| Module delivery | You pre-stage modules on the node | LCM downloads modules from the pull endpoint |
| Drift correction | Only on next push, or the LCM’s own consistency check | LCM re-pulls + re-applies on its own timer |
| Failure mode | Controller down = no new config | Pull server down = nodes keep last-known-good |
| Reporting | You collect it (events/Test) |
Optional report server aggregates status |
You configure the LCM with a meta-configuration — a separate Configuration decorated with [DSCLocalConfigurationManager()] whose only child is a Settings block. This is where ApplyAndAutoCorrect — the setting that makes the agent remediate on every check rather than merely report — is turned on:
[DSCLocalConfigurationManager()]
Configuration LcmAutoCorrect {
Node localhost {
Settings {
RefreshMode = 'Push'
ConfigurationMode = 'ApplyAndAutoCorrect'
ConfigurationModeFrequencyMins = 30 # how often it re-checks + corrects
RefreshFrequencyMins = 30 # (pull) how often it re-fetches
RebootNodeIfNeeded = $false
ActionAfterReboot = 'ContinueConfiguration'
AllowModuleOverwrite = $true
}
}
}
LcmAutoCorrect -OutputPath .\LcmMeta
Set-DscLocalConfigurationManager -Path .\LcmMeta -Verbose
The three ConfigurationMode values are the single most important LCM decision:
ConfigurationMode |
Behaviour after first apply | Drift is… | Use when |
|---|---|---|---|
ApplyOnly |
Applies once, never touches the box again | Yours to find and fix | One-shot provisioning; another tool owns steady-state |
ApplyAndMonitor |
Applies, then logs drift to the event log; does not fix | Reported, human-approved remediation | Audit-only fleets; change must be approved |
ApplyAndAutoCorrect |
Applies, then re-applies every ConfigurationModeFrequencyMins |
Corrected automatically | Security-critical state that must never drift |
The full LCM setting surface — every knob, its default, and why you’d touch it:
| LCM setting | What it controls | Default | Notes / limit |
|---|---|---|---|
RefreshMode |
Push vs Pull vs Disabled | Push |
Disabled turns the LCM off entirely |
ConfigurationMode |
Auto-correct / monitor / apply-once | ApplyAndMonitor |
The self-heal switch |
ConfigurationModeFrequencyMins |
Consistency-check interval | 15 | Floor 15; lower values are clamped |
RefreshFrequencyMins |
Pull re-fetch interval | 30 | Must be a multiple of ConfigurationModeFrequencyMins |
RebootNodeIfNeeded |
Auto-reboot when a resource requests it | $false |
Set $true only where surprise reboots are safe |
ActionAfterReboot |
Resume or stop config after a reboot | ContinueConfiguration |
Keep default for multi-step configs |
AllowModuleOverwrite |
Overwrite modules on pull | $false |
$true when iterating on module versions |
RebootNodeIfNeeded + ActionAfterReboot |
Together handle multi-reboot configs | — | AD/role installs often need this |
StatusRetentionTimeInDays |
How long to keep status data | 10 | Reporting retention |
ConfigurationModeFrequencyMinshas a documented floor of 15 minutes — setting it lower is silently clamped. Thirty is a sane production default: frequent enough to close drift windows, infrequent enough to avoid churn on 800 nodes all hammering the same check.
For a pull server you’d set RefreshMode = 'Pull' and add a ConfigurationRepositoryWeb block with the endpoint URL and a registration key. Note the deprecation reality: Microsoft has deprecated the Windows-hosted DSC Pull Server feature. For greenfield pull at scale the supported direction is Azure Automation State Configuration or Azure Machine Configuration (covered below), or a community pull endpoint. The on-box push plus ApplyAndAutoCorrect model is fully supported and is what I lean on when pairing DSC with Ansible — it needs no server at all, just a scheduled push and an autonomous LCM.
DSC v3, Azure Machine Configuration, and the runtime split
This is the section that saves you a day of confusion. There are two DSCs, and mixing their docs is the most common self-inflicted wound in Windows automation right now.
Windows PowerShell 5.1 DSC is the WMI/CIM-based system everything above uses: Configuration functions, MOF compilation, the LCM, Start-DscConfiguration, the (deprecated) pull server. It is Windows-only, in-box on every supported Windows Server, and stable but no longer receiving feature investment.
DSC v3 is a ground-up rewrite: a standalone, cross-platform executable (dsc, written in Rust) with no LCM, no MOF, and no pull server. Resources are described by JSON/YAML manifests and can be authored in any language that can read stdin and write stdout JSON. You invoke it imperatively — dsc config get, dsc config test, dsc config set — against a YAML/JSON configuration document. It has no resident agent and no autonomous timer of its own; something else schedules it. That “something else,” in Azure, is Machine Configuration.
Azure Machine Configuration (formerly Azure Policy guest configuration) is the fleet-compliance service that audits and optionally enforces in-guest settings on Azure VMs and Arc-enabled servers (including on-prem and other clouds). It ships a guest configuration agent onto the machine; that agent runs DSC-based configurations on a schedule and reports compliance back to Azure Policy. It is the supported, cloud-native successor to DSC pull for reporting and enforcement at scale, and it uses the DSC engine under the hood.
The comparison you must internalise — which runtime owns what:
| Aspect | Windows PowerShell 5.1 DSC | DSC v3 | Azure Machine Configuration |
|---|---|---|---|
| Runtime | WMI/CIM + LCM | Standalone dsc executable |
Guest-config agent (uses DSC engine) |
| Artifact | MOF (compiled) | JSON/YAML config document | Signed .zip guest-config package |
| Resident agent | LCM (always present) | None | Guest configuration extension/agent |
| Autonomous drift correction | Yes (ApplyAndAutoCorrect) |
No (invoked externally) | Yes (audit + optional ApplyAndAutoCorrect) |
| Platform | Windows only | Windows, Linux, macOS | Azure + Arc (on-prem/other clouds) |
| Delivery at scale | Push / (deprecated) pull server | You orchestrate | Azure Policy assignment |
| Reporting | You collect | You collect | Azure Policy compliance dashboard |
| Resource authoring | PowerShell classes/MOF schema | Any language via manifest | PowerShell DSC resources (packaged) |
| Status | Stable, no new features | Actively developed | GA, recommended for Azure/Arc fleets |
The routing rule I use, stated plainly:
| If you need… | Use |
|---|---|
| On-box self-heal on Windows Server, no cloud dependency | 5.1 DSC with LCM ApplyAndAutoCorrect |
| Cross-platform declarative resources invoked by a pipeline | DSC v3 (dsc config set) |
| Fleet compliance + enforcement reported to Azure Policy | Machine Configuration (Azure/Arc) |
| To replace a deprecated on-prem DSC pull server | Machine Configuration or Azure Automation State Config |
| Ad-hoc orchestrated changes across mixed OS | Ansible (not DSC at all) |
A minimal taste of the DSC v3 imperative model (note: no MOF, no LCM — this is a one-shot invocation you’d wrap in a pipeline):
# baseline.dsc.config.yaml — a DSC v3 configuration document
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Ensure SMB1 server disabled
type: Microsoft.Windows/Registry
properties:
keyPath: HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters
valueName: SMB1
valueData:
DWord: 0
# Test compliance (no change), then enforce — DSC v3 CLI
dsc config test --file baseline.dsc.config.yaml
dsc config set --file baseline.dsc.config.yaml
And the Azure side — assign a built-in machine-configuration policy that audits (and optionally enforces) an in-guest baseline across every Arc-connected Windows Server:
# Assign a Machine Configuration (guest config) policy to a resource group
az policy assignment create \
--name "win-securebaseline" \
--display-name "Windows security baseline (machine config)" \
--policy "<builtin-guest-config-policy-definition-id>" \
--scope "/subscriptions/<sub-id>/resourceGroups/rg-win-fleet" \
--location centralindia \
--mi-system-assigned # managed identity for the guest-config agent
The load-bearing takeaway: if your requirement is autonomous drift correction on Windows Server with no cloud dependency, you want 5.1 DSC’s LCM — and that is exactly the piece we pair with Ansible below. If your requirement is cloud-reported compliance across a hybrid fleet, you want Machine Configuration. DSC v3 sits in the middle as the modern engine, most valuable when a pipeline (or Ansible) invokes it.
Managing Windows from Ansible: WinRM, PSRP, and SSH
Ansible reaches Windows over one of three transports. The default winrm connection plugin speaks WS-Management; the newer psrp plugin uses the PowerShell Remoting Protocol over WinRM and is faster for multi-task plays because it reuses a single runspace; and modern Windows can accept SSH via Win32-OpenSSH, which the ansible.windows collection supports and which sidesteps a lot of WinRM/Kerberos complexity.
First, stand up a listener. The canonical bootstrap is Microsoft’s ConfigureRemotingForAnsible.ps1, but for production you want an HTTPS listener on 5986 bound to a real certificate — not the script’s self-signed default:
# On the Windows host: HTTPS WinRM listener bound to a known cert, Basic/CredSSP off
$cert = Get-ChildItem Cert:\LocalMachine\My |
Where-Object Subject -eq 'CN=web01.corp.example.com'
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS `
-Address * -CertificateThumbPrint $cert.Thumbprint -Force
Set-Item WSMan:\localhost\Service\Auth\Basic $false
Set-Item WSMan:\localhost\Service\Auth\CredSSP $false
New-NetFirewallRule -DisplayName 'WinRM HTTPS' -Direction Inbound `
-LocalPort 5986 -Protocol TCP -Action Allow
Then the inventory. Prefer Kerberos in a domain (it enables constrained delegation and avoids putting credentials on the wire); for non-domain hosts, NTLM over HTTPS is acceptable. Avoid Basic and CredSSP unless you have a hard requirement and understand the credential-exposure trade-off.
[web]
web01.corp.example.com
web02.corp.example.com
[web:vars]
ansible_connection=psrp
ansible_port=5986
ansible_psrp_auth=kerberos
ansible_psrp_cert_validation=validate
Validate connectivity with win_ping before any real play — it confirms the listener, auth, and certificate chain in one shot:
ansible web -m ansible.windows.win_ping
The transport/auth decision matrix — pick the row that matches your environment, not the one that’s easiest to bootstrap:
| Transport | Auth options | Default port | Speed (multi-task) | Double-hop | When to use |
|---|---|---|---|---|---|
winrm |
Kerberos, NTLM, CredSSP, Basic, Certificate | 5985/5986 | Baseline (new shell/task) | Kerberos deleg / CredSSP | Broad compatibility, older hosts |
psrp |
Kerberos, NTLM, CredSSP, Basic, Certificate | 5985/5986 | Faster (reuses runspace) | Kerberos deleg / CredSSP | Many tasks per host; prefer this |
ssh |
Public key, password | 22 | Fast | Native (agent forwarding) | Modern Windows; avoids Kerberos setup |
The authentication options, ranked by what they expose:
| Auth method | Credential on the wire? | Needs HTTPS? | Double-hop | Notes |
|---|---|---|---|---|
| Kerberos | No (tickets) | No (but recommended) | Yes (constrained deleg) | Best in a domain; needs krb5 + realm config |
| NTLM | Hashed challenge/response | Strongly yes | No | Fine for non-domain over HTTPS |
| Certificate | No (client cert) | Yes | No | Great for automation identities; maps to a local user |
| CredSSP | Yes (delegated, encrypted) | Yes | Yes | Avoid unless you need the second hop |
| Basic | Yes (base64!) | Mandatory | No | Local accounts only; last resort |
Idempotent Ansible roles and the win_ → ansible.windows migration
The Windows modules are written to be idempotent: they inspect current state and report changed only when they actually mutate something. Two disciplines: use the declarative modules (win_feature, win_service, win_regedit) rather than dropping to win_shell, which Ansible cannot reason about for change detection; and always fully-qualify module names with their collection.
There’s a naming migration to get right. The bare win_* names were the legacy short form; modern content uses fully-qualified collection names (FQCNs) — ansible.windows.win_feature, community.windows.win_firewall_rule, chocolatey.chocolatey.win_chocolatey. The bare names still resolve for now, but pin the collections and use FQCNs so a role is unambiguous and version-locked.
Here is the IIS baseline from the DSC section, expressed as an Ansible role task file:
# roles/web_baseline/tasks/main.yml
- name: Ensure IIS web server role is present
ansible.windows.win_feature:
name: Web-Server
state: present
include_management_tools: true
register: iis_feature
- name: Ensure W3SVC is running and automatic
ansible.windows.win_service:
name: W3SVC
state: started
start_mode: auto
- name: Disable SMBv1 server via registry
ansible.windows.win_regedit:
path: HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters
name: SMB1
data: 0
type: dword
- name: Reboot only if the feature install demands it
ansible.windows.win_reboot:
when: iis_feature.reboot_required
Two idempotency disciplines worth internalising. First, guard reboots on the module’s own signal — win_feature returns reboot_required, so reboot conditionally, never unconditionally. Second, when forced into win_command/win_shell, add creates/removes guards or a changed_when expression so a re-run doesn’t falsely report changed:
- name: Run a one-shot installer only once
ansible.windows.win_command: C:\setup\app-install.exe /quiet
args:
creates: C:\Program Files\App\app.exe # skip if already installed
Run with --check to verify a role is a no-op against already-converged hosts. A correctly authored role reports ok=... with changed=0 on the second pass — that is your idempotency proof.
The collection landscape you’ll actually pull from, and what lives where:
| Collection | Contains | Example modules | Install |
|---|---|---|---|
ansible.windows |
Core, supported Windows modules | win_feature, win_service, win_regedit, win_dsc, win_reboot, win_updates |
ansible-galaxy collection install ansible.windows |
community.windows |
Broader community Windows modules | win_firewall_rule, win_domain_membership, win_scheduled_task, win_iis_website |
ansible-galaxy collection install community.windows |
microsoft.ad |
Active Directory management | microsoft.ad.user, microsoft.ad.group, microsoft.ad.domain |
ansible-galaxy collection install microsoft.ad |
chocolatey.chocolatey |
Chocolatey package management | win_chocolatey, win_chocolatey_source |
ansible-galaxy collection install chocolatey.chocolatey |
The ansible.windows core modules you’ll type most, mapped to the DSC resource they replace — this is the Rosetta stone between the two worlds:
ansible.windows module |
Manages | DSC resource equivalent | Key idempotent parameters |
|---|---|---|---|
win_feature |
Roles/features | WindowsFeature |
name, state, include_management_tools |
win_service |
Service state/startup | Service |
name, state, start_mode |
win_regedit |
Registry values | Registry |
path, name, data, type |
win_user |
Local users | User |
name, password, groups, state |
win_group |
Local groups | Group |
name, state |
win_group_membership |
Group members | Group (Members) |
name, members, state |
win_environment |
Env variables | Environment |
name, value, level, state |
win_file |
Files/dirs | File |
path, state |
win_copy / win_get_url |
File content | File (Contents/source) |
src/url, dest (checksum-guarded) |
win_reboot |
Reboot + wait | LCM RebootNodeIfNeeded |
(guard on reboot_required) |
win_updates |
Windows Update | (WSUS/xWindowsUpdate) |
category_names, state |
win_dsc |
Any DSC resource | (bridge to DSC) | resource_name + resource props |
The idempotency rules, module by module — the ones that trip people up:
| Module | Idempotent by default? | The gotcha | The fix |
|---|---|---|---|
win_feature |
Yes | Reboot may be required post-install | Guard win_reboot on .reboot_required |
win_service |
Yes | start_mode: delayed vs auto are distinct states |
Match the exact start_mode you want |
win_regedit |
Yes | Wrong type (e.g. string vs dword) reports perpetual change |
Set type to the actual registry type |
win_command |
No (runs every time) | Reports changed on every run |
Add creates/removes or changed_when |
win_shell |
No | Same as above, plus shell parsing | Same; prefer a declarative module |
win_copy |
Yes (checksum) | Large files re-checksum each run (slow) | Use win_get_url for remote sources |
win_updates |
Yes-ish | Changes each run as new patches appear | Expected; treat changed as informational |
win_dsc |
Depends on resource | Resource must be WMF 5.1-compatible & present | Pre-install the module; test with check mode |
Combining DSC resources inside Ansible with win_dsc
This is the payoff of knowing both tools. Ansible’s ansible.windows.win_dsc module invokes any installed DSC resource directly — no MOF compilation, no LCM scheduling. You get DSC’s mature resource implementations (especially for niche state like xWebsite bindings, xWebAppPool recycling, or intricate security settings) driven by Ansible’s orchestration, inventory, and check mode. The module maps DSC resource properties one-to-one to task parameters.
- name: Ensure a website via the DSC Service resource through Ansible
ansible.windows.win_dsc:
resource_name: Service
Name: W3SVC
State: Running
StartupType: Automatic
- name: Configure a registry value via the DSC Registry resource
ansible.windows.win_dsc:
resource_name: Registry
Key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters
ValueName: SMB1
ValueData: '0'
ValueType: Dword
Ensure: Present
- name: Create an IIS app pool via a richer xWebAdministration DSC resource
ansible.windows.win_dsc:
resource_name: xWebAppPool
Name: shop-pool
State: Started
managedRuntimeVersion: '' # No Managed Code
idleTimeout: '00:00:00' # disable idle recycle
win_dsc honours check mode by calling the resource’s Test method and reporting what would change, and it surfaces the resource version so you can pin it. The decision rule I use: reach for win_dsc when a high-quality DSC resource already encapsulates complex logic you’d otherwise hand-roll in win_shell; use native ansible.windows modules for the common cases because they’re faster and need no DSC module installed on the target.
One caveat:
win_dscrequires the resource module present on the target, and the resource must be compatible with the WMF 5.1 DSC engine. Resources that depend on the cross-platform DSC v3 runtime will not load here —win_dscspeaks to the classic engine only.
When to use win_dsc versus a native module versus a full LCM deployment:
| Approach | Agent on box? | Autonomous drift correction | Orchestration | Best for |
|---|---|---|---|---|
Native ansible.windows module |
No | No | Full (Ansible) | Common state, mixed OS, speed |
win_dsc (Ansible → DSC resource) |
No (resource module only) | No (runs when Ansible runs) | Full (Ansible) | Niche DSC resource, no LCM wanted |
DSC LCM ApplyAndAutoCorrect |
Yes (LCM) | Yes | None (per-node) | Must-never-drift security state |
| Ansible ships MOF + sets LCM | Yes (LCM) | Yes | Full (Ansible triggers) | Best of both: orchestrated + self-healing |
Detecting and auto-remediating configuration drift
Drift is the whole point, and you want remediation at two layers.
Layer one — the LCM’s own loop. With ApplyAndAutoCorrect, every Windows host with an applied MOF self-corrects on its consistency-check timer with no controller involved. This is the safety net that keeps a box compliant even if your Ansible control node is offline for a week. Audit what the LCM actually did from the operational event log:
# Did anything drift and get corrected on this node in the last 50 events?
Get-WinEvent -LogName 'Microsoft-Windows-DSC/Operational' -MaxEvents 50 |
Where-Object Message -match 'not in the desired state|in desired state' |
Select-Object TimeCreated, Id, Message | Format-Table -Wrap
Layer two — Ansible-driven detection and remediation. Run the converging play on a schedule (cron on the control node, or AWX/AAP). Because every task is idempotent, a scheduled run is drift remediation: anything that drifted gets pulled back, and the run’s change count tells you how much drifted.
# Detect only — check mode plus diff, never mutates
ansible-playbook site.yml --check --diff
# Remediate — drop --check; idempotent tasks correct drift in place
ansible-playbook site.yml --diff
Wire the change count into alerting. A clean nightly run should be changed=0; a non-zero count means something mutated state out-of-band between runs and is worth a ticket. Parse the play recap or emit machine-readable JSON via a callback plugin into your observability pipeline.
How each tool detects and remediates drift, side by side:
| Capability | DSC (LCM) | Ansible |
|---|---|---|
| Detection mechanism | Test-DscConfiguration on the consistency timer |
--check --diff on a schedule |
| Autonomous (no controller) | Yes | No (needs cron/AWX to run) |
| Remediation | Automatic (ApplyAndAutoCorrect) |
On a non---check run |
| Detection cadence | ConfigurationModeFrequencyMins (≥15) |
Whatever your scheduler sets |
| Drift record | Microsoft-Windows-DSC/Operational event log |
Play recap / callback JSON |
| “How much drifted?” signal | Event count of “not in desired state” | Play recap changed=N |
| Works when network is down | Yes | No |
| Central reporting | Machine Config / report server | AAP dashboard / custom callback |
The decision table for which drift-correction layer to lean on:
| If the setting is… | Correct it with |
|---|---|
| Security-critical and must survive a partition | DSC LCM ApplyAndAutoCorrect |
| Application config that changes on each release | Ansible scheduled converge |
| Something a human must approve before fixing | DSC ApplyAndMonitor (alert, don’t auto-fix) |
| Cross-host and order-sensitive | Ansible (handlers, serial) |
| Needing cloud compliance evidence | Machine Configuration audit |
Testing with Pester and ansible-lint in CI
Never ship a DSC configuration or an Ansible role that hasn’t been compiled and structurally validated in CI. On the DSC side, Pester (the PowerShell test framework) asserts that a configuration compiles to a MOF and that the MOF contains the resources you expect. Pester v5 syntax:
# WebBaseline.Tests.ps1
Describe 'WebBaseline configuration' {
BeforeAll {
. $PSScriptRoot\WebBaseline.ps1
$out = Join-Path $TestDrive 'mof'
WebBaseline -NodeName 'TEST01' -OutputPath $out
}
It 'compiles a MOF for the node' {
Join-Path $TestDrive 'mof\TEST01.mof' | Should -Exist
}
It 'declares the IIS WindowsFeature resource' {
Get-Content (Join-Path $TestDrive 'mof\TEST01.mof') -Raw |
Should -Match 'MSFT_RoleResource'
}
}
$result = Invoke-Pester -Path .\tests -PassThru
if ($result.FailedCount -gt 0) { throw "Pester failed: $($result.FailedCount)" }
On the Ansible side, lint and syntax-check on every push, and lean on --check against a throwaway test host in a later stage. For a real integration test, Molecule spins up an ephemeral target and runs the role plus an idempotence assertion (covered in depth in idempotent Ansible collections and Molecule testing):
ansible-lint roles/ playbooks/
ansible-playbook site.yml --syntax-check
molecule test # converge, idempotence check, verify, destroy
A minimal GitHub Actions pipeline ties both together — the PowerShell job on a Windows runner (DSC compilation needs WMF 5.1), the lint job on Linux:
name: config-mgmt-ci
on: [push, pull_request]
jobs:
dsc-pester:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install DSC resources
shell: powershell
run: Install-Module PSDscResources -Force -Scope CurrentUser
- name: Run Pester
shell: powershell
run: |
$r = Invoke-Pester -Path .\tests -PassThru
if ($r.FailedCount -gt 0) { exit 1 }
ansible-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ansible-lint
- run: ansible-lint roles/ playbooks/
The CI gates each tool needs, and what each catches before a node ever sees the change:
| Gate | Tool | Catches | Runner |
|---|---|---|---|
| MOF compilation | Pester + Configuration |
Syntax errors, missing resources, bad params | Windows |
| MOF content assertion | Pester + Should -Match |
Wrong/removed resource, wrong value | Windows |
| YAML/play syntax | ansible-playbook --syntax-check |
Malformed YAML, unknown modules | Linux |
| Best-practice lint | ansible-lint |
Non-idempotent patterns, bad naming, deprecations | Linux |
| Idempotence | Molecule / --check twice |
Tasks that report changed on re-run |
Linux + ephemeral target |
| Static analysis (PS) | PSScriptAnalyzer |
Style/bug rules in resources/configs | Windows |
Side-by-side worked baseline: the same state, three ways
To make the DSC-vs-Ansible-vs-both choice concrete, here is one baseline — IIS present, W3SVC running/automatic, SMBv1 server disabled — expressed in all three models. The prose above shows each in isolation; this is the direct comparison so you can see, line for line, what each style buys and costs.
The three implementations at a glance:
| Facet | DSC configuration (LCM push) | Ansible role (ansible.windows) |
Ansible → win_dsc |
|---|---|---|---|
| Where logic runs | Compiled to MOF, LCM applies on box | Control node opens session, runs remotely | Control node invokes DSC resource on box |
| Idempotency source | Resource Test/Set |
Module internal check | DSC resource Test/Set |
| Self-heals when control node down | Yes (ApplyAndAutoCorrect) |
No | No |
| Needs software on target | LCM (always present) + resource modules | Nothing (WinRM/SSH) | DSC resource module on target |
| Ordering primitive | DependsOn |
Task order + handlers | Task order |
| Reboot handling | LCM (RebootNodeIfNeeded) |
win_reboot guarded on reboot_required |
win_reboot |
| Secrets | Certificate-encrypted MOF | ansible-vault / external |
ansible-vault |
| Best when | Must never drift, no controller | Orchestrated, mixed OS, ad-hoc | Niche DSC resource, no LCM |
Model A — DSC configuration (source of truth compiled to MOF, enforced autonomously). This is the WebBaseline from earlier, paired with an ApplyAndAutoCorrect LCM so it self-heals every 30 minutes with no controller.
Model B — Ansible role (orchestrated, agentless). This is roles/web_baseline/tasks/main.yml from earlier, run on a schedule so each run remediates drift; it needs a live control node but coordinates across the whole fleet and mixed OS in one play.
Model C — Ansible driving DSC via win_dsc, so you get DSC’s resource quality under Ansible’s orchestration without standing up the LCM or a MOF pipeline:
# roles/web_baseline_dsc/tasks/main.yml — same state, DSC resources via Ansible
- name: IIS via DSC WindowsFeature resource
ansible.windows.win_dsc:
resource_name: WindowsFeature
Name: Web-Server
Ensure: Present
- name: W3SVC via DSC Service resource
ansible.windows.win_dsc:
resource_name: Service
Name: W3SVC
State: Running
StartupType: Automatic
- name: SMBv1 off via DSC Registry resource
ansible.windows.win_dsc:
resource_name: Registry
Key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters
ValueName: SMB1
ValueData: '0'
ValueType: Dword
Ensure: Present
The mature combined pattern is not “pick one” — it’s Ansible orchestrates, DSC’s LCM backstops the non-negotiables. Ansible owns application deployment and the broad config surface (Model B); a small, high-value MOF (SMBv1 off, TLS registry settings, a handful of services) runs under the LCM in ApplyAndAutoCorrect (Model A) so the security-critical subset self-heals regardless of control-node reachability. win_dsc (Model C) is the tactical bridge for the odd niche resource in between.
Architecture at a glance
Picture the two control planes stacked over the same fleet of Windows Servers, because that stacking is the architecture. Trace it from the top down.
At the top sits your source of truth in Git: DSC Configuration functions and LCM meta-configurations on one side, Ansible roles/playbooks/inventory on the other. Nothing reaches a node without passing the CI gate — Pester compiles every configuration to a MOF and asserts its contents on a Windows runner, while ansible-lint and --syntax-check (and, for real coverage, Molecule) validate every role on a Linux runner. A red build never ships.
Below CI, two delivery paths fan out to the fleet. The Ansible control node (or an AWX/AAP controller) reaches each Windows host agentlessly over a WinRM HTTPS listener on 5986 (Kerberos in the domain, NTLM-over-HTTPS outside it) or over SSH on 22; it opens a session, runs each task’s check-then-change logic in place, reports changed/ok, and disconnects. It converges the fleet on a schedule, coordinates cross-host sequencing, and can invoke DSC resources inline via win_dsc. In parallel, the DSC path compiles configurations to a MOF per node; a push (Start-DscConfiguration, itself often triggered by Ansible) hands that MOF to the LCM resident on every box, which stores it and — this is the load-bearing part — re-runs each resource’s Test, applies Set on drift, and repeats every ConfigurationModeFrequencyMins (≥15) with no controller involved.
On each Windows Server, then, two enforcement mechanisms coexist: the LCM autonomously holding the security-critical MOF in desired state on its own timer, and Ansible episodically converging the broader surface when it runs. For hybrid fleets, a third arrow points to the cloud: the Azure Machine Configuration guest-config agent (over Azure Arc) audits and optionally enforces the same kind of in-guest baseline and reports compliance to Azure Policy, giving you a cloud-side dashboard on top of the on-box loops. The reader following the flow sees the whole method: define state as code, gate it in CI, deliver it via Ansible (breadth, orchestration) and/or the LCM (autonomy, self-heal), and observe drift through DSC’s operational event log, Ansible’s change count, and Azure Policy compliance — three vantage points onto the single question “does production match its definition right now?”
The mnemonic for the whole picture: Ansible pushes when it runs; the LCM holds when nobody’s watching; Machine Config reports it to the cloud. Route each setting to the plane whose guarantee matches the setting’s blast radius.
Real-world scenario
Meridian Retail ran roughly 420 Windows IIS/middleware hosts across three regions — Central India (primary), plus DR in West Europe and Southeast Asia — configured by a single Ansible control node in the primary datacenter, on AAP. Traffic was seasonal; the platform team was five engineers; the config-management surface was “everything above the OS image”: IIS roles, app pools, a dozen services, TLS registry settings, and a CIS-derived security baseline. Nightly converges ran clean, and everyone trusted the setup — until a regional network event turned a latent design flaw into a compliance incident.
The trigger was an eleven-hour partition between the primary datacenter and the Southeast Asia region during a submarine-cable fault. With the control node unreachable, AAP couldn’t converge the SEA hosts. That alone would have been fine — the boxes kept serving. But during the same window, an application team hit a production incident on those hosts and, unable to wait for change control, RDP’d in and applied manual registry “hotfixes” directly, one of which re-enabled SMBv1 as a shot-in-the-dark fix for a file-share timeout. Nothing pulled that change back, because Ansible only converges when it runs — and it could not run. When connectivity returned, the nightly converge did correct most of it, but a subset of hosts had been rebooted mid-window and the converge skipped them on a transient WinRM error that nobody triaged. Two days later a quarterly audit scan flagged SMBv1 enabled on nine production hosts — a finding with regulatory teeth.
The post-incident analysis named the real flaw precisely: an agentless tool cannot remediate what it cannot reach. Ansible was the only enforcement plane, and its guarantee — “correct on the next run” — is worthless during a partition, which is exactly when humans are most likely to hand-edit production. The fix was not to abandon Ansible; it was to stop asking Ansible to do a job it structurally can’t, and let DSC’s LCM be the always-on backstop for the security-critical subset.
They carved out a small, high-value MOF — SMBv1 server disabled, the TLS 1.0/1.1 disable registry keys, three services pinned running, and the audit-policy registry values — and set every host’s LCM to ApplyAndAutoCorrect at 30 minutes. Ansible still owned application deployment and the broad configuration surface; the non-negotiable security state now self-healed independent of the control node.
[DSCLocalConfigurationManager()]
Configuration SecurityBackstop {
Node localhost {
Settings {
RefreshMode = 'Push'
ConfigurationMode = 'ApplyAndAutoCorrect'
ConfigurationModeFrequencyMins = 30
}
}
}
SecurityBackstop -OutputPath .\Backstop
Set-DscLocalConfigurationManager -Path .\Backstop -Verbose
They also fixed the operational miss: the AAP job now fails loudly on any host it can’t reach (rather than logging a transient error and moving on), and a nightly report compares each host’s Microsoft-Windows-DSC/Operational log for “not in desired state” events against a baseline, so LCM self-corrections are visible — a correction is a signal that someone touched production out of band.
The next partition, four months later, was a non-event: the SEA hosts kept re-applying the security MOF every half hour regardless of control-node reachability; when an engineer again disabled a service by hand during an incident, the LCM put it back within 30 minutes and the drift showed up in the nightly report as a ticket. The audit scan stayed green. The lesson the team wrote into their runbook, verbatim: “Ansible for orchestration and breadth; the LCM for autonomous drift correction on the settings that must never drift. Anything with a compliance consequence needs an on-box agent behind it — the network is not a dependency you get to assume.”
Costs barely moved: the LCM is in-box (no license, no new infrastructure), the MOF is tiny, and the 30-minute consistency check is negligible CPU. The only real cost was authoring and testing one small DSC configuration and its Pester tests — a day of work against a regulatory finding they never wanted to repeat.
Advantages and disadvantages
The two tools have genuinely different shapes, and the honest trade-off is not “which is better” but “which guarantee do you need for this setting.”
| Dimension | PowerShell DSC (LCM) — advantage / disadvantage | Ansible — advantage / disadvantage |
|---|---|---|
| Autonomy | + Self-heals with no controller (ApplyAndAutoCorrect) |
− Only converges when it runs |
| Orchestration | − Per-node MOF; weak cross-host sequencing | + Plays, handlers, serial, rolling updates |
| Agent footprint | − LCM always present (but in-box) | + Agentless; nothing to install/patch on target |
| OS reach | − 5.1 DSC is Windows-only | + One inventory for Linux + Windows |
| Ad-hoc “run now” | − No imperative one-off | + ansible -m runs anything anywhere now |
| Learning curve | − MOF/LCM/pull mental model is steep | + YAML + inventory is approachable |
| Ecosystem direction | − 5.1 frozen; pull server deprecated | + Actively developed collections |
| Reporting at scale | + Machine Config → Azure Policy dashboards | + AAP dashboards + callbacks |
| Secrets | − MOF clear-text unless cert-encrypted | + ansible-vault, external brokers, no artifact on target |
| Failure during partition | + Keeps enforcing last-known-good | − Enforcement gap for the duration |
DSC’s LCM is right when a setting must never drift even when the network is down — security baselines, compliance-relevant registry state, must-run services. Its autonomy is the whole point, and the deprecated pull server doesn’t matter if you push once and let ApplyAndAutoCorrect do the rest. It’s the wrong tool for cross-host orchestration, mixed-OS estates, and anything you need to run once, now, everywhere.
Ansible is right for orchestration, breadth, and mixed estates — deploy an app across a fleet in a controlled order, manage Linux and Windows from one inventory, run ad-hoc fixes, and coordinate multi-host changes with handlers and serial. Its weakness is the flip side of agentlessness: it can only enforce when it runs, so it cannot be your sole guarantee for state with a compliance consequence during an outage.
The mature answer, as the scenario shows, is both — Ansible as the orchestration plane, the LCM as the autonomous backstop for the non-negotiable subset, and win_dsc as the tactical bridge for niche resources.
Hands-on lab
Build the same baseline both ways on one Windows Server, prove idempotency and drift correction in each, then tear down. You need a Windows Server 2022 VM (Azure Standard_B2s in Central India is fine and cheap) with WMF 5.1 (in-box), and a Linux/WSL box with ansible-core for the Ansible half. All commands are copy-pasteable.
Step 1 — Prepare the Windows target (run in an elevated PowerShell on the VM).
Install-Module PSDscResources -Force -Scope AllUsers
Get-DscResource -Module PSDscResources | Select-Object Name # confirm resources present
Expected: a list including WindowsFeature, Service, Registry, Group, Environment.
Step 2 — Author and compile the DSC configuration. Save as WebBaseline.ps1, then compile:
Configuration WebBaseline {
param([string[]]$NodeName = 'localhost')
Import-DscResource -ModuleName PSDscResources -ModuleVersion 2.12.0.0
Node $NodeName {
WindowsFeature IIS { Name = 'Web-Server'; Ensure = 'Present' }
Service W3SVC {
Name = 'W3SVC'; State = 'Running'; StartupType = 'Automatic'
DependsOn = '[WindowsFeature]IIS'
}
Registry DisableSmb1Server {
Key = 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters'
ValueName = 'SMB1'; ValueData = '0'; ValueType = 'Dword'; Ensure = 'Present'
}
}
}
WebBaseline -OutputPath .\WebBaseline
Expected: WebBaseline\localhost.mof is created. Get-Content .\WebBaseline\localhost.mof shows MSFT_RoleResource, MSFT_ServiceResource, MSFT_RegistryResource.
Step 3 — Put the LCM in auto-correct mode, then apply.
[DSCLocalConfigurationManager()]
Configuration Lcm { Node localhost { Settings {
RefreshMode = 'Push'; ConfigurationMode = 'ApplyAndAutoCorrect'
ConfigurationModeFrequencyMins = 15; RebootNodeIfNeeded = $false } } }
Lcm -OutputPath .\Lcm
Set-DscLocalConfigurationManager -Path .\Lcm -Verbose
Start-DscConfiguration -Path .\WebBaseline -Wait -Verbose -Force
Step 4 — Validate and prove idempotency.
Test-DscConfiguration -Detailed # InDesiredState : True
Start-DscConfiguration -UseExisting -Wait -Verbose # re-apply: no resource reports a change
Expected: Test-DscConfiguration returns InDesiredState : True; the re-apply logs no Set.
Step 5 — Force drift and prove the LCM corrects it.
Stop-Service W3SVC
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters' -Name SMB1 -Value 1
Start-DscConfiguration -UseExisting -Wait -Verbose # simulate the consistency check now
(Get-Service W3SVC).Status # Running
(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters').SMB1 # 0
Expected: the service is back to Running and SMB1 back to 0. In production the LCM does this automatically every 15 minutes; -UseExisting just triggers it on demand.
Step 6 — Now the Ansible half (run from the Linux/WSL control node). Install collections and set inventory (assumes the WinRM HTTPS listener from the transport section is up):
ansible-galaxy collection install ansible.windows community.windows
cat > inventory.ini <<'EOF'
[web]
winlab.corp.example.com
[web:vars]
ansible_connection=psrp
ansible_port=5986
ansible_psrp_auth=ntlm
ansible_user=labadmin
ansible_password=<from-vault-in-real-life>
EOF
ansible -i inventory.ini web -m ansible.windows.win_ping
Expected: "ping": "pong".
Step 7 — Run the equivalent Ansible role and prove idempotency. Create site.yml:
- hosts: web
tasks:
- ansible.windows.win_feature: { name: Web-Server, state: present }
- ansible.windows.win_service: { name: W3SVC, state: started, start_mode: auto }
- ansible.windows.win_regedit:
path: HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters
name: SMB1
data: 0
type: dword
ansible-playbook -i inventory.ini site.yml # first run: changed>0
ansible-playbook -i inventory.ini site.yml --check --diff # second run: changed=0
Expected: the first run reports changed; the --check run reports changed=0 — your idempotency proof.
Step 8 — Teardown.
# On the Windows target: revert the LCM to defaults and remove applied config
Remove-DscConfigurationDocument -Stage Current, Pending, Previous
[DSCLocalConfigurationManager()]
Configuration LcmReset { Node localhost { Settings { ConfigurationMode = 'ApplyAndMonitor' } } }
LcmReset -OutputPath .\LcmReset; Set-DscLocalConfigurationManager -Path .\LcmReset
Uninstall-WindowsFeature Web-Server # optional: remove IIS
If you spun up an Azure VM for this, delete its resource group (az group delete -n rg-dsc-lab --yes) to stop all charges.
Common mistakes & troubleshooting
The failure modes that eat the most time, with the exact way to confirm and the real fix:
| # | Symptom | Root cause | Confirm (exact command / path) | Fix |
|---|---|---|---|---|
| 1 | LCM never corrects forced drift | ConfigurationMode is ApplyAndMonitor or ApplyOnly |
(Get-DscLocalConfigurationManager).ConfigurationMode |
Reapply meta-config with ApplyAndAutoCorrect |
| 2 | ConfigurationModeFrequencyMins = 5 ignored |
Below the 15-min floor; silently clamped | (Get-DscLocalConfigurationManager).ConfigurationModeFrequencyMins shows 15 |
Set ≥15 and stop expecting sub-15 |
| 3 | MOF compile fails: “resource not found” | Module not installed or version mismatch in Import-DscResource |
Get-DscResource -Module PSDscResources |
Install-Module the exact -ModuleVersion you import |
| 4 | Credential in MOF is plaintext | No certificate encryption configured | Open the .mof; the password is readable |
Add CertificateFile/Thumbprint config-data block |
| 5 | Ansible: win_command reports changed every run |
No creates/changed_when guard |
Re-run with --check — still changed |
Add creates:/removes: or changed_when: |
| 6 | Ansible over WinRM fails: connection refused | No HTTPS listener / firewall on 5986 | Test-NetConnection winhost -Port 5986 |
Create the HTTPS listener + firewall rule |
| 7 | Kerberos auth fails: “server not found in database” | Wrong SPN / realm not in krb5.conf |
kinit user@REALM on the control node |
Fix realm/KDC config; register HTTP/ SPN |
| 8 | win_dsc fails: resource not found on target |
Resource module not present on the node | On the node: Get-DscResource -Name <Resource> |
Install-Module the resource on the target |
| 9 | win_regedit perpetual changed |
type mismatch (string vs dword) |
Compare type: to the actual registry type |
Set type to the true registry value type |
| 10 | Config applies but reboots the box unexpectedly | Resource requested reboot + RebootNodeIfNeeded=$true |
(Get-DscLocalConfigurationManager).RebootNodeIfNeeded |
Set $false; handle reboots deliberately |
| 11 | DSC v3 examples don’t work with Start-DscConfiguration |
Mixing DSC v3 docs into a 5.1 LCM deployment | dsc --version vs $PSVersionTable.PSVersion |
Use the runtime that matches your artifact |
| 12 | Ansible converge “succeeds” but skipped a host | Transient WinRM error swallowed; host unreached | Grep the play recap for unreachable=1 |
Fail the job on unreachable>0; retry+alert |
| 13 | Machine Config policy shows “not applicable” | Guest-config agent/managed identity missing | Azure Policy compliance → resource details | Enable system-assigned MI; install guest-config extension |
| 14 | Two tools fight over the same setting | DSC and Ansible both manage the identical key | Both a changed (Ansible) and a “not in desired state” (DSC) every cycle |
Assign each setting to exactly one owner |
Two anti-patterns worth calling out beyond the table. Split-brain enforcement — DSC’s LCM and an Ansible converge both managing the same registry value — produces a perpetual flap (each tool “corrects” the other’s idempotent no-op timing) and pointless churn; assign every setting to a single owner. And imperative logic in a declarative document: if a DSC configuration contains if/foreach deciding whether to do something at apply time, that logic belongs inside a resource’s Test/Set, not in the MOF-producing script, or you lose idempotency and testability.
Best practices
- Route each setting by blast radius. Must-never-drift + compliance consequence → DSC LCM
ApplyAndAutoCorrect. Orchestrated / cross-host / mixed-OS → Ansible. Niche resource, no LCM wanted →win_dsc. - Never plaintext-credential a MOF. Certificate-encrypt every
PSCredentialvia configuration data (CertificateFile+Thumbprint). On the Ansible side,ansible-vaultor an external broker — never secrets in plain inventory. - Pin versions everywhere.
Import-DscResource -ModuleVersionin configs;collections:/requirements.ymlwith explicit versions for Ansible. Unpinned modules make “works on my machine” a production incident. - Make idempotency a CI gate, not a hope. Pester compiles every configuration;
ansible-lint+ a second--check(or Molecule idempotence) proveschanged=0on re-run. - One owner per setting. Prevent split-brain: a given key/service/feature is managed by DSC or Ansible, never both.
- Guard reboots explicitly. DSC: set
RebootNodeIfNeededdeliberately and useActionAfterReboot=ContinueConfigurationfor multi-step configs. Ansible:win_rebootguarded on the module’sreboot_required, never unconditional. - HTTPS WinRM only, best auth available. Listener on 5986 with a real cert; Kerberos in a domain, NTLM-over-HTTPS outside; Basic and CredSSP off unless a hard requirement demands them.
- Prefer declarative modules over shell. Reach for
win_shell/win_commandlast, always withcreates/changed_when. Everywin_shellis a place idempotency can silently break. - Keep the enforced MOF small and high-value. Put only the non-negotiable subset under the LCM; let Ansible carry the broad, frequently-changing surface. A giant MOF is hard to reason about and reboot-prone.
- Make LCM self-corrections visible. A correction means someone touched production out of band — parse
Microsoft-Windows-DSC/Operationalfor “not in desired state” and raise a ticket, don’t just let the box quietly heal. - Fail loud on unreached hosts. An Ansible converge that skips a host on a transient error is a silent compliance gap — fail the job on
unreachable>0, retry, and alert. - Don’t mix the two DSC runtimes. Keep 5.1 (MOF/LCM) and DSC v3 (
dscCLI) docs and artifacts separate; know which enginewin_dscand Machine Configuration each speak to.
Security notes
Configuration management touches every host with elevated rights, so it is a security surface — treat it accordingly and see gMSA, Windows LAPS and service-account credential hardening for the identity mechanics.
MOF credential encryption is mandatory, not optional. A MOF sits on the node’s disk in clear text. To ship a PSCredential, compile against the public half of a certificate and let the LCM decrypt with the private key at apply time:
$cfgData = @{ AllNodes = @(@{
NodeName = 'WEB01'
CertificateFile = 'C:\certs\dsc-pub.cer' # public key, present at compile time
Thumbprint = 'A1B2C3...' # private key installed on the node
}) }
# The configuration references $Node.Thumbprint; the LCM decrypts at apply time.
Least privilege for the automation identity. Ansible should authenticate as a scoped account, ideally a gMSA (no stored password) or certificate identity mapped to a local user, granted only the rights the plays need — not Domain Admin. For DSC pull/Machine Config, the guest-configuration agent runs under a system-assigned managed identity scoped to exactly its policy assignment.
Transport security. WinRM must be HTTPS (5986) with a valid certificate; disable Basic and CredSSP (CredSSP delegates full credentials to the target — a lateral-movement gift if the target is compromised). Prefer Kerberos, which never puts a reusable secret on the wire.
Secrets never in the artifact or the repo. ansible-vault encrypts variable files at rest; better still, fetch secrets at runtime from an external broker (see HashiCorp Vault as a central secrets broker) so nothing sensitive lives in Git or on the target longer than a play.
Enforce security state with the LCM specifically because it survives a partition. As the scenario showed, the settings with a compliance consequence — SMBv1 off, TLS versions, audit policy — belong under ApplyAndAutoCorrect so a hand-edit during an outage is reverted within the consistency window, not “at the next converge, whenever the network comes back.”
The security-relevant controls in each tool, and the safe setting:
| Control | DSC | Ansible | Safe setting |
|---|---|---|---|
| Credential at rest | Certificate-encrypted MOF | ansible-vault / external broker |
Never plaintext |
| Transport | (LCM local; pull over HTTPS) | WinRM HTTPS 5986 / SSH | HTTPS + valid cert |
| Auth on the wire | n/a (local) / cert (pull) | Kerberos > NTLM > cert | Avoid Basic/CredSSP |
| Automation identity | Guest-config managed identity | gMSA / cert-mapped local user | Least privilege, not DA |
| Reboot safety | RebootNodeIfNeeded deliberate |
win_reboot guarded |
No surprise reboots |
| Drift on security keys | ApplyAndAutoCorrect |
Scheduled converge (weaker) | LCM for must-never-drift |
Cost & sizing
The good news for the wallet: both tools are open-source engines with no per-node license, and the biggest cost is your time authoring and testing, not runtime infrastructure. The nuances are in the control-plane and cloud pieces.
DSC costs effectively nothing to run: the LCM is in-box on every Windows Server, a MOF is a small text file, and a 30-minute consistency check is negligible CPU (a Test sweep of a handful of resources is milliseconds). There is no server to run if you use push + ApplyAndAutoCorrect. If you need pull/reporting, the cost shifts to Azure: Machine Configuration bills through Azure Policy guest configuration — free for Azure-native VMs, and a modest per-server monthly charge for Arc-enabled (on-prem/other-cloud) servers, plus the Arc agent itself (the agent is free; connected-machine management is the charge). Budget roughly the low hundreds of INR per Arc server per month for guest-config at time of writing; confirm current pricing before committing a large hybrid fleet.
Ansible is free as ansible-core; the cost is the control plane if you go beyond a cron’d control node. AWX (upstream) is free but self-supported; Ansible Automation Platform (AAP) is a paid subscription priced per managed node, which buys you the web UI, RBAC, scheduling, credential store, and support (detail in Ansible Automation Platform at enterprise scale). A single Linux control-node VM to run scheduled converges is cheap — an Azure Standard_B2s in Central India is on the order of low four-figure INR per month, and one node comfortably drives hundreds of Windows hosts.
Rough sizing and cost signals:
| Item | Cost driver | Rough figure | Notes |
|---|---|---|---|
| DSC LCM (push + auto-correct) | None | ₹0 | In-box; the cheapest option that self-heals |
| Consistency-check overhead | CPU per check | Negligible (ms) | Even at 15-min cadence on 800 nodes |
| Azure Machine Config (Azure VMs) | Guest config | Free | Native VMs; audit + enforce |
| Azure Machine Config (Arc servers) | Per connected server/mo | ~low-hundreds INR/server/mo | Plus Arc connectivity; verify current pricing |
| Ansible control node VM | Compute | ~₹1,500–3,500/mo (B2s) |
One node drives hundreds of hosts |
| AWX | Self-support | ₹0 (software) | You run/patch/scale it |
| AAP subscription | Per managed node | Paid (quote) | UI, RBAC, scheduling, support |
| Engineer time | Authoring + CI + testing | The real cost | Amortised across every node forever |
Sizing guidance: a single control node scales to hundreds of Windows hosts for scheduled converges; past ~500–1000 nodes or when you need RBAC/scheduling/audit, move to AWX/AAP with execution nodes near each region to cut WinRM round-trip latency. For DSC, the LCM scales trivially (each node enforces itself), so “DSC scaling” is really about delivery and reporting — push from Ansible for small fleets, Machine Configuration for large hybrid ones. The cheapest robust design for a must-never-drift baseline is push-once + ApplyAndAutoCorrect: zero ongoing infrastructure cost and autonomous enforcement.
Interview & exam questions
Q1. What are the three methods every DSC resource implements, and which one makes DSC idempotent?
Get (report current state), Test (return a boolean: does current match desired?), and Set (make it match). Idempotency comes from the contract that Set runs only when Test returns $false — the Test-before-Set gate.
Q2. Explain the difference between ApplyOnly, ApplyAndMonitor, and ApplyAndAutoCorrect.
ApplyOnly applies once and never touches the box again. ApplyAndMonitor applies, then logs drift to the event log but does not fix it (audit-only). ApplyAndAutoCorrect applies and re-applies on every consistency check — the self-healing mode you use for state that must never drift.
Q3. Why can’t Ansible alone be your compliance-enforcement mechanism for a partition-prone fleet? Ansible is agentless and converges only when it runs. During a network partition the control node can’t reach the hosts, so any drift introduced during the outage (e.g. a hand-edit) goes uncorrected until connectivity returns. State with a compliance consequence needs an on-box agent — DSC’s LCM — that enforces autonomously.
Q4. What is a MOF, and why must you never put a plaintext credential in one?
A MOF (Managed Object Format) is the compiled, logic-free desired-state document the LCM enforces, stored on the node’s disk. Because it’s stored in clear text, an embedded PSCredential would be readable on disk; you must certificate-encrypt credentials via configuration data (CertificateFile + Thumbprint).
Q5. Distinguish Windows PowerShell 5.1 DSC from DSC v3.
5.1 DSC is WMI/CIM-based: Configuration functions, MOF, the LCM, Start-DscConfiguration, the (deprecated) pull server; Windows-only. DSC v3 is a standalone cross-platform dsc executable with no LCM, no MOF, and a JSON/YAML resource model, invoked imperatively (dsc config set); it powers Azure Machine Configuration.
Q6. What does win_dsc do, and what’s its main constraint?
ansible.windows.win_dsc lets an Ansible play invoke any installed DSC resource directly (no MOF, no LCM), mapping resource properties to task parameters. Its constraint: the resource module must be present on the target and compatible with the WMF 5.1 DSC engine — DSC v3-only resources won’t load.
Q7. Which WinRM auth methods put credentials on the wire, and which don’t? Basic (base64) and CredSSP (delegated) put credentials on the wire — avoid unless required, and only over HTTPS. Kerberos (tickets) and certificate auth do not send a reusable secret; NTLM sends a hashed challenge/response and should still run over HTTPS. Prefer Kerberos in a domain.
Q8. You forced drift, ran the LCM’s consistency check, and nothing corrected. What’s the first thing to check?
(Get-DscLocalConfigurationManager).ConfigurationMode. If it’s ApplyAndMonitor or ApplyOnly, the LCM won’t remediate — reapply a meta-configuration with ApplyAndAutoCorrect. Also confirm a MOF is actually stored (Get-DscConfiguration).
Q9. How do you prove an Ansible role is idempotent?
Run it once (expect changed>0), then run it again with --check (or just re-run): a correctly authored role reports changed=0 on the second pass. Molecule automates this as an “idempotence” assertion in CI.
Q10. What is “split-brain” enforcement and how do you avoid it? It’s DSC and Ansible both managing the same setting, so each perpetually “corrects” the other’s timing and the value flaps with pointless churn. Avoid it by assigning every setting a single owner — DSC or Ansible, never both.
Q11. Why is ConfigurationModeFrequencyMins = 5 a mistake?
It’s below the documented 15-minute floor and is silently clamped to 15. You’ll believe you’re checking every 5 minutes while the LCM checks every 15 — set a realistic value (30 is a good production default) and don’t design around sub-15 cadence.
Q12. When would you choose Azure Machine Configuration over the LCM push model? When you need cloud-reported compliance and enforcement across a hybrid fleet (Azure VMs + Arc-enabled on-prem/other-cloud servers) with an Azure Policy dashboard, rather than autonomous on-box enforcement with no cloud dependency. Machine Config also replaces the deprecated on-prem DSC pull server for reporting at scale.
Quick check
- Which DSC resource method must return a boolean, and what does the LCM do based on it?
- Name the
ConfigurationModevalue that makes the LCM remediate drift automatically. - Your Ansible task uses
win_commandand reportschangedon every run. What’s missing? - Which two DSC runtimes exist, and which one has no LCM and no MOF?
- What single design change would have prevented the Meridian Retail SMBv1 audit finding?
Answers
Test— it returns$true/$false; the LCM runsSetonly whenTestreturns$false(drift). That gate is where idempotency lives.ApplyAndAutoCorrect— re-applies everyConfigurationModeFrequencyMins. (ApplyAndMonitoronly logs;ApplyOnlynever re-checks.)- A
creates/removesguard or achanged_whenexpression —win_command/win_shellrun every time and reportchangedunless you tell Ansible when a change actually happened. - Windows PowerShell 5.1 DSC (WMI/CIM + LCM + MOF) and DSC v3 (standalone
dscexecutable). DSC v3 has no LCM and no MOF. - Putting the security-critical settings under the LCM in
ApplyAndAutoCorrectso they self-heal during the partition — instead of relying on Ansible, which can’t converge when it can’t reach the host.
Glossary
- Resource (DSC) — a PowerShell module implementing
Get/Test/Setfor one kind of state (a feature, service, registry value). The unit of idempotency. - Configuration (DSC) — a PowerShell function (keyword
Configuration) that declares resources and their desired values; compiles to MOF. - MOF (Managed Object Format) — the compiled, logic-free desired-state document produced per node and enforced by the LCM; stored on disk in clear text.
- LCM (Local Configuration Manager) — the DSC agent baked into every Windows box; applies a MOF and re-applies it on the consistency timer.
- Meta-configuration — a
[DSCLocalConfigurationManager()]-decorated configuration that configures the LCM itself (refresh/config mode, frequencies, reboot behaviour). ConfigurationMode— LCM setting:ApplyOnly,ApplyAndMonitor, orApplyAndAutoCorrect; decides whether drift is ignored, reported, or corrected.RefreshMode— LCM setting:Push(config sent to the node) vsPull(node fetches from a server).- DSC v3 — the cross-platform successor: a standalone
dscexecutable with no LCM, no MOF, and a JSON/YAML resource model; powers Azure Machine Configuration. - Azure Machine Configuration — the Azure/Arc guest-configuration service that audits and optionally enforces in-guest state via the DSC engine and reports compliance to Azure Policy (formerly guest configuration).
- WinRM (Windows Remote Management) — the WS-Management listener (TCP 5985 HTTP / 5986 HTTPS) Ansible uses to reach Windows.
- PSRP (PowerShell Remoting Protocol) — Ansible’s faster
psrpconnection plugin over WinRM; reuses a runspace across tasks. - Collection (Ansible) — a namespaced bundle of modules (
ansible.windows,community.windows); the modern packaging that replaced barewin_*names. win_dsc— theansible.windowsmodule that invokes an installed DSC resource directly from a play (no MOF/LCM).- Idempotency — the property that re-running produces no further change;
changed=0on the second Ansible pass, orTestreturning$truein DSC. - Drift — live configuration diverging from the declared desired state; what both tools exist to detect and remediate.
Ensure— the DSC property idiom:Present(make it exist as specified) orAbsent(make sure it’s gone).
Next steps
- Group Policy as Code — the GPO slice of the same drift problem, and how it complements DSC/Ansible on domain-joined hosts.
- Windows Server hardening and the WSUS patch pipeline — the baseline you’re often enforcing with the LCM, plus patch orchestration around it.
- Idempotent Ansible collections and Molecule testing — go deeper on proving idempotency and testing roles against ephemeral targets in CI.
- Ansible Automation Platform at enterprise scale — controller architecture, RBAC, scheduling, and execution environments when you outgrow a cron’d control node.
- gMSA, Windows LAPS and service-account credential hardening — how your automation authenticates to Windows without plaintext passwords.