PSSession and Invoke-Command for Repeatable Remote Work
Use PSSession and Invoke-Command as explicit resources: reuse remote state, inspect remote capability, move files where appropriate, preserve structured output, and clean up reliably.
Learning objectives
- Create, inspect, reuse, and remove PSSessions.
- Choose interactive Enter-PSSession versus non-interactive Invoke-Command appropriately.
- Reason correctly about local and remote paths, modules, and variables.
- Transfer FileSystem items with Copy-Item -ToSession/-FromSession.
- Inspect structured/deserialized remote output and preserve provenance.
- Build a two-target inventory workflow with explicit partial-failure records.
1. A PSSession gives repeated remote work a lifecycle
Repeatedly creating temporary remote connections adds negotiation overhead and loses remote session state. A PSSession gives you a named object that represents a persistent remote PowerShell session. You create it, use it for one or more commands, inspect it, and remove it when finished.
# Windows WSMan example; target must already be configured and authorized.
$session = New-PSSession -ComputerName 'server01'
$session | Select-Object Id,Name,ComputerName,State,ConfigurationName
Remove-PSSession -Session $session2. Create, inspect, use, clean up
$session = New-PSSession -ComputerName 'server01'
try {
Invoke-Command -Session $session -ScriptBlock {
[pscustomobject]@{
Host = [System.Net.Dns]::GetHostName()
Pid = $PID
Version = $PSVersionTable.PSVersion.ToString()
Home = $HOME
}
}
}
finally {
Remove-PSSession -Session $session -ErrorAction SilentlyContinue
}The finally block makes cleanup part of the contract. A long-running administration shell that leaks unused sessions eventually consumes local and remote resources.
3. Enter-PSSession is interactive and single-target
Enter-PSSession is useful for diagnosis because your prompt moves into one remote session. It is not a good fleet-automation primitive because a human is driving it interactively.
$session = New-PSSession -ComputerName 'server01'
Enter-PSSession -Session $session
# ...interactive commands run remotely...
# Exit-PSSession
Remove-PSSession -Session $session4. Remote session state belongs to the remote session
A persistent session can retain variables and functions created in that session. Those are not local variables.
$s = New-PSSession -ComputerName 'server01'
Invoke-Command -Session $s -ScriptBlock {
$script:ScanStartedUtc = [datetime]::UtcNow
}
Invoke-Command -Session $s -ScriptBlock {
[pscustomobject]@{
StartedUtc = $script:ScanStartedUtc
NowUtc = [datetime]::UtcNow
}
}
Remove-PSSession $s5. Paths, modules, and commands are resolved remotely
A local file path is not automatically visible remotely. A module installed on your workstation is not automatically installed in the remote session. Ask the remote machine for its own capability.
Invoke-Command -Session $session -ScriptBlock {
[pscustomobject]@{
CurrentPath = (Get-Location).Path
Home = $HOME
GitExists = [bool](Get-Command git -ErrorAction SilentlyContinue)
Modules = @(Get-Module -ListAvailable | Select-Object -ExpandProperty Name -Unique).Count
}
}6. Copy-Item can transfer files through a PSSession
The FileSystem provider supports Copy-Item -ToSession and -FromSession. The source and destination are evaluated on different machines, so name the directions explicitly in your mental model.
$lab = Join-Path ([System.IO.Path]::GetTempPath()) 'ps-ch14-copy'
New-Item -ItemType Directory -Path $lab -Force | Out-Null
$localFile = Join-Path $lab 'inventory-request.json'
@{ RequestedUtc=[datetime]::UtcNow; Detail='basic' } |
ConvertTo-Json | Set-Content -LiteralPath $localFile -Encoding utf8
# When $session exists, ask the remote host to construct its own paths.
# $remoteRequest = Invoke-Command -Session $session -ScriptBlock {
# Join-Path $HOME 'inventory-request.json'
# }
# $remoteResult = Invoke-Command -Session $session -ScriptBlock {
# Join-Path $HOME 'inventory-result.json'
# }
# Copy-Item -LiteralPath $localFile -Destination $remoteRequest -ToSession $session
# Copy-Item -Path $remoteResult -Destination $lab -FromSession $session
Remove-Item -LiteralPath $lab -Recurse -ForceFor small data, prefer passing objects/parameters through the remoting protocol. File transfer is useful when the artifact itself is the contract.
7. Return small structured objects, not screen text
Remote inventory should emit properties you can aggregate. Avoid formatting remotely; it replaces useful objects with formatting instructions before serialization.
$inventoryBlock = {
[pscustomobject]@{
Host = [System.Net.Dns]::GetHostName()
OS = [System.Runtime.InteropServices.RuntimeInformation]::OSDescription
PSVersion = $PSVersionTable.PSVersion.ToString()
Architecture= [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
CheckedUtc = [datetime]::UtcNow
}
}
Invoke-Command -Session $session -ScriptBlock $inventoryBlock |
Select-Object PSComputerName,Host,OS,PSVersion,Architecture,CheckedUtc8. Inspect deserialization instead of being surprised by it
$result = Invoke-Command -Session $session -ScriptBlock {
Get-Process -Id $PID
}
[pscustomobject]@{
TypeName = $result.PSObject.TypeNames[0]
HasKillMethod = [bool]($result.PSObject.Methods['Kill'])
RemotePid = $result.Id
}If you need to invoke a live method or access a live handle, execute that operation on the remote side and return only the result.
9. Connection failure and command failure are separate records
Fleet code should never assume that because one session was created, every later command will work. Networks change, remote services restart, credentials expire, and commands can fail independently.
try {
$result = Invoke-Command -Session $session -ScriptBlock {
Get-Item -LiteralPath (Join-Path $HOME 'does-not-exist') -ErrorAction Stop
} -ErrorAction Stop
}
catch {
[pscustomobject]@{
Target = $session.ComputerName
Success = $false
ErrorType = $_.Exception.GetType().FullName
Message = $_.Exception.Message
ErrorId = $_.FullyQualifiedErrorId
}
}10. Two-target inventory workflow: real and simulated paths
If you have two authorized lab VMs, create sessions and invoke the same inventory block. If you do not, use the simulation below to practice result contracts and partial failure without pretending simulation is real remoting.
# Real-lab shape (Windows WSMan example):
# $sessions = New-PSSession -ComputerName 'lab01','lab02'
# try { Invoke-Command -Session $sessions -ScriptBlock $inventoryBlock }
# finally { $sessions | Remove-PSSession }
# Safe simulation for any machine:
$targets = 'lab01','lab02'
$simulated = foreach ($target in $targets) {
$started = [System.Diagnostics.Stopwatch]::StartNew()
try {
if ($target -eq 'lab02') { throw 'Simulated unreachable host' }
[pscustomobject]@{
Target = $target
Success = $true
DurationMs = $started.ElapsedMilliseconds
Result = [pscustomobject]@{ PSVersion=$PSVersionTable.PSVersion.ToString() }
Error = $null
}
} catch {
[pscustomobject]@{
Target=$target; Success=$false; DurationMs=$started.ElapsedMilliseconds
Result=$null; Error=$_.Exception.Message
}
}
}
$simulated11. Verification checklist
- You create and remove PSSessions intentionally.
- You use Enter-PSSession for diagnosis, not bulk automation.
- You know remote paths/modules/commands belong to the remote host.
- You understand Copy-Item -ToSession/-FromSession direction.
- You emit structured remote inventory and preserve PSComputerName.
- You inspect deserialized types before relying on methods.
- You represent per-target failure as data.
12. Common mistakes
- Creating sessions and never removing them.
- Assuming a local module or path exists remotely.
- Formatting remote data before it crosses the boundary.
- Ignoring
PSComputerNameand losing provenance. - Treating file transfer as the only way to pass small configuration data.
- Letting one target exception abort a fleet report with no per-target record.
13. Knowledge check
Question 1. What persists in a PSSession that does not persist in a one-off temporary session?
Question 2. Why should Remove-PSSession appear in cleanup logic?
Question 3. What do -ToSession and -FromSession describe?
Question 4. Where is $HOME evaluated inside a remote script block?
Question 5. Why should fleet inventory emit objects rather than Format-Table output?
14. Summary and next bridge
PSSessions turn remoting into a manageable resource lifecycle: connect, inspect capability, invoke structured commands, optionally transfer files, preserve provenance, handle failure, and clean up. Lesson 4 keeps PSRP but swaps the Windows WSMan transport for SSH, which makes the model usable across Windows, Linux, and macOS.
15. Authoritative references
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.