【PowerShell】SSH接続後にコマンドを自動実行する方法|リモート操作の効率化

【PowerShell】SSH接続後にコマンドを自動実行する方法|リモート操作の効率化 PowerShell

PowerShellからSSHでサーバーに接続した直後に所定のコマンドを自動実行できると、手作業の反復や入力ミスを大幅に減らせます。
ここではOpenSSHクライアントを前提に、単発コマンドの実行、複数コマンドの一括実行、スクリプトの送信実行、終了コードとログの扱い、sudoや疑似TTYなど実務で遭遇する論点までを段階的に解説します。

最小構成:接続後に単発のコマンドを実行する

SSHの引数にコマンドを渡すと、接続直後にそのコマンドが実行され、処理が終わると同時にセッションが切断されます。
PowerShellでは外部コマンド呼び出し演算子を使うと引数の扱いが明確になります。

# uname -a を実行して結果を標準出力へ
& ssh user@host "uname -a"

PowerShell 7以降であれば標準出力はUTF-8で扱われます。Windows PowerShell 5.xで文字化けする場合は $OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new() を事前に設定します。

複数コマンドや環境依存のコマンドを安全に渡す

複数のシェル組み込みや環境変数展開を伴う場合は、リモート側のシェルに明示的に解釈させると安全です。
Linuxに接続するケースでは /usr/bin/env bash -lc でログインシェル相当の解釈をさせるのが定石です。

# パッケージ更新とサービス状態確認を一括実行
$cmd = "/usr/bin/env bash -lc 'sudo -n apt-get update && systemctl is-active nginx'"
& ssh user@host $cmd

PowerShellのダブルクォートは変数展開されるため、リモート側に渡す部分はシングルクォートで囲い、必要に応じてエスケープします。
sudoでパスワード入力を避けるには対象コマンドに限ってNOPASSWDを許可し、実行時は sudo -n を付けます。

スクリプトを送って実行する(標準入力でのインライン実行)

ローカルにある短い処理をそのままリモートで実行したい場合は、ヒアストリングで標準入力に渡す方法が手軽です。
ファイルを配らずに単発実行でき、CIなどの一回限りの処理に向きます。

$script = @'
set -euo pipefail
echo "whoami => $(whoami)"
echo "hostname => $(hostname)"
mkdir -p /tmp/sample
date > /tmp/sample/ts.txt
'@

$bytes = [Text.Encoding]::UTF8.GetBytes($script)
$stream = New-Object System.IO.MemoryStream(,$bytes)

$si = New-Object System.Diagnostics.ProcessStartInfo
$si.FileName  = "ssh"
$si.Arguments = "user@host /usr/bin/env bash -lc 'cat > /tmp/run.sh && bash /tmp/run.sh'"
$si.RedirectStandardInput  = $true
$si.RedirectStandardOutput = $true
$si.RedirectStandardError  = $true
$si.UseShellExecute = $false

$p = New-Object System.Diagnostics.Process
$p.StartInfo = $si
$p.Start() | Out-Null
$stream.CopyTo($p.StandardInput.BaseStream)
$p.StandardInput.Close()
$p.WaitForExit()

$p.StandardOutput.ReadToEnd() | Write-Host
$p.StandardError.ReadToEnd()  | Write-Host
$LASTEXITCODE = $p.ExitCode

この例ではスクリプトを一時ファイルに保存してから実行しています。直接 bash -s に流し込む形でも構いません。

ローカルのスクリプトを配布して実行する(scp連携)

ある程度のボリュームがある処理は、先にリモートへ配布してから実行すると可読性が高くなります。
設定ファイルなど付随ファイルがある場合にも適しています。

# スクリプトを転送して実行し、終わったら後片付け
& scp .\deploy.sh user@host:/tmp/deploy.sh
& ssh user@host "chmod +x /tmp/deploy.sh && /usr/bin/env bash /tmp/deploy.sh && rm /tmp/deploy.sh"

転送が失敗した場合に備え、終了コードを必ず確認します。PowerShellでは直前のネイティブコマンドの終了コードが $LASTEXITCODE に入ります。

終了コードとログの取り扱い

失敗検知や監査のために、標準出力と標準エラーをそれぞれ収集し、終了コードで分岐します。
簡潔に済ませたいときは Start-Process -PassThru -Wait を使うと扱いやすくなります。

$psi = @{
  FilePath    = "ssh"
  ArgumentList= @("user@host","/usr/bin/env bash -lc 'echo OK; false'")  # 最後はわざと失敗
  RedirectStandardOutput = $true
  RedirectStandardError  = $true
  UseShellExecute = $false
  PassThru = $true
  Wait = $true
}
$p = Start-Process @psi
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
if ($p.ExitCode -ne 0) {
  "NG`n$out`n$err" | Out-File -FilePath ".\ssh-run.err.log" -Encoding utf8 -Append
  throw "Remote failed with exit $($p.ExitCode)"
} else {
  "OK`n$out" | Out-File -FilePath ".\ssh-run.ok.log" -Encoding utf8 -Append
}

複数台への同時実行が必要なら、このパターンを前回解説した並列実行フレームに載せ替えるだけで拡張できます。

疑似TTYが必要なコマンドを扱う

対話前提のコマンドや、色付き出力を要求するツールは疑似TTYが必要なことがあります。
非対話運用が原則ですが、どうしても必要な場合は -tt を付けます。

& ssh -tt user@host "/usr/bin/env bash -lc 'tput colors; id'"

対話パスワードが必要なsudoなどは自動化に不向きです。NOPASSWDの限定許可や、権限を落とした専用ユーザーなど、運用で解決します。

接続安定性とセキュリティのオプション

回線断が起きやすい環境ではKeepAliveを有効化すると安定します。最上段のHostブロックで共通設定を入れておくとミスが減ります。

# ~/.ssh/config の例(PowerShellから同じ設定が利用される)
Host *
  ServerAliveInterval 30
  ServerAliveCountMax 3
  StrictHostKeyChecking accept-new
  IdentitiesOnly yes

パスフレーズ付き鍵はWindowsのssh-agentで事前登録しておくと、ジョブ中にプロンプトが止まる事故を防げます。

トラブルシュートの勘所

コマンドが手元で展開されてしまうときはクォートの層を見直し、まずは一語の echo から段階的に複雑化して原因を切り分けます。
設定が反映されないときは ssh -v で詳細を確認し、どの鍵や設定ファイルが使われているかを確かめます。
リモートがbashでない環境では /usr/bin/env sh -lc に置き換えて実行します。

まとめ

PowerShellからのSSH自動実行は「引数にコマンドを渡す」「複数コマンドはbash -lcに包む」「短い処理は標準入力で流し込む」「大きな処理はscpで配布して実行」の四つを押さえると堅牢に運用できます。
終了コードで失敗を検知し、ログを永続化し、sudoやTTYの要件を事前に整理すれば、日常の運用作業を安全かつ効率的に自動化できます。