PowerShellのリモート処理をAWS Windows Serverで使ってみて

はじめに

この記事はPowerShell Advent Calendar 2013の1日目の記事です。
今年に入って、AWS上のWindows Serverを操作するためにPowerShellのリモート機能を使うことがありました。
そこで得た知見を書き残すのが、本エントリの主旨です。

対象と技術選定

今回はAWS上のWindows Server 2008 R2がメインターゲットでした。
これらのサーバーに対して、別のWindowsからのリモート処理を実行するための仕組みを整備する仕事をしました。
解決策はいくつか候補が上がりましたが、以下の理由でPowerShellのリモート機能を使う事にしました。

  • Windows Server 2008 R2には標準でPowerShell v2.0以降*1インストールされている
  • クライアント側も特別なソフトウェアをインストールする必要がない
  • Windows以外からも使える可能性がある (後述)
  • sshdの導入・設定の手間、etc...

あと、今回は特定のPrivate Network(VPC)内だけで運用される想定であったので、HTTPS周りの話には触れません。

セットアップ

AWSで用意されている日本語版のWindows Serverに限りますが、Enable-PSRemotingができない問題にハマりました。
詳しくは前回のエントリを参照してください。

おそらく、問題の特定に一番時間がかかったところです...

終了コード関連

これはコマンドプロンプトへの連携に際して生じる問題です。
PowerShellの内部では、終了時のエラーコードは $lastExitCode という変数に入るようになっています。

PowerShellの内部でPowerShellを呼び出した場合はこの$lastExitCodeという変数が使えますが、
コマンドプロンプトからpowershell.exeを呼び出した場合は、デフォルトで0(成功)か1(失敗)のみが%ERRORLEVEL%*2に記録されるという仕様になっています。

REM "exit 0"を実施
>powershell.exe .\test-exit0.ps1
>echo %ERRORLEVEL%
0

REM "exit 9"を実施
>powershell.exe .\test-exit9.ps1
>echo %ERRORLEVEL%
1

これを回避するには、"-File"オプションを付けてファイルを指定することで問題を回避できます。
"-Command"オプションでスクリプトブロックを渡すことでも解決できますが、-Fileオプションを使った方が簡潔です。

REM "exit 0"を実施
>powershell.exe -File .\test-exit0.ps1
>echo %ERRORLEVEL%
0

REM "exit 9"を実施
>powershell.exe -File .\test-exit9.ps1
>echo %ERRORLEVEL%
9

また、今回はリモートコンピュータ上でプログラムを実行するので、そのプログラムの実行結果を取ってくる必要がありました。
これも$lastExitCodeを使って解決できます。

リモートに接続した場合、リモート側にクライアント側とは別のセッションが起動してその上でコマンドが実行されます。
そのため、最初にセッションを作り、コマンド群を実行して、最後に$lastExitCodeを取得してくれば、コマンド群の最後に実行された終了コードを取得できます。
具体的にはこんな感じで、任意のコマンドを実行できる仕組みが簡潔につくれます。

# 実行したいコマンド(exit 9999を行うバッチファイルを実施する)
$commands = "cd C:\; .\exit9999.bat"

# User情報を作成
# ここでは平文からパスワードを暗号化しているが、ユーザ名とパスワードはダミーです。
$user = "dummy-user"
$password = ConvertTo-SecureString -AsPlainText -Force "dummy-pass";
$credential = New-Object System.Management.Automation.PSCredential($user, $password)

$session = New-PSSession -ComputerName "dummy-host" -Credential $credential
Invoke-Command -Session $session -ArgumentList $commands -ScriptBlock { Invoke-Expression $Args[0] }

# リモートで実行された $executeCommand の結果出力された$lastExitCodeを取得する。
$exitCode = Invoke-Command -Session $session -ScriptBlock { $lastExitCode }
Remove-PSSession -Session $session

exit $exitCode

リモートセッションを構築した場合、接続先のセッションは以下の特性を持つようです。
(仕様が見つけられなかったので、動作した推測でしかありませんが参考までに)

  • ホームディレクトリは接続ユーザーのDocuments以下(C:\Users\[ユーザー名]\Documents)である
    • そのため、バッチの作りが悪く、ログ出力先ディレクトリがカレント固定の場合はcd(Set-Location)でディレクトリ移動をした後にバッチを実行する必要がある
  • Invoke-CommandのScriptBlock内で処理を打ち切りたい場合はreturnを利用する

