tech.guitarrapc.cóm

Technical updates

PowerShellからMSDeployを実行する

MSDeployは、 Web配置ツール (Web Deploy) によるアプリケーションパッケージの展開を可能にします。

IIS マネージャー用の Web 配置ツールの概要.aspx)

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

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

ここに記述があります。

Web 配置のコマンド ラインの構文.aspx)

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を利用してみましょう。このやり方が面倒な点は、-RedirectStandardOutputがAppend出来ないので、一旦外部ファイルに逃がす必要がある点です。 また、記述にある通りただのforeachをぶんまわすのでは対象ホストが1-3個程度ならいいのですが、10~となるとパッケージの大きさによってはとっても時間がかかります。

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

Start-Process を workflow で並列実行

workflowを使って5本並列で実行しましょう。workflowを使えば、foreach -parallelを使って並列にmsdeploy実行できます。

StandatdOutputを、ログに取り込む場合は、workflowの外部で読み取ってください。ファイル読み取りGet-Contentと書き込みOut-Fileが競合することを避けるためです。 workflowを使うことで、10 - 50程度の台数へ一斉配置する際でも大きく効率化されます。 更に高速化することも考えていますが、サクッと並列実行を実装可能な点ではworkflow便利。

# - 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 = "配置管理者パスワード"

古いログを消すにはこうします。

Get-ChildItem -Path $logfolder -Filter "tmp*" | Remove-Item -Force

MSDeployを実行するにはこうします。

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

ログを取得するにはこうします。

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

まとめ

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