ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Azure Web Apps に Parameters.xml を使って WebDeploy する

ASP.NET MVC アプリケーションの WebDeploy パッケージを Azure Web Apps にデプロイする際に、Web.config に埋め込まれた Storage などの接続文字列を書き換えるために zip ファイルを展開する作業が非常に手間でした。

調べてみると、デプロイする際に Parameters.xml で Web.config の値を書き換えられることを知りました。この方法なら、zip ファイルを展開する必要はないですし、本番環境で使う接続文字列をソースコード管理に入れることなく、安心して開発できます。

前準備

ASP.NET MVC アプリケーションの Web.config に、Azure Storage の接続文字列を定義します。

#Web.conifg
<configuration>
  <appSettings>
    <add key="MyStorage" value="UseDevelopmentStorage=true"/>
  </appSettings>
</configuration>

プロジェクトの直下に Parameters.xml を追加し、書き換えルールを定義します。

#Parameters.xml
<?xml version="1.0" encoding="utf-8" ?>
<parameters>
  <parameter name="appSettings_MyStorage" description="Azure Storage の接続文字列">
    <parameterEntry kind="XmlFile" defaultValue="UseDevelopmentStorage=true" scope="\\Web.config$" match="//appSettings/add[@key='MyStorage']/@value" />
  </parameter>
</parameters>

Visual Studio から WebDeploy パッケージを作成すると、WebApplication1.SetParameters.xml に書き換えの設定が追加されていることが分かります。

#WebApplication1.SetParameters.xml
<?xml version="1.0" encoding="utf-8"?>
<parameters>
  <setParameter name="IIS Web Application Name" value="Default Web Site/WebApplication1_deploy" />
  <setParameter name="appSettings_MyStorage" value="UseDevelopmentStorage=true" />
</parameters>

この value の値を Azure Storage の接続文字列に変更すれば、Web.config を書き換えてデプロイすることができます。

コマンドからデプロイする

WebDeploy のコマンドを使って、デプロイします。「-setParamFile」の引数に SetParameters.xml のパスを渡します。ここでは、Azure Web Apps を 「deploytest」という名前で作成して、デプロイします。ユーザー名やパスワードは、Azure ポータルからダウンロードできる Publish プロファイルに記載されています。

#msdeploy.exe
"C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe"
    -verb:sync -source:package="C:\Users\xxx\Desktop\Test\WebApplication1.zip"
    -dest:auto,ComputerName="https://deploytest.scm.azurewebsites.net:443/msdeploy.axd?site=deploytest",UserName='$deploytest',Password='xxx',AuthType='Basic',IncludeAcls='true'
    -enableRule:DoNotDeleteRule -setParam:"IIS Web Application Name"='deploytest'
    -setParamFile:"C:\Users\xxx\Desktop\Test\WebApplication1.SetParameters.xml"

C# のコードからデプロイする

コマンドはどうも苦手なので、やっぱり C# でコードを書いてデプロイしたいです。以前に帝国兵さんが書かれた記事とほぼ同じですが、SetParameters.xml を渡せるように改良しました。

コンソールアプリケーションなどを作成し、WebDeploy に必要なライブラリを NuGet からインストールしておきます。

引数には、Publish プロファイル、WebDeploy パッケージ、SetParameters.xml のパスを渡します。SetParameters.xml から読み取った値を DeploymentObject のパラメーターにセットしている部分がポイントです。

#WebAppsPublisherHelpler.cs
public static class WebAppsPublisherHelpler
{
    public static DeploymentChangeSummary Publish(string publishSettingsPath, string sourcePath, string parametersPath)
    {
        if (String.IsNullOrEmpty(publishSettingsPath)) throw new ArgumentNullException("publishSettingsPath");
        if (String.IsNullOrEmpty(sourcePath)) throw new ArgumentNullException("sourcePath");
        if (!File.Exists(publishSettingsPath)) throw new Exception(String.Format("{0}: Not found.", publishSettingsPath));
        if (!File.Exists(sourcePath)) throw new Exception(String.Format("{0}: Not found.", sourcePath));
        if (Path.GetExtension(sourcePath).Equals(".zip", StringComparison.InvariantCultureIgnoreCase) == false) throw new Exception("Extension supports only zip.");
 
        // PublishSettings
        var document = XElement.Load(publishSettingsPath);
        var profile = document.XPathSelectElement("//publishProfile[@publishMethod='MSDeploy']");
        if (profile == null)
        {
            throw new Exception(String.Format("{0}: Not a valid publishing profile.", publishSettingsPath));
        }
        var publishUrl = profile.SafeGetAttribute("publishUrl");
        var userName = profile.SafeGetAttribute("userName");
        var password = profile.SafeGetAttribute("userPWD");
        var siteName = profile.SafeGetAttribute("msdeploySite");
        var webDeployServer = string.Format(@"https://{0}/msdeploy.axd?site={1}", publishUrl, siteName);
 
        // Set up deployment
        var destinationOptions = new DeploymentBaseOptions{ ComputerName = webDeployServer, UserName = userName, Password = password, AuthenticationType = "basic", IncludeAcls = true, TraceLevel = TraceLevel.Info };
        destinationOptions.Trace += (sender, e) =>
        {
            Trace.TraceInformation(e.Message);
        };
        var syncOptions = new DeploymentSyncOptions { DoNotDelete = true };  // Please change as you want
 
        DeploymentChangeSummary result;
        try
        {
            // Start deployment
            using (var deploy = DeploymentManager.CreateObject(DeploymentWellKnownProvider.Package, sourcePath, new DeploymentBaseOptions()))
            {
                // Apply package parameters
                foreach (var p in deploy.SyncParameters)
                {
                    switch (p.Name)
                    {
                        case "IIS Web Application Name":
                            p.Value = siteName;
                            break;
                        default:
                            // SetParameters.xml
                            if (!String.IsNullOrEmpty(parametersPath))
                            {
                                var parameters = XElement.Load(parametersPath);
                                var setParameter = parameters.XPathSelectElement(String.Format("//setParameter[@name='{0}']", p.Name));
                                if (setParameter != null)
                                {
                                    p.Value = setParameter.SafeGetAttribute("value");
                                }
                            }
                            break;
                    }
                }
                result = deploy.SyncTo(DeploymentWellKnownProvider.Auto, siteName, destinationOptions, syncOptions);
            }
        }
        catch (Exception)
        {
            throw;
        }
        return result;
    }
 
}
public static class MethodExtention
{
    public static string SafeGetAttribute(this XElement node, string attribute, string defaultValue = null)
    {
        var attr = node.Attribute(attribute);
        return attr == null ? defaultValue : attr.Value;
    }
}

まとめ

Cloud Services では、構成ファイル(cscfg)に Storage の接続文字列を設定してデプロイできますが、Web Apps でも Parameters.xml を使うと同じようにデプロイできます。業務系の Web アプリケーションであっても、Web Apps ファーストで積極的に使っていきたいと思います。