Blog › ICP guides
PowerShell developer on retainer: Azure automation, Windows engineering, and WinRM remoting on monthly retainer
September 24, 2026 · ~20 min read
A financial services firm had an Azure Automation runbook library that managed VM provisioning, storage operations, and compliance reporting across three subscriptions. The team ran a scheduled Az module update in January — upgrading Az.Compute from 5.9.0 to 6.0.0 — and the nightly VM provisioning runbook started silently failing. No error appeared in the Azure Automation job log. The runbook status showed Completed. But 14 VMs that should have been created each night were not being created. The monitoring team noticed the missing capacity three days later when workload scheduling began to fail. The visible symptom was missing VMs; the underlying cause required reading Az.Compute 6.0 breaking changes documentation to find.
A PowerShell developer on Azure retainer traced the problem in two hours. Az.Compute 6.0.0 changed the -NetworkInterfaceId parameter of New-AzVM from accepting a single string to requiring a string array. The runbook was passing a single string: New-AzVM -ResourceGroupName $rg -Location $location -VM $vmConfig -NetworkInterfaceId $nicId. With Az.Compute 6.0, the cmdlet received a type mismatch and returned a non-terminating error — but the runbook had been written with $ErrorActionPreference = 'Continue' at the top level, the default PowerShell behavior, meaning non-terminating errors were logged to the error stream but did not stop execution or change the job exit status. The VM creation was silently skipped. The fix was -NetworkInterfaceId @($nicId) for the parameter and $ErrorActionPreference = 'Stop' with try/catch blocks around all resource creation cmdlets. Reviewing all 14 runbooks in the Az.Compute 6.0 breaking change context identified two additional parameter type changes in Set-AzVMDiskEncryptionExtension and New-AzDisk. The retainer work produced three parameter fixes and error handling additions across 14 runbooks. A PowerShell developer on monthly retainer does this category of work continuously: auditing Azure Automation runbooks against Az module breaking changes before silent failures accumulate in nightly jobs, designing JEA role capability files with minimum-privilege cmdlet constraints before scope drift creates compliance violations, and configuring WinRM remoting for double-hop scenarios before credential delegation failures block production automation.
PowerShell language fundamentals, advanced functions, and module design
PowerShell's foundational design principle — pipeline semantics with objects, not text — distinguishes it from bash and other Unix shells. When Get-Process | Where-Object CPU -gt 100 | Select-Object Name, CPU, Id | Export-Csv processes.csv -NoTypeInformation executes, each cmdlet receives and emits fully typed .NET objects: Where-Object receives System.Diagnostics.Process objects and accesses the CPU property directly as a numeric comparison rather than parsing text; Select-Object projects three named properties; Export-Csv serializes the property values with proper quoting. This object pipeline eliminates the fragile text parsing that makes bash scripts brittle when command output format changes. Advanced functions — PowerShell functions with [CmdletBinding()] — participate in the same cmdlet behavior as compiled cmdlets: they support -Verbose, -Debug, -ErrorAction, -WhatIf, -Confirm common parameters automatically; they support pipeline input processing with begin/process/end blocks; and they support named parameter sets with ParameterSetName for mutually exclusive argument groups.
Parameter validation attributes on advanced function parameters enforce input contracts at the PowerShell runtime level before the function body executes: [ValidateSet('Development','Staging','Production')] restricts a string parameter to three allowed values with tab-completion support in the console; [ValidateRange(1, 65535)] validates an integer is within a TCP port range; [ValidateScript({ Test-Path $_ -PathType Leaf })] validates that a file path exists and is a file, not a directory; [ValidatePattern('^[A-Z]{2}\d{6}$')] validates a string against a regular expression. The ValueFromPipeline attribute allows a parameter to accept values from the pipeline: [Parameter(ValueFromPipeline)][string]$ComputerName — the process { } block then executes once per piped value. Error handling uses try/catch/finally with terminating errors (exceptions) caught in catch [System.Exception] { } blocks; non-terminating errors (cmdlet-generated error records) are converted to terminating errors by setting $ErrorActionPreference = 'Stop' or by passing -ErrorAction Stop to individual cmdlet calls. Module design uses a module manifest .psd1 file declaring the module's root module, version, GUID, required modules, and explicitly exported functions: Export-ModuleMember -Function Get-Report, New-Report in the module script limits the public API surface, keeping internal helper functions private and preventing name collision in the caller's scope.
PowerShell classes — introduced in PowerShell 5.0 — provide syntax for defining .NET types directly in PowerShell scripts: class ServerConfig { [string]$Hostname; [int]$Port = 443; [bool]$TlsEnabled = $true; ServerConfig([string]$h, [int]$p) { $this.Hostname = $h; $this.Port = $p }; [string]ToString() { return "$($this.Hostname):$($this.Port)" } }. Classes support constructor overloads, property getters and setters, static methods, and inheritance via : BaseClass syntax. For output formatting, custom .ps1xml format files define how objects display in the console — View elements specifying which properties appear in table or list format when the object type is piped to Format-Table or Format-List. SecureString handling for credentials: $password = ConvertTo-SecureString 'plaintext' -AsPlainText -Force creates a SecureString from a plaintext value (only for automation scripts where the plaintext is already in a secure store); $cred = New-Object PSCredential('username', $securePassword) creates a PSCredential; in Azure Automation, Get-AutomationPSCredential -Name 'MyCredential' retrieves a credential stored in the Automation account's credential asset without exposing the plaintext.
PowerShell remoting, WinRM configuration, and JEA least-privilege design
PowerShell remoting uses the WS-Management (WinRM) protocol to execute commands on remote systems. Invoke-Command -ComputerName server01 -ScriptBlock { Get-Service Spooler } runs a script block on the remote machine over WinRM, using the current user's Kerberos ticket for authentication in a domain environment. New-PSSession -ComputerName server01 creates a persistent session for multiple commands without per-command authentication overhead; Enter-PSSession -Session $session provides an interactive remote shell. WinRM listener configuration requires winrm quickconfig for HTTP (port 5985, appropriate for domain-joined machines on a secure network) or manual HTTPS listener creation for cross-domain or internet-facing scenarios: New-SelfSignedCertificate -DnsName server01.contoso.com -CertStoreLocation 'Cert:\LocalMachine\My' creates a certificate; New-WSManInstance winrm/config/Listener -SelectorSet @{Transport='HTTPS'} -ValueSet @{Hostname='server01.contoso.com'; CertificateThumbprint=$cert.Thumbprint} creates the HTTPS listener. The double-hop problem — where credentials cannot be forwarded from the jump server to a second remote target — occurs because Kerberos authentication issues a ticket for the first hop that cannot be delegated further unless explicit delegation is configured. CredSSP (Credential Security Support Provider) resolves double-hop by encrypting and forwarding the user's credentials to the first hop, which then uses them for the second hop: Enable-WSManCredSSP -Role Client -DelegateComputer '*.contoso.com' on the originating machine; Enable-WSManCredSSP -Role Server on the jump server; Invoke-Command -ComputerName jumpserver -Authentication CredSSP -Credential $cred -ScriptBlock { Invoke-Command -ComputerName targetserver ... }.
Just Enough Administration (JEA) creates constrained PowerShell session configurations where users authenticate with their own credentials but execute only a precisely scoped set of cmdlets and functions, regardless of their actual system privileges. JEA is configured through two file types: role capability files (.psrc) define what a particular role is allowed to do — VisibleCmdlets lists allowed cmdlets optionally with per-parameter constraints, VisibleFunctions lists allowed functions, VisibleExternalCommands lists allowed executables, VisibleProviders lists allowed PS drives; session configuration files (.pssc) map AD groups or usernames to role capabilities and define the session environment — RunAsVirtualAccount = $true makes the session run as a local virtual account with specified privileges, isolating the elevated context from the user's identity. A role capability file for a helpdesk service restart role: VisibleCmdlets = @{ Name = 'Restart-Service'; Parameters = @{ Name = 'Name'; ValidateSet = @('Spooler','W32Time','WinRM') } }, 'Get-Service', 'Get-EventLog' — the Parameters constraint restricts Restart-Service to only the three named services, even though the role runs with elevated virtual account permissions. Registering the session: Register-PSSessionConfiguration -Name 'HelpdeskEndpoint' -Path 'C:\JEA\helpdesk.pssc' -Force. Users connect with Enter-PSSession -ComputerName server01 -ConfigurationName HelpdeskEndpoint and receive a constrained session where the allowed cmdlets and their parameter constraints are enforced.
PowerShell Desired State Configuration (DSC) manages configuration drift in Windows environments. A DSC configuration is a PowerShell script that uses a Configuration block containing Node blocks and resource declarations: Configuration WebServerConfig { Import-DscResource -ModuleName PSDesiredStateConfiguration; Node 'WebServer01' { WindowsFeature IIS { Name = 'Web-Server'; Ensure = 'Present' }; File IndexPage { DestinationPath = 'C:\inetpub\wwwroot\index.html'; SourcePath = '\\fileserver\web\index.html'; Ensure = 'Present'; Type = 'File'; Checksum = 'SHA-256' } } }. Running WebServerConfig -OutputPath 'C:\DSC' compiles the configuration to a MOF file; Start-DscConfiguration -Path 'C:\DSC' -Wait -Verbose applies it. The Local Configuration Manager (LCM) on each target node enforces the configuration on a schedule — Set-DscLocalConfigurationManager configures the LCM's refresh mode (Push or Pull), refresh interval, and what happens on configuration drift (ApplyAndMonitor reports drift without correcting it; ApplyAndAutoCorrect automatically remediates). Test-DscConfiguration -ComputerName server01 reports whether the node is compliant with its current MOF without making changes — used in monitoring and auditing workflows to detect drift without triggering remediation.
Azure PowerShell automation and Managed Identity design
Azure PowerShell uses the Az module family — Az.Accounts, Az.Compute, Az.Storage, Az.Network, Az.KeyVault, and others — to manage Azure resources. Connect-AzAccount authenticates interactively for development; for automation, service principal authentication uses Connect-AzAccount -ServicePrincipal -ApplicationId $appId -TenantId $tenantId -Credential $psCred where $psCred is a PSCredential with the service principal client secret. Certificate-based service principal authentication — Connect-AzAccount -ServicePrincipal -ApplicationId $appId -TenantId $tenantId -CertificateThumbprint $thumbprint — is preferred over secrets for production runbooks because certificates can be rotated without changing the runbook code. In Azure Automation runbooks, Managed Identity authentication replaces deprecated Run As Accounts: Connect-AzAccount -Identity authenticates the runbook using the Automation account's system-assigned Managed Identity, which must be granted RBAC roles on the target subscriptions (e.g., New-AzRoleAssignment -ObjectId $managedIdentityObjectId -RoleDefinitionName 'Contributor' -Scope "/subscriptions/$subscriptionId").
Azure VM lifecycle management with Az.Compute uses a VM configuration object pattern for creation — the $vmConfig object accumulates configuration settings before the VM is committed: $vmConfig = New-AzVMConfig -VMName $name -VMSize 'Standard_D4s_v3'; $vmConfig = Set-AzVMOperatingSystem -VM $vmConfig -Windows -ComputerName $name -Credential $adminCred -ProvisionVMAgent -EnableAutoUpdate; $vmConfig = Set-AzVMSourceImage -VM $vmConfig -PublisherName 'MicrosoftWindowsServer' -Offer 'WindowsServer' -Skus '2022-datacenter-azure-edition' -Version 'latest'; $vmConfig = Add-AzVMNetworkInterface -VM $vmConfig -Id $nic.Id; New-AzVM -ResourceGroupName $rg -Location 'eastus' -VM $vmConfig. Az.Storage operations for blob management: $storageCtx = New-AzStorageContext -StorageAccountName $accountName -UseConnectedAccount creates an OAuth-authenticated context using the current logged-in principal; Set-AzStorageBlobContent -Container $containerName -File $localFile -Blob $blobName -Context $storageCtx -Force uploads a file. Az module versioning requires explicit pinning in production runbooks — specifying Requires -Modules @{ModuleName='Az.Compute'; ModuleVersion='5.9.0'} at the top of runbooks prevents automatic upgrades from introducing breaking changes, at the cost of missing security patches; the operationally correct approach is to pin the minimum version, run the runbook in an automation test environment after each Az module upgrade, and promote pinned versions after testing.
Azure Automation hybrid worker configuration extends runbooks to on-premises and non-Azure resources: the Hybrid Runbook Worker agent runs on Windows or Linux machines and connects to the Automation account, allowing runbooks to execute locally with access to on-premises Active Directory, SQL Server, file shares, and internal APIs that are not reachable from the Azure-side runbook sandbox. Hybrid workers authenticate to Azure using Managed Identity when available, or a workspace-linked automation credential. The runbook targets a hybrid worker group with Start-AzAutomationRunbook -AutomationAccountName $aa -Name $runbook -RunOn 'HybridWorkerGroup01' -Parameters $params. Automation schedules trigger runbooks on a cron-like basis: New-AzAutomationSchedule -AutomationAccountName $aa -ResourceGroupName $rg -Name 'NightlyProvision' -StartTime (Get-Date '02:00:00') -DayInterval 1 -TimeZone 'UTC'; Register-AzAutomationScheduledRunbook -RunbookName $runbook -ScheduleName 'NightlyProvision' -AutomationAccountName $aa -ResourceGroupName $rg.
How HourTab tracks PowerShell developer retainer hours
PowerShell developer retainers — particularly in Azure Automation and JEA contexts — produce work-to-deliverable ratios that are invisible in the diff. The session that resolved the silent VM creation failures produced three parameter type fixes and $ErrorActionPreference = 'Stop' additions across 14 runbook files. The session involved reading the Az.Compute 6.0 breaking changes documentation for the eight cmdlets used across the runbook library, identifying which parameter type changes affected the nightly provisioning runbook, writing a test script to reproduce the silent failure by intentionally passing a single string to -NetworkInterfaceId and observing the non-terminating error swallowed by 'Continue', applying @($nicId) wrapping for the array parameter, converting all resource creation cmdlets to use -ErrorAction Stop inside try/catch blocks, running the corrected runbook against the development subscription to verify successful VM creation, and documenting the Az module version pinning policy to prevent silent breaking change adoption in the future. The log entry “fixed VM provisioning, 2h” gives the client no path from 2 hours to the 3-day detection delay that the silent failure caused — because nothing in three parameter wraps and error handling additions communicates the Az module breaking change documentation analysis that identified the root cause across 14 runbooks.
HourTab gives PowerShell developers a public retainer-hours URL they share with each client at the start of the engagement. The client opens the URL and sees the current burn-down without logging in. For Azure Automation and Windows engineering retainers specifically, the work log format carries the weight: each entry should name the Az cmdlet parameter change and error handling scope (New-AzVM -NetworkInterfaceId @($nicId) — @() array wrap for Az.Compute 6.0 type change; $ErrorActionPreference='Stop' + try/catch added to all 14 runbooks; silent VM creation failure: 14 VMs/night → provisioning correctly), the WinRM/CredSSP configuration change and double-hop verification (Enable-WSManCredSSP -Role Server on jump01; -Authentication CredSSP added to Invoke-Command; double-hop Invoke-Command from workstation→jump01→sqlserver01: Access Denied → authenticated; SQL query return: 847 rows vs 0 before), the JEA role capability constraint change with scope audit (Restart-Service VisibleCmdlets Parameters ValidateSet @('Spooler','W32Time','WinRM') — wildcard Service* removed; 3 services constrained vs all services before; Get-PSSessionCapability -Username helpdesk01 output verified: 4 visible cmdlets vs 127 before), and the DSC configuration drift report (Test-DscConfiguration -ComputerName webserver01..10: 3 non-compliant nodes (missing index.html checksum mismatch); Start-DscConfiguration applied; compliance: 7/10 → 10/10). That entry takes five minutes to write and turns the client’s next check-in from a forty-minute explanation of what Az.Compute breaking changes mean for nightly provisioning into a two-sentence acknowledgment that VMs are being created correctly and the JEA scope audit is complete.
The retainer model fits PowerShell platform engineering because the language's primary deployment contexts — Azure Automation runbooks, Windows infrastructure management, JEA-secured administrative endpoints — are long-lived automation platforms where the Az module versions, Windows Server versions, and Active Directory configurations evolve continuously. A runbook library that works correctly against Az.Compute 5.9 silently breaks against 6.0 when a team member clicks “Update modules” in the Azure portal without reading the breaking changes log. A JEA role capability file that correctly constrains helpdesk users on day one drifts toward wildcard permissions over 18 months as new service restart requests are handled by adding Service* instead of extending the ValidateSet. A WinRM configuration that works for single-hop Invoke-Command fails immediately for any automation that requires a jump server or database server access pattern. A project contract closes when the current runbook failure or JEA audit finding is resolved. A PowerShell retainer stays open for the next Az module breaking change that silently affects a different runbook, the next JEA role capability drift that a quarterly access review identifies, and the next Windows Server upgrade that changes WinRM default settings and requires listener reconfiguration.
Track PowerShell developer retainer hours without the status emails
HourTab gives PowerShell and Azure automation engineers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: PowerShell developer retainers
What does a PowerShell developer on retainer typically do?
A PowerShell developer on monthly retainer provides ongoing advisory across core PowerShell (advanced function [CmdletBinding()]/[Parameter()] design with ValidateSet/ValidateScript/ValidateRange, try/catch/$ErrorActionPreference/ErrorAction Stop error handling, $script:/$global:/$env: scope management, module .psd1 manifest and Export-ModuleMember API design, PSCustomObject pipeline output, SecureString/PSCredential credential handling), remoting (New-PSSession/Invoke-Command session management, WinRM HTTPS listener configuration, CredSSP vs Kerberos double-hop delegation, JEA role capability .psrc VisibleCmdlets/Parameters/ValidateSet design, session configuration .pssc RoleDefinitions, Register-PSSessionConfiguration, SSH-based remoting), Azure PowerShell (Connect-AzAccount service principal and Managed Identity, Az.Compute VM lifecycle, Az.Storage blob operations, Azure Automation runbook Managed Identity, Az module version management), and scripting patterns (Import-Csv/ConvertFrom-Json, Select-Object/Where-Object/Sort-Object/Group-Object pipelines, Export-Csv/ConvertTo-Json, Invoke-RestMethod REST integration).
What PowerShell work is most underlogged in a retainer?
Az module breaking change migration (Az.Compute 5.9→6.0 -NetworkInterfaceId type change from string to array causing silent New-AzVM failure with $ErrorActionPreference='Continue'; 14–24 hours invisible in @() wraps and try/catch additions across 14 runbooks), WinRM double-hop CredSSP configuration (Invoke-Command jump→target Access Denied due to Kerberos delegation stop at first hop; Enable-WSManCredSSP -Role Server/-Client and -Authentication CredSSP fix; 10–18 hours invisible in two WSManCredSSP commands), and JEA role capability scope drift (VisibleCmdlets wildcard Service* allowing all service names instead of constrained ValidateSet; compliance finding; 8–16 hours invisible in VisibleCmdlets Parameters ValidateSet narrowing and Register-PSSessionConfiguration re-registration) are the three most systematically underlogged PowerShell retainer categories.
What are typical PowerShell developer retainer rates?
Entry-level PowerShell developers (1–3 years, basic cmdlets, simple pipeline operations, basic Azure VM management, straightforward remoting) bill at $75–$130/hr. Mid-level PowerShell engineers (3–7 years, advanced function [CmdletBinding()]/[Parameter()] design, Azure Automation runbook Managed Identity, WinRM HTTPS configuration, JEA role capability design, Az module version management, DSC resource authoring) bill at $120–$215/hr. Senior PowerShell architects (7+ years, JEA constrained endpoint design for PCI/SOX compliance, Azure Automation hybrid worker, DSC partial configuration composition, pull server architecture, custom DSC resource MOF schema authoring, complex Az cross-subscription automation) bill at $175–$315/hr. Firm rates run $145–$255/hr. Monthly retainer amounts: $2,500–$6,000/mo for advisory (15–30 hrs), $8,500–$18,000/mo for full Azure automation platform or enterprise JEA/DSC engagements.
What should a PowerShell developer retainer agreement include?
A PowerShell developer retainer agreement should specify PowerShell scope ([CmdletBinding()]/[Parameter()] with ValidateSet/ValidateScript/ValidateRange, try/catch/$ErrorActionPreference/ErrorAction Stop, $script:/$global:/$env: scope, module .psd1 manifest and Export-ModuleMember, PSCustomObject, SecureString/PSCredential, PowerShell class syntax), remoting scope (WinRM HTTP/HTTPS listener configuration, CredSSP vs Kerberos double-hop, JEA .psrc VisibleCmdlets/Parameters/ValidateSet design, .pssc RoleDefinitions, Register-PSSessionConfiguration, SSH remoting), Azure scope (Connect-AzAccount service principal and -Identity Managed Identity, Az.Compute VM lifecycle, Az.Storage, Azure Automation runbook authoring, Az module version pinning policy), scripting scope (pipeline operations, Import-Csv/Export-Csv, ConvertTo/ConvertFrom-Json, Invoke-RestMethod, scheduled jobs), and hour logging specifics (Az cmdlet parameter type fix scope and error handling additions, WinRM/CredSSP configuration and double-hop verification, JEA VisibleCmdlets scope audit and re-registration).
How should PowerShell developer retainer hours be logged?
Log each PowerShell retainer session with: advisory category ([CmdletBinding()]/[Parameter()] attribute design, ValidateSet/ValidateScript/ValidateRange validation, try/catch/$ErrorActionPreference/ErrorAction Stop scope, $script:/$global:/$env: scope management, module .psd1 manifest Export-ModuleMember API surface, PSCustomObject pipeline output, SecureString/PSCredential handling, PowerShell class constructor/method syntax, WinRM winrm quickconfig HTTP/HTTPS listener Set-WSManInstance, CredSSP Enable-WSManCredSSP -Role Client/Server with DelegateComputer, Kerberos delegation, JEA .psrc VisibleCmdlets Parameters ValidateSet design, .pssc RoleDefinitions RunAsVirtualAccount, Register-PSSessionConfiguration, SSH New-PSSession cross-platform, Connect-AzAccount -ServicePrincipal/-Identity, Az.Compute New-AzVM/Get-AzVM/Start-AzVM/Stop-AzVM, Az.Storage Set-AzStorageBlobContent/New-AzStorageContext, Azure Automation Get-AutomationPSCredential Managed Identity, Az module version breaking change audit, Select-Object/Where-Object/Sort-Object/Group-Object pipeline, Export-Csv -NoTypeInformation, ConvertTo-Json -Depth, Invoke-RestMethod, DSC Configuration Node Resource Test-DscConfiguration), specific runbook, script, or session configuration file, diagnostic tool (Azure Automation job log: non-terminating error Az.Compute; Get-WinEvent -LogName CAPI2: CredSSP certificate chain; Get-PSSessionCapability: JEA visible cmdlet count; Test-WSMan: WinRM connectivity; Test-DscConfiguration: compliance status), fix applied with rationale, before/after metric (VMs created: 0/night silent fail → 14/night; Invoke-Command: Access Denied → authenticated; JEA cmdlets: 127 → 4 constrained; DSC compliance: 7/10 → 10/10), and hours.