次の方法で共有


ASP.NET

Topshelf および Katana: Web とサービスの統一アーキテクチャ

Wes McClure

コード サンプルをダウンロードする

IIS を使用して ASP.NET Web アプリケーションをホストするのは、10 年以上にわたる業界標準です。このようなアプリケーションのビルド プロセスは比較的シンプルですが、配置のプロセスはシンプルとは言えません。配置には、アプリケーション構成の階層や IIS の履歴の微妙な差異に関する十分な知識と、サイト、アプリケーション、仮想ディレクトリを準備する面倒な作業が必要になります。インフラストラクチャの重要な部分の多くが、手動で構成した IIS コンポーネントのアプリケーション外部に存在することになることもよくあります。

アプリケーションがシンプルな Web 要求に対応するだけでなく、時間がかかる要求や反復ジョブなどの処理作業をサポートする必要が生じると、これを IIS 内でサポートするのはかなり困難になります。多くの場合、別の Windows サービスを作成してこのようなコンポーネントをホストすることが解決策になります。しかし、これでは、同じ作業が重複するまったく別の配置プロセスが必要になります。Web のプロセスとサービスのプロセスの間で通信を行うようになるともう限界です。かなりシンプルなアプリケーションが極めて複雑なアプリケーションへとすぐに変貌します。

図 1 に、このアーキテクチャの代表的な形態を示します。Web 層は、簡単な要求の処理とシステムへの UI 提供を担当します。時間がかかる要求はサービスに委託されます。サービスは、反復ジョブや処理の制御も行います。さらに、サービスは、現在の作業や今後の作業に関する状態を Web 層に提供して UI に含めます。

Traditional Separated Web and Service Architecture
図 1 従来型の Web とサービスが分離されたアーキテクチャ

新しいアプローチ

さいわい、Web とサービスのアプリケーションの開発や配置をシンプルにする新しいテクノロジが登場しています。Katana プロジェクト (katanaproject.codeplex.com、英語) および OWIN (owin.org、英語) が提供する仕様により、IIS からホスト機能を取り出して Web アプリケーションを自身でホストできるようになり、WebApi や SignalR など、どこにでもある ASP.NET コンポーネントもサポートできます。この Web セルフホストを Topshelf (topshelf-project.com、英語) と一緒に基本的なコンソール アプリケーションに埋め込んで、Windows サービスを簡単に作成できます。その結果、Web とサービスのコンポーネントを、同じプロセスにサイドバイサイドで配置することができます (図 2 参照)。これにより、外部の通信層、個別のプロジェクト、および個別の配置プロシージャを開発するオーバーヘッドがなくなります。

Unified Web and Service Architecture with Katana and Topshelf
図 2 Katana と Topshelf による Web とサービスの統一アーキテクチャ

この機能は、まったく新しいものではありません。Topshelf は何年も前から Windows サービスの配置の簡素化をサポートしており、Nancy などのオープン ソース セルフホスト Web フレームワークもたくさんあります。ただし、OWIN が Katana プロジェクトに発展するまで、IIS で Web アプリケーションをホストするための業界標準になり得るものはありませんでした。また、Nancy などの多くのオープン ソース コンポーネントが Katana プロジェクトと連携するため、選りすぐりの柔軟なフレームワークを実現できます。

Topshelf はオプションのように思えますが、違います。サービスの配置を簡単にする機能がないと、セルフホスト型の Web 開発はかなり厄介になる可能性があります。Topshelf は、サービスをコンソール アプリケーションのように扱うことで、サービスとしてホストされることを抽象化して、サービスの開発を簡素化します。Topshelf は配置時に、Windows サービスとしてのアプリケーションのインストールと起動を自動的に処理します。つまり、InstallUtil を処理するオーバーヘッドがなく、サービス プロジェクトとサービス コンポーネントに微妙な差異はなく、何か問題が発生したときにデバッガーをサービスにアタッチすることもありません。Topshelf では多くのパラメーター (サービス名など) をコードで指定することも、コマンド ラインからのインストール中に構成することもできます。

