Azure Webサイトへの発行の自動化について

こんにちは、帝国兵です。この記事はWindows Azure Advent Calendarの21日目になります。

Azure Webサイトは簡単にWebサイトを立ち上げられるサービスです。すでにいろいろなところで使い方が紹介されているので、どんなものなのかはそちらを参照してくださいませ。たとえばしばやん先生のこれ

- [私がWindows Azure Webサイトを大好きな７つの理由](http://shiba-yan.hatenablog.jp/entry/20131201/1385823617)

この記事ではWebサイトへのコンテンツの発行(publish)の自動化についてちょっと触れてみたいと思います。kuduについての記事を期待した方、ごめんなさい。そっちはまたの機会に。

# 発行の自動化の目的

Azure WebサイトへはVisual Studioの操作で簡単に発行できますし、発行作業を自動化するためのスクリプトを自動生成したりしてくれるのですが、実行時に制約がでてきます。つまりスクリプトを実行するマシンにVisual Studioかmsbuildがないとダメとか、projファイル(＝ソースコード)にアクセスできる必要があるとかですね。そういう実行環境依存をできるだけ減らすことで、どこからでも安定して発行処理を実行できます。そんなことして意味あるのかと言われそうですが、例えばこんな感じで使えます。

- 同一コンテンツのサイトをスケールアウトせずに沢山作る
- 複数種類のコンテンツのサイトを沢山、頻繁に作る
- 数分ごとにサイトを新規作成してコンテンツを発行してサイトを削除する

そんなことしませんか？うーんたしかにクラウド作るのでない限り必要ないかも。。。まあいいや。誰かの参考になることもあるでしょう。

さて前置きが長くなりましたが、本記事の技術的なトピックは以下の2点です。

1. Azure WebサイトのpublishSettingsファイルの中身、および
1. それを使ってpublishするコード

まあ簡単ですね。

# 準備

まずは空のサイトを作るところから始めましょう。左下の「新規」ボタンから「WEBサイト」→「簡易作成」を選んで、適当なURLと地域を選んでから「WEBサイトの作成」をクリックします。

![空のWEBサイトを作成](https://qiita-image-store.s3.amazonaws.com/0/32767/dcd58741-493e-935d-115e-c5e4ba4e08bd.jpeg)

10秒ほどでサイトが作成されます。

![WEBサイト一覧](https://qiita-image-store.s3.amazonaws.com/0/32767/683eb29e-7fb2-078e-8c46-e4db00440759.jpeg)

出来たサイト(この場合は azureadventcalendar21 )の行をクリックして選択してください。この時サイト名（「名前」フィールド）をクリックすると **ダッシュボード** にページが切り替わります。

![ダッシュボードその1](https://qiita-image-store.s3.amazonaws.com/0/32767/a64bf900-8ed6-a03e-249a-7d7450163996.jpeg)

画面下にある「参照」ボタンを押してブラウズしてみましょう。どうでもいいですけど参照って意味が分かりづらいと思うんですよね。「開く」とかの方がいいと思うんだけどなあ。

![作成された空のサイト](https://qiita-image-store.s3.amazonaws.com/0/32767/574731d5-8fa1-3197-5194-a1b5f26088bf.jpeg)

使い慣れた人にはおなじみのページですが、ギャラリからWordPressしか作成したことのない人にはなじみが薄いかも知れません。余談ですが、この「簡易作成」で作成されたサイトには site\wwwroot\hostingstart.html というファイルが自動的に作られ、デフォルトページはこのファイルをポイントしています。FileZillaで見るとこんな感じです。

![FileZillaで見た簡易作成サイトのコンテンツ一覧](https://qiita-image-store.s3.amazonaws.com/0/32767/f2ec23a9-4564-993d-714f-327b7e3d7693.jpeg)

デフォルトページにマップされるファイル名は予め優先順位付きでいくつか設定されており、hostingstart.html はもっとも低い優先度になっています。なので、たとえば index.html をこのサイトのアップロードすれば、自動的にデフォルトページは index.html を指します。このファイル名リストはいつでも変更することが可能です。

話が横に逸れてしまいましたが、このあたりちゃんと説明した記事が見つからなかったのでよしとしましょう。

# publishSettingsファイルは2種類ある

本題のpublishSettingsに戻ります。ダッシュボードから「発行プロファイルのダウンロード」をクリックします。ブラウザがファイルのダウンロードの許可を求めてくるので適当なフォルダに保存してください。

![ダッシュボードその2](https://qiita-image-store.s3.amazonaws.com/0/32767/59883249-f2fe-0238-1000-1a94a4609a8c.jpeg)

ファイル名はデフォルトでは **<サイト名>.PublishSettings** になります。この長ったらしい拡張子のファイルを使うことで、いちいち手で認証手続きをせずに発行処理を行えます。VisualStudioもWebMatrixも内部ではこのファイルを使っています。初代WebMatrixを使ったことのある方は、手動でこのファイルをインポートしていたのでご存知ですよね。

さてWindows Azureが使う publishSettings には実は2種類あります。

1. Azureのサブスクリプションに関するpublishSettings
2. Webサイト用のpublishSettings

1番目は、サブスクリプションに対しての管理証明書のリストです。クラウドサービス等のAzureが提供するサービス全体に適用されます。これを使うとクラウドサービスに対しての各種自動化処理が可能になります。再起動したり再イメージ化したりは当然のこと、自動でデプロイしたりアップグレードしたりできます。楽しいですよ。

本記事では2番目の方を解説します。先ほどダウンロードしたやつです。中身はこんな感じです。見やすいように整形してあります。

```xml
<publishData>
    <publishProfile profileName="azureadventcalendar21 - Web Deploy"
            publishMethod="MSDeploy"
            publishUrl="waws-prod-hk1-001.publish.azurewebsites.windows.net:443"
            msdeploySite="azureadventcalendar21"
            userName="$azureadventcalendar21"
            userPWD="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
            destinationAppUrl="http://azureadventcalendar21.azurewebsites.net"
            SQLServerDBConnectionString=""
            mySQLDBConnectionString=""
            hostingProviderForumLink=""
            controlPanelLink="http://windows.azure.com">
        <databases/>
    </publishProfile>
    <publishProfile profileName="azureadventcalendar21 - FTP"
            publishMethod="FTP"
            publishUrl="ftp://waws-prod-hk1-001.ftp.azurewebsites.windows.net/site/wwwroot"
            ftpPassiveMode="True"
            userName="azureadventcalendar21\$azureadventcalendar21"
            userPWD="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
            destinationAppUrl="http://azureadventcalendar21.azurewebsites.net"
            SQLServerDBConnectionString=""
            mySQLDBConnectionString=""
            hostingProviderForumLink=""
            controlPanelLink="http://windows.azure.com">
        <databases/>
    </publishProfile>
</publishData>
```
見ればわかる感じですが軽く説明しますと、大きく2つの<publishProfile>セクションが含まれています。最初がWebDeployを使った発行用で、次がFTP用です。

- **WebDeploy** とはWindows用デプロイツール Web Deploy のことです。Web Platform Installerからインストールできます。転送時に複数のファイルをまとめてバルクとして圧縮し、リスタート可能な専用プロトコルで転送します。とても高速で私は気に入って使っています。デフォルトでは8172番ポートなのですが、WebサイトではHTTPSと同じ443を使います。
- **FTP** はUNIX環境でも使える汎用な転送プロトコルですが、ファイルを1個ずつ送るので遅いです。正直さすがに時代遅れ感が否めませんが、自分のサイトにアップロード済みのコンテンツを直接見たい時にとても重宝します。

発行時の認証はuserNameとuserPWDアトリビュートの値を使います。はいBasic認証です、すみません。基本的にFTPとWebDeployどちらも同じですが、FTPの場合はuserNameは「サイト名+"\"+ユーザ名」という形式になっています。FileZillaで接続する場合は全部入力してください。
userNameの頭にドル記号がついているのはそのサイトだけで有効なユーザ名です。他に作ってあるWebサイトでは使えません。ドル記号がついていない場合はgit等のソースコントロールで使われますが、ここでは説明しません。

あとはデータベースの設定とかが必要に応じて自動的に付加されます。ここでは簡易設定で作ったので空っぽですね。

# WebDeployで発行するコード

社内ではC#で書いて共用ライブラリとして使っています。MSDeployのライブラリについてはあまりドキュメントがないんですが、ネットを探していたらありました。さすがですね。

- [MSDeployをAPIとして、C#のコードから実行する](http://techblog.hilife-jp.info/2013/07/msdeploy-from-csharp/)

このリンク先の記事に必要なことがかなり書いてあるのでモチベーションがだだ下がりな感じですが、頑張ります。上述のpublishSettingsファイルを読み込んでもろもろのインスタンスに値を放り込んでから SyncTo() を呼び出せばOKなのですが、必要な設定がやたら多いのが特徴です。

Web Platform InstallerからWeb Deployment Toolsをインストールしましょう。あるいはもしインストール済みのマシンがあれば、そのマシンから Microsoft.Web.Deployment.Dll だけを持ってきてプロジェクトに足して参照に追加してやればWeb Deployment Toolsをインストールする必要すらありません。依存DLLもないのでお手軽です。

とにかくソースを載せます。発行元コンテンツはフォルダ、ZIPパッケージ、単体ファイルどれでもいけますが、データベースを使うパッケージの場合はサイト作成時に別途説明が必要なので後述します。

```csharp
using System;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.Web.Deployment;

namespace WebSitePublisher
{
    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;
        }
    }

    public enum ContentType
    {
        Pacakge,
        Folder,
        File
    }

    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length < 2)
            {
                Console.Error.WriteLine("Usage: {0} <publishSettings> <source>", Path.GetFileName(Assembly.GetEntryAssembly().Location));
                Environment.Exit(1);
            }
            var publishSettingsPath = args[0];
            var sourcePath = args[1];
            ContentType contentType = ContentType.File;

            if (!File.Exists(publishSettingsPath))
            {
                Console.Error.WriteLine("{0}: Not found.", publishSettingsPath);
                Environment.Exit(1);
            }
            if (Directory.Exists(sourcePath))
            {
                contentType = ContentType.Folder;
            }
            else if (!File.Exists(sourcePath))
            {
                Console.Error.WriteLine("{0}: Not found.", sourcePath);
                Environment.Exit(1);
            }
            else if (Path.GetExtension(sourcePath).Equals(".zip", StringComparison.InvariantCultureIgnoreCase))
            {
                contentType = ContentType.Pacakge;
            }

            var document = XElement.Load(publishSettingsPath);
            var profile = document.XPathSelectElement("//publishProfile[@publishMethod='MSDeploy']");

            if (profile == null)
            {
                Console.Error.WriteLine("{0}: Not a valid publishing profile.", publishSettingsPath);
                Environment.Exit(1);
            }

            var publishUrl = profile.SafeGetAttribute("publishUrl");
            var destinationAppUrl = profile.SafeGetAttribute("destinationAppUrl");
            var userName = profile.SafeGetAttribute("userName");
            var password = profile.SafeGetAttribute("userPWD");
            var siteName = profile.SafeGetAttribute("msdeploySite");

            // Database related attributes
            var databaseInfo = new SqlConnectionStringBuilder();
            var databaseSection = profile.XPathSelectElements("./databases/add").FirstOrDefault();
            if (databaseSection != null)
            {
                databaseInfo.ConnectionString = databaseSection.SafeGetAttribute("connectionString");
            }
            var webDeployServer = string.Format(@"https://{0}/msdeploy.axd?site={1}", publishUrl, siteName);

            Console.WriteLine("Publishing {0} to {1}", sourcePath, destinationAppUrl);

            // Set up deployment
            var sourceProvider = contentType == ContentType.Pacakge ? DeploymentWellKnownProvider.Package : DeploymentWellKnownProvider.ContentPath;
            var destinationProvider = contentType == ContentType.Pacakge ? DeploymentWellKnownProvider.Auto : DeploymentWellKnownProvider.ContentPath;

            var sourceOptions = new DeploymentBaseOptions();
            var destinationOptions = new DeploymentBaseOptions
            {
                ComputerName = webDeployServer,
                UserName = userName,
                Password = password,
                AuthenticationType = "basic",
                IncludeAcls = true,
                TraceLevel = System.Diagnostics.TraceLevel.Info
            };
            destinationOptions.Trace += (sender, e) => Console.WriteLine(e.Message);

            var destinationPath = siteName;
            if (contentType == ContentType.File)
            {
                var filename = new FileInfo(sourcePath).Name;
                destinationPath += "/" + filename;
            }

            var syncOptions = new DeploymentSyncOptions { DoNotDelete = true };  // Please change as you want

            // Start deployment
            using (var deploy = DeploymentManager.CreateObject(sourceProvider, sourcePath, sourceOptions))
            {
                // Apply package parameters
                foreach (var p in deploy.SyncParameters)
                {
                    switch (p.Name)
                    {
                        case "IIS Web Application Name":
                        case "AppPath":
                            p.Value = siteName;
                            break;
                        case "DbServer":
                            p.Value = databaseInfo.DataSource;
                            break;
                        case "DbName":
                            p.Value = databaseInfo.InitialCatalog;
                            break;
                        case "DbUsername":
                        case "DbAdminUsername":
                            p.Value = databaseInfo.UserID;
                            break;
                        case "DbPassword":
                        case "DbAdminPassword":
                            p.Value = databaseInfo.Password;
                            break;
                    }
                }

                var changeSummary = deploy.SyncTo(destinationProvider, destinationPath, destinationOptions, syncOptions);

                Console.WriteLine("Deployment finsihed.");
                Console.WriteLine("Added: " + changeSummary.ObjectsAdded);
                Console.WriteLine("Updated: " + changeSummary.ObjectsUpdated);
                Console.WriteLine("Deleted: " + changeSummary.ObjectsDeleted);
                Console.WriteLine("Total errors: " + changeSummary.Errors);
                Console.WriteLine("Total changes: " + changeSummary.TotalChanges);
            }
        }
    }
}
```

destinationOptions.Traceにリスナハンドラを足して途中経過を表示するようにしています。結構沢山のログを吐くので邪魔だったらこの行をコメントアウトすれば、終了時にのみ結果だけ表示するようになります。サンプルコードなので例外はハンドルしていません。例外が起きたらexeが落ちます。実際に使うときはusing節をtry～catchで囲んでください。

# WordPressサイトをギャラリを使わず作ってみよう

WordPressは内部でMySQLを使用しています。しかしこの記事の上の方で「簡易作成」で作ったサイトには関連するデータベースがありません。なのでpublishSettingsにもデータベース接続文字列が含まれていません。これを使ってWordPressを発行しようとしてもエラーになります。

ではどうするか？簡易作成のほかに「カスタム作成」という機能があります。これを使うと「空のサイト＋データベース」の作成が簡単にできます。やってみましょう。

左下から「新規」→「WEBサイト」→「カスタム作成」を選択します。

![カスタム作成](https://qiita-image-store.s3.amazonaws.com/0/32767/9029516f-c905-d468-bc90-19854f06dab3.jpeg)

「Webサイトの作成」というダイアログが表示されるので、適当な名前と地域を選択します。今度は「データベース」という項目があるので「新しいMySQLデータベースを作成します」を選んでください。DB接続文字列はそのままでOKです。

![データベースを指定して作成](https://qiita-image-store.s3.amazonaws.com/0/32767/3f690890-502f-2da3-c315-0bbc0a5d85cc.jpeg)

「新しいMySQLデータベース」ダイアログでは、できればサイトと同じ地域を選びましょう。ClearDBのライセンス条項にチェックをつけてOKをクリックすれば作成できます。この辺りの手順はギャラリからのサイト作成と同じですね。

![MySQLデータベース作成](https://qiita-image-store.s3.amazonaws.com/0/32767/c724191a-05db-f818-6e54-78f89542b2a7.jpeg)

サイトのダッシュボードから発行プロファイルをダウンロードして、WordPressを発行してみましょう。注意点は、WordPressのサイトから **ではなく** Web Platform Installerからリンクが張られているIISバージョンを使う必要があることです。現在の最新版は http://wordpress.org/wordpress-3.8-IIS.zip ですので、これをローカルにダウンロードしてください。日本語版は残念ながらまだのようですね。

```bat
WebSitePublish.exe anotherwordpresstest.azurewebsites.net.PublishSettings wordpress-3.8-IIS.zip
```

こうしてDB接続文字列が正しく渡されると、DeploymentObject.SyncParametersで定義されたデータベース関連のDeploymentSyncParameterインスタンスに設定を埋め込んでいきます。

# 終わりに

Azure Webサイトの基礎的な知識でかつ深い部分ではないものと考えたところ、この発行処理について書くことにしました。後半駆け足で説明不足な点はお詫びいたします。これとAzure Command Line Tools等を併用すれば、一日中自動的にWordPressサイトを作っては壊し続けることができます。楽しいですね。

クラウドは「コンピュータ群」のリソース管理と並列・分散処理技術の集合です。なので自動化処理とそれに必要なAPIがすべて備わっています。既存のソフトをIaaSのVMで動かすだけでは、ほんのごく一部の機能しか使っていないことになります。皆さんにAzureをもっと楽しんで使ってもらえたら幸いです。

もっと楽しいプログラミングを！
それでは！