Chapter 14Lesson 03~170 minutes

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.

PSSessionInvoke-CommandAutomationObjects

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 $session

2. 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 $session

4. 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 $s

5. 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 -Force

For 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,CheckedUtc

8. 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
        }
    }
}
$simulated

11. 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 PSComputerName and 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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.