tech.guitarrapc.cóm

Technical updates

PowerShellにおけるWindows Workflow Foundation 4.0 (WF)利用のすすめ

Windows PowerShellでの並列実行というと、 PipeLine処理かWorkflowが利用しやすいところです。

Workflowは通常のImplicit Scriptingと異なりマネージドな仕組みです。 このため、WorkflowはWindows PowerShellだけでは困難な長時間にわたるスクリプト処理の並列化、エラー発生時の再開処理などをくみこみやすい優れた方法と言えます。

ただしWorkflowはPowerShellのサブセットです。一見するとfunctionと記述が似ておりとっつき安い反面、同じ記法と思いきや色々と制限があります。

今回は、Workflowの概要、サンプル、そしてハマり易い制限について紹介しましょう。 Workflowについて、それなりに長い記事となっていますのでご注意を....!

Workflowについて

WorkflowはWindows PowerShell 3.0 (Windows 8, Windows Server 2012~標準) から日常のシーンでも利用しやすくなりました。私もちょくちょく使っています。

さて、 PowerShell 3.0で利用されているWindows Workflow Foundation 4.0 (WF) に関してはTechNetの説明が比較的わかりやすいです。

TechNet - Windows PowerShell ワークフローについて

ワークフローとは、時間のかかるタスクを実行する手順や、複数のコンピューター間で複数の手順の調整を必要とする手順をプログラミングで連結した手順のシーケンスです。
Windows PowerShellワークフローでは、IT担当者や開発者は、頻繁に発生して時間のかかる、反復可能、並行化可能、中断可能、停止可能、および再起動可能という特徴を備えた、複数のコンピューターの管理アクティビティのシーケンスまたはワークフロー内の個別の手順をワークフローとして作成できます。

設計上、ワークフローは、ネットワークの停止、コンピューターの再起動、停電など、予期しない停止や中断により、中断および再開される場合があります。ワークフローは移植可能です。

つまり、XAMLファイルにエクスポートしたり、XAMLからインポートしたりできます。カスタム セッション構成により、ワークフローまたはワークフロー内のアクティビティは、委任されたIT担当者または下位のIT担当者が実行できます。 セッション構成には、必要に応じて、ワークフローの各段階にある管理対象ノードへ渡される異なるユーザー アカウントの資格情報を含めることができます。

日本語の記事は数が少ないのですが、勉強になる記事として紹介します。

私個人としては、この動画がわかりやすく感じます。

ここで従来のImplicit ScriptingとWorkflowの違いが対比されています。 「クラウドコンピューティングな世界、複数のノードに対して長時間処理を並行して行いエラーでも継続可能にする」、これがWorkflowをPowerShellに取り込んだ開発者の意図です。

Workflow Script
Indivisual activities are isolated Indivisual activities share state
State is managed allowing for persistence State is unmanaged, persistence is not supported
Flow of data is explicit, activities have well defined inpits and outputs Lots of implicit coupling
Strongly typed, early-bound, lexical scoping Slushy typing, late-bound, dynamic scoping
Plannned Ad hoc

PowerShell Workflow(PSW)の目的、利点、仕組みに関しては、 Windows PowerShell Blogで詳しく解説されています。

この記事が、日本語で紹介されていないのは悲しいですね。

Workflowをどんな時に使うのか

スクリプト記述では困る時、それがWorkflowを活用するチャンスです。 PowerShell開発者の、この一言に利用する機会が示されています。

Reliably executing long-running tasks across multiple computers, devices or IT processes

TechNetでは以下のように表現されています。

# 一連の時間のかかるタスクの自動化

時間のかかるタスクの調整およびリモート監視。アクティビティ (ワークフロー内の手順) の状態および進行状況をいつでも表示できます。

# エラー回復の自動化

ワークフローは、コンピューターの再起動など、計画された中断も計画外の中断も切り抜けます。実行中のワークフローを一時停止した後、一時停止した時点からワークフローを再開できます。永続化により、ワークフローの状態およびメタデータは、特定の時点 (開始時、終了時、および作成者が指定した任意の時点) で保存 (または保持) されるため、ワークフローを最初から再開するのではなく、最後に保持されたタスクから再開できます。

# 接続と切断

