Writing Idempotent PowerShell for SQL Server in Ansible
SqlServerDsc covers most of a standard SQL Server baseline out of the box—memory (SqlMemory), MAXDOP (SqlMaxDop), sp_configure options (SqlConfiguration), trace flags (SqlTraceFlag), and TCP ports (SqlProtocolTcpIp).
Eventually, though, every production build hits settings that have no dedicated SqlServerDsc resource, such as increasing SQL Server Error Log retention (NumErrorLogs) from the default 6 files to 30.
If you drop a raw PowerShell snippet into win_powershell to configure that setting, two things break immediately:
- Every playbook run reports
changed—even when the server is already set to 30. - Running
ansible-playbook --checkfor a dry-run drift audit also reportschangedon every run while silently skipping your PowerShell code altogether.
Here are the three ways to configure custom SQL Server settings idempotently in Ansible. I'll use the NumErrorLogs setting for every example so you can compare them side by side.
- Why raw scripts break Ansible audits: what actually happens when you run a standard PowerShell snippet in
win_powershell(both normally and with--check). - Method 1 — The T-SQL
SqlScriptQueryDSC pattern: howGetQuery,TestQuery, andSetQueryload.sqlfiles into memory and enforce idempotency with T-SQL. - Method 2 — The DSC
Scriptresource pattern: howGetScript,TestScript, andSetScriptwork with PowerShell SMO—and where inline Jinja2 string injection gets brittle. - Method 3 — The State-Aware
win_powershellpattern: how to useparameters:,[CmdletBinding(SupportsShouldProcess)],$Ansible.Changed,$Ansible.Diff, and$Ansible.Resultin a single self-contained script.
What Happens When You Run Raw PowerShell in Ansible
Out of the box, SQL Server only keeps 6 error logs before recycling the oldest file.
I don't know which is more frustrating: losing the error log you actually need because an instance gets restarted frequently, or waiting minutes for SSMS to open a single massive error log because the instance has been running for months without cycling it.
One of the first things I configure on every new SQL Server build is increasing error log retention to 30 and adding a scheduled SQL Agent job to cycle the error log daily at midnight.
Because SqlServerDsc doesn't have a SqlErrorLog resource, the tempting shortcut is to call SMO inside win_powershell:
---
# ❌ ANTI-PATTERN: Naive imperative PowerShell in Ansible
- name: Configure SQL Server Error Log Retention (Naive)
hosts: database_servers
gather_facts: false
tasks:
- name: Set SQL Server error log count to 30 (Naive)
ansible.windows.win_powershell:
script: |
Import-Module SqlServer
$srv = New-Object Microsoft.SqlServer.Management.Smo.Server("localhost")
$srv.NumberOfLogFiles = 30
$srv.Alter()When I run ansible-playbook configure_errorlogs_naive.yml --limit sql1.sandbox.local in my lab, it updates the registry value (HKLM\Software\Microsoft\MSSQLServer\MSSQLServer\NumErrorLogs) to 30.
When I run the exact same playbook a second time, Ansible still reports:
TASK [Set SQL Server error log count to 30 (Naive)] ****************************
changed: [sql1.sandbox.local]And when I run the playbook in dry-run audit mode (ansible-playbook configure_errorlogs_naive.yml --limit sql1.sandbox.local --check):
TASK [Set SQL Server error log count to 30 (Naive)] ****************************
changed: [sql1.sandbox.local]Wait. Why did --check report changed when sql1.sandbox.local is already set to 30 error logs?
When I re-run it with -v (ansible-playbook configure_errorlogs_naive.yml --limit sql1.sandbox.local --check -v), look at the JSON output:
TASK [Set SQL Server error log count to 30 (Naive)] ****************************
changed: [sql1.sandbox.local] => {
"changed": true,
"debug": [],
"error": [],
"host_err": "",
"host_out": "",
"information": [],
"msg": "skipped, running in check mode",
"output": [],
"result": {},
"verbose": [],
"warning": []
}By default, ansible.windows.win_powershell assumes any script that finishes without an error modified the server ($Ansible.Changed = $true).
Even worse, during --check mode, if your script does not explicitly declare [CmdletBinding(SupportsShouldProcess)], win_powershell skips executing your PowerShell code entirely ("msg": "skipped, running in check mode") and hardcodes changed: true.
Every dry-run audit reports false-positive configuration drift without ever connecting to SQL Server.
Now that you can recognize the trap, let's look at how you can avoid it, make this task idempotent, and ensure --check returns the correct results.
Method 1: The T-SQL Way (SqlServerDsc + SqlScriptQuery)
If your team prefers pure T-SQL and wants SqlServerDsc to enforce the Get / Test / Set lifecycle without copying .sql files to the target server, SqlServerDsc includes a resource called SqlScriptQuery (alongside its file-based sibling, SqlScript).
Using Ansible's lookup('file', ...) plugin, you can read your .sql scripts directly from the Control Node and pass them into GetQuery, TestQuery, and SetQuery in memory:
---
# configure_errorlogs_sqlscript.yml
# Method 1: The T-SQL Way (SqlServerDsc + SqlScriptQuery)
- name: Configure SQL Server Error Log Retention (SqlScriptQuery DSC)
hosts: database_servers
gather_facts: false
vars:
sql_max_error_logs: 30
tasks:
- name: Set SQL Server instance number of error logs to {{ sql_max_error_logs }}
ansible.windows.win_dsc:
resource_name: SqlScriptQuery
Id: "SetMaxNumErrorLogs"
GetQuery: "{{ lookup('file', 'files/Get-NumErrorLogs.sql') }}"
TestQuery: "{{ lookup('file', 'files/Test-NumErrorLogs.sql') }}"
SetQuery: "{{ lookup('file', 'files/Set-NumErrorLogs.sql') }}"
Variable:
- "MaxErrorLogs={{ sql_max_error_logs }}"
QueryTimeout: 600
ServerName: "localhost"
InstanceName: "MSSQLSERVER"
Encrypt: "Optional"How GetQuery, TestQuery, SetQuery, and Variable Work Together
The SqlScriptQuery DSC resource maps three T-SQL queries to PowerShell DSC's three execution phases and passes Ansible variables into Invoke-Sqlcmd using sqlcmd scripting tokens ($(MaxErrorLogs)):
TestQuery(Test-NumErrorLogs.sql) — Runs on Every Playbook Execution (Including--checkMode):SqlScriptQueryexecutesTest-NumErrorLogs.sqlfirst to decide whether the server is already in the desired state. How does a T-SQL script return$trueor$falseto DSC? Through SQL error severity:- If the script completes without throwing an error (
PRINT),Test-TargetResourcereturns$trueand Ansible reports ok. - If the script raises an error (
RAISERROR(..., 16, 1)),Test-TargetResourcecatches it, returns$false, and triggersSetQuery.
-- Test-NumErrorLogs.sql DECLARE @Results TABLE (Value VARCHAR(300), Data INT); DECLARE @data INT; INSERT INTO @Results EXEC xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'NumErrorLogs'; SELECT @data = Data FROM @Results; SET @data = ISNULL(@data, 0); IF @data <> $(MaxErrorLogs) BEGIN RAISERROR('Number of error logs does not match the desired configuration. Updating...', 16, 1); END ELSE BEGIN PRINT 'Number of error logs matches the desired state. Skipping...'; END- If the script completes without throwing an error (
SetQuery(Set-NumErrorLogs.sql) — Runs Only WhenTestQueryFails: WhenTest-NumErrorLogs.sqlraises severity 16 (and Ansible is not in--checkmode),SqlScriptQueryexecutesSet-NumErrorLogs.sql:-- Set-NumErrorLogs.sql USE [master]; GO EXEC xp_instance_regwrite N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'NumErrorLogs', REG_DWORD, $(MaxErrorLogs); GOGetQuery(Get-NumErrorLogs.sql) — Returns Current State: Queries the current registry value for DSC state inspection (Get-TargetResource). Even thoughTestQuerymakes 100% of theokvs.changeddecision during an Ansible run,SqlScriptQuerymarksGetQueryas a mandatory parameter in the DSC schema:-- Get-NumErrorLogs.sql DECLARE @Results TABLE (Value VARCHAR(300), Data INT); DECLARE @data INT; INSERT INTO @Results EXEC xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'NumErrorLogs'; SELECT @data = Data FROM @Results; PRINT ISNULL(@data, 0);
How Variable: Passes Dynamic Values into T-SQL:
Because SqlScriptQuery passes Variable: ["MaxErrorLogs={{ sql_max_error_logs }}"] to Invoke-Sqlcmd -Variable, sqlcmd substitutes $(MaxErrorLogs) inside Test-NumErrorLogs.sql and Set-NumErrorLogs.sql at runtime—so you never have to hardcode 30 inside your .sql files.
The Trade-Off of SqlScriptQuery
SqlScriptQuery (and its file-based counterpart SqlScript) is a great fit when your team wants to keep logic in pure T-SQL or deploy multi-statement SQL scripts like standard maintenance jobs. For a single setting like NumErrorLogs, however, it has two drawbacks:
- Three
.sqlfiles per setting: Every configuration item requires maintainingGet-*.sql,Test-*.sql, andSet-*.sql(including a mandatoryGet-*.sqlscript whose output Ansible never displays). - No
--diffor custom return payload: Because DSC only returns a boolean fromTest-TargetResource, it cannot return a before/after property diff ($Ansible.Diff) or custom structured output ($Ansible.Result) to Ansible.
Method 2: The DSC Script Resource (GetScript / TestScript / SetScript)
If you prefer PowerShell SMO over T-SQL xp_instance_regwrite calls, you can use the built-in Windows PSDesiredStateConfiguration Script resource with inline PowerShell blocks for GetScript, TestScript, and SetScript:
---
# configure_errorlogs_dsc_script.yml
# Method 2: The DSC Script Resource (GetScript / TestScript / SetScript)
- name: Configure SQL Server Error Log Retention (DSC Script)
hosts: database_servers
gather_facts: false
vars:
sql_max_error_logs: 30
tasks:
- name: Set SQL Server error log count to 30
ansible.windows.win_dsc:
resource_name: Script
GetScript: |
Import-Module SqlServer
$srv = New-Object Microsoft.SqlServer.Management.Smo.Server("localhost")
return @{ Result = [string]$srv.NumberOfLogFiles }
TestScript: |
Import-Module SqlServer
$srv = New-Object Microsoft.SqlServer.Management.Smo.Server("localhost")
return ($srv.NumberOfLogFiles -eq {{ sql_max_error_logs }})
SetScript: |
Import-Module SqlServer
$srv = New-Object Microsoft.SqlServer.Management.Smo.Server("localhost")
$srv.NumberOfLogFiles = {{ sql_max_error_logs }}
$srv.Alter()While the DSC Script resource runs without staging .sql files on disk and properly supports --check mode via TestScript, look closely at its limitations:
- Jinja2 string injection (
{{ sql_max_error_logs }}): Because the DSCScriptresource has noparameters:dictionary, Ansible variables must be injected directly into the raw PowerShell string before execution. For an integer like30that looks harmless, but as soon as you pass instance names, file paths, or passwords containing quotes, the PowerShell parser breaks. - Duplicated connection settings:
GetScript,TestScript, andSetScriptexecute as separate script blocks, forcing you to repeat the SMO connection setup in all three blocks. - No
--diffor custom return payload: The Windows DSC Local Configuration Manager (LCM) only returns a boolean fromTestScript—it cannot return a before/after property diff ($Ansible.Diff) or custom structured output ($Ansible.Result) to Ansible.
When you want zero temporary files on disk, real typed PowerShell parameters, structured JSON output, and red/green --diff support, use state-aware win_powershell.
Method 3: State-Aware win_powershell (configure_errorlogs.yml)
ansible.windows.win_powershell exposes a built-in $Ansible automatic variable inside your PowerShell session. By following four rules, a single .ps1 file gets the same Get / Test / Set behavior as a DSC resource—running entirely in memory over WinRM or OpenSSH.
Rule 1: Pass Typed parameters: Instead of Jinja2 String Interpolation
Never inject {{ my_var }} directly inside a PowerShell script block. Instead, declare a standard PowerShell param() block at the top of your .ps1 file and pass variables through the win_powershell parameters: dictionary (configure_errorlogs.yml):
---
# configure_errorlogs.yml
# ✅ STATE-AWARE: Idempotent win_powershell with SupportsShouldProcess, $Ansible.Changed, and $Ansible.Diff
- name: Configure SQL Server Error Log Retention
hosts: database_servers
gather_facts: false
vars:
sql_instance_name: "MSSQLSERVER"
sql_max_error_logs: 30
tasks:
- name: Ensure SQL Server Error Log retention matches corporate baseline
ansible.windows.win_powershell:
script: "{{ lookup('file', 'files/set_num_error_logs.ps1') }}"
parameters:
SqlInstanceName: "{{ sql_instance_name }}"
MaxErrorLogs: "{{ sql_max_error_logs }}"
register: errorlog_result
- name: Display structured $Ansible.Result payload
ansible.builtin.debug:
var: errorlog_result.resultRule 2: Add [CmdletBinding(SupportsShouldProcess)] for --check Mode
When you run ansible-playbook configure_errorlogs.yml --check, win_powershell inspects your script's abstract syntax tree (AST) before running it:
- Without
SupportsShouldProcess: Ansible skips running your PowerShell code ("msg": "skipped, running in check mode") and marks the taskchanged: true. - With
[CmdletBinding(SupportsShouldProcess)]: Ansible executes your script and automatically passes-WhatIf:$trueinto the PowerShell invocation when--checkis active.
Rule 3: Test-Before-Set and Gate Changes with $PSCmdlet.ShouldProcess()
Read the live SQL Server state first ($srv.NumberOfLogFiles). Note that when SQL Server has never had its error log limit customized in the registry, SMO returns -1 (meaning the default 6 logs).
Compare the current value to $MaxErrorLogs:
- If they already match, leave
$Ansible.Changed = $falseand return immediately. - If they do not match, set
$Ansible.Changed = $trueand wrap the actual$srv.Alter()call insideif ($PSCmdlet.ShouldProcess(...)). During--checkmode,ShouldProcessreturns$false—so Ansible reportschanged("would change") only when drift actually exists, without modifying SQL Server.
Rule 4: Populate $Ansible.Changed, $Ansible.Diff, and $Ansible.Result
Before reading the full script, click through the four stages it runs on every execution:
### 1. Bind Parameters | CmdletBinding
Declares `[CmdletBinding(SupportsShouldProcess = $true)]` so `--check` runs the script with `-WhatIf`, then starts from `$Ansible.Changed = $false`.
`[CmdletBinding(SupportsShouldProcess = $true)]`
### 2. Get Current State | SMO Read
Connects to the instance through SMO and reads `NumberOfLogFiles`, treating SMO's `-1` as SQL Server's default of 6.
`$rawCurrent = $srv.NumberOfLogFiles`
### 3. Test for Drift | Compare
If the current value already matches `$MaxErrorLogs`, the script returns early and Ansible reports **ok**. If not, it sets `$Ansible.Changed = $true` and records the before/after values in `$Ansible.Diff`.
`if ($rawCurrent -eq $MaxErrorLogs) { return }`
### 4. Gate the Change | ShouldProcess
`$srv.Alter()` runs only inside `$PSCmdlet.ShouldProcess()`. On a normal run SQL Server is updated; under `--check` the call is skipped and the task reports what would change.
`if ($PSCmdlet.ShouldProcess($targetServer, "Set NumberOfLogFiles")) { $srv.Alter() }`Here is the complete, state-aware [set_num_error_logs.ps1] script putting all four rules together:
# files/set_num_error_logs.ps1
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory = $false)]
[string]$SqlInstanceName = "MSSQLSERVER",
[Parameter(Mandatory = $false)]
[ValidateRange(6, 99)]
[int]$MaxErrorLogs = 30
)
$ErrorActionPreference = "Stop"
# 1. Default to no change
$Ansible.Changed = $false
$Ansible.Result = @{
Changed = $false
Success = $false
InstanceName = $SqlInstanceName
PreviousValue = $null
CurrentValue = $null
Message = ""
}
try {
Import-Module SqlServer -ErrorAction Stop
$targetServer = if ($SqlInstanceName -eq "MSSQLSERVER" -or $SqlInstanceName -eq "DEFAULT") {
"localhost"
} else {
"localhost\$SqlInstanceName"
}
# 2. GET PHASE: Read current NumberOfLogFiles via SMO (-1 means SQL Server default of 6)
$srv = New-Object Microsoft.SqlServer.Management.Smo.Server($targetServer)
$rawCurrent = $srv.NumberOfLogFiles
$effectiveCurrent = if ($rawCurrent -eq -1) { 6 } else { $rawCurrent }
$Ansible.Result.PreviousValue = $effectiveCurrent
$Ansible.Result.CurrentValue = $effectiveCurrent
# 3. TEST PHASE: Compare current state against desired state
if ($rawCurrent -eq $MaxErrorLogs) {
$Ansible.Result.Success = $true
$Ansible.Result.Message = "NumberOfLogFiles on '$targetServer' already matches desired value ($MaxErrorLogs)."
return
}
# Drift detected -> Flag task as changed and record before/after diff for --diff mode
$Ansible.Changed = $true
$Ansible.Result.Changed = $true
$Ansible.Diff = @{
before = @{ NumberOfLogFiles = $effectiveCurrent }
after = @{ NumberOfLogFiles = $MaxErrorLogs }
}
# 4. SET PHASE: Gated by ShouldProcess (skipped automatically during --check mode)
if ($PSCmdlet.ShouldProcess($targetServer, "Set NumberOfLogFiles from $effectiveCurrent to $MaxErrorLogs")) {
$srv.NumberOfLogFiles = $MaxErrorLogs
$srv.Alter()
$Ansible.Result.CurrentValue = $MaxErrorLogs
$Ansible.Result.Success = $true
$Ansible.Result.Message = "Updated NumberOfLogFiles on '$targetServer' from $effectiveCurrent to $MaxErrorLogs."
}
else {
# Running in --check mode (-WhatIf)
$Ansible.Result.CurrentValue = $effectiveCurrent
$Ansible.Result.Success = $true
$Ansible.Result.Message = "[CHECK MODE] NumberOfLogFiles on '$targetServer' drifted ($effectiveCurrent vs $MaxErrorLogs) and would be updated."
}
}
catch {
$Ansible.Failed = $true
$Ansible.Result.Success = $false
$Ansible.Result.Message = "Failed to configure NumberOfLogFiles on '$SqlInstanceName': $($_.Exception.Message)"
}Testing All 4 Passes in the Lab (--check --diff, Converge, Re-Run, and Clean --check)
When I run [configure_errorlogs.yml] against sql1.sandbox.local (starting from the default 6 error logs), watch how each pass behaves. Click ▶ Replay to see all four in sequence, then read the full output for each pass below:
===bash $ ansible-playbook configure_errorlogs.yml --limit sql1.sandbox.local --check --diff TASK [Ensure SQL Server Error Log retention matches corporate baseline] ******** - "NumberOfLogFiles": 6 + "NumberOfLogFiles": 30 changed: [sql1.sandbox.local] $ ansible-playbook configure_errorlogs.yml --limit sql1.sandbox.local TASK [Ensure SQL Server Error Log retention matches corporate baseline] ******** changed: [sql1.sandbox.local] $ ansible-playbook configure_errorlogs.yml --limit sql1.sandbox.local TASK [Ensure SQL Server Error Log retention matches corporate baseline] ******** ok: [sql1.sandbox.local] $ ansible-playbook configure_errorlogs.yml --limit sql1.sandbox.local --check TASK [Ensure SQL Server Error Log retention matches corporate baseline] ******** ok: [sql1.sandbox.local] [ RESULT ] sql1.sandbox.local : ok=2 changed=0 unreachable=0 failed=0
Pass 1: Dry-Run Audit Detects Drift
(ansible-playbook configure_errorlogs.yml --limit sql1.sandbox.local --check --diff)
Because the script declares [CmdletBinding(SupportsShouldProcess = $true)] and populates $Ansible.Diff, --check --diff executes the SMO read, spots that sql1 is currently at 6 error logs, and prints the exact before/after diff without modifying the server:
TASK [Ensure SQL Server Error Log retention matches corporate baseline] ********
--- before
+++ after
@@ -1,3 +1,3 @@
{
- "NumberOfLogFiles": 6
+ "NumberOfLogFiles": 30
}
changed: [sql1.sandbox.local]
TASK [Display structured $Ansible.Result payload] ******************************
ok: [sql1.sandbox.local] => {
"errorlog_result.result": {
"Changed": true,
"CurrentValue": 6,
"InstanceName": "MSSQLSERVER",
"Message": "[CHECK MODE] NumberOfLogFiles on 'localhost' drifted (6 vs 30) and would be updated.",
"PreviousValue": 6,
"Success": true
}
}Pass 2: Enforce the Baseline
(ansible-playbook configure_errorlogs.yml --limit sql1.sandbox.local)
When I run the playbook without --check, the ShouldProcess block executes $srv.Alter(), updating NumberOfLogFiles to 30:
TASK [Ensure SQL Server Error Log retention matches corporate baseline] ********
changed: [sql1.sandbox.local]
TASK [Display structured $Ansible.Result payload] ******************************
ok: [sql1.sandbox.local] => {
"errorlog_result.result": {
"Changed": true,
"CurrentValue": 30,
"InstanceName": "MSSQLSERVER",
"Message": "Updated NumberOfLogFiles on 'localhost' from 6 to 30.",
"PreviousValue": 6,
"Success": true
}
}Pass 3: Re-Run to Prove Idempotency
(ansible-playbook configure_errorlogs.yml --limit sql1.sandbox.local)
When I run it again, the Test phase sees $rawCurrent -eq 30, leaves SQL Server untouched, and reports ok:
TASK [Ensure SQL Server Error Log retention matches corporate baseline] ********
ok: [sql1.sandbox.local]
TASK [Display structured $Ansible.Result payload] ******************************
ok: [sql1.sandbox.local] => {
"errorlog_result.result": {
"Changed": false,
"CurrentValue": 30,
"InstanceName": "MSSQLSERVER",
"Message": "NumberOfLogFiles on 'localhost' already matches desired value (30).",
"PreviousValue": 30,
"Success": true
}
}Pass 4: Re-Run --check to Prove Clean Drift Audits
(ansible-playbook configure_errorlogs.yml --limit sql1.sandbox.local --check)
Unlike the naive script that reported changed on every --check run, running --check now executes the Test phase against sql1.sandbox.local, confirms NumberOfLogFiles is 30, and reports ok:
TASK [Ensure SQL Server Error Log retention matches corporate baseline] ********
ok: [sql1.sandbox.local]
TASK [Display structured $Ansible.Result payload] ******************************
ok: [sql1.sandbox.local] => {
"errorlog_result.result": {
"Changed": false,
"CurrentValue": 30,
"InstanceName": "MSSQLSERVER",
"Message": "NumberOfLogFiles on 'localhost' already matches desired value (30).",
"PreviousValue": 30,
"Success": true
}
}What's the Difference Between $Ansible.Changed and $Ansible.Result.Changed?
Look closely at [set_num_error_logs.ps1] and you'll notice it sets both $Ansible.Changed and properties inside $Ansible.Result (Changed, Success, PreviousValue, CurrentValue, Message).
Here is the distinction between the two:
### $Ansible.Changed (top-level property) | ok - Controls Ansible's native task status: green `ok` or yellow `changed` - Drives the `changed=1` counter in the final `PLAY RECAP` - Defaults to `$true`, so set `$Ansible.Changed = $false` when nothing needs to change --- ### $Ansible.Result (custom dictionary) | ok - Returned to your playbook in the registered variable (`errorlog_result.result.Message`, `errorlog_result.result.PreviousValue`) - Ansible does **not** inspect `$Ansible.Result.Changed` or `$Ansible.Result.Success` on its own - Use it for `debug` output, `changed_when:` / `failed_when:`, and downstream `when:` assertions
Best Practice for Production Playbooks: Set both inside your .ps1 script ($Ansible.Changed / $Ansible.Failed for native Ansible task reporting, and $Ansible.Result for structured output messages and downstream when: assertions). That way your YAML task stays clean without needing extra changed_when: or failed_when: boilerplate code.
Your Next Step
Whenever you hit a SQL Server setting that SqlServerDsc doesn't cover with a dedicated resource:
- Use SqlServerDsc's
SqlScriptQueryresource (GetQuery,TestQuery,SetQuery) when you are deploying multi-statement T-SQL scripts like standard DBA maintenance jobs. - Use state-aware
win_powershell(parameters:,SupportsShouldProcess,$Ansible.Changed,$Ansible.Diff, and$Ansible.Result) when you want a self-contained, parameterized PowerShell check-and-set task in a single.ps1file with full--check --diffvisibility.
Coming Up Next — Leveling Up from win_powershell to a Custom Ansible Module:
What if you want to reuse this logic across dozens of playbooks without writing script: "{{ lookup('file', ...) }}" and parameters: every time—or what if you're wondering whether you should build a custom PowerShell DSC resource vs. a custom Ansible Windows module?
In an upcoming post, I'll take set_num_error_logs.ps1 and turn it into a first-class win_sql_errorlogs: Ansible module on the Control Node using Ansible.Basic.AnsibleModule—with zero PowerShell modules to install on your target SQL Servers.
References & Further Reading
- Microsoft Learn — Everything you wanted to know about ShouldProcess: A practical deep dive on how
[CmdletBinding(SupportsShouldProcess)],-WhatIf,-Confirm, and$PSCmdlet.ShouldProcess()work in PowerShell. - Microsoft Learn —
about_Functions_CmdletBindingAttribute(SupportsShouldProcess): Official PowerShell reference for theCmdletBindingattribute and itsSupportsShouldProcessargument. - Ansible Documentation —
ansible.windows.win_powershellModule: Official documentation coveringparameters:,--checkmode handling withSupportsShouldProcess, and the built-in$Ansiblevariable ($Ansible.Changed,$Ansible.Diff,$Ansible.Result). - SqlServerDsc Wiki —
SqlScriptQueryResource: Documentation and examples for running in-memoryGetQuery,TestQuery, andSetQueryT-SQL scripts withSqlServerDsc.

The Database Platform Automation Roadmap
Move from checklists and scattered scripts to desired-state Ansible playbooks—so you can stop wondering whether production still matches what you built. Includes the 60-second self-audit and 23-page field guide (PDF).

