P08: Remote Server Management Tool
P08: Remote Server Management Tool
Project Overview
What youโll build: A comprehensive tool for managing multiple Windows serversโcheck service status, deploy configuration files, restart services, collect logsโall from your workstation. This is the backbone of enterprise Windows administration.
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced |
| Time Estimate | 2 weeks |
| Programming Language | PowerShell |
| Knowledge Area | Windows Administration, Networking, Security |
| Prerequisites | Intermediate PowerShell, access to test servers/VMs |
| Coolness Level | Level 1: Pure Corporate Snoozefest |
| Business Potential | 3. The โService & Supportโ Model |
| Main Book | โPowerShell in Depthโ by Don Jones et al. |
Learning Objectives
After completing this project, you will be able to:
- Master PowerShell Remoting - Understand the complete WinRM/WSMan architecture and how remote execution actually works under the hood
- Manage PSSession lifecycles - Create, reuse, monitor, and cleanup persistent sessions efficiently across server fleets
- Execute commands in parallel - Run operations on dozens or hundreds of machines simultaneously with proper throttling
- Handle credentials securely - Implement proper credential storage, delegation, and avoid common security pitfalls like CredSSP misuse
- Transfer files remotely - Copy configuration files, scripts, and logs to/from remote machines over remoting sessions
- Build resilient distributed tools - Handle network failures, timeouts, partial failures, and aggregate errors meaningfully
- Design for scale - Structure code that works for 3 servers and 300 servers without refactoring
- Implement enterprise logging - Track every operation for audit trails and troubleshooting
Deep Theoretical Foundation
PowerShell Remoting Architecture: WinRM and WSMan
PowerShell Remoting is built on top of WS-Management (WS-Man), an industry-standard SOAP-based protocol for managing systems. Microsoftโs implementation is the Windows Remote Management (WinRM) service.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PowerShell Remoting Architecture โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Your Workstation โ โ Remote Server โ โ
โ โ โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โ HTTPS/5986 โ โโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ PowerShell.exe โ โ HTTP/5985 โ โ WinRM Service โ โ โ
โ โ โ โ โ โโโโโโโโโโโโโโโโโโ>โ โ โ โ โ
โ โ โ Invoke-Command โ โ WS-Man/SOAP โ โ Listener โ โ โ
โ โ โ Enter-PSSession โ โ + Encryption โ โ โ โ โ โ
โ โ โ New-PSSession โ โ โ โ v โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โ โ โ wsmprovhost.exe โ โ โ
โ โ โ โ โ โ (Host Process) โ โ โ
โ โ v โ โ โ โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โ โ โ v โ โ โ
โ โ โ WSMan Provider โ โ โ โ PowerShell.exe โ โ โ
โ โ โ (Client-side) โ โ โ โ (Your Script) โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Protocol Layers: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Application: PowerShell Remoting Protocol (MS-PSRP) โ โ
โ โ Transport: WS-Management (SOAP messages) โ โ
โ โ Security: Kerberos, NTLM, or Certificate Authentication โ โ
โ โ Encryption: HTTP + Message Encryption OR HTTPS (TLS) โ โ
โ โ Network: TCP 5985 (HTTP) or 5986 (HTTPS) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why WS-Man over DCOM?
The legacy approach (used by WMIโs Get-WmiObject) used DCOM (Distributed COM), which is:
- Firewall-hostile (uses dynamic port ranges)
- Difficult to secure
- Complex to troubleshoot
- Not designed for modern distributed systems
WS-Man uses standard HTTPS/HTTP, traverses firewalls cleanly, and provides predictable behavior.
The WinRM Service
On every Windows machine, the WinRM service:
- Listens on configured ports (default 5985/5986)
- Authenticates incoming connections
- Spawns a host process (
wsmprovhost.exe) for each session - Manages session lifecycle, timeouts, and resource limits
Understanding the Security Model
Authentication Flow:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ 1. Client connects to WinRM listener โ
โ โ โ
โ v โ
โ 2. Authentication negotiation (SPNEGO) โ
โ โโ> Domain environment: Kerberos preferred โ
โ โโ> Workgroup: NTLM fallback โ
โ โ โ
โ v โ
โ 3. Authorization check โ
โ โโ> User must be in local Administrators OR โ
โ โโ> User must be in Remote Management Users group โ
โ โ โ
โ v โ
โ 4. Session established with user's security context โ
โ โโ> Commands run as the authenticated user โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Session Management: Enter-PSSession, Invoke-Command, PSSessions
PowerShell provides three distinct approaches to remote execution:
1. Enter-PSSession (Interactive)
# Opens an interactive remote shell
Enter-PSSession -ComputerName Server01
# Your prompt changes:
[Server01]: PS C:\Users\Admin> Get-Process
[Server01]: PS C:\Users\Admin> exit # Returns to local session
When to use: Troubleshooting, exploration, one-off commands. NOT for automation.
2. Invoke-Command with -ComputerName (Implicit Session)
# Creates connection, runs command, closes connection
Invoke-Command -ComputerName Server01 -ScriptBlock {
Get-Service -Name "Spooler"
}
Lifecycle:
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Connection โ --> โ Command โ --> โ Connection โ
โ Established โ โ Execution โ โ Closed โ
โ (~500ms) โ โ (~variable) โ โ (~100ms) โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
When to use: Single commands, simple scripts, when session state doesnโt matter.
3. Explicit PSSessions (Persistent)
# Create sessions to multiple servers
$sessions = New-PSSession -ComputerName Server01, Server02, Server03
# Reuse sessions for multiple operations
Invoke-Command -Session $sessions -ScriptBlock { Get-Service }
Invoke-Command -Session $sessions -ScriptBlock { Get-Process }
Invoke-Command -Session $sessions -ScriptBlock { Get-EventLog -LogName System -Newest 10 }
# Copy files (requires sessions)
Copy-Item -Path ".\config.xml" -Destination "C:\App\config.xml" -ToSession $sessions[0]
# Clean up
Remove-PSSession $sessions
Lifecycle:
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Sessions โ --> โ Command 1 โ --> โ Command 2 โ --> โ Sessions โ
โ Created Once โ โ Execution โ โ Execution โ โ Removed โ
โ (~500msรN) โ โ (~fast) โ โ (~fast) โ โ (~100ms) โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ โ
v v
State preserved State preserved
(variables, modules, working directory)
When to use:
- Multiple commands to the same servers
- File copy operations (REQUIRED)
- When you need to preserve state between commands
- Performance-critical scenarios
Session Decision Matrix
| Scenario | Best Approach | Why |
|---|---|---|
| Single command, single server | Invoke-Command -ComputerName | Simple, no cleanup needed |
| Single command, many servers | Invoke-Command -ComputerName | Parallel by default |
| Multiple commands, same servers | New-PSSession + Invoke-Command -Session | Reuse connections |
| File copy operations | New-PSSession + Copy-Item -ToSession | Required for file ops |
| Interactive exploration | Enter-PSSession | Real-time feedback |
| Long-running operations | New-PSSession with error handling | Control over lifecycle |
Parallel Execution: ForEach-Object -Parallel and -ThrottleLimit
The Problem with Serial Execution
# This is SLOW - each server waits for the previous
$servers = @("Server01", "Server02", "Server03", "Server04", "Server05")
foreach ($server in $servers) {
Invoke-Command -ComputerName $server -ScriptBlock {
Start-Sleep -Seconds 2 # Simulating work
Get-Service
}
}
# Total time: 5 servers ร 2 seconds = 10+ seconds
Invoke-Commandโs Built-in Parallelism
# This is FAST - all servers execute simultaneously
$servers = @("Server01", "Server02", "Server03", "Server04", "Server05")
Invoke-Command -ComputerName $servers -ScriptBlock {
Start-Sleep -Seconds 2
Get-Service
}
# Total time: ~2-3 seconds (parallel execution)
Controlling Parallelism with -ThrottleLimit
# Default ThrottleLimit is 32 concurrent connections
# Lower it when servers can't handle simultaneous load:
Invoke-Command -ComputerName $largeServerList -ThrottleLimit 10 -ScriptBlock {
# Heavy operation
Get-ChildItem -Path "C:\" -Recurse -File | Measure-Object
}
ThrottleLimit Guidelines
| Server Count | ThrottleLimit | Rationale |
|---|---|---|
| 1-10 | Default (32) | No throttling needed |
| 10-50 | 20-30 | Slight throttling |
| 50-200 | 10-20 | Prevent network saturation |
| 200+ | 5-10 | Heavy throttling, consider batching |
ForEach-Object -Parallel (PowerShell 7+)
# Modern parallel processing with more control
$servers | ForEach-Object -Parallel {
$server = $_
# Each iteration runs in its own runspace
$result = Invoke-Command -ComputerName $server -ScriptBlock {
Get-Service -Name "Spooler"
}
# Return structured result
[PSCustomObject]@{
Server = $server
SpoolerStatus = $result.Status
Timestamp = Get-Date
}
} -ThrottleLimit 10
Understanding the Execution Model
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Invoke-Command -ComputerName Server1,Server2,Server3 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ PowerShell Engine โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Runspace Pool (ThrottleLimit) โ โ
โ โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ โ
โ โ โ Runspace 1 โ โ Runspace 2 โ โ Runspace 3 โ ... โ โ
โ โ โ โ โ โ โ โ โ โ
โ โ โ Server01 โ โ Server02 โ โ Server03 โ โ โ
โ โ โ connection โ โ connection โ โ connection โ โ โ
โ โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ โ
โ โ โ โ โ โ โ
โ โ v v v โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Results Aggregation โ โ โ
โ โ โ PSComputerName property added to each output object โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Credential Management and Delegation (CredSSP)
The Double-Hop Problem
When you connect to a remote server and try to access a third resource (network share, another server, SQL database), your credentials donโt automatically forward:
โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
โ Your PC โ -------> โ Server01 โ ---X---> โ FileShareโ
โ โ Creds OK โ โ No Creds โ โ
โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
โ
โโโ "Access Denied" when trying to reach FileShare
Why does this happen?
Kerberos and NTLM donโt forward credentials by defaultโthis is a security feature! Your password hash on Server01 canโt be used to authenticate elsewhere.
Solution 1: CredSSP (Credential Security Support Provider)
# Enable CredSSP on CLIENT (your workstation)
Enable-WSManCredSSP -Role Client -DelegateComputer "*.contoso.com" -Force
# Enable CredSSP on SERVER (target servers)
Enable-WSManCredSSP -Role Server -Force
# Use CredSSP authentication
$cred = Get-Credential
Invoke-Command -ComputerName Server01 -Authentication CredSSP -Credential $cred -ScriptBlock {
# Now can access network resources
Get-ChildItem "\\FileServer\Share"
}
CredSSP Security Warning
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CredSSP SECURITY RISKS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ CredSSP sends your ACTUAL CREDENTIALS to the remote server. โ
โ โ
โ This means: โ
โ - A compromised server can capture your credentials โ
โ - Your credentials exist in memory on the remote server โ
โ - Credential theft attacks (Mimikatz) can harvest them โ
โ โ
โ ONLY use CredSSP when: โ
โ 1. You trust the remote server completely โ
โ 2. The server has proper security hardening โ
โ 3. You need double-hop and have no alternative โ
โ 4. You're using a service account, not your admin credentials โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Solution 2: Kerberos Constrained Delegation (Preferred)
Configured in Active Directory, allows specific services to delegate to specific resources without exposing credentials:
Administrator configures in AD:
Server01 can delegate to โ FileServer (CIFS service only)
Solution 3: Resource-Based Constrained Delegation (RBCD)
Modern approach where the resource controls who can delegate to it:
# On FileServer, allow Server01 to access it via delegation
Set-ADComputer -Identity FileServer -PrincipalsAllowedToDelegateToAccount Server01$
Secure Credential Storage
# NEVER store credentials in plain text!
# Method 1: Export-Clixml (encrypted to current user on current machine)
$cred = Get-Credential
$cred | Export-Clixml -Path "$HOME\ServerCred.xml"
# Load later (only works for same user on same machine)
$cred = Import-Clixml -Path "$HOME\ServerCred.xml"
# Method 2: Windows Credential Manager
# Store
[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]
$vault = New-Object Windows.Security.Credentials.PasswordVault
$credential = New-Object Windows.Security.Credentials.PasswordCredential("ServerManager", "admin", "password")
$vault.Add($credential)
# Method 3: Azure Key Vault or HashiCorp Vault for enterprise
File Copying to/from Remote Machines
Copy-Item -ToSession and -FromSession
# Copy TO remote server
$session = New-PSSession -ComputerName Server01
Copy-Item -Path "C:\Local\config.xml" -Destination "C:\Remote\config.xml" -ToSession $session
# Copy FROM remote server
Copy-Item -Path "C:\Remote\logs\" -Destination "C:\Local\CollectedLogs\" -FromSession $session -Recurse
# Copy entire folders
Copy-Item -Path "C:\DeployPackage\" -Destination "C:\App\" -ToSession $session -Recurse -Force
Remove-PSSession $session
Understanding the Transfer Mechanism
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ File Transfer over PS Remoting โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Copy-Item -ToSession โ
โ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ โ
โ โ Local File โ ------> โ Serialized via WS-Man โ ------> โ Remote โ โ
โ โ C:\config.xmlโ Chunked โ (SOAP messages, base64) โ โ File โ โ
โ โ โ ~512KB โ Encrypted in transit โ โ โ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ โ
โ โ
โ Performance Characteristics: โ
โ - Slower than SMB for large files (SOAP overhead) โ
โ - Works through firewalls (single port) โ
โ - Encrypted by default โ
โ - No need for file shares โ
โ โ
โ Best Practices: โ
โ - Use for config files, scripts (<10MB) โ
โ - For large files, consider BITS or direct SMB โ
โ - Compress before transfer: Compress-Archive โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Handling Large Transfers
# For large deployments, compress first
$source = "C:\DeployPackage"
$archive = "$env:TEMP\deploy.zip"
# Compress locally
Compress-Archive -Path "$source\*" -DestinationPath $archive -Force
# Transfer single file
Copy-Item -Path $archive -Destination "C:\Temp\deploy.zip" -ToSession $session
# Extract on remote
Invoke-Command -Session $session -ScriptBlock {
Expand-Archive -Path "C:\Temp\deploy.zip" -DestinationPath "C:\App\" -Force
Remove-Item "C:\Temp\deploy.zip"
}
Security Considerations
Network Security
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Security Checklist โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ TRANSPORT SECURITY: โ
โ [ ] Use HTTPS (port 5986) for cross-network/internet โ
โ [ ] HTTP (port 5985) acceptable on trusted internal networks โ
โ [ ] Messages encrypted regardless of HTTP/HTTPS โ
โ โ
โ AUTHENTICATION: โ
โ [ ] Prefer Kerberos (domain environments) โ
โ [ ] Avoid Basic authentication (sends password in clear) โ
โ [ ] Use certificate authentication for workgroup scenarios โ
โ โ
โ AUTHORIZATION: โ
โ [ ] Limit who can remote (Remote Management Users group) โ
โ [ ] Consider JEA (Just Enough Administration) for delegation โ
โ [ ] Audit remote sessions (Event Log) โ
โ โ
โ CREDENTIAL HANDLING: โ
โ [ ] Never store passwords in scripts โ
โ [ ] Use encrypted credential files or vaults โ
โ [ ] Avoid CredSSP unless absolutely necessary โ
โ [ ] Use service accounts, not personal admin accounts โ
โ โ
โ MONITORING: โ
โ [ ] Log all remote operations โ
โ [ ] Forward WinRM events to SIEM โ
โ [ ] Alert on unusual remote session patterns โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
WinRM Configuration Hardening
# View current configuration
winrm get winrm/config
# Key security settings
winrm set winrm/config/service '@{AllowUnencrypted="false"}'
winrm set winrm/config/service/auth '@{Basic="false"}'
winrm set winrm/config/service/auth '@{Kerberos="true"}'
# Limit maximum concurrent operations per user
winrm set winrm/config/winrs '@{MaxConcurrentUsers="10"}'
winrm set winrm/config/winrs '@{MaxShellsPerUser="5"}'
Complete Project Specification
Functional Requirements
| ID | Requirement | Priority | Description |
|---|---|---|---|
| F1 | Multi-server connection | Must Have | Connect to 1-100+ servers via PS Remoting |
| F2 | Service status query | Must Have | Check service status across fleet |
| F3 | Service control | Must Have | Start/stop/restart services remotely |
| F4 | File deployment | Must Have | Copy configuration files to servers |
| F5 | Log collection | Must Have | Gather log files from remote servers |
| F6 | Arbitrary script execution | Must Have | Run custom scripts on remote machines |
| F7 | Credential management | Should Have | Secure credential storage and retrieval |
| F8 | Parallel execution | Should Have | Concurrent operations with throttling |
| F9 | Session pooling | Should Have | Reuse connections efficiently |
| F10 | Operation logging | Should Have | Audit trail of all operations |
| F11 | Server inventory | Should Have | Maintain list of managed servers |
| F12 | Per-server error tracking | Should Have | Report errors per server, not fail-all |
| F13 | Connection validation | Nice to Have | Test connectivity before operations |
| F14 | Rollback support | Nice to Have | Undo file deployments |
| F15 | Progress reporting | Nice to Have | Real-time progress for long operations |
Non-Functional Requirements
| ID | Requirement | Metric |
|---|---|---|
| NF1 | Performance | 10 servers queried in < 10 seconds |
| NF2 | Performance | 100 servers queried in < 60 seconds |
| NF3 | Reliability | Handle 20% server failures gracefully |
| NF4 | Timeout | Individual server timeout: 30 seconds |
| NF5 | Security | Credentials never in logs or output |
| NF6 | Compatibility | Works with domain and workgroup machines |
| NF7 | Logging | All operations logged with timestamp |
| NF8 | Modularity | Installable as PowerShell module |
Real World Outcome
When complete, youโll have a professional server management toolkit:
Multi-Server Query Examples
# Query SQL services across database servers
Get-ServiceStatus -ComputerName $DBServers -ServiceName "SQL*" -Credential $cred
# Output:
# ComputerName ServiceName DisplayName Status StartType
# ------------ ----------- ----------- ------ ---------
# DBSRV01 MSSQLSERVER SQL Server (MSSQLSERVER) Running Automatic
# DBSRV01 SQLSERVERAGENT SQL Server Agent Running Automatic
# DBSRV02 MSSQLSERVER SQL Server (MSSQLSERVER) Stopped Automatic <-- ALERT!
# DBSRV02 SQLSERVERAGENT SQL Server Agent Stopped Automatic
# DBSRV03 MSSQLSERVER SQL Server (MSSQLSERVER) Running Automatic
# DBSRV03 SQLSERVERAGENT SQL Server Agent Running Automatic
# DBSRV04 ERROR Connection Failed - -
Parallel Execution Output
# Deploy configuration to 50 web servers
$result = Deploy-ConfigFile -ComputerName $WebServers `
-SourcePath ".\web.config" `
-DestinationPath "C:\inetpub\wwwroot\web.config" `
-Backup `
-Credential $cred
# Real-time progress:
# [=============================>....................] 58% - 29/50 servers complete
#
# Deploying to WEB01... Success
# Deploying to WEB02... Success
# Deploying to WEB03... Failed (Access Denied)
# Deploying to WEB04... Success
# ...
$result | Where-Object Status -eq 'Failed' | Format-Table ComputerName, Error
# ComputerName Error
# ------------ -----
# WEB03 Access is denied. (Exception from HRESULT: 0x80070005)
# WEB17 The WinRM client cannot process the request (timeout)
# WEB32 The network path was not found
Error Handling Across Servers
# Restart services with comprehensive error handling
$restartResult = Restart-RemoteService -ComputerName $AppServers `
-ServiceName "MyAppService" `
-Wait `
-TimeoutSeconds 120 `
-Credential $cred
# Summary output:
#
# Remote Service Restart Results
# ==============================
# Total Servers: 25
# Successful: 22
# Failed: 3
# Duration: 45 seconds
#
# Failures:
# ---------
# APPSRV05: Service 'MyAppService' not found
# APPSRV12: Failed to stop service within timeout
# APPSRV19: Connection refused (WinRM not running)
#
# All successful servers now have MyAppService in 'Running' state.
Credential Prompt Handling
# First run - prompts for credentials
$manager = New-ServerManager -ServerListPath ".\servers.json" -SaveCredential
# Credential prompt appears:
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# โ Windows PowerShell credential request โ
# โ โ
# โ Enter your credentials for server management. โ
# โ โ
# โ User name: [DOMAIN\AdminUser_____________] โ
# โ Password: [**************************___] โ
# โ โ
# โ [ OK ] [ Cancel ] โ
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Credentials saved encrypted to $HOME\ServerManager_cred.xml
# Subsequent runs - no prompt
$manager = New-ServerManager -ServerListPath ".\servers.json"
# Loads saved credentials automatically
Log Collection Workflow
# Collect application logs from last 24 hours
$logs = Get-RemoteLogs -ComputerName $WebServers `
-LogPath "C:\Logs\Application*.log" `
-Since (Get-Date).AddHours(-24) `
-LocalDestination "C:\CollectedLogs\$(Get-Date -Format 'yyyy-MM-dd')" `
-Credential $cred
# Progress:
# Collecting from WEB01... 3 files (12.4 MB)
# Collecting from WEB02... 5 files (28.1 MB)
# Collecting from WEB03... 2 files (4.2 MB)
# ...
#
# Summary:
# Total files collected: 145
# Total size: 342.7 MB
# Stored in: C:\CollectedLogs\2025-01-15\
# View collected files
$logs | Group-Object ComputerName |
Select-Object Name, Count, @{N='TotalMB';E={[math]::Round(($_.Group | Measure-Object Size -Sum).Sum/1MB, 2)}}
# Name Count TotalMB
# ---- ----- -------
# WEB01 3 12.40
# WEB02 5 28.10
# WEB03 2 4.20
# ...
Solution Architecture
Component Diagram
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Server Management Module โ
โ ServerManager.psm1 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ PUBLIC FUNCTIONS โ โ
โ โ (Exported in .psd1 manifest, callable by users) โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Get-ServiceStatus โ โ Deploy-ConfigFile โ โ Get-RemoteLogs โ โ โ
โ โ โ โ โ โ โ โ โ โ
โ โ โ Query services โ โ Copy files to โ โ Collect files โ โ โ
โ โ โ across servers โ โ remote servers โ โ from servers โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Start-Remote โ โ Stop-Remote โ โ Restart-Remote โ โ โ
โ โ โ Service โ โ Service โ โ Service โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Invoke-Remote โ โ Test-Server โ โ New-Server โ โ โ
โ โ โ Script โ โ Connection โ โ Manager โ โ โ
โ โ โ Run arbitrary PS โ โ Validate remoting โ โ Initialize module โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โ calls โ
โ v โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ PRIVATE FUNCTIONS โ โ
โ โ (Internal use only, not exported) โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Get-ServerSession โ โ Write-Operation โ โ ConvertTo- โ โ โ
โ โ โ โ โ Log โ โ ServerResult โ โ โ
โ โ โ Session pool mgmt โ โ Audit logging โ โ Normalize output โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Get-Saved โ โ Save- โ โ Test- โ โ โ
โ โ โ Credential โ โ Credential โ โ SessionHealth โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โ uses โ
โ v โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ MODULE STATE โ โ
โ โ $script:SessionPool - Hash table of active PSSessions โ โ
โ โ $script:Credential - Cached credential object โ โ
โ โ $script:Config - Module configuration โ โ
โ โ $script:LogPath - Path to operation log โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ MODULE FILES โ
โ โ
โ ServerManager/ โ
โ โโโ ServerManager.psd1 # Module manifest โ
โ โโโ ServerManager.psm1 # Main module (dot-sources others) โ
โ โโโ Public/ โ
โ โ โโโ Get-ServiceStatus.ps1 โ
โ โ โโโ Start-RemoteService.ps1 โ
โ โ โโโ Stop-RemoteService.ps1 โ
โ โ โโโ Restart-RemoteService.ps1 โ
โ โ โโโ Deploy-ConfigFile.ps1 โ
โ โ โโโ Get-RemoteLogs.ps1 โ
โ โ โโโ Invoke-RemoteScript.ps1 โ
โ โ โโโ Test-ServerConnection.ps1 โ
โ โ โโโ New-ServerManager.ps1 โ
โ โโโ Private/ โ
โ โ โโโ Get-ServerSession.ps1 โ
โ โ โโโ Write-OperationLog.ps1 โ
โ โ โโโ ConvertTo-ServerResult.ps1 โ
โ โ โโโ Get-SavedCredential.ps1 โ
โ โ โโโ Save-Credential.ps1 โ
โ โ โโโ Test-SessionHealth.ps1 โ
โ โโโ Config/ โ
โ โ โโโ servers.json # Default server inventory โ
โ โโโ Logs/ โ
โ โโโ operations.log # Operation audit log โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Session Reuse Strategy
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Session Pool Management โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ User calls: Get-ServiceStatus -ComputerName Server01,Server02,Server03 โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Get-ServerSession โ โ
โ โ โ โ
โ โ 1. Check $script:SessionPool for existing sessions โ โ
โ โ โ โ
โ โ SessionPool = @{ โ โ
โ โ "Server01" = [PSSession] State=Opened โ โ
โ โ "Server02" = [PSSession] State=Closed (stale!) โ โ
โ โ } โ โ
โ โ โ โ
โ โ 2. For each requested server: โ โ
โ โ โ โ
โ โ Server01: Found in pool, state=Opened -> REUSE โ โ
โ โ Server02: Found in pool, state=Closed -> REMOVE, CREATE NEW โ โ
โ โ Server03: Not in pool -> CREATE NEW โ โ
โ โ โ โ
โ โ 3. Update pool and return sessions โ โ
โ โ โ โ
โ โ SessionPool = @{ โ โ
โ โ "Server01" = [PSSession] State=Opened (reused) โ โ
โ โ "Server02" = [PSSession] State=Opened (new) โ โ
โ โ "Server03" = [PSSession] State=Opened (new) โ โ
โ โ } โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Session Lifecycle: โ
โ โ
โ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โ
โ โ Created โ -> โ Used โ -> โ Used โ -> โ Idle โ -> โ Removed โ โ
โ โ โ โ Cmd 1 โ โ Cmd 2 โ โ Timeout โ โ โ โ
โ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โ
โ โ
โ Cleanup Triggers: โ
โ - Explicit: Clear-ServerSessions โ
โ - Automatic: Module unload โ
โ - Health check: Stale session detected โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Error Aggregation Approach
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Error Handling Strategy โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Principle: PARTIAL SUCCESS IS SUCCESS โ
โ - Never fail entirely because one server is unreachable โ
โ - Report per-server status โ
โ - Let the caller decide how to handle failures โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Invoke-Command Error Handling โ โ
โ โ โ โ
โ โ $invokeParams = @{ โ โ
โ โ ComputerName = $servers โ โ
โ โ ScriptBlock = { ... } โ โ
โ โ ErrorAction = 'SilentlyContinue' # Don't throw โ โ
โ โ ErrorVariable = 'remoteErrors' # Capture errors โ โ
โ โ } โ โ
โ โ โ โ
โ โ $results = Invoke-Command @invokeParams โ โ
โ โ โ โ
โ โ # Results contain successful responses with PSComputerName โ โ
โ โ # remoteErrors contains failure details โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Result Object Structure: โ
โ โ
โ [PSCustomObject]@{ โ
โ ComputerName = "Server01" โ
โ Status = "Success" | "Failed" | "Warning" โ
โ Data = <operation-specific data> โ
โ Error = $null | "Error message" โ
โ Timestamp = Get-Date โ
โ Duration = [TimeSpan] โ
โ } โ
โ โ
โ Error Categories: โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Connection Errors โ WinRM not running, firewall, DNS failure โ โ
โ โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ Auth Errors โ Access denied, invalid credentials โ โ
โ โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ Execution Errors โ Script threw exception, timeout โ โ
โ โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ Resource Errors โ Service not found, file not exists โ โ
โ โโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Phased Implementation Guide
Phase 1: Single Server Remoting (3-4 hours)
Goal: Establish the foundationโconnect to one remote machine and execute commands.
What Youโll Learn:
- WinRM configuration on both ends
- Basic Invoke-Command syntax
- Credential handling fundamentals
Steps:
- Configure WinRM on target server
# Run on target server (as Administrator) Enable-PSRemoting -Force winrm quickconfig -Force # Verify listener winrm enumerate winrm/config/listener - Configure trusted hosts (for workgroup)
# On your workstation (if not domain-joined) Set-Item WSMan:\localhost\Client\TrustedHosts -Value "Server01" -Force - Create basic connection test
function Test-RemoteConnection { param( [Parameter(Mandatory)] [string]$ComputerName, [PSCredential]$Credential ) $params = @{ ComputerName = $ComputerName ScriptBlock = { [PSCustomObject]@{ Hostname = $env:COMPUTERNAME User = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name PSVersion = $PSVersionTable.PSVersion.ToString() Time = Get-Date } } } if ($Credential) { $params['Credential'] = $Credential } try { $result = Invoke-Command @params -ErrorAction Stop Write-Host "Connected to $($result.Hostname) as $($result.User)" -ForegroundColor Green return $result } catch { Write-Error "Failed to connect: $_" return $null } } - Create simple service query
function Get-RemoteService { param( [Parameter(Mandatory)] [string]$ComputerName, [string]$ServiceName = "*", [PSCredential]$Credential ) $params = @{ ComputerName = $ComputerName ScriptBlock = { param($Name) Get-Service -Name $Name | Select-Object Name, DisplayName, Status } ArgumentList = $ServiceName } if ($Credential) { $params['Credential'] = $Credential } Invoke-Command @params }
Verification:
$cred = Get-Credential
Test-RemoteConnection -ComputerName "Server01" -Credential $cred
Get-RemoteService -ComputerName "Server01" -ServiceName "Spooler" -Credential $cred
Phase 2: Multi-Server with Sessions (4-5 hours)
Goal: Scale to multiple servers with efficient session management.
What Youโll Learn:
- New-PSSession for persistent connections
- Session reuse patterns
- Handling multiple targets in parallel
Steps:
- Create session pool manager
# Module-scoped state $script:SessionPool = @{} function Get-ServerSession { [CmdletBinding()] param( [Parameter(Mandatory)] [string[]]$ComputerName, [PSCredential]$Credential ) $sessions = @() $newServers = @() foreach ($server in $ComputerName) { # Check for existing valid session if ($script:SessionPool.ContainsKey($server)) { $existing = $script:SessionPool[$server] if ($existing.State -eq 'Opened') { Write-Verbose "Reusing session for $server" $sessions += $existing continue } # Session is stale, remove it Write-Verbose "Removing stale session for $server" $script:SessionPool.Remove($server) } $newServers += $server } # Create new sessions for servers not in pool if ($newServers.Count -gt 0) { Write-Verbose "Creating sessions for: $($newServers -join ', ')" $sessionParams = @{ ComputerName = $newServers ErrorAction = 'SilentlyContinue' ErrorVariable = 'sessionErrors' } if ($Credential) { $sessionParams['Credential'] = $Credential } $newSessions = New-PSSession @sessionParams foreach ($session in $newSessions) { $script:SessionPool[$session.ComputerName] = $session $sessions += $session } # Report failures foreach ($err in $sessionErrors) { Write-Warning "Failed to create session: $($err.TargetObject) - $($err.Exception.Message)" } } return $sessions } function Clear-ServerSessions { [CmdletBinding()] param() if ($script:SessionPool.Count -gt 0) { Write-Verbose "Closing $($script:SessionPool.Count) sessions" $script:SessionPool.Values | Remove-PSSession -ErrorAction SilentlyContinue $script:SessionPool.Clear() } } - Create multi-server service status
function Get-ServiceStatus { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [string[]]$ComputerName, [string]$ServiceName = "*", [PSCredential]$Credential, [int]$ThrottleLimit = 32 ) begin { $allServers = @() } process { $allServers += $ComputerName } end { $sessions = Get-ServerSession -ComputerName $allServers -Credential $Credential if ($sessions.Count -eq 0) { Write-Warning "No sessions available" return } $results = Invoke-Command -Session $sessions -ThrottleLimit $ThrottleLimit -ScriptBlock { param($Pattern) Get-Service -Name $Pattern -ErrorAction SilentlyContinue | Select-Object Name, DisplayName, Status, StartType } -ArgumentList $ServiceName -ErrorAction SilentlyContinue -ErrorVariable remoteErrors # Transform results foreach ($result in $results) { [PSCustomObject]@{ ComputerName = $result.PSComputerName ServiceName = $result.Name DisplayName = $result.DisplayName Status = $result.Status StartType = $result.StartType } } # Report errors foreach ($err in $remoteErrors) { [PSCustomObject]@{ ComputerName = $err.TargetObject ServiceName = 'ERROR' DisplayName = 'Connection/Execution Failed' Status = '-' StartType = '-' Error = $err.Exception.Message } } } }
Verification:
$servers = @("Server01", "Server02", "Server03")
$cred = Get-Credential
# First call creates sessions
Get-ServiceStatus -ComputerName $servers -ServiceName "Spooler" -Credential $cred
# Second call reuses sessions (check verbose output)
Get-ServiceStatus -ComputerName $servers -ServiceName "W32Time" -Credential $cred -Verbose
# Cleanup
Clear-ServerSessions
Phase 3: Parallel Execution (3-4 hours)
Goal: Optimize for large server fleets with proper throttling and progress reporting.
What Youโll Learn:
- ForEach-Object -Parallel (PowerShell 7)
- Custom progress reporting
- Timeout handling
Steps:
- Add progress reporting
function Invoke-ParallelServerCommand { [CmdletBinding()] param( [Parameter(Mandatory)] [string[]]$ComputerName, [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [object[]]$ArgumentList, [PSCredential]$Credential, [int]$ThrottleLimit = 10, [int]$TimeoutSeconds = 60, [switch]$ShowProgress ) $total = $ComputerName.Count $completed = 0 $results = [System.Collections.Concurrent.ConcurrentBag[object]]::new() # PowerShell 7+ parallel execution if ($PSVersionTable.PSVersion.Major -ge 7) { $ComputerName | ForEach-Object -Parallel { $server = $_ $cred = $using:Credential $sb = $using:ScriptBlock $args = $using:ArgumentList $resultBag = $using:results $invokeParams = @{ ComputerName = $server ScriptBlock = $sb ErrorAction = 'Stop' } if ($args) { $invokeParams['ArgumentList'] = $args } if ($cred) { $invokeParams['Credential'] = $cred } try { $output = Invoke-Command @invokeParams $resultBag.Add([PSCustomObject]@{ ComputerName = $server Status = 'Success' Data = $output Error = $null }) } catch { $resultBag.Add([PSCustomObject]@{ ComputerName = $server Status = 'Failed' Data = $null Error = $_.Exception.Message }) } } -ThrottleLimit $ThrottleLimit } else { # PowerShell 5.1 fallback - use runspaces or Invoke-Command's built-in parallelism $sessions = Get-ServerSession -ComputerName $ComputerName -Credential $Credential $output = Invoke-Command -Session $sessions -ScriptBlock $ScriptBlock ` -ArgumentList $ArgumentList -ThrottleLimit $ThrottleLimit ` -ErrorAction SilentlyContinue -ErrorVariable errors foreach ($item in $output) { $results.Add([PSCustomObject]@{ ComputerName = $item.PSComputerName Status = 'Success' Data = $item Error = $null }) } foreach ($err in $errors) { $results.Add([PSCustomObject]@{ ComputerName = $err.TargetObject Status = 'Failed' Data = $null Error = $err.Exception.Message }) } } return $results } - Add timeout wrapper
function Invoke-WithTimeout { [CmdletBinding()] param( [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [int]$TimeoutSeconds = 30, [string]$OperationName = "Operation" ) $job = Start-Job -ScriptBlock $ScriptBlock $completed = $job | Wait-Job -Timeout $TimeoutSeconds if ($completed) { $result = Receive-Job -Job $job Remove-Job -Job $job return $result } else { Stop-Job -Job $job Remove-Job -Job $job throw "$OperationName timed out after $TimeoutSeconds seconds" } }
Phase 4: File Deployment (4-5 hours)
Goal: Implement secure file transfer to remote servers.
What Youโll Learn:
- Copy-Item -ToSession mechanics
- Backup strategies
- Verification after transfer
Steps:
- Create file deployment function
function Deploy-ConfigFile { [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')] param( [Parameter(Mandatory)] [string[]]$ComputerName, [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string]$SourcePath, [Parameter(Mandatory)] [string]$DestinationPath, [PSCredential]$Credential, [switch]$Backup, [switch]$Force, [switch]$Verify ) $results = @() $sessions = Get-ServerSession -ComputerName $ComputerName -Credential $Credential foreach ($session in $sessions) { $server = $session.ComputerName if (-not $PSCmdlet.ShouldProcess($server, "Deploy $SourcePath to $DestinationPath")) { continue } try { # Create backup if requested if ($Backup) { $backupResult = Invoke-Command -Session $session -ScriptBlock { param($Path) if (Test-Path $Path) { $backupPath = "$Path.$(Get-Date -Format 'yyyyMMddHHmmss').bak" Copy-Item -Path $Path -Destination $backupPath -Force return $backupPath } return $null } -ArgumentList $DestinationPath if ($backupResult) { Write-Verbose "Backed up existing file to $backupResult on $server" } } # Ensure destination directory exists Invoke-Command -Session $session -ScriptBlock { param($Path) $dir = Split-Path -Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } } -ArgumentList $DestinationPath # Copy file Copy-Item -Path $SourcePath -Destination $DestinationPath -ToSession $session -Force:$Force # Verify if requested $verified = $true if ($Verify) { $sourceHash = (Get-FileHash -Path $SourcePath -Algorithm SHA256).Hash $destHash = Invoke-Command -Session $session -ScriptBlock { param($Path) (Get-FileHash -Path $Path -Algorithm SHA256).Hash } -ArgumentList $DestinationPath $verified = ($sourceHash -eq $destHash) } $results += [PSCustomObject]@{ ComputerName = $server Status = if ($verified) { 'Success' } else { 'VerificationFailed' } DestinationPath = $DestinationPath BackupPath = $backupResult Verified = $verified Error = $null } } catch { $results += [PSCustomObject]@{ ComputerName = $server Status = 'Failed' DestinationPath = $DestinationPath BackupPath = $null Verified = $false Error = $_.Exception.Message } } } return $results } - Create log collection function
function Get-RemoteLogs { [CmdletBinding()] param( [Parameter(Mandatory)] [string[]]$ComputerName, [Parameter(Mandatory)] [string]$LogPath, [DateTime]$Since, [string]$LocalDestination = ".\CollectedLogs", [PSCredential]$Credential, [switch]$Compress ) # Create local destination $timestamp = Get-Date -Format 'yyyy-MM-dd_HHmmss' $destRoot = Join-Path $LocalDestination $timestamp if (-not (Test-Path $destRoot)) { New-Item -ItemType Directory -Path $destRoot -Force | Out-Null } $sessions = Get-ServerSession -ComputerName $ComputerName -Credential $Credential $results = @() foreach ($session in $sessions) { $server = $session.ComputerName $serverDir = Join-Path $destRoot $server if (-not (Test-Path $serverDir)) { New-Item -ItemType Directory -Path $serverDir -Force | Out-Null } try { # Get list of matching files $files = Invoke-Command -Session $session -ScriptBlock { param($Path, $Since) Get-ChildItem -Path $Path -File -ErrorAction SilentlyContinue | Where-Object { -not $Since -or $_.LastWriteTime -gt $Since } | Select-Object FullName, Name, Length, LastWriteTime } -ArgumentList $LogPath, $Since Write-Verbose "Found $($files.Count) files on $server" foreach ($file in $files) { try { $localPath = Join-Path $serverDir $file.Name Copy-Item -Path $file.FullName -Destination $localPath -FromSession $session $results += [PSCustomObject]@{ ComputerName = $server FileName = $file.Name Size = $file.Length LastModified = $file.LastWriteTime LocalPath = $localPath Status = 'Collected' Error = $null } } catch { $results += [PSCustomObject]@{ ComputerName = $server FileName = $file.Name Size = $file.Length LastModified = $file.LastWriteTime LocalPath = $null Status = 'Failed' Error = $_.Exception.Message } } } # Compress if requested if ($Compress -and $files.Count -gt 0) { $archivePath = "$serverDir.zip" Compress-Archive -Path "$serverDir\*" -DestinationPath $archivePath -Force Remove-Item -Path $serverDir -Recurse -Force Write-Verbose "Compressed logs from $server to $archivePath" } } catch { $results += [PSCustomObject]@{ ComputerName = $server FileName = 'ERROR' Size = 0 LastModified = $null LocalPath = $null Status = 'Failed' Error = $_.Exception.Message } } } return $results }
Phase 5: Logging and Error Handling (3-4 hours)
Goal: Add enterprise-grade logging and comprehensive error management.
What Youโll Learn:
- Structured logging patterns
- Error categorization
- Audit trail requirements
Steps:
- Create operation logger
$script:LogPath = Join-Path $PSScriptRoot "Logs\operations.log" function Write-OperationLog { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Operation, [Parameter(Mandatory)] [string]$Target, [ValidateSet('Info', 'Warning', 'Error', 'Success')] [string]$Level = 'Info', [string]$Message, [string]$User = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name ) $logDir = Split-Path $script:LogPath -Parent if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } $entry = [PSCustomObject]@{ Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff' User = $User Operation = $Operation Target = $Target Level = $Level Message = $Message } $logLine = $entry | ConvertTo-Json -Compress Add-Content -Path $script:LogPath -Value $logLine # Also write to verbose stream $color = switch ($Level) { 'Error' { 'Red' } 'Warning' { 'Yellow' } 'Success' { 'Green' } default { 'White' } } Write-Host "[$($entry.Timestamp)] $Level - $Operation on $Target: $Message" -ForegroundColor $color } - Create service restart with full logging
function Restart-RemoteService { [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')] param( [Parameter(Mandatory)] [string[]]$ComputerName, [Parameter(Mandatory)] [string]$ServiceName, [PSCredential]$Credential, [switch]$Wait, [int]$TimeoutSeconds = 120, [switch]$Force ) $results = @() $sessions = Get-ServerSession -ComputerName $ComputerName -Credential $Credential Write-OperationLog -Operation "RestartService" -Target "$ServiceName on $($ComputerName -join ',')" -Level Info -Message "Starting service restart operation" foreach ($session in $sessions) { $server = $session.ComputerName if (-not $PSCmdlet.ShouldProcess("$ServiceName on $server", "Restart service")) { continue } Write-OperationLog -Operation "RestartService" -Target "$ServiceName on $server" -Level Info -Message "Initiating restart" try { $restartResult = Invoke-Command -Session $session -ScriptBlock { param($SvcName, $ShouldWait, $Timeout) $svc = Get-Service -Name $SvcName -ErrorAction Stop $originalStatus = $svc.Status # Stop service Stop-Service -Name $SvcName -Force -ErrorAction Stop if ($ShouldWait) { $svc.WaitForStatus('Stopped', [TimeSpan]::FromSeconds($Timeout / 2)) } # Start service Start-Service -Name $SvcName -ErrorAction Stop if ($ShouldWait) { $svc.WaitForStatus('Running', [TimeSpan]::FromSeconds($Timeout / 2)) } # Return final status $finalSvc = Get-Service -Name $SvcName [PSCustomObject]@{ Name = $finalSvc.Name DisplayName = $finalSvc.DisplayName OriginalStatus = $originalStatus CurrentStatus = $finalSvc.Status } } -ArgumentList $ServiceName, $Wait.IsPresent, $TimeoutSeconds $results += [PSCustomObject]@{ ComputerName = $server ServiceName = $restartResult.Name DisplayName = $restartResult.DisplayName OriginalStatus = $restartResult.OriginalStatus CurrentStatus = $restartResult.CurrentStatus Status = 'Success' Error = $null } Write-OperationLog -Operation "RestartService" -Target "$ServiceName on $server" -Level Success -Message "Service restarted. Now: $($restartResult.CurrentStatus)" } catch { $results += [PSCustomObject]@{ ComputerName = $server ServiceName = $ServiceName DisplayName = $null OriginalStatus = $null CurrentStatus = 'Unknown' Status = 'Failed' Error = $_.Exception.Message } Write-OperationLog -Operation "RestartService" -Target "$ServiceName on $server" -Level Error -Message $_.Exception.Message } } # Summary $successful = ($results | Where-Object Status -eq 'Success').Count $failed = ($results | Where-Object Status -eq 'Failed').Count Write-OperationLog -Operation "RestartService" -Target $ServiceName -Level Info -Message "Completed. Success: $successful, Failed: $failed" return $results }
Testing Strategy
Unit Tests
| Test ID | Test Case | Steps | Expected Result |
|---|---|---|---|
| UT01 | Single server connection | Test-ServerConnection -ComputerName localhost | Returns connection info |
| UT02 | Invalid server | Test-ServerConnection -ComputerName โnonexistentโ | Returns error, no exception |
| UT03 | Service query pattern | Get-ServiceStatus -ComputerName localhost -ServiceName โSpoolerโ | Returns spooler status |
| UT04 | Wildcard service query | Get-ServiceStatus -ComputerName localhost -ServiceName โW*โ | Returns multiple services |
| UT05 | Session caching | Call Get-ServerSession twice | Second call reuses session |
| UT06 | Stale session handling | Force close session, call function | New session created |
Integration Tests
| Test ID | Test Case | Steps | Expected Result |
|---|---|---|---|
| IT01 | Multi-server query | Query 3+ servers | Results from all servers |
| IT02 | Partial failure | Include one invalid server | Results from valid servers, error for invalid |
| IT03 | File deployment | Deploy config to 2 servers | File exists on both |
| IT04 | File with backup | Deploy with -Backup | Original file backed up |
| IT05 | Log collection | Collect logs from 2 servers | Files in local folder, organized by server |
| IT06 | Service restart | Restart service, verify | Service running after restart |
| IT07 | Parallel execution | Query 10 servers | Completes in < 30 seconds |
Performance Tests
| Test ID | Test Case | Target | Actual |
|---|---|---|---|
| PT01 | 10 servers, service query | < 10 seconds | ย |
| PT02 | 50 servers, service query | < 30 seconds | ย |
| PT03 | 100 servers, service query | < 60 seconds | ย |
| PT04 | File deploy (1MB) to 10 servers | < 60 seconds | ย |
| PT05 | Session creation 20 servers | < 15 seconds | ย |
Test Environment Setup
# Create test VMs (Hyper-V example)
$vmNames = @("TestSrv01", "TestSrv02", "TestSrv03")
foreach ($name in $vmNames) {
# Create VM
New-VM -Name $name -MemoryStartupBytes 2GB -NewVHDPath "C:\VMs\$name.vhdx" -NewVHDSizeBytes 40GB
# Configure after OS install:
# Enable-PSRemoting -Force
# Set-NetFirewallRule -Name "WINRM-HTTP-In-TCP" -Enabled True
}
Common Pitfalls and Debugging Tips
Problem 1: WinRM Not Configured
Symptoms:
The WinRM client cannot process the request. If the authentication scheme
is different from Kerberos, or if the client computer is not joined to a domain...
Solutions:
# On target server (as Admin)
Enable-PSRemoting -Force
# If in workgroup, on your workstation:
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "Server01,Server02" -Force
# Verify
Test-WSMan -ComputerName Server01
Problem 2: Firewall Blocking
Symptoms:
WinRM cannot complete the operation. Verify that the specified computer name
is valid, that the computer is accessible over the network...
Solutions:
# On target server
Set-NetFirewallRule -Name "WINRM-HTTP-In-TCP" -Enabled True
# Or create rule
New-NetFirewallRule -Name "WinRM HTTP" -DisplayName "WinRM HTTP" -Direction Inbound -LocalPort 5985 -Protocol TCP -Action Allow
Problem 3: Credential Issues
Symptoms:
Access is denied.
Solutions:
# Verify group membership on target
Invoke-Command -ComputerName Server01 -ScriptBlock {
net localgroup "Remote Management Users"
net localgroup "Administrators"
}
# Add user if needed (on target)
Add-LocalGroupMember -Group "Remote Management Users" -Member "DOMAIN\User"
Problem 4: Session Timeouts
Symptoms:
The session state is not valid for this operation.
Solutions:
# Increase idle timeout (on target)
winrm set winrm/config/winrs '@{IdleTimeout="7200000"}' # 2 hours
# In code, implement session health check
function Test-SessionHealth {
param([System.Management.Automation.Runspaces.PSSession]$Session)
if ($Session.State -ne 'Opened') { return $false }
try {
Invoke-Command -Session $Session -ScriptBlock { 1 } -ErrorAction Stop
return $true
}
catch {
return $false
}
}
Problem 5: Double-Hop Authentication
Symptoms:
Access is denied when accessing network resources from remote session.
Solutions:
# Option 1: CredSSP (use with caution)
Enable-WSManCredSSP -Role Client -DelegateComputer "*.domain.com" -Force
Enable-WSManCredSSP -Role Server -Force # On target
Invoke-Command -ComputerName Server01 -Authentication CredSSP -Credential $cred -ScriptBlock {
Get-ChildItem "\\FileServer\Share"
}
# Option 2: Pass credentials explicitly
Invoke-Command -ComputerName Server01 -ScriptBlock {
param($cred)
New-PSDrive -Name "Z" -PSProvider FileSystem -Root "\\FileServer\Share" -Credential $cred
Get-ChildItem "Z:\"
Remove-PSDrive "Z"
} -ArgumentList $cred
Problem 6: Large Data Transfer Failures
Symptoms:
The message could not be transferred within the allotted time.
Solutions:
# Increase message size limits (on target)
winrm set winrm/config '@{MaxEnvelopeSizekb="8192"}'
# In code, compress before transfer
Compress-Archive -Path ".\LargeFolder" -DestinationPath ".\archive.zip"
Copy-Item ".\archive.zip" -Destination "C:\Temp\" -ToSession $session
Invoke-Command -Session $session -ScriptBlock {
Expand-Archive -Path "C:\Temp\archive.zip" -DestinationPath "C:\Destination"
}
Debugging Checklist
[ ] Can you ping the target server?
[ ] Is WinRM service running on target? (Get-Service WinRM)
[ ] Is firewall allowing 5985/5986?
[ ] Are you using correct credentials?
[ ] Is user in Remote Management Users or Administrators?
[ ] Is TrustedHosts configured (workgroup)?
[ ] Is DNS resolving the hostname correctly?
[ ] Are you hitting session limits on the target?
[ ] Check WinRM event logs: Get-WinEvent -LogName Microsoft-Windows-WinRM/Operational
Extensions and Challenges
Easy Extensions
- HTML Report Generation: Add
-GenerateReportswitch to produce styled HTML output - Email Notifications: Send alerts when services are down or deployments fail
- Scheduled Execution: Create scheduled task wrapper for regular health checks
- Custom Service List: Configuration file for monitored services per environment
Medium Extensions
- Rolling Restarts: Implement staged service restarts (restart 10% at a time)
- Rollback Capability: Store deployment history, enable rollback to previous config
- Dependency Tracking: Restart dependent services in correct order
- Real-time Dashboard: PowerShell Universal dashboard showing server status
Advanced Extensions
- Ansible-style Playbooks: Define multi-step operations in YAML, execute declaratively
- Drift Detection: Compare server configs against golden baseline
- Integration with ITSM: Create ServiceNow tickets for failures
- Kubernetes-style Desired State: Define desired state, tool enforces it continuously
Challenge Projects
- Blue/Green Deployment: Implement zero-downtime deployment with health checks
- Configuration Management: Build a mini-DSC alternative
- Security Hardening Tool: Audit and apply security baselines remotely
- Disaster Recovery: Automate full server rebuild from config
Books That Will Help
| Topic | Book | Chapter | Why It Matters |
|---|---|---|---|
| PowerShell Remoting Fundamentals | Learn PowerShell in a Month of Lunches (4th Ed) by Don Jones & Travis Plunk | Ch. 10-13: Remoting | The definitive introduction to PowerShell remoting, covers WinRM setup, session management, and troubleshooting |
| Advanced Remoting Patterns | PowerShell in Depth (2nd Ed) by Don Jones, Richard Siddaway, Jeffrey Hicks | Ch. 10: โRemote Controlโ | Deep dive into session management, background jobs, and enterprise remoting patterns |
| Remoting Security | PowerShell in Depth (2nd Ed) by Don Jones, Richard Siddaway, Jeffrey Hicks | Ch. 11: โSessionsโ | Credential handling, delegation, and JEA implementation |
| WinRM Architecture | Windows Server Administration Fundamentals by Microsoft Press | Remoting sections | Understanding WS-Man, WinRM service internals, and configuration |
| Windows Security Model | Windows Security Internals by James Forshaw | Ch. 4-5: โSecurity Descriptorsโ and โAccess Tokensโ | Understanding why credential delegation is complex and how Windows security works |
| Authentication Protocols | Windows Security Internals by James Forshaw | Ch. 8: โKerberosโ | Deep understanding of Kerberos, NTLM, and why double-hop is problematic |
| Module Development | The PowerShell Scripting and Toolmaking Book by Don Jones & Jeff Hicks | Ch. 15-18 | Building professional, distributable PowerShell modules |
| Error Handling at Scale | Windows PowerShell Cookbook (3rd Ed) by Lee Holmes | Error handling recipes | Practical patterns for robust error management |
| Enterprise Automation | PowerShell for Sysadmins by Adam Bertram | Ch. 8-10: Remote management | Real-world scenarios and best practices |
Self-Assessment Checklist
Before considering this project complete, verify you can:
Core Functionality
- Connect to remote servers using PowerShell Remoting
- Query service status across multiple servers simultaneously
- Start, stop, and restart services on remote machines
- Copy configuration files to remote servers
- Collect log files from remote servers to local machine
- Execute arbitrary scripts on remote machines
Session Management
- Create and reuse PSSession objects efficiently
- Detect and replace stale sessions automatically
- Clean up sessions properly on script completion
- Explain the difference between implicit and explicit sessions
Parallel Execution
- Run operations on 10+ servers in parallel
- Configure appropriate ThrottleLimit for your environment
- Explain why parallel is faster than serial execution
- Handle individual server timeouts without blocking others
Security
- Store and retrieve credentials securely
- Explain why CredSSP is risky
- Configure WinRM for secure operation
- Credentials never appear in logs or output
Error Handling
- Handle partial failures gracefully (some servers succeed, some fail)
- Report errors per-server without failing entire operation
- Log all operations with timestamps for audit trail
- Provide meaningful error messages to users
Architecture
- Module is properly structured (Public/Private folders)
- Functions support pipeline input
- WhatIf/Confirm support for destructive operations
- Module can be installed and imported cleanly
Knowledge Verification
- Can explain how WinRM/WSMan works
- Can troubleshoot โaccess deniedโ errors
- Can explain the double-hop problem and solutions
- Can set up remoting between two machines from scratch
Quick Reference
Essential Commands
# Enable remoting on a server
Enable-PSRemoting -Force
# Test remoting connection
Test-WSMan -ComputerName Server01
# Interactive session
Enter-PSSession -ComputerName Server01 -Credential $cred
# Run command on multiple servers
Invoke-Command -ComputerName Server01,Server02 -ScriptBlock { Get-Service }
# Create persistent sessions
$sessions = New-PSSession -ComputerName Server01,Server02
# Copy file to remote
Copy-Item -Path ".\file.txt" -Destination "C:\Temp\" -ToSession $session
# Copy file from remote
Copy-Item -Path "C:\Logs\*.log" -Destination ".\Logs\" -FromSession $session
# Clean up sessions
Remove-PSSession $sessions
Common WinRM Configuration
# View configuration
winrm get winrm/config
# Quick configure
winrm quickconfig
# Set trusted hosts (client side, workgroup)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "Server01,Server02" -Force
# Increase limits
winrm set winrm/config '@{MaxEnvelopeSizekb="8192"}'
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="1024"}'
Troubleshooting Commands
# Check WinRM service
Get-Service WinRM
# View WinRM events
Get-WinEvent -LogName Microsoft-Windows-WinRM/Operational -MaxEvents 50
# Test connectivity
Test-NetConnection -ComputerName Server01 -Port 5985
# Verify session state
Get-PSSession | Select-Object ComputerName, State, Availability