ユーザーは、ワークフローを実行したままで、ワークフロー セッションとの接続を確立したり切断したりできます。たとえば、クライアント コンピューターからログオフしたりクライアント コンピューターを再起動したりして、ワークフローを中断することなく、別のコンピューター (自宅のコンピューターなど) から実行中のワークフローを監視できます。ユーザーは、ワークフローを実行したままで、サーバーとの接続を確立したり切断したりできます。

# 複数コンピューターの管理

多数の管理対象ノードにワークフロー タスクを同時に適用できます。Windows PowerShell ワークフローでは、PSComputerName ワークフロー共通パラメーターなどの共通パラメーターが自動的にワークフローに追加され、複数コンピューターの管理が可能になります。そのままの状態でワークフローで使用できるパラメーターの詳細については、「付録」を参照してください。


# 単一のタスクによる複雑なエンド ツー エンドのプロセスの実行

シナリオ全体を実装する関連スクリプトを単一のワークフローに集約できます。

# タスクのスケジュール設定

ワークフロー タスクは、他の Windows PowerShell コマンドレットまたはスクリプトと同様に、スケジュールを設定し、特定の条件を満たしたときにトリガーできます。

そして、ゴールについてもPowerShell開発者は述べています。

- Minimizing the complexity of automating across a large number of cloud or datacenter computers and devices.
- Creating an ecosystem where ISVs and partners can build solutions on top of Windows PowerShell Workflow and the artifacts can be shared with the community and used in any solution.

簡潔にいうとこういうことです。

- 数多くの DCにおける PC や機器のAutomation (自動化) の複雑さを最小限にとどめる
- パートナーで Windows PowerShell Workflow を軸としたエコシステムの構築と、その内容をコミュニティで共有、各場面での解決策に利用できるようにすること

これは、Jeffrey Snover自身がKey Notesなどでも繰り返し述べているWindows PowerShellで求める姿と言えそうです。

NICconf Opening and Keynote: Modernizing Windows Server/Modernizing Ourselves - by Jeffrey Snover

PowerShellは、それ単独でもクラウド上 (AzureやAWSなどのIaaS)、あるいはオンプレミスやローカル環境に複数のWindows Serverをおいて管理しなくてはいけない時に強力な力を発揮します。 が、PowerShellコマンドレットはそれ自体では処理速度に難があるのは事実です。 このような場合に、Workflowと組み合わせることでPowerShellはより安定して高速に処理が可能となります。

Workflowを使ってみよう

どんな時に、どんな風にWorkflowを使うのか、考えてみましょう。

Workflowをどんな時に使っているの?

私自身、現職においてPowerShellでサーバー管理Moduleを作成しています。 近日公開しますが、このModuleにおいてもWorkflowは取り入れており、AWSクラウド上の数多くのWindowsサーバーを管理、維持する中で、その力がいかんなく発揮されています。 Workflowの持つ制限や特性から、全てをWorkflowで行っているわけではありませんが、その恩恵に日々預かっています。

従来の Scripting を知っていれば Workflow は簡単に利用できる

従来のWindows PowerShell Scriptingとは、 funcntionやScriptBlock、 foreach 、 変数など基本的な構文です。この知識さえあれば、Workflowの利用に際して新しく覚えることはわずかなものです。

Workflow独自の構文として、よく使うところでは以下の構文です。

workflow
inlinescript { }
foreachparallel
parallel { }
sequence { }

特にinlinescript {}foreach -parallelが特徴的です。 細かい使い方は本記事では扱いませんが、このリンクが大事な情報の多くを網羅しているので、ぜひ目を通して試してください。

サンプルコード

参考までに、PowerShellのforeachではできない、 parallelなforeachをWorkflowを使って書いてみましょう。 サンプルはGitHubに挙げておきます。 https://github.com/guitarrapc/PowerShellUtil/tree/master/WorkflowTest

通常の PowerShell による foreach

これは、10.0.3.100と10.0.3.150のPCに対してTest-Connectionを行った場合です。

foreach ($pc in @("10.0.3.100","10.0.3.150"))
{
    Test-Connection -ComputerName $pc
}

当然、10.0.3.100を実行した後に、 10.0.3.150が実行されています。

Source        Destination     IPV4Address      IPV6Address                              Bytes    Time(ms)
------        -----------     -----------      -----------                              -----    --------
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0

Workflow による ふつーの foreach

簡単にworkflowでふつーのforeachをしてみましょう。

