読者です 読者をやめる 読者になる 読者になる

tech.guitarrapc.cóm

C#, PowerShell, Unity, Cloud, Serverless Technical Update and Features

PowerShell から MSDeploy を実行する

MSDeploy は、 Web 配置ツール (Web Deploy) によるアプリケーションパッケージの展開を可能にします。
IIS マネージャー用の Web 配置ツールの概要
このMSDeploy を使えば、ASP.NET MVC アプリをIIS ホストへ ファイル展開、同期することが容易になるため、非常に強力で利用すべき機能です。 MSDeploy には、よくコマンドラインでの利用構文が紹介されますが、PowerShell もサポートしています。 そこで、今回は、 PowerShell による MSDeploy の実行について見てみましょう。

MS-DOSコマンドでのコマンドライン構文

ここに記述があります。
Web 配置のコマンド ラインの構文
基本は、この構文です。
Msdeploy.exe コマンド ラインの主要な要素は、動詞 ("操作" とも呼ばれます)、同期元、同期先 (任意指定)、および操作設定 (任意指定) です。動詞と同期元は必須です。同期先は、動詞によって必要な場合と必要でない場合があります。任意指定の操作設定では、コマンドの実行方法を変更できます。

PowerShellでのWeb 配置の使用

PowreShell での利用も簡単で、変更点はこれだけです。
Web 配置のコマンドの verb、source、および dest の各引数の後ろのコロン (:) を等号記号 (=) に変更します。
例:
# cmd コマンド例:
msdeploy -verb:sync -source:metakey=/lm/w3svc/1 -dest:metakey=/lm/w3svc/2 -verbose

# PowerShell コマンド例:
.\msdeploy.exe -verb=sync -source=metakey=/lm/w3svc/1 -dest=metakey=/lm/w3svc/2 -verbose
もう少し本格的なコマンドで見てみましょう。
# cmd コマンド例:
"C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe" -verb:sync -source:package="C:\パッケージパス\パッケージ.zip" -dest:auto,computerName="http://対象ホストIP/MSDeployAgentService",userName="配置管理者ユーザー",password="配置管理者パスワード",includeAcls="False" -disableLink:AppPoolExtension -disableLink:ContentExtension -disableLink:CertificateExtension -setParamFile:"C:\パラメーターxmlパス/パラメータ.xml"

# PowerShell コマンド例:
$packagepath = "C:\パッケージパス"
$parameterxml = "C:\パラメータxml"
$hostip = "対象ホストIPAddress"
$user = "配置管理者ユーザー"
$pass = "配置管理者パスワード"

."C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe" -verb=sync -source=package="$packagepath" "-dest=auto,computerName=""http://$hostip/MSDeployAgentService"",userName=""$user"",password=""$pass"",includeAcls=""False""" -disableLink:AppPoolExtension -disableLink:ContentExtension -disableLink:CertificateExtension -setParamFile:"$parameterxml"

PowerShellでのWeb 配置のコード

PowerShell での展開のメリットは、PowerShell での制御が可能である事です。 では実際に PowerShell で展開する方法を考えてみます。

System.Diagnotic.Process での配置

展開には、 msdeploy.exe つまり 外部コマンドを利用することになります。 そこで、 まずは外部コマンドの制御が自由に扱える System.Diagnotic.Process を使ってみましょう。 このやり方は、StandardOutput などの制御も楽なんですが、パッケージ展開が止まってしまってます。 まだ原因を探っていませんがどうもほげりました。
$msdeploy = "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe"
$user = "配置管理者ユーザー"
$Password = "配置管理者パスワード"