Katana と Topshelf を使用して Web とサービスのコンポーネントを統一する方法を示すために、今回はシンプルな SMS メッセージング アプリケーションをビルドします。まず、メッセージの受信とメッセージをキューに登録して送信するための API から始めます。ここでは、時間がかかる要求の処理がいかに容易になるかについて説明します。その後、保留中のメッセージ数を返すための API クエリ メソッドを追加して、Web コンポーネントからサービスの状態をクエリするのも容易になること示します。

次に、管理用のインターフェイスを追加して、セルフホスト Web コンポーネントに、リッチな Web インターフェイスをビルドする余地がまだあることを示し、メッセージ処理を仕上げるために、メッセージがキューに登録されたときにそのメッセージを送信するコンポーネントを追加して、サービス コンポーネントを含めるところを紹介します。

さらに、このアーキテクチャの最も優れた部分の 1 つを強調するために、psake スクリプトを作成して配置の簡潔さをお見せします。

Katana と Topshelf を組み合わせるメリットに重点を置くため、両方のプロジェクトを詳しく説明することはしません。詳細については、「Katana プロジェクトの概要」(https://msdn.microsoft.com/ja-jp/magazine/dn451439.aspx) および「Topshelf を使用して Windows サービスを簡単に作成する」(bit.ly/1h9XReh、英語) を参照してください。

必要なのはコンソール アプリケーションだけ

Topshelf は、シンプルなコンソール アプリケーションを出発点として、Windows サービスの開発と配置を容易にするために存在します。SMS メッセージング アプリケーションに着手するには、C# コンソール アプリケーションを作成後、パッケージ マネージャー コンソールから Topshelf NuGet パッケージをインストールします。

コンソール アプリケーションの起動時には、Topshelf の HostFactory を構成して、アプリケーションのホスティングを開発時にはコンソールとして、運用時にはサービスとして抽象化します。

private static int Main()
{
  var exitCode = HostFactory.Run(host =>
  {
  });
  return (int) exitCode;
}

HostFactory は終了コードを返します。この終了コードは、サービスのインストール中にエラーを検出してインストールを停止するのに役立ちます。このホスト構成機能は、アプリケーション コードへのエントリ ポイントを表すカスタム型を指定するサービス メソッドを提供します。Topshelf は Windows サービスの作成を簡素化するためのフレームワークなので、Topshelf はこのサービス メソッドをホスト対象のサービスとみなします。

host.Service<SmsApplication>(service =>
{
});

次に、SmsApplication 型を作成して、セルフホスト型の Web サーバーと従来型の Windows サービス コンポーネントをスピンアップするロジックを含めます。このカスタム型には、少なくとも、アプリケーションの起動時または停止時に実行する動作を含めます。

public class SmsApplication
{
  public void Start()
  {
  }
  public void Stop()
  {
  }
}

このサービス型に単純な従来の CLR オブジェクト (POCO) を使用することにしたので、Topshelf にラムダ式を提供して SmsApplication 型のインスタンスを構築し、start メソッドと stop メソッドを指定します。

service.ConstructUsing(() => new SmsApplication());
service.WhenStarted(a => a.Start());
service.WhenStopped(a => a.Stop());

Topshelf により、コードで多くのサービス パラメーターを構成できるようになるため、今回は SetDescription、SetDisplayName、および SetServiceName を使用して、運用環境にインストールするサービスについての説明と名前を指定します。

host.SetDescription("An application to manage
     sending sms messages and provide message status.");
  host.SetDisplayName("Sms Messaging");
  host.SetServiceName("SmsMessaging");
  host.RunAsNetworkService();

最後に、RunAsNetworkService を使用して、サービスを構成してネットワーク サービス アカウントとして実行することを Topshelf に指示します。これは、環境に合ったアカウントに変更できます。サービス オプションの詳細については、bit.ly/1rAfMiQ (英語) の Topshelf 構成ドキュメントを参照してください。

コンソール アプリケーションとしてサービスを実行するのは、実行可能ファイルを起動するのと同じぐらい簡単です。図 3 に、SMS 実行可能ファイルの起動時と停止時の出力を示します。これはコンソール アプリケーションなので、Visual Studio でアプリケーションを実行するのと同じ動作を体験できます。

Running the Service as a Console Application
図 3 コンソール アプリケーションとしてのサービスの実行

API を組み込む

Topshelf を適切に構成したら、アプリケーションの API への取り組みを開始します。Katana プロジェクトは、OWIN パイプラインをセルフホストするためのコンポーネントを提供するため、セルフホストのコンポーネントを組み込むために Microsoft.Owin.SelfHost パッケージをインストールします。このパッケージは複数のパッケージを参照しますが、セルフホストに重要なのは 2 つのパッケージです。1 つは Microsoft.Owin.Hosting パッケージで、OWIN パイプラインをホストおよび実行するための一連のコンポーネントを提供します。もう 1 つは Microsoft.Owin.Host.HttpListener パッケージで、HTTP サーバーの実装を提供します。

SmsApplication の内部で、Microsoft.Owin.Hosting パッケージで提供される WebApp 型を使用してセルフホスト Web アプリケーションを作成します。

protected IDisposable WebApplication;
public void Start()
{
  WebApplication = WebApp.Start<WebPipeline>("http://localhost:5000");
}

WebApp の Start メソッドには、OWIN パイプラインを構成する型を指定するジェネリック パラメーターと、要求をリッスンする URL という 2 つのパラメーターが必要です。Web アプリケーションは破棄可能なリソースです。SmsApplication インスタンスの停止時には、Web アプリケーションを破棄します。

public void Stop()
{
  WebApplication.Dispose();
}

OWIN を使用する 1 つのメリットは、使い慣れたさまざまなコンポーネントを利用できることです。まず、WebApi を使用して API を作成します。Microsoft.AspNet.WebApi.Owin パッケージをインストールして、WebApi を OWIN パイプラインに組み込む必要があります。次に、WebPipeline 型を作成して、OWIN パイプラインの構成および WebApi ミドルウェアの挿入を行います。さらに、WebApi を構成して属性のルーティングを使用します。

public class WebPipeline
{
  public void Configuration(IAppBuilder application)
  {
    var config = new HttpConfiguration();
    config.MapHttpAttributeRoutes();
    application.UseWebApi(config);
  }
}

これで、API メソッドを作成して、メッセージの受信とメッセージをキューに登録して送信できるようになります。

public class MessageController : ApiController
{
  [Route("api/messages/send")]
  public void Send([FromUri] SmsMessageDetails message)
  {
    MessageQueue.Messages.Add(message);
  }
}

SmsMessageDetails には、メッセージのペイロードを含みます。送信アクションはメッセージをキューに追加し、その後非同期に処理します。MessageQueue はグローバルな BlockingCollection です。つまり、実際のアプリケーションでは、持続性やスケーラビリティといった問題についても考慮が必要になります。

public static readonly BlockingCollection<SmsMessageDetails> Messages;

Web とサービスを分離したアーキテクチャでは、メッセージの送信など時間のかかる要求の非同期処理を受け渡すには、Web とサービスのプロセスの間で通信が必要になります。API のメソッドを追加してサービスの状態をクエリすると、さらに通信のオーバーヘッドが増加します。統一型のアプローチでは、Web とサービスのコンポーネントの間での状態情報の共有がシンプルになります。これを示すために、API に PendingCount クエリを追加します。

[Route("api/messages/pending")]
public int PendingCount()
{
  return MessageQueue.Messages.Count;
}

リッチな UI をビルドする

API は便利ですが、セルフホスト Web アプリケーションでは、まだビジュアル インターフェイスをサポートする必要があります。将来的には、OWIN ミドルウェアとして ASP.NET MVC フレームワークや派生製品を利用できるようになると予想されますが、現時点では、Nancy が互換性のある、Razor ビュー エンジンの中核をサポートするパッケージを含んだツールです。

今回は、Nancy.Owin パッケージをインストールして Nancy のサポートを追加し、Nancy.Viewengines.Razor によって Razor ビュー エンジンを組み込みます。Nancy を OWIN パイプラインに接続するために、WebApi の登録後に Nancy を登録して、API にマップしたルートをキャプチャしないようにします。Nancy はリソースが見つからない場合に既定でエラーを返しますが、WebApi は処理できない要求をパイプラインに戻します。

application.UseNancy();

OWIN パイプラインと Nancy を併用する場合の詳細については、「OWIN で Nancy をホストする」(bit.ly/1gqjIye、英語) を参照してください。

管理用の状態インターフェイスをビルドするには、Nancy モジュールを追加し状態ルートをマップして状態ビューをレンダリングし、ビュー モデルとして保留中のメッセージ数を渡します。

public class StatusModule : NancyModule
{
  public StatusModule()
  {
    Get["/status"] =
      _ => View["status", MessageQueue.Messages.Count];
  }
}

現時点のビューはあまり魅力的ではなく、保留中のメッセージの数を単純に表示しているだけです。

<h2>Status</h2>
There are <strong>@Model</strong> messages pending.

そこで、シンプルな Bootstrap ナビゲーション バーを使用してビューを飾り付けます (図 4 参照)。Bootstrap を使用するには、Bootstrap スタイル シートの静的コンテンツをホストする必要があります。

Administrative Status Page
図 4 管理用の状態ページ

Nancy を使用して静的コンテンツをホストすることもできますが、OWIN のメリットはミドルウェアの混在とマッチングなので、代わりに、Katana プロジェクトの一部として新しくリリースされた Microsoft.Owin.StaticFiles パッケージを使用します。Microsoft.Owin.StaticFiles パッケージは、ファイル サービスを提供するミドルウェアを提供します。今回は OWIN パイプラインの起動でこのミドルウェアを追加するため、Nancy の静的ファイル サービスは起動しません。

application.UseFileServer(new FileServerOptions
{
  FileSystem = new PhysicalFileSystem("static"),
  RequestPath = new PathString("/static")
});

FileSystem パラメーターは、サービスを提供すファイルの検索場所をファイル サーバーに指示します。今回は static というフォルダーを使用しています。RequestPath は、このコンテンツの要求をリッスンするルート プレフィックスを指定します。この場合、static という名前を反映していますが、一致させる必要はありません。レイアウトで次のリンクを使用して bootstrap スタイル シートを参照します (基本的に、static フォルダー内の CSS フォルダーに Bootstrap スタイル シートを配置します)。

<link rel="stylesheet" href="/static/css/bootstrap.min.css">

静的コンテンツとビューについて

次に進む前に、セルフホスト Web アプリケーションの開発時に役立つと思われるヒントを紹介します。通常、静的コンテンツと MVC ビューを出力ディレクトリにコピーするように設定するため、セルフホスト Web のコンポーネントは、現在実行中のアセンブリからの相対ディレクトリでコンテンツやビューを検索できます。このようなディレクトリ構造にすると負荷が高く、忘れやすいだけでなく、ビューや静的コンテンツが変更されるとアプリケーションの再コンパイルが必要になり、生産性が損なわれます。そこで、静的コンテンツやビューを出力ディレクトにコピーする代わりに、Nancy などのミドルウェアや FileServer を構成して開発フォルダーにマップすることをお勧めします。

既定では、コンソール アプリケーションのデバッグ出力ディレクトリは bin/Debug なので、開発時は、カレント ディレクトリの 2 つ上のディレクトリを検索して Bootstrap スタイル シートを含む static フォルダーを見つけるように、FileServer に指示します。

FileSystem = new PhysicalFileSystem(IsDevelopment() ? 
  "../../static" : "static")

次に、Nancy にビューの検索場所を指示するため、カスタム NancyPathProvider を作成します。

public class NancyPathProvider : IRootPathProvider
{
  public string GetRootPath()
  {
    return WebPipeline.IsDevelopment()
      ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, 
      @"..\..\")
      : AppDomain.CurrentDomain.BaseDirectory;
  }
}

繰り返しになりますが、Visual Studio の開発モードで実行する場合は、同じチェック機能を使用して、ベース ディレクトリの 2 つ上のディレクトリを確認します。IsDevelopment の実装は読者の皆さんにお任せします。シンプルな構成設定にしたり、Visual Studio からアプリケーションが起動されたときに検出するコードを作成することができます。

このカスタム ルート パス プロバイダーを登録するには、カスタム NancyBootstrapper を作成し、既定の RootPathProvider プロパティをオーバーライドして、NancyPathProvider のインスタンスを作成します。

public class NancyBootstrapper : DefaultNancyBootstrapper
{
  protected override IRootPathProvider RootPathProvider
  {
    get { return new NancyPathProvider(); }
  }
}

Nancy を OWIN パイプラインに追加するとき、オプションで NancyBootstrapper のインスタンスを渡します。

application.UseNancy(options => 
  options.Bootstrapper = new NancyBootstrapper());

メッセージを送信する

メッセージを受信できてようやく作業の半分が終了しましたが、まだメッセージを送信するプロセスが必要です。このプロセスは、従来型のアーキテクチャでは独立したサービスに配置するプロセスです。今回の統一型ソリューションでは、アプリケーション開始時に起動する SmsSender を簡単に追加できます。このプロセスは SmsApplication の Start メソッドに追加します (実際のアプリケーションでは、このリソースを停止および破棄するための機能を追加します)。

public void Start()
{
  WebApplication = WebApp.Start<WebPipeline>("http://localhost:5000");
  new SmsSender().Start();
}

SmsSender の Start メソッドの内部で、メッセージを送信する時間がかかるタスクを起動します。

public class SmsSender
{
  public void Start()
  {
    Task.Factory.StartNew(SendMessages, 
      TaskCreationOptions.LongRunning);
  }
}

WebApi の送信アクションはメッセージを受け取ると、メッセージをブロッキング コレクションのメッセージ キューに追加します。今回は SendMessages メソッドを作成してメッセージが到着するまでブロックします。これは、GetConsumingEnumerable を支える抽象化のおかげで可能になります。一連のメッセージを受け取ると、メッセージの送信を即座に開始します。

private static void SendMessages()
{
  foreach (var message in MessageQueue.Messages.GetConsumingEnumerable())
  {
    Console.WriteLine("Sending: " + message.Text);
  }
}

複数の SmsSender インスタンスをスピンアップすればメッセージの送信容量を簡単に拡張できます。実際のアプリケーションでは、CancellationToken を GetConsumingEnumerable に渡して列挙を安全に停止することを考えます。ブロッキング コレクションの詳細については、bit.ly/QgiCM7 (英語) および bit.ly/1m6sqlI (英語) で役立つ情報を確認してください。

簡単で手軽な配置

Katana と Topshelf のおかげで、サービスと Web アプリケーションを組み合わせて開発するのが簡単かつわかりやすくなります。この強力な組み合わせの優れたメリットの 1 つは、驚くほどシンプルな配置プロセスです。ここでは、psake (github.com/psake/psake、英語) を使用したシンプルな 2 ステップの配置について説明します。これは、実際の運用で使用する堅牢なシナリオとは言えません。使用するツールに関係なく、実際に、配置プロセスがいかにシンプルになるかをデモするだけです。

最初のステップは、アプリケーションのビルドです。ソリューションへのパスを指定して msbuild を呼び出すビルド タスクを作成し、リリース ビルドを作成します (ビルドは bin/Release に出力されます)。

properties {
  $solution_file = "Sms.sln"
}
task build {
  exec { msbuild $solution_file /t:Clean /t:Build /p:Configuration=Release /v:q }
}

2 つ目のステップでは、アプリケーションをサービスとして配置します。ビルド タスクに依存する配置タスクを作成し、インストール場所へのパスを保持する delivery ディレクトリを宣言します。今回は説明が簡単になるように、単純にローカル ディレクトリに配置しました。その後、delivery ディレクトリ内のコンソール アプリケーションの実行可能ファイルを指す executable 変数を作成します。

task deploy -depends build {
  $delivery_directory = "C:\delivery"
  $executable = join-path $delivery_directory 'Sms.exe'

まず、配置タスクは、delivery ディレクトリが存在するかどうかをチェックします。delivery ディレクトリが見つかれば、サービスを既に配置したと見なします。今回の場合、配置タスクはそのサービスをアンインストールし、delivery ディレクトリを削除します。

if (test-path $delivery_directory) {
  exec { & $executable uninstall }
  rd $delivery_directory -rec -force 
}

次に、ビルド出力を delivery ディレクトリにコピーして新しいコードを配置後、views フォルダーと static フォルダーを delivery ディレクトリにコピーします。

copy-item 'Sms\bin\Release' $delivery_directory -force -recurse -verbose
copy-item 'Sms\views' $delivery_directory -force -recurse -verbose
copy-item 'Sms\static' $delivery_directory -force -recurse –verbose

最後に、サービスをインストールして起動します。

exec { & $executable install start }

サービスの配置時、ファイル サーバーが static フォルダーを見つけられない場合、IsDevelopment 実装が false を返すか、Access Denied 例外が発生するようにします。また、毎回の配置でサービスを再インストールすることが問題になる場合があります。そこで、サービスが既にインストールされている場合にそのサービスを停止して更新した後、サービスを起動する方法もあります。

ご覧のとおり、配置は驚くほどシンプルです。IIS と InstallUtil から困難な問題が完全に取り除かれます。つまり、配置プロセスは 2 回ではなく 1 回になり、Web とサービスの層が通信する方法について懸念する必要がありません。Web とサービスが統一されたアプリケーションをビルドするので、配置タスクを繰り返し実行できます。

今後の展望

この組み合わせモデルが役立つかどうか判断する最適な方法は、リスクの低いプロジェクトを見つけてこのモデルを試してみることです。このアーキテクチャを使用してアプリケーションを開発および配置するのはかなりシンプルです。たとえば MVC に Nancy を使用する場合、習得に時間がかかりますが、OWIN を使用するのが優れている点は、この組み合わせアプローチがうまくいかなくても、依然として ASP.NET ホスト (Microsoft.Owin.Host.SystemWeb) を使用して IIS 内で OWIN パイプラインをホストできることです。試してみて、何かを感じてみてください。

Wes McClure は専門知識を活かして、クライアントが高品質なソフトウェアを迅速に提供できるようにサポートし、クライアントが顧客にもたらす価値を飛躍的に高められるようにします。彼は、ソフトウェア開発に関するすべてのことについて講演することを楽しんでいます。Pluralsight の執筆者であり、devblog.wesmcclure.com (英語) で自身の経験について投稿しています。彼の連絡先は wes.mcclure@gmail.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Howard Dierking (マイクロソフト)、Damian Hickey、Chris Patterson (RelayHealth)、Chris Ross (マイクロソフト)、および Travis Smith に心より感謝いたします。
Howard Dierking は Windows Azure フレームワークとツール チームのプログラム マネージャーを務め、ASP.NET、NuGet、および Web API に重点的に取り組んでいます。以前には、MSDN マガジンの編集長の経験や、Microsoft Learning のデベロッパー認定プログラムを指揮したことがあります。マイクロソフトに勤務する前は、分散システム専門の開発者兼アプリケーション アーキテクトとして 10 年間仕事に取り組んでいました。

Chris Ross は、マイクロソフトのシニア ソフトウェア設計エンジニアを務め、ネットワークおよび OWIN に重点的に取り組んでいます。

Chris Patterson は、McKesson Corporation の接続ビジネスである RelayHealth の設計者であり、患者、プロバイダー、薬局、および金融機関をつないで医療の提供を促進するアプリケーションおよびサービスの設計および開発を担っています。彼は、Topshelf および MassTransit の主な貢献者であり、テクニカル コミュニティへの貢献を理由として、Microsoft によって Most Valuable Professional に認定されました。

Damian Hickey は、DDD\CQRS\ES ベースのアプリケーションを専門とするソフトウェア開発者です。彼は、.NET オープン ソース ソフトウェアの支持者であり、Nancy、NEventStore などのさまざまなプロジェクトに貢献しています。彼は時折、聴衆の要請に応じて講演を行ったり、http://dhickey.ie (英語) にブログ記事を投稿することもあります。彼の連絡先は dhickey@gmail.com (英語のみ) です。Twitter は @randompunter (英語) です。

Travis Smith は、Atlassian および Atlassian Marketplace を支持する開発者です。彼は、オープン Web テクノロジ、多言語テクノロジ、および新たな web テクノロジの推進に尽力しています。彼は、Topshelf および MassTransit を含む複数のオープン ソース プロジェクトの貢献者です。開発者のイベントで、優れたソフトウェアの作成について熱心に講演する彼に会うことができます。または、http://travisthetechie.com (英語) をご覧ください。