メモリ上限問題

PowerShellのリモート接続は、内部でWinRS(Windows RemoteShell)の仕組みを利用します。
そのため、設定もWinRSの設定に依存するのですが、この中に1つだけ想定外のパラメータがあります。
それは、「1つのシェルが利用できるメモリの上限」です。
デフォルトでは150MBという、何とも微妙な値が設定されていました*3
これに気づかず、「ほとんどうまく行くが、大型のバッチを起動させると失敗する」「Java系のツールをリモートから実行すると軒並み動かない」という現象が発生しました。

実際にどのような設定かはwinrmのコマンドで確認できます。
内容のMaxMemoryPerShellMBが利用可能なメモリのサイズです。

>winrm g winrm/config/winrs
Winrs
    AllowRemoteShellAccess = true
    IdleTimeout = 180000
    MaxConcurrentUsers = 5
    MaxShellRunTime = 2147483647
    MaxProcessesPerShell = 15
    MaxMemoryPerShellMB = 150
    MaxShellsPerUser = 5

修正をするには、この値を適切な値に書き換えることで対処可能です。
上限を設定したくない場合は0を設定すればよいです。
つまり、(管理者権限のあるコマンドプロンプトで)以下のコマンドを実行します。

winrm set winrm/config/winrs @{MaxMemoryPerShellMB="0"}

この手のコマンドを実行する場合、PowerShellだと@などを別の意味に解釈して死ぬケースがあるので注意が必要です。
Javaのクラスパス指定のセミコロン(Windows限定のパス区切り文字)などでも死んでくれます。
PowerShellで実施したい場合はシングルクォートなどで文字列と認識させてあげます。

# PowerShellの場合は''を付ける
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="0"}'

32bit版との兼ね合い

64bit版のマシンの場合、32bit版のPowerShellと64bit版のPowerShellの2つがあります。
そして、これらの設定は共有されていません
そのため、設定(Set-ExecutePolicyの設定)を正しく行ったのになぜかリモートから起動しないという問題が発生しました。

実際に発生した問題の流れは以下の通りです。

  • 32bitのプログラムがPowerShellを外部実行
  • 32bitのPowerShellが起動される
  • 32bitのPowerShellのExecutionPolicyがデフォルト(Restricted)であり、外部ファイルの読み込みを一切行えない

なお、この問題はリモート処理だけで発生するわけではありません。
ジョブ実行系のツール(製品、社内製問わず)が32bitである場合、発生しえます。

そのため、powershell.exeの"-ExecutionPolicy"オプションを利用するか、32bit環境を使っていないからと言って見捨てないようにしましょう。

Windows以外からのアクセス

PowerShellがリモート機能として使っているWinRM(WS-Management)はSOAPベースのHTTP/HTTPSで通信を行います。
そのため、理論上はWebサービスからWindowsをリモート操作可能です。

一応実験を行い、curlのみでLinuxからWindowsにログインしてコマンドを発行できることは確認してあります。
私が実験したのと同じような仕組みをpythonで実現しているオープンソースpywinrmがあるので、中身を見てみるとどのような仕組みで通信をしているのかが分かると思います。

ただ、試してみたところまだ良い認証の仕組みを使えていない(実験した両者はベーシック認証による認証を確認したのみ)ので、
そこさえ適切にクリアできれば、LinuxからWindowssshを入れなくても楽にアクセスする手段を得られるのではないかと考えています。

おわりに

業務でリモート処理に使ったPowerShellでハマった所を列挙してみました。

タイトルに"AWS Windows Server"と打ちましたが、セットアップで困った以外は普通のWindows Serverで作業しているのと同じ感覚で使えました。
強いて問題を上げるとするなら、SecurityGroupのTCP5985(デフォルト)を開けるなど、環境設定で多少考慮することがあるぐらいです。

HTTPSの認証周りや、WinRMを適切にLinuxから使う方法などに興味があるので、今後調べて行きたいなと考えています。

*1:リモート機能が利用可能なのは、バージョン2以降

*2:bashの$?です

*3:調べてみると、最新のWin2008R2だと設定が1024MBになってました