workflow test-workflowforeach{

    param(
    $computerName
    )

    foreach ($pc in $computerName)
    {
        Test-Connection -ComputerName $pc
    }

}

test-workflowforeach -computerName ("10.0.3.100","10.0.3.150")

workflow構文で書いても、PowerShellのforeachと同様の結果になります。

Source        Destination     IPV4Address      IPV6Address                              Bytes    Time(ms)  PSComputerName
------        -----------     -----------      -----------                              -----    --------  --------------
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge

Workflow による parallel な foreach

workflowでparallelなforeachをするのは簡単です。 foreachに -parallelスイッチを付けます。

workflow test-workflowforeachparallel{

    param(
    $computerName
    )

    foreach -parallel ($pc in $computerName)
    {
        Test-Connection -ComputerName $pc
    }

}

test-workflowforeachparallel -computerName ("10.0.3.100","10.0.3.150")

parallelを付けたforeach内では、実行順序は保障されません。 そのため、両者の結果が入り混じって返ってきます。 今回は10.0.3.150と10.0.3.100交互に返ってきていますが、必ずしもそうなる保証はどこにもなく偶々です。

Source        Destination     IPV4Address      IPV6Address                              Bytes    Time(ms)  PSComputerName
------        -----------     -----------      -----------                              -----    --------  --------------
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       1         fuga
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga

Workflow に対応していない一部の PowerShellコマンドレットをInlineScript構文 で実行する

Workflowに対応していないPowerShellコマンドレットを実行するには、 InlineScript構文を用います。

この構文内では、ほぼすべてのWindows PowerShellコマンドレットが「Workflowに管理されていない通常のPowerShellセッション」として実行されます。たとえば、 .ps1ファイルをWorkflowで実行する時にもInlineScript構文を用います。

InlineScript構文はWorkflowのみで有効でありPowerShellのfunction構文では利用できません。Workflow内のInlineScript構文内で実行された通常のPowerShellセッションの動作、出力はセッション完了時にInlineScript処理を終えて、結果をWorkflowに返します。

例えば、先ほどのforeach -parallel処理内部でInlineScript構文を利用してみましょう。

workflow test-workflowforeachparallelInlineScript{

    param(
    $computerName
    )

    foreach -parallel ($pc in $computerName)
    {
        # InlineScript を付けると Invoke-Command や Invoke-Expression など 通常の PowerShell コマンドレットの多くが利用できるようになる
        InlineScript
        {
            # InlineScript内で、InlineScript 外の変数を取得する場合は using: 構文を用いる (ScriptBlock と同様)
            Test-Connection $using:pc
        }
    }

}

# Connection テスト
test-workflowforeachparallelInlineScript -computerName @("10.0.3.100","10.0.3.150")

InlineScript構文を付けると、構文内部でPowerShellセッションが実行されている間は、 InlineScript 実行中とポップアップが出ます。

なお、今回示す例の実行結果はparallelを付けたforeachと変わらず、対象としたPCそれぞれの応答結果が実行順序の保障なく入り混じって返ってきます。

Source        Destination     IPV4Address      IPV6Address                              Bytes    Time(ms)  PSComputerName
------        -----------     -----------      -----------                              -----    --------  --------------
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       1         fuga
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga
WHITE-DEPL... 10.0.3.150      10.0.3.150       ::1                                      32       0         hoge
WHITE-DEPL... 10.0.3.100      10.0.3.100                                                32       0         fuga

Workflow の foreach -parallel と 通常の foreach での速度差

単純に2つのPCに対しての比較を見てみましょう。

workflow test-workflowforeachparallelInlineScript{

    param(
    $computerName
    )

    foreach -parallel ($pc in $computerName)
    {
        # InlineScript を付けると Invoke-Command や Invoke-Expression など 通常の PowerShell コマンドレットの多くが利用できるようになる
        InlineScript
        {
            # InlineScript内で、InlineScript 外の変数を取得する場合は using: 構文を用いる (ScriptBlock と同様)
            Test-Connection $using:pc
        }
    }

}

# 通常の foreach の場合
# TotalSeconds      : 6.1301668 sec
Measure-Command {
    @("10.0.3.100","10.0.3.150") | %{Test-Connection $_}
}

# foreach -parallel の場合
# TotalSeconds      : 3.1872539 sec
Measure-Command {
    test-workflowforeachparallelInlineScript -computerName @("10.0.3.100","10.0.3.150")
}

このように、 foreach -parallelのほうが高速に完了していることがわかります。 どうでしょうか、簡単にですがfunctionをworkflowに変えるだけで使えそうな気分になりましたか?

Workflowの注意点

一見するとメリットが目につくWorkflowですが、通常のPowerShell Scriptingと全く同じではなく注意すべき点が複数あります。 それを見てみましょう。

Workflow はマルチスレッドではない

Workflowを行う理由として、「foreachのsequentialな実行を避けてparallelに並列/非同期実行したい」というのがあります。 この時に、マルチスレッドないことを把握しておかないと落とし穴になりかねません。 @Ahfさんが詳しくまとめていらっしゃるので、foreach -parallelを利用する場合は必ず目を通すことをおすすめします。

てすとぶろぐ - ワークフロー上のアクティビティを非同期に複数動作させるには てすとぶろぐ - PowerShell 3.0 の ForEach –parallel はマルチスレッドではない

私の経験事例では、シングルスレッドでの非同期 => これはマルチスレッド上での非同期と動作が異なり処理の途切れるタイミングがない場合は、 シングルスレッド上で5つずつ走ることになります。そのため、5つを超える多数のノードに対する実行の場合は、 workflowではうまく動作しない場合があります。

  • 例:処理と処理の間にSleepを仕込んだScriptBlockをworkflowで5を超える複数ノードに一斉実行時で、 一斉にコマンドを送信してほしくとも制限された数ずつ送り、 sleepでの切り替えもスムーズにいかず詰まる場合など

この場合は、 WorkflowではなくAsyncに動作する非同期モジュールを作成して利用する方法があります。

Workflow の実行プロセス数の制限

Workflowを実行して、Powershell.exeを終了して、また立ち上げてWorkflowを実行して...... を繰り返していると、 突如リモートPCへの接続出来なくなるケースがあります。 これは、PowerShellのJob同様同時にWmiPrvSE.exeプロセスで管理するWSManInstance数が上限に達したため発生します。

※ このコネクションは再利用されるため、 一度WorkdlowでつないだPowerShell.exeを落とさずに再利用している場合は、まず発生しません。

WSManInstanceセッションの取得

workflow内部などでこのコードを仕込むことでWSManInstanceセッションが取得できます。 .countでセッション数も取得可能です。

$WSManInstance = Get-WSManInstance shell -Enumerate
$WSManInstance.count

WSManInstance数の上限

WSManInstance数の上限は 25 のため、 22程度で削除やリセットを書けることも視野に入れたほうがいいでしょう。 ただし、削除/リセット時に持っているWorkflowセッションも切れるため、そもそも再利用するようにすべきですが。

WSManInstanceセッションの削除

現在のWSMainInstanceセッションは以下のコードで削除も可能です。

# Will remove specific session you select include current. (In this command will be all session)
$WSManInstance | %{Remove-WSManInstance -ConnectionURI http://localhost:5985/wsman shell @{shellid=$_.ShellId}}

現在の WSManInstance数をリセットする

WSManInstance数の削除は、都度選択が大変なため、リセットも視野に入れましょう。リセットはWinRMサービスのリスタートで行えます。

Restart-Service -Name WinRM -Force -PassThru -ErrorAction Stop

Workflow構文内での変数指定

少し癖があるので、気を付けてください。 構文と参照の組み合わせにより変わります。

Using Variables in Script Workflows

構文

  • Loops/Control構文
  • Parallel / Sequence構文
  • InlineScipt構文

参照

  • Workflow変数の参照
  • Workflow変数の変更
  • Workflow内部変数がWorkflowスコープで見られるか
Rules for Nested Statements in Workflows View workflow variable values Change workflow variable values Internal variables are visible in workflow scope
Loops/Control Statements Yes Yes Yes
Parallel / Sequence Yes No. Use $Workflow. No, return the variable.
InlineScript No. Use $Using. No. Return the variable. $Workflow is invalid. No, return the variable.

InlineScript でも実行できない コマンドレット

実はこれが問題です。 Workflowでサポート対象外のPowerShell動作一覧があります。

Hey, Scripting Guy! Blog PowerShell Workflows: Restrictions

Other cmdlets that haven’t been turned into workflow activities are shown in the following table. You can use them in InlineScript blocks.

Unsupported cmdlet (group) Reason
Alias, FormatData, History, Location, PSDrive, Transcript, TypeDate, *Variable, Connect/Disconnect-Wsman Only change Windows PowerShell セッション so not needed in workflow
Show-Command, Show-ControlPanelItem, Get-Credential, Show-EventLog, Out-Gridview, Read-Host, Debug-Process Workflows don’t support interactive cmdlets
BreakPoint, Get-PSCallStack, Set-PSDebug Workflows don’t support script debugging
Transaction Workflows don’t support transactions
Format* No formatting support
PSsession, *PSsessionoption Remoting controlled by workflow
Export-Console,Get-ControlPanelItem, Out-Default, Out-Null, Write-Host, Export-ModuleMember, Add-PSSnapin, Get-PSSnapin, Remove-PSSnapin, Trace-Command

たとえば、 Write-hostやRead-Hostが当てはまります。例でみましょう。

workflow test-workflow{

    param($hoge)
    Write-Host "hoge"
}

実行しようとすると、エラーが出てそもそもworkflowとして構文登録自体できません。

発生場所 行:7 文字:5
+     Write-Host "hoge"
+     ~~~~~~~~~~~~~~~~~
'Write-Host' コマンドを呼び出すことができません。このモジュールからの他のコマンドはワークフロー アクティビティとしてパッケージ化されていますが、このコマンドは除外されました。原因として、コマンドで Windows PowerShell の対話セッションが必要か、コマンドの動作がワークフローに適していないことが考えられます。このコマンドを実行するには、コマンドが独立して呼び出されるインライン スク
リプト (InlineScript { Write-Host }) にコマンドを含めてください。
    + CategoryInfo          : ParserError: (:) [], ParseException
    + FullyQualifiedErrorId : CommandActivityExcluded

例外として、localhostでの実行可否について記述があります。

There are a number of cmdlets that are local execution only in workflows.
Add-Member Compare-Object ConvertFrom-Csv ConvertFrom-Json
ConvertFrom-StringData Convertパス ConvertTo-Csv ConvertTo-Html
ConvertTo-Json ConvertTo-Xml ForEach-Object Get-Host
Get-Member Get-Random Get-Unique Group-Object
Measure-Command Measure-Object New-PSSessionOption New-PSTransportOption
New-TimeSpan Out-Default Out-Host Out-Null
Out-String Select-Object Sort-Object Update-List
Where-Object Write-Debug Write-Error Write-Host
Write-Output Write-Progress Write-Verbose

WriteHostは、あいにくとInlineScriptに囲んでlocalhostに対して行ってもできない場合があるため、既存のコードをworkflowで使おうと思った時は検証を要します。

workflow test-workflow{

    param($hoge)
    InlineScript
    {
        Write-Host "hoge"
    }
}
test-workflow -hoge 1 -PSComputerName localhost

だめですね。

ユーザーにダイアログを表示するコマンドの実行に失敗しました。ホスト プログラムまたはそのコマンドの種類では、ユーザーの操作はサポートされていません。ユーザーの操作をサポートしているホスト プログラム (Windows PowerShell コンソール、Windows PowerShell ISE など)
を使用し、ユーザーの操作をサポートしていないコマンドの種類 (Windows PowerShell ワークフローなど) からダイアログの表示に関連するコマンドを削除してください。
    + CategoryInfo          : NotImplemented: (:) [Write-Host], HostException
    + FullyQualifiedErrorId : HostFunctionNotImplemented,Microsoft.PowerShell.Commands.WriteHostCommand
    + PSComputerName        : [localhost]

Write-Hostは使えませんが、 Write-OutputやWrite-Warning 、 Write-Verboseは利用可能です。

このworkflowに対応していないcmdletの問題は、Workflow自体がリモート実行使えない要因になるためコマンドレットの利用には気を付けてください。

あいにくとPowerShell 4.0でも改善報告がないため、難しい問題です。

InlineScript を用いた Invoke-Command の利用にはScriptBlockの生成が必要

InlineScriptを用いるとInvoke-Commandが利用可能ですがScriptBlockを渡す際に一段階処理が必要です。例えば、 workflowに {Invoke-RestMethod -Method Get -Uri google.com} をScriptBlockとして渡す処理を見てみましょう。

workflow Invoke-WorkflowParallel{

    [コマンドレットBinding(DefaultParameterSetName = "ScriptBlock")]
    param(
        [parameter(
            Mandatory = 1,
            Position = 0,
            ParameterSetName = "ScriptBlock")]
        [scriptblock]
        $scriptBlock,

        [parameter(
            Mandatory = 1,
            Position = 0,
            ParameterSetName = "Expression")]
        [string]
        $expression,

        [parameter(
            Mandatory = 1,
            Position = 0,
            ParameterSetName = "other")]
        [string]
        $argumentlist,

        [parameter(
            Mandatory = 1,
            Position = 1)]
        [array]
        $array
    )

    if ($scriptBlock)
    {
        foreach -Parallel ($a in $array)
        {
            inlinescript
            {
                # これはだめ
                # Invoke-Command -ScriptBlock {&$usingScriptBlock}
                # これもだめ
                # Invoke-Command -ScriptBlock {$usingScriptBlock}
                # これもだめ
                # Invoke-Command -ScriptBlock $usingScriptBlock

                # ScriptBlock は生成してから行う
                $WorkflowScript = [ScriptBlock]::Create($using:ScriptBlock)
                Invoke-Command -ScriptBlock {&$WorkflowScript} -ErrorAction Stop
            }
        }
    }
    elseif ($expression)
    {
        foreach -Parallel ($a in $array)
        {
            inlinescript
            {
                # Invoke-Command とことなり生成が不要
                Invoke-Expression -Command $using:expression
            }
        }
    }
    elseif ($argumentlist)
    {
        # ScriptBlock でも Expression でもなく直接 PowerShell cmdlet を利用することは可能
        Write-Verbose -message "$argumentlist"
        Invoke-RestMethod -Method Get -Uri google.com
    }
}

# ScriptBlock で実行する場合
Invoke-WorkflowParallel -scriptBlock {Invoke-RestMethod -Method Get -Uri google.com} -array 1,2,3,4,5

# Expression で実行する場合
Invoke-WorkflowParallel -expression "Invoke-RestMethod -Method Get -Uri google.com" -array 1,2,3,4,5

# 直接 workflow に記述してある内容を実行する
Invoke-WorkflowParallel -argumentlist "直接Workflow内部に記述した内容を実行する" -array 1,2,3,4,5 -Verbose

このように、 Workflow内部でInvoke-Commandを実行するときは、[ScriptBlock]::Create({$scriptblock}) すると素直に渡せます。

Workflow 内部では パラメータ名の省略ができない

繰り返しますが、 WorkflowはPowerShellのサブセットです。 そのため、 WorkflowではPowerShell Scriptingで行っているような位置パラメータによるパラメータ名の省略が出来ません。 日頃触っているとついパラメータ名を省略して手早く書いてしまいますが、Workflowではきっちり書いてください。

このようなコードで分かります。

workflow test-workflowOmitParameterName{

    # パラメータ名が省略されているので workflow として登録できない。
    Write-Warning "hoge"
}

実行するとエラーが出ます。

At line:3 char:19
+     Write-Warning "hoge"
+                   ~~~~~~
Positional parameters are not supported in a Windows PowerShell Workflow. To invoke this command, use explicit parameter names with all values. For example: "Command -Parameter <value>".
    + CategoryInfo          : ParserError: (:) [], ParseException
    + FullyQualifiedErrorId : PositionalParametersNotSupported

この場合は、 -messageパラメータを宣言します。

workflow test-workflowOmitParameterNameCorrect{

    # パラメータ名をきっちり宣言すれば問題ない
    Write-Warning -message "hoge"
}

test-workflowOmitParameterNameCorrect

実行できました。

WARNING: [localhost]:hoge

最後に

先日公開したPS-SumoLogicAPI Moduleでもparallelスイッチでworkflowによるforeach -parallel (){} を呼び出しています。

@Ahfさんが指摘している通り、すべからくworkflowにしたり、maxthrottleを変更しても効果が薄いためどのような時に使うか考慮は必要です。

が、PowerShellとしては簡単に使える強力な手法ではあります。目的に合わせて利用すると良いでしょう。

特に、複数ノードがターゲットであったり、長時間実行する処理などで効果を発します。 ぜひ活用してください。