foreach ($deploygroup in $deploygroups)
{
    # define arguments of msdeploy
    [string]$arguments = @(
        "-verb:sync",
        "-source:package=$zip",
        "-dest:auto,computerName=`"http://$deploygroup/MSDeployAgentService`",userName=$user,password=$Password,includeAcls=`"False`"",
        "-disableLink:AppPoolExtension",
        "-disableLink:ContentExtension",
        "-disableLink:CertificateExtension",
        "-setParam:`"IIS Web Application Name`"=`"W3C1hogehoge`"")
                
    # Start Process
    "running msdeploy to $deploygroup" | Out-LogHost -logfile $log -showdata
                                         
        # Deploy内容が存在した際に 実行されにゃいお (更新があった場合にのみ走らないので却下です)
        $processinfo = New-Object System.Diagnostics.ProcessStartInfo
        $processinfo.FileName = $msdeploy
        $processinfo.RedirectStandardError = $true
        $processinfo.RedirectStandardOutput = $true
        $processinfo.UseShellExecute = $false
        $processinfo.Arguments = $arguments
                        
        $process = New-Object System.Diagnostics.Process
        $process.StartInfo = $processinfo
        $process.Start() > $null
        $process.WaitForExit()

        $output = @()
        $output = $process.StandardError.ReadToEnd()
        $output += $process.StandardOutput.ReadToEnd()
        $output | Out-LogHost -logfile $log -hidedata
}

Start-Process での配置

ならばしょうがないと、Start-Processを利用してみましょう。
$msdeploy = "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe"
$user = "配置管理者ユーザー"
$Password = "配置管理者パスワード"

foreach ($deploygroup in $deploygroups)
{
    # define arguments of msdeploy
    [string]$arguments = @(
        "-verb:sync",
        "-source:package=$zip",
        "-dest:auto,computerName=`"http://$deploygroup/MSDeployAgentService`",userName=$user,password=$Password,includeAcls=`"False`"",
        "-disableLink:AppPoolExtension",
        "-disableLink:ContentExtension",
        "-disableLink:CertificateExtension",
        "-setParam:`"IIS Web Application Name`"=`"W3C1hogehoge`"")
                
    # Start Process
    "running msdeploy to $deploygroup" | Out-LogHost -logfile $log -showdata
                                         
        # foreach が sequencial で一向に終わらにゃいお
        Start-Process -FilePath $msdeploy -ArgumentList $arguments -Wait -RedirectStandardOutput $tmplog -RedirectStandardError $tmperrorlog -NoNewWindow
        Get-Content -Path $tmplog -Encoding Default | Out-File -FilePath $log -Encoding utf8 -Append
        Get-Content -Path $tmperrorlog -Encoding Default | Out-File -FilePath $log -Encoding utf8 -Append
        if ($tmplog) {Remove-Item -Path $tmplog -Force}
        if ($tmperrorlog) {Remove-Item -Path $tmperrorlog -Force}
}
このやり方が面倒な点は、-RedirectStandardOutput が Append 出来ないので、一旦外部ファイルに逃がす必要がある点です。 また、記述にある通りただの foreach をぶんまわすのでは 対象ホストが 1-3 個程度ならいいのですが、10~ となるとパッケージの大きさによってはとっても時間がかかります。

Start-Process を workflow で並列実行

しょうがないです。 workflow で 5本並列で実行しましょう。 非同期ではありません。並列実行です。 まずは workflow で Start-Process による msdeploy 実行を、 foreach -parallel とします。
# - msdeploy workflow -#

workflow Invoke-msdeployParallel{
    param(
        [parameter(
            position = 0,
            mandatory)]
        [string]
        $deploygroups,

        [parameter(
            position = 1,
            mandatory)]
        [string]
        $msdeploy,

        [parameter(
            position = 2,
            mandatory)]
        [string]
        $zip,

        [parameter(
            position = 3,
            mandatory)]
        [string]
        $user,

        [parameter(
            position = 4,
            mandatory)]
        [string]
        $Password,

        [parameter(
            position = 5,
            mandatory)]
        [string]
        $log,

        [parameter(
            position = 6,
            mandatory)]
        [string]
        $logfolder
    )

    foreach -parallel ($deploygroup in $deploygroups)
    {
        # setup tmplog
        $logfolder = $workflow:logfolder
        $log = $workflow:log
        $ipstring = "$deploygroup".Replace(".","")
        $tmplog = Join-Path -Path $logfolder -ChildPath $("tmp" + $ipstring +".log")
        $tmperrorlog = Join-Path -Path $logfolder -ChildPath $("tmperror" + $ipstring +".log")

        # define arguments of msdeploy
        [string]$arguments = @(
            "-verb:sync",
            "-source:package=$workflow:zip",
            "-dest:auto,computerName=`"http://$deploygroup/MSDeployAgentService`",userName=$($workflow:user),password=$($workflow:Password),includeAcls=`"False`"",
            "-disableLink:AppPoolExtension",
            "-disableLink:ContentExtension",
            "-disableLink:CertificateExtension",
            "-setParam:`"IIS Web Application Name`"=`"W3C1hogehoge`"")
                
        # Start Process
        $msdeploy = $workflow:msdeploy
        Write-Warning -Message "[$(Get-Date)][message][""running msdeploy to $deploygroup""]"
        "[$(Get-Date)][message][""running msdeploy to $deploygroup""]" | Out-File -FilePath $tmplog -Encoding utf8 -Append
            Start-Process -FilePath $msdeploy -ArgumentList $arguments -Wait -RedirectStandardOutput $tmplog -RedirectStandardError $tmperrorlog -NoNewWindow
    }
}
あとは、これを呼び出し実行するだけです。
$msdeploy = "C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe"
$user = "配置管理者ユーザー"
$Password = "配置管理者パスワード"

# remove old tmp log files
Get-ChildItem -Path $logfolder -Filter "tmp*" | Remove-Item -Force

# run msdeploy
Invoke-msdeployParallel -deploygroups $deploygroups -msdeploy $msdeploy -zip $zip -user $user -Password $Password -log $log -logfolder $logfolder

# Read Logfiles
$result = @()
foreach ($deploygroup in $deploygroups)
{
    # setup tmplog
    $logfolder = $logfolder
    $log = $log
    $ipstring = "$deploygroup".Replace(".","")
    $tmplog = Join-Path -Path $logfolder -ChildPath $("tmp" + $ipstring +".log")
    $tmperrorlog = Join-Path -Path $logfolder -ChildPath $("tmperror" + $ipstring +".log")
                    
    $result += "[$((Get-Item $tmplog).LastWriteTime)][message][Result of MSDeploy for {$deploygroup}]"
    $result += Get-Content -Path $tmplog -Encoding Default -Raw
    $result += Get-Content -Path $tmperrorlog -Encoding Default -Raw
}
StandatdOutput を、ログに取り込む場合は、workflow の外部で読み取ってください。 これは、 ファイル読み取り Get-Content と 書き込み Out-File のプロセスが競合することを避けるためです。 workflow を使うことで、10 - 50 程度の台数へ一斉配置する際でも大きく効率化されます。 更に高速化することも考えていますが、サクッと並列実行を実装可能な点では workflow は便利です。

まとめ

速度を求める場合は、 PowerShell ではなく C# で実行コードを書いて置くべきでしょう。 しかし、PowerShell で記述することで、自動化の一部に容易に組み込めるメリットもあります。 このような外部コマンドとの連携 + 自動化 は PowerShell を使っていて頻繁に利用したくなるので、ぜひ参考になれば幸いです。