适用于 PowerShell 5.1 及以上版本(Windows)
在现代混合 IT 环境中,Windows 和 Linux 服务器往往共存于同一基础设施。Ansible 作为无代理(agentless)的配置管理工具,原生支持通过 WinRM 协议管理 Windows 主机,而 PowerShell 正是 Ansible 在 Windows 端执行任务的核心引擎——每个 Ansible 模块在 Windows 上最终都会转化为 PowerShell 脚本执行。
理解 PowerShell 与 Ansible 的集成方式,不仅可以帮助运维团队构建跨平台的自动化流水线,还能在现有 PowerShell 脚本资产的基础上无缝接入 Ansible 生态。本文将从连接配置、自定义模块编写和脚本集成三个层面,展示如何在 PowerShell 环境中高效使用 Ansible。
配置 WinRM 连接 Ansible 通过 WinRM 与 Windows 通信。在开始之前,需要确保目标 Windows 主机的 WinRM 服务已正确配置。微软提供了专门的配置脚本,但生产环境中往往需要更精细的控制。
以下 PowerShell 函数用于配置 WinRM,支持 HTTP 和 HTTPS 两种传输方式,并输出 Ansible inventory 所需的连接参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 function Set-AnsibleWinRM { [CmdletBinding ()] param ( [bool ]$UseHTTPS = $true , [string ]$CertificateThumbprint ) $service = Get-Service -Name WinRM -ErrorAction SilentlyContinue if ($service .Status -ne 'Running' ) { Start-Service -Name WinRM Write-Host "WinRM 服务已启动" } $winrmPath = 'WSMan:\localhost\Service\Auth' Set-Item -Path "$winrmPath \Basic" -Value $true -Force Set-Item -Path "$winrmPath \CredSSP" -Value $true -Force Write-Host "基本认证和 CredSSP 已启用" if ($UseHTTPS ) { if (-not $CertificateThumbprint ) { $cert = New-SelfSignedCertificate ` -DnsName $env:COMPUTERNAME ` -CertStoreLocation 'Cert:\LocalMachine\My' ` -FriendlyName 'Ansible WinRM HTTPS' $CertificateThumbprint = $cert .Thumbprint Write-Host "自签名证书已创建,指纹: $CertificateThumbprint " } $existingListener = Get-ChildItem -Path 'WSMan:\localhost\Listener' | Where-Object { $_ .Keys -match 'Transport=HTTPS' } if ($existingListener ) { Remove-Item -Path "WSMan:\localhost\Listener\$ ($existingListener .Name)" -Recurse -Force } New-Item -Path 'WSMan:\localhost\Listener' -Transport HTTPS -Address * ` -CertificateThumbprint $CertificateThumbprint -Force | Out-Null Write-Host "HTTPS 监听器已创建" } $ports = @ { HTTP = 5985 ; HTTPS = 5986 } foreach ($proto in @ ('HTTP' , 'HTTPS' )) { $port = $ports [$proto ] $ruleName = "Ansible WinRM $proto " $existingRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue if (-not $existingRule ) { New-NetFirewallRule -DisplayName $ruleName ` -Direction Inbound ` -Action Allow ` -Protocol TCP ` -LocalPort $port | Out-Null Write-Host "防火墙规则已添加: $ruleName (端口 $port )" } } $ansibleUser = "$env:COMPUTERNAME \Administrator" $ansiblePort = if ($UseHTTPS ) { 5986 } else { 5985 } $ansibleScheme = if ($UseHTTPS ) { 'https' } else { 'http' } Write-Host "`n--- Ansible Inventory 配置参考 ---" Write-Host "[windows]" Write-Host "$ ($env:COMPUTERNAME .ToLower()) ansible_host=<目标IP>" Write-Host "[windows:vars]" Write-Host "ansible_user=$ansibleUser " Write-Host "ansible_password=<密码>" Write-Host "ansible_connection=winrm" Write-Host "ansible_winrm_transport=basic" Write-Host "ansible_winrm_server_cert_validation=ignore" Write-Host "ansible_port=$ansiblePort " }
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 WinRM 服务已启动 基本认证和 CredSSP 已启用 自签名证书已创建,指纹: A1B2C3D4E5F6789012345678901234567890ABCD HTTPS 监听器已创建 防火墙规则已添加: Ansible WinRM HTTP (端口 5985) 防火墙规则已添加: Ansible WinRM HTTPS (端口 5986) --- Ansible Inventory 配置参考 --- [windows] winserver01 ansible_host =<目标IP> [windows:vars] ansible_user =WINSERVER01\Administratoransible_password =<密码>ansible_connection =winrmansible_winrm_transport =basicansible_winrm_server_cert_validation =ignoreansible_port =5986
编写 Ansible 自定义模块 Ansible 的 Windows 模块本质上就是遵循特定输入输出约定的 PowerShell 脚本。当内置模块无法满足需求时,可以编写自定义模块,直接复用已有的 PowerShell 函数和模块。
自定义模块需要遵循 Ansible 的 JSON 通信协议:从标准输入读取 JSON 参数,通过 Exit-Json 返回成功结果,或通过 Fail-Json 返回失败信息。以下示例实现了一个检查 Windows 服务状态的自定义模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 $ErrorActionPreference = 'Stop' function Exit-Json { param ([hashtable ]$Result ) $Result .ansible_facts = $Result .ansible_facts $jsonOutput = $Result | ConvertTo-Json -Depth 10 -Compress Write-Output $jsonOutput exit 0 } function Fail-Json { param ([hashtable ]$Result , [string ]$Message ) $Result .failed = $true $Result .msg = $Message $jsonOutput = $Result | ConvertTo-Json -Depth 10 -Compress Write-Output $jsonOutput exit 1 } $params = @ { name = '' state = 'started' start_mode = 'auto' check_only = $false } $result = @ { changed = $false services = @ () } $serviceNames = $params .name -split ',' | ForEach-Object { $_ .Trim() }$allServices = @ ()foreach ($svcName in $serviceNames ) { $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue if (-not $svc ) { Fail-Json -Result $result -Message "服务 '$svcName ' 不存在" } $wmiSvc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$svcName '" $serviceInfo = @ { name = $svcName displayname = $svc .DisplayName status = $svc .Status.ToString() starttype = $wmiSvc .StartMode } if (-not $params .check_only) { if ($wmiSvc .StartMode -ne $params .start_mode) { Set-Service -Name $svcName -StartupType $params .start_mode $serviceInfo .starttype = $params .start_mode $result .changed = $true } $desiredState = $params .state if ($desiredState -eq 'started' -and $svc .Status -ne 'Running' ) { Start-Service -Name $svcName $serviceInfo .status = 'Running' $result .changed = $true } elseif ($desiredState -eq 'stopped' -and $svc .Status -ne 'Stopped' ) { Stop-Service -Name $svcName -Force $serviceInfo .status = 'Stopped' $result .changed = $true } } $allServices += $serviceInfo } $result .services = $allServices $result .msg = "已检查 $ ($allServices .Count) 个服务" Exit-Json -Result $result
执行结果示例(Ansible playbook 调用后的 JSON 输出):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 TASK [检查并修复服务状态] ********************************************************ok: [winserver01] => { "changed" : true, "services" : [ { "name" : "wuauserv" , "displayname" : "Windows Update" , "status" : "Running" , "starttype" : "auto" }, { "name" : "WinRM" , "displayname" : "Windows Remote Management (WS-Management)" , "status" : "Running" , "starttype" : "auto" } ], "msg" : "已检查 2 个服务" }
将现有 PowerShell 脚本封装为 Ansible Playbook 许多团队已经积累了大量的 PowerShell 运维脚本。通过 Ansible 的 script 模块或 win_shell 模块,可以直接在 playbook 中调用这些脚本,但更推荐的做法是使用 win_task 或结构化的方式封装,以便获得更好的幂等性和错误处理。
以下函数演示如何从一个目录中的 PowerShell 脚本自动生成 Ansible playbook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 function New-AnsiblePlaybookFromScripts { [CmdletBinding ()] param ( [Parameter (Mandatory )] [string ]$ScriptPath , [Parameter (Mandatory )] [string ]$OutputFile , [string ]$TargetHosts = 'windows' ) $scripts = Get-ChildItem -Path $ScriptPath -Filter '*.ps1' | Sort-Object -Property Name if ($scripts .Count -eq 0 ) { Write-Warning "目录 $ScriptPath 中没有找到 PowerShell 脚本" return } $tasks = @ () foreach ($script in $scripts ) { $scriptName = $script .BaseName $description = $scriptName $firstLine = Get-Content -Path $script .FullName -TotalCount 1 if ($firstLine -match '^\s*#\s*(.+)' ) { $description = $Matches [1 ] } $task = @ { name = $description script = $script .FullName register = "result_$ ($scriptName -replace '[^a-zA-Z0-9]', '_')" } $tasks += $task Write-Host " 已添加任务: $description ($ ($script .Name))" } $playbook = @ ( @ { name = "执行 PowerShell 运维脚本" hosts = $TargetHosts gather_facts = $false tasks = $tasks } ) $yamlLines = @ ( '---' '' "- name: 执行 PowerShell 运维脚本" " hosts: $TargetHosts " ' gather_facts: false' ' tasks:' ) foreach ($script in $scripts ) { $scriptName = $script .BaseName $firstLine = Get-Content -Path $script .FullName -TotalCount 1 $description = $scriptName if ($firstLine -match '^\s*#\s*(.+)' ) { $description = $Matches [1 ] } $regName = "result_$ ($scriptName -replace '[^a-zA-Z0-9]', '_')" $yamlLines += @ ( " - name: $description " " script: scripts/$ ($script .Name)" " register: $regName " '' ) } Set-Content -Path $OutputFile -Value $yamlLines -Encoding UTF8 Write-Host "`nPlaybook 已生成: $OutputFile " Write-Host "共包含 $ ($scripts .Count) 个任务" }
执行结果示例:
1 2 3 4 5 6 7 8 已添加任务 : 清理临时文件 (01-CleanTempFiles.ps1) 已添加任务 : 检查磁盘空间 (02-CheckDiskSpace.ps1) 已添加任务 : 更新 Windows (03-InstallUpdates.ps1) 已添加任务 : 重启挂起检测 (04-CheckRebootPending.ps1) 已添加任务 : 导出系统信息 (05-ExportSystemInfo.ps1) Playbook 已生成 : site.yml 共包含 5 个任务
在 PowerShell 中调用 Ansible Playbook 除了从 Ansible 端管理 Windows,有时也需要从 Windows 主机本身触发 Ansible 操作。例如在 CI/CD 流水线中,由 PowerShell 编排整个部署流程。以下函数封装了通过 WSL 或远程 Linux 跳板机执行 Ansible playbook 的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 function Invoke-AnsiblePlaybook { [CmdletBinding ()] param ( [Parameter (Mandatory )] [string ]$PlaybookPath , [string ]$Inventory = 'inventory/hosts' , [hashtable ]$ExtraVars , [bool ]$UseWSL = $true , [string ]$RemoteHost ) $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $logFile = "ansible_run_$ {timestamp}.log" $extraVarsStr = '' if ($ExtraVars ) { $varParts = @ () foreach ($key in $ExtraVars .Keys) { $varParts += "$key =$ ($ExtraVars [$key ])" } $extraVarsStr = " --extra-vars `"$ ($varParts -join ' ')`"" } $ansibleCmd = "ansible-playbook -i $Inventory $extraVarsStr $PlaybookPath " if ($UseWSL ) { Write-Host "通过 WSL 执行: $ansibleCmd " $result = wsl bash -c "$ansibleCmd 2>&1 | tee $logFile " } else { if (-not $RemoteHost ) { throw "不使用 WSL 时必须指定 RemoteHost 参数" } Write-Host "通过 SSH 在 $RemoteHost 执行: $ansibleCmd " $result = ssh $RemoteHost "$ansibleCmd 2>&1 | tee $logFile " } $successPattern = 'ok=.*changed=.*unreachable=0.*failed=0' $lastLine = ($result | Where-Object { $_ -match 'PLAY RECAP' -or $_ -match 'ok=' }) | Select-Object -Last 1 if ($lastLine -match 'failed=0' -and $lastLine -match 'unreachable=0' ) { Write-Host "`nPlaybook 执行成功" -ForegroundColor Green } else { Write-Host "`nPlaybook 执行存在问题,请检查日志: $logFile " -ForegroundColor Red } return $result }
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 通过 WSL 执行: ansible-playbook -i inventory/production --extra-vars "target=web_servers action=deploy" site.yml PLAY [部署 Web 应用] **** **** **** **** **** **** **** **** **** **** **** **** **** **** *** TASK [Gathering Facts] ** **** **** **** **** **** **** **** **** **** **** **** **** **** *** ok: [web01.vichamp.com] ok: [web02.vichamp.com] TASK [拉取最新代码] ** **** **** **** **** **** **** **** **** **** **** **** **** **** **** *** changed: [web01.vichamp.com] changed: [web02.vichamp.com] TASK [重启应用服务] ** **** **** **** **** **** **** **** **** **** **** **** **** **** **** *** changed: [web01.vichamp.com] changed: [web02.vichamp.com] PLAY RECAP ** **** **** **** **** **** **** **** **** **** **** **** **** **** **** **** **** *** web01.vichamp.com : ok=3 changed=2 unreachable=0 failed=0 web02.vichamp.com : ok=3 changed=2 unreachable=0 failed=0 Playbook 执行成功
注意事项
WinRM 认证安全 :生产环境务必使用 HTTPS 传输,并配合域账户或证书认证。基本认证(Basic Auth)以明文传输密码,仅适合测试环境。如果使用域环境,推荐配置 Kerberos 认证,安全性更高且支持委派。
执行策略与脚本签名 :Ansible 在 Windows 端执行脚本时,WinRM 会话的执行策略可能与交互式会话不同。确保 Ansible 连接用户的执行策略允许脚本运行(RemoteSigned 或 Unrestricted),同时注意自定义模块中的 Set-ExecutionPolicy 不会影响系统全局策略。
双跃点(Double Hop)问题 :当 Ansible 通过 WinRM 连接到 Windows 主机后,该主机再尝试访问网络资源(如文件共享、远程数据库)时,会遇到凭据委派失败的问题。解决方案包括启用 CredSSP、使用 Kerberos 约束委派,或在目标主机上使用 Invoke-Command 配合 -Authentication CredSSP 参数。
模块幂等性 :编写自定义 Ansible 模块时,务必保证幂等性——同一模块多次执行应产生相同结果。在修改任何状态之前先检查当前状态,仅在确实需要变更时才标记 changed = true。这样可以让 playbook 安全地重复执行。
输出编码问题 :PowerShell 的输出编码默认可能不是 UTF-8,导致中文路径或错误信息在 Ansible 日志中显示为乱码。建议在模块开头设置 $OutputEncoding = [System.Text.Encoding]::UTF8,并在 playbook 中配置 ansible_winrm_codepage = 65001。
WSL 与 Ansible 控制节点 :在 Windows 上通过 WSL 运行 Ansible 是开发环境的常见方案,但需注意 WSL 的文件系统性能问题。Playbook 和 inventory 文件建议放在 Linux 文件系统(如 ~/ansible/)下而非挂载的 Windows 盘(/mnt/c/)上,可以显著提升执行速度。