次の方法で共有



2015 年 8 月

Volume 30 Number 8

クラウド接続型モバイル アプリ - Azure Web Apps と WebJobs を使用した Web サービスの作成

Erik Reitan

今日では、多くのモバイル アプリが、価値ある興味深いデータを提供する 1 つ以上の Web サービスに接続されています。このようなアプリを設計および開発する際は、これらの Web サービスを REST API から直接呼び出した後、その応答をクライアント内で処理するのが最も簡単な方法です。ただし、この方法には多くの欠点があります。たとえば、すべてのネットワーク呼び出しとクライアント側処理に、貴重なバッテリと帯域幅が消費されます。さらに、大量のクライアント側処理を、特にローエンドのハードウェアで実行すると時間がかかり、アプリの応答性が低下します。また、さまざまな Web サービスによる制限事項の調整が必要になり、クライアント側ソリューションだけを、より多くのユーザーに拡張するのは容易ではありません。

結果として、多くのシナリオ、特に複数のソースからデータを取り出すシナリオでは、特定のタスクのオフロード先として使用できる独自のバックエンドを作成することになります。ASP.NET、Microsoft Azure、および Visual Studio のクロス プラットフォーム開発ツールを担当するマイクロソフトのコンテンツ開発チームは、社内作業の一環として、このような方法の具体例を作成しました。

今回の 2 部構成の連載では、同チームが考案した方法を説明し、アプリの開発中に直面した課題の一部と得られた教訓について紹介します。「Altostratus」(高層雲と呼ばれる変わった雲) と命名したこのアプリは、Stack Overflow と Twitter から「会話」と呼んでいる特定のトピックを検索します。これら 2 つのプロバイダーを選択した理由は、どちらも優れた Web API を備えているためです。アプリの 2 つの主要コンポーネントは次のとおりです。

  • Azure 上でホストされるクラウド バックエンド。このバックエンドは定期的にプロバイダーに要求を行い、データをクライアントに最も適した形式に集約します。これにより、調整上の問題が回避され、プロバイダーでの待ち時間に関する問題が緩和されます。また、クライアント側処理が最小限に抑えられ、クライアントからのネットワーク要求の数が減少します。ただし、その代償の 1 つとして、Web ジョブの実行が一定間隔になるため、リアルタイム データは取得されません。
  • Windows、Android、および iOS 上で動作するように Xamarin を使用して作成した軽量のモバイル クライアント アプリ (図 1 参照)。このモバイル クライアントは、集約済みのデータをバックエンドからフェッチし、ユーザーに表示します。ローカル データベース内のデータ キャッシュの同期状態が維持されるため、優れたオフライン エクスペリエンスと、高速起動時間も実現されます。

Android タブレット (左)、Windows Phone (中央)、および iPhone (右) 上で動作する Xamarin モバイル クライアント
図 1 Android タブレット (左)、Windows Phone (中央)、および iPhone (右) 上で動作する Xamarin モバイル クライアント

ユーザーは必要に応じて、ソーシャル プロバイダー (Google や Facebook) を使用してモバイル クライアントにサインインします。サインイン後、ユーザーはクライアントとの通信をさらに最適化する基本設定を構成できます。具体的には、受信対象のテーマと、各テーマ内の会話の最大数を選択できます。ユーザー基本設定はバックエンドに格納されるため、ユーザーがどのクライアントからサインインしても、同じエクスペリエンスが得られます。こうした考え方をデモするため、同じバックエンドと対話するシンプルな Web クライアントも作成しました。

今回のプロジェクトの一環として、Visual Studio と Visual Studio Online に組み込まれたアプリケーション ライフサイクル管理 (ALM) ツールを使用することによって、スプリントと作業バックログを管理し、さらに自動化された単体テスト、継続的インテグレーション (CI)、および継続的なデプロイ (CD) を実行することも考えています。

今回の 2 部構成の連載では、このプロジェクトについて詳しく説明します。第 1 部では、バックエンドと ALM ツール (DevOps) の使用法に注目します。また、直面した課題の一部と、得られた教訓も紹介します。その一例を次に示します。

  • Web アプリ以外のアプリへのパスワードのデプロイを安全に自動化する方法。
  • Azure オートメーションのタイムアウトに対処する方法。
  • 効率的でわかりやすいバックグラウンド処理。
  • CI/CD の制限事項とその回避策。

第 2 部では、認証を追加し、クライアント側データ キャッシュの同期を管理して複数のモバイル クライアント プラットフォームをターゲットにするための Xamarin の使用法について説明します。

アーキテクチャ

図 2 は、Altostratus ソリューションの大まかなアーキテクチャを示しています。

  • バックエンドでは、Azure App Service を使用して Web アプリをホストし、Azure SQL Database を使用してデータをリレーショナル データベース内に格納します。データ アクセスには Entity Framework (EF) を使用します。
  • Azure WebJobs を使用して Stack Overflow と Twitter からのデータを集約し、それをデータベースに書き込む、定期バックグラウンド タスクを実行します。この Web ジョブを簡単に拡張して、他のソースからのデータを集約することもできます。
  • モバイル クライアントの作成には Xamarin を使用し、バックエンドとの通信にはシンプルな REST API を使用します。
  • REST API の実装には、ASP.NET Web API を使用します。これは Microsoft .NET Framework 内で HTTP サービスを作成するためのフレームワークです。
  • Web クライアントは、比較的単純な JavaScript アプリです。今回はデータ バインドに KnockoutJS ライブラリを、AJAX 呼び出しには jQuery を使用しています。

Altostratus のアーキテクチャ
図 2 Altostratus のアーキテクチャ

データベース スキーマ

データベース スキーマの定義とバックエンド SQL データベースの管理には、EF Code First を使用します。図 3 に示すように、データベースには次のエンティティが格納されます。

  • Provider: Twitter や Stack Overflow などのデータ ソース。
  • Conversation: プロバイダーからのアイテム。Stack Overflow の場合、これは回答付きの質問に相当します。Twitter の場合、これはツイートに相当します (返信付きのツイートにすることもできますが、この機能は実装しませんでした)。
  • Category: "Azure" や "ASP.NET" などの会話のテーマ。
  • Tag: 特定のカテゴリとプロバイダーを対象とする検索文字列。これらは、Stack Overflow のタグ ("azure-web-sites") と Twitter のハッシュタグ ("#azurewebsites") に相当します。バックエンドは、これらを使用してプロバイダーへの照会を行います。エンド ユーザーがこれらの文字列を目にすることはありません。
  • UserPreference: ユーザー単位の基本設定を格納します。
  • UserCategory: UserPreference と Category の結合テーブルを定義します。

Altostratus データ モデル
図 3 Altostratus データ モデル

一般的に、Altostratus アプリ内の作成、読み取り、更新、削除 (CRUD) のコードは、EF Code First では代表的なものです。このコードには特別に考慮することが 1 つあり、それはデータベースのシードと移行への対処です。Code First では、プログラムが初めてデータへのアクセスを試みたときにデータベースが存在しなかった場合、新しいデータベースを作成してシードします。データへのアクセスを初めて試みる処理は Web ジョブ内で行われることが多いため、Web ジョブの Main メソッドには、EF に MigrateDatabaseToLatestVersion イニシャライザーを使用させる、以下のようなコードを用意します。

static void Main()
{
  Task task;
  try
  {
    Database.SetInitializer<ApplicationDbContext>(
      new MigrateDatabaseToLatestVersion<ApplicationDbContext,
        Altostratus.DAL.Migrations.Configuration>());

このコードがないと、Web ジョブは既定で CreateDatabaseIfNotExists イニシャライザーを使用します。このイニシャライザーは、データベースをシードしません。その結果、空のデータベースが作成され、アプリが空のテーブルからデータを読み取ろうとしてエラーが発生します。

Web ジョブの設計

Web ジョブは、バックグラウンドでタスクを実行する場合の理想的なソリューションを提供します。以前はこの作業を行うために、専用の Azure ワーカー ロールを実行する必要がありました。Web ジョブを Azure Web アプリ上で実行する場合、追加コストがかかることはありません。ワーカー ロールと比較した場合の Web ジョブの利点については、Troy Hunt のブログ記事「Azure WebJobs Are Awesome and You Should Start Using Them Right Now!」(便利な Azure WebJobs を今すぐ使ってみよう、bit.ly/1c28yAk、英語) を参照してください。

今回のソリューションに使用する Web ジョブは、Twitter データを取得し、Stack Overflow データを取得し、古いデータを消去する 3 つの関数を定期的に実行します。これらの関数は独立していますが、同じ EF コンテキストを共有するため、順番に実行する必要があります。Web ジョブ完了後、Azure ポータルは各関数の状態を表示します (図 3 参照)。関数が完了した場合、その関数は緑の「成功」メッセージがマークされ、例外がスローされた場合、その関数は赤の「失敗」メッセージがマークされます。

Altostratus では Stack Overflow と Twitter の無料のプロバイダー API を使用するため、関数が失敗することはよくあります。特にクエリは制限され、クエリの制限を超えるとプロバイダーから調整エラーが返されます。これがバックエンドを作成することにした初めの大きな理由の 1 つです。モバイル アプリからプロバイダーに直接要求する方が簡単ですが、ユーザーの数が増加すれば、すぐにこの調整制限に達してしまいます。バックエンドを使用すれば、データを収集して集約するために、定期的な要求をわずか数回実行するだけで済みます。

チームが直面した課題の 1 つは、Web ジョブのエラー処理に関するものです。通常、Web ジョブ内のいずれかの関数が例外をスローすると、Web ジョブのインスタンスが終了し、残りの関数は実行されずに、Web ジョブ全体が失敗としてマークされます。すべてのタスクを実行し、関数レベルでエラーを表示するには、Web ジョブ内で例外をキャッチする必要があります。今回は Main 内のいずれかの関数が例外をスローした場合、最後の例外をログに記録して再スローするため、Web ジョブが「失敗」としてマークされます。図 4 の擬似コードは、この方法を示しています (プロジェクトのダウンロードにはコード全体が含まれます)。

図 4 例外のキャッチと再スロー

static void Main()
{
  Task task;
  try
  {
    Exception _lastException = null;
    try
    {
      task = host.CallAsync("Twitter");
      task.Wait();
    }
    catch (Exception ex)
    {
      _lastException = ex;
    }
    try
    {
      task = host.CallAsync("StackOverflow");
      task.Wait();
    }
    catch (Exception ex)
    {
      _lastException = ex;
    }
    task = host.CallAsync("Purge Old Data");
    task.Wait();
    if (_lastException != null)
    {
      throw _lastException;
    }
  }
  catch (Exception ex)
  {
  }
}

図 4 では、最後の例外のみが Web ジョブ レベルで表示されています。関数レベルでは、すべての例外がログに記録されるため、失われる例外はありません。図 5 は、Web ジョブが正常に実行された場合のダッシュボードを示しています。各関数の詳細を表示すると、診断出力を確認できます。

正常に実行された Web ジョブ
図 5 正常に実行された Web ジョブ

REST API の設計

モバイル アプリは、ASP.NET Web API 2 を使用して実装したシンプルな REST API を介して、バックエンドと通信します (ASP.NET 5 では Web API が MVC 6 フレームワークに統合されているため、これら両方を Web アプリ内に組み込みやすくなっています)。

図 6 に、今回の REST API をまとめます。

図 6 REST API の概要

GET api/categories カテゴリを取得する
GET api/conversations?from=iso-8601-date 会話を取得する
GET api/userpreferences ユーザーの基本設定を取得する
PUT api/userpreferences ユーザーの基本設定を更新する

すべての応答は JSON 形式です。たとえば、図 7 は会話に対する HTTP 応答を示しています。

図 7 会話に対する HTTP 応答

HTTP/1.1 200 OK
Content-Length: 93449
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Tue, 21 Apr 2015 22:38:47 GMT
[
  {
    "Url": "http://twitter.com/815911142/status/590317675412262912",
    "LastUpdated": "2015-04-21T00:54:36",
    "Title": "Tweet by rickraineytx",
    "Body": "Everything you need to know about #AzureWebJobs is here.
      <a href=\"http://t.co/t2bywUQoft\"">http://t.co/t2bywUQoft</a>",
    "ProviderName": "Twitter",
    "CategoryName": "Azure"
  },
  // ... Some results deleted for space
]

会話の場合、要求を認証する必要はありません。つまり、モバイル アプリは常に、ユーザーに最初のログインを求めることなく、意味のあるデータを表示できます。ただし今回は、ユーザーのログイン時にバックエンドでさらなる最適化を実行する方法もデモしようと考えています。ユーザーはこのクライアント アプリを使用して、表示する会話のカテゴリと会話制限数を選択できます。このため、要求が認証された (ユーザーがアプリにログインした) 場合、バックエンドは基本設定に基づいて、応答を自動的にフィルター処理します。これにより、クライアント側で処理すべきデータの量が制限されるため、必要な帯域幅、メモリ、記憶域、バッテリが徐々に少なくなります。

この conversations API は、オプションのパラメーターとしてクエリ文字列内の "from" も受け取ります。このパラメーターが指定されると、その日付の後に更新された会話のみが含まれるように、結果がフィルター選択されます。

GET api/conversations?from=2015-04-20T03:59Z

これにより、応答のサイズが最小限に抑えられます。今回のモバイル クライアントは、このパラメーターを使用して、クライアント キャッシュの同期に必要なデータのみを要求します。

クエリ文字列を使用して、要求が行われるときに毎回ユーザーの基本設定を通知するよう API を設計することもできます。この場合は、基本設定がクライアント内でのみ保持されます。そのため、認証を行う必要はありません。この方法は、基本的なシナリオでは機能しますが、今回用意したのは、クエリ文字列だけでは不十分な、より複雑な状況に合わせて拡張できる例です。基本設定をバックエンドに格納すれば、ユーザーがどのクライアントからログインしても、そのユーザーの設定が自動的に適用されます。

データ転送オブジェクト (DTO)

REST API は、データベース スキーマと通信に関する表現との境界になります。今回は、次のような理由から、EF モデルを直接シリアル化しません。

  • 外部キーなど、クライアントには必要のない情報が含まれる。
  • API がオーバーポスティング攻撃を受けやすくなるおそれがある (オーバーポスティング攻撃は、更新対象として公開する予定がなかったデータベース フィールドをクライアントが更新したときに発生します。この状況が発生する可能性があるのは、入力を十分に検証することなく、HTTP 要求のペイロードを直接 EF モデルに変換するときです。詳細については、bit.ly/1It1wl2 (英語) を参照してください)。
  • EF モデルの "形状" がデータベース テーブルの作成用に設計されており、クライアントにとって最適ではない。

したがって、今回は一連のデータ転送オブジェクト (DTO) を作成しています。これらの DTO は、REST API 応答の形式を定義する C# クラスにすぎません。たとえば、今回作成したカテゴリ用の EF モデルは次のとおりです。

public class Category
{
  public int CategoryID { get; set; }
  [StringLength(100)]
  public string Name { get; set; }  // e.g: Azure, ASP.NET
  public ICollection<Tag> Tags { get; set; }
  public ICollection<Conversation> Conversations { get; set; }
}

この Category エンティティには、主キー (CategoryID) とナビゲーション プロパティ (Tags と Conversations) があります。ナビゲーション プロパティによって、EF 内の関係を簡単に理解できるようになります。たとえば、カテゴリ用のタグをすべて検索できます。

次のように、クライアントがカテゴリを要求するときに指定する必要があるのは、カテゴリ名のリストのみです。

[ "Azure", "ASP.NET" ]

この変換は、Web API コントローラー メソッド内で LINQ Select ステートメントを使用して簡単に実行できます。

public IEnumerable<string> GetCategories()
{
  return db.Categories.Select(x => x.Name);
}

UserPreference エンティティは、次のようにやや複雑です。

public class UserPreference
{
  // FK to AspNetUser table Id   
  [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
  public string ApplicationUser_Id { get; set; }
  public int ConversationLimit { get; set; }    
  public int SortOrder { get; set; }            
  public ICollection<UserCategory> UserCategory { get; set; }
  [ForeignKey("ApplicationUser_Id")]
  public ApplicationUser AppUser { get; set; }
}

ApplicationUser_Id はユーザー テーブルへの外部キーです。UserCategory は交差テーブルを指し、多対多の関係をユーザー基本設定とカテゴリとの間に作成します。

クライアントに公開される、このエンティティの構造は次のとおりです。

public class UserPreferenceDTO
{
  public int ConversationLimit { get; set; }
  public int SortOrder { get; set; }
  public ICollection<string> Categories { get; set; }
}

この場合、外部キーや交差テーブルなど、データベース スキーマの実装の詳細は公開されず、カテゴリ名は文字列のリスト内にフラット化されます。

UserPreference を UserPreferenceDTO に変換するための LINQ 式はかなり複雑になるため、代わりに AutoMapper を使用しました。AutoMapper はオブジェクト型をマップするライブラリです。マッピングを一度定義して、AutoMapper を使用して自動的にマッピングを実行するというのが今回の考え方です。

次のように、アプリの起動時に AutoMapper を構成します。

Mapper.CreateMap<Conversation, ConversationDTO>();
Mapper.CreateMap<UserPreference, UserPreferenceDTO>()
  .ForMember(dest => dest.Categories,
             opts => opts.MapFrom(
               src => src.UserCategory.Select(
                 x => x.Category.Name).ToList()));
              // This clause maps ICollection<UserCategory> to a flat
              // list of category names.

CreateMap への最初の呼び出しでは、AutoMapper の既定のマッピング規則を使用して、Conversation を ConversationDTO にマップします。UserPreference の場合、マッピングはこれほど単純ではなく、若干追加の構成があります。

AutoMapper を一度構成したら、次のように、オブジェクトのマッピングを簡単に実行できます。

var prefs = await db.UserPreferences
  .Include(x => x.UserCategory.Select(y => y.Category))  
  .SingleOrDefaultAsync(x => x.ApplicationUser_Id == userId);
var results = AutoMapper.Mapper.Map<UserPreference,
  UserPreferenceDTO>(prefs);

有能な AutoMapper がこのコードを LINQ ステートメントに変換し、データベースに対して SELECT が実行されます。

認証と承認

ユーザーの認証には、ソーシャル ログイン (Google と Facebook) を使用します (認証プロセスについては、第 2 部で詳しく説明します)。Web API によって要求が認証された後、Web API コントローラーがこの情報を使用して要求を承認し、user データベース内でそのユーザーを検索します。

REST API を承認済みユーザーに制限するため、今回はコントローラー クラスを [Authorize] 属性で装飾します。

[Authorize]
public class UserPreferencesController : ApiController

これで、api/userpreferences への要求が承認されなかった場合、Web API から次のような 401 エラーが自動的に返されます。

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
WWW-Authenticate: Bearer
Date: Tue, 21 Apr 2015 23:55:47 GMT
Content-Length: 68
{
  "Message": "Authorization has been denied for this request."
}

Web API によって WWW-Authenticate ヘッダーが応答に追加されたことがわかります。これは、サポートされる認証方式の種類 (この場合は OAuth2 ベアラー トークン) をクライアントに指示します。

既定では、[Authorize] は、すべての認証済みユーザーが承認され、匿名要求のみがブロックされることを想定します。また、指定したロール (Admins など) のユーザーに承認を制限したり、より複雑な承認シナリオに備えてカスタム承認フィルターを実装することもできます。

さらに興味深いのは、conversations API の場合です。今回は匿名要求を許可しますが、要求が認証されるときに追加のロジックを適用します。次のコードは、現在の要求が認証されるかどうかをチェックし、認証される場合はユーザー基本設定をデータベースから取得します。

if (User.Identity.IsAuthenticated)
{
  string userId = User.Identity.GetUserId();
  prefs = await db.UserPreferences
    .Include(x => x.UserCategory.Select(y => y.Category))
    .Where(x => x.ApplicationUser_Id == userId).SingleOrDefaultAsync();
}

prefs が NULL 以外の場合、基本設定を使用して EF クエリを形成します。匿名要求の場合は、既定のクエリを実行するだけです。

データのフィード

前述のように、今回のアプリでは Stack Overflow と Twitter からデータを取り出します。今回の設計原則の 1 つは、データを複数の異なるソースから取得し、それを正規化した 1 つのソースに集約することです。その結果、クライアントは特定のプロバイダーのデータ形式を意識する必要がなくなり、クライアントとバックエンドの対話が単純化されます。バックエンドには、Web API やクライアントでの変更を必要としないで、追加のソースを簡単に集約できるプロバイダー モデルを実装します。このプロバイダーは次のように一貫性のあるインターフェイスを公開します。

interface IProviderAPI
{
   Task<IEnumerable<Conversation>> GetConversationsAsync(
     Provider provider, Category category,
     IEnumerable<Tag> tags, DateTime from, int maxResults, TextWriter logger);
}

Stack Overflow と Twitter の API には多くの機能が用意されていますが、その機能の多さが複雑さにつながっています。今回調査したところ、NuGet パッケージの StacMan と LINQtoTwitter を使用することで、これらの API の使用がより簡単になることがわかりました。LINQtoTwitter と StacMan は資料が豊富でサポートも充実したオープン ソースで、C# で簡単に使用できます。

パスワードなどの機密情報の処理

今回チームでは、「パスワードなどの機微なデータを ASP.NET と Azure App Service にデプロイする際のベスト プラクティス」(bit.ly/1zlNiQI、英語) を参考にしました。ここでは、ソース コードでパスワードをチェックしないよう指示しています。今回は、機密情報はローカル開発コンピューターの補助構成ファイル内にのみ格納します。また、アプリを Azure にデプロイするために、Windows PowerShell または Azure Portal を使用します。

この方法は Web アプリに対して適切に機能します。次のマークアップを使用して、機密情報を web.config ファイルの外部に移動します。

<appSettings file="..\..\AppSettingsSecrets.config">
</appSettings>

ファイルをソース ディレクトリから 2 つ上のレベルに移動すると、このファイルは完全にソリューション ディレクトリの外部に移動し、ソース管理には追加されなくなります。

コンソール アプリ (Web ジョブ) が使用する app.config ファイルは、相対パスをサポートせず、絶対パスをサポートします。絶対パスを使用すると、機密情報をプロジェクト ディレクトリの外部に移動できます。次のマークアップは、機密情報を C:\secrets\AppSettingsSecrets.config ファイルに追加し、機密情報以外のデータを app.config ファイルに追加します。

<configuration>
  <appSettings file="C:\secrets\AppSettingsSecrets.config">
    <add key="TwitterMaxThreads" value="24" />
    <add key="StackOverflowMaxThreads" value="24" />
    <add key="MaxDaysForPurge" value="30" />
  </appSettings>
</configuration>

また、Windows PowerShell スクリプトでこの機密情報を処理するために、Export-CliXml コマンドレットを使用して暗号化した機密情報をエクスポートし、Import-CliXml を使用して読み取ります。

すべてを自動化

DevOps のベスト プラクティスは、すべてを自動化することです。元々、Windows PowerShell スクリプトを記述した目的は、今回のアプリが必要とする Azure リソース (Web アプリ、データベース、ストレージ) を作成することと、接続文字列の設定やアプリ設定の機密情報など、すべてのリソースをフックすることでした。

Visual Studio の配置ウィザードは Azure へのデプロイを自動化するのに役立ちますが、アプリをデプロイおよび構成するには、ほかにも次のような手動の手順を実行する必要があります。

  • 管理者アカウントのパスワードを Azure SQL Database に入力する。
  • Web アプリと Web ジョブ用のアプリ設定の機密情報を入力する。
  • Web ジョブの監視をフックするための Web ジョブ ストレージ アカウントの文字列を入力する。
  • Facebook と Google の開発者コンソール内にあるデプロイ URL を更新して、OAuth ソーシャル ログインを有効にする。

新しいデプロイ URL を指定するには、OAuth プロバイダー認証 URL を更新する必要があるため、カスタム ドメイン名を使用しないで最後の手順を自動化することはできません。今回の Windows PowerShell スクリプトでは、必要なすべての Azure リソースを作成し、それらをフックするため、自動化できるものはすべて自動化します。

Visual Studio から初めて Web ジョブをデプロイする場合、Web ジョブの実行スケジュールをセットアップするよう求められます (今回は 3 時間おきに Web ジョブを実行します)。本稿執筆時点では、Windows PowerShell で Web ジョブのスケジュールを設定する方法はありません。作成とデプロイの Windows PowerShell スクリプトを実行後、Visual Studio からの Web ジョブのデプロイ時に 1 回限りの追加手順を実行してスケジュールを設定するか、ポータル上でスケジュールを設定します。

しばらく使ってわかったことですが、この Windows PowerShell スクリプトは、リソースの作成を試行している間にタイムアウトする場合があります。従来の Azure PowerShell の方法では、リソース作成時のランダムな失敗に簡単には対処できません。正常に作成されたどのリソースを削除する場合でも、複雑なスクリプトを実行する必要があり、このスクリプトは、削除するつもりがなかったリソースまで削除してしまうおそれがあります。スクリプトが成功した場合でも、テストの終了後、そのスクリプトによって作成されたすべてのリソースを削除する必要があります。テスト実行後に削除する必要があるリソースを追跡する作業は、複雑で間違いが起こりやすいものです。

リソース作成のタイムアウトは Azure の欠陥ではないことに注意してください。データ サーバーや Web アプリなどの複雑なリソースをリモートで作成する作業は、本来時間がかかります。タイムアウトと失敗に対処するには、最初からクラウド アプリを設計しなければなりません。

Azure リソース マネージャー (ARM) による問題解決

ARM を使用すると、リソースをグループとして作成できます。これにより、すべてのリソースを簡単に作成し、さらに一時的な障害にも対処できるようになるため、スクリプトがリソースの作成に失敗した場合でも、その操作を再試行できます。また、クリーンアップも簡単です。リソース グループを削除するだけで、依存関係のあるすべてのオブジェクトが自動的に削除されます。

一時的な障害は頻繁には発生せず、通常は 1 回の再試行で操作が成功します。次の Windows PowerShell スクリプトのスニペットは、ARM テンプレートの使用時に、線形バックオフを含む再試行ロジックを実装するためのシンプルな方法を示しています。

$cnt = 0
$SleepSeconds = 30$ProvisioningState = 'Failed'while ( [string]::Compare($ProvisioningState, 'Failed', $True) -eq 0 -and ($cnt -lt 4 ) ){   My-New-AzureResourceGroup -RGname $RGname `    -WebSiteName $WebSiteName -HostingPlanName $HostingPlanName
  $RGD = Get-AzureResourceGroupDeployment -ResourceGroupName $RGname  $ProvisioningState = $RGD.ProvisioningState  Start-Sleep -s ($SleepSeconds * $cnt)  $cnt++}

My-New-AzureResourceGroup は、New-AzureResourceGroup コマンドレットへの呼び出しをラップするために作成した Windows PowerShell 関数で、ARM テンプレートを使用して Azure リソースを作成します。New-AzureResourceGroup の呼び出しは、ほぼ必ず成功しますが、テンプレートによって指定されたリソースの作成はタイムアウトすることがあります。

どのリソースも作成されなかった場合、プロビジョニング状態は Failed になり、スクリプトはスリープ状態になってから再試行されます。再試行時には、既に正常に作成されているリソースは再作成されません。上記のスクリプトは 3 回再試行されます (4 回連続で失敗した場合は、ほぼ間違いなく一時的ではないエラーです)。

複数回行っても有効になるのは 1 回だけという ARM がもたらす効果は、多くのリソースを作成するスクリプトには非常に役立ちます。この再試行ロジックは、すべてのデプロイに必要なわけではありませんが、これが有効な状況では、ARM によって提供されるそのオプションを利用できます。「Azure リソース マネージャーでの Windows PowerShell の使用」(https://azure.microsoft.com/ja-jp/documentation/articles/powershell-azure-resource-manager/) を参照してください。

ビルド統合と自動デプロイ

Visual Studio Online によって、継続的インテグレーション (CI) のセットアップが簡単になりました。Team Foundation Server (TFS) にコードがチェックインされるたびに、自動的にビルドがトリガーされ、単体テストが実行されます。今回は継続的配信 (CD) も採用しました。ビルドと自動単体テストが成功すると、アプリは自動的に Azure テスト サイトにデプロイされます。Azure への CI/CD のセットアップについては、ドキュメント ページ「Visual Studio Online を使用した Azure への継続的な配信」(https://azure.microsoft.com/ja-jp/documentation/articles/cloud-services-continuous-delivery-use-vso/) を参照してください。

開発サイクルの初期段階で、3 人以上のメンバーがソース コードを頻繁にチェックインしたころは、既定のビルド定義トリガーを使用して、ビルド、テスト、デプロイのサイクルを、月曜日から金曜日までの午前 3 時に開始していました。これは効果があり、すべてのメンバーが、毎朝作業を開始するときに Web サイト上で簡易テストを実行できました。コード ベースが安定し、チェックインの頻度が減少してその重要性が高まったと思われたときに、トリガーを CI モードに設定して、各チェックインの発生時にプロセスがトリガーされるようにしました。また、低リスクの変更が頻繁に行われる「コード クリーンアップ」フェーズに達した時点で、トリガーをロール ビルドに設定しました。この場合、ビルド、テスト、デプロイのサイクルが最大で 1 時間に 1 回行われます。図 8 は、デプロイとテスト カバレッジを含むビルドの概要を示しています。

ビルドの概要
図 8 ビルドの概要

まとめ

今回は、データを集約および処理し、それをモバイル クライアントに提供するクラウド バックエンドを作成する場合の考慮事項をいくつか説明しました。今回作成したサンプル アプリの個々の部分はそれほど複雑ではありませんが、クラウド ベースのソリューションに特有の多くの不確定要素が含まれています。また、Visual Studio Online を使用して、小規模チームがどのように、DevOps 専用の管理者を用意することなく継続的ビルドと継続的デプロイを実行できるかについても説明しました。

第 2 部では、クライアント アプリについて、および Xamarin Form を使用することによって、どのようにプラットフォーム固有のコードを最小限に抑えながら、簡単に複数のプラットフォームをターゲットにできるかを詳しく説明します。また、ソーシャル ログインに関する OAuth2 の謎にも迫ります。


Rick Anderson は、マイクロソフトのシニア プログラミング ライターであり、ASP.NET MVC、Microsoft Azure、および Entity Framework を担当しています。Twitter は、twitter.com/RickAndMSFT (英語) からフォローできます。

Kraig Brockschmidt は、マイクロソフトのシニア コンテンツ開発者であり、クロス プラットフォーム モバイル アプリを担当しています。『HTML、CSS、JavaScript によるプログラミング Windows ストアアプリ』(日経 BP 社、2013 年) の著者であり、kraigbrockschmidt.com (英語) でブログも執筆しています。

Tom Dykstra は、マイクロソフトのシニア コンテンツ開発者であり、Microsoft Azure と ASP.NET を担当しています。

Erik Reitan は、マイクロソフトのシニア コンテンツ開発者であり、Microsoft Azure と ASP.NET を担当しています。Twitter は、twitter.com/ReitanErik (英語) からフォローできます。

Mike Wasson は、マイクロソフトのコンテンツ開発者です。長年の間、Win32 マルチメディア API のドキュメントを作成してきました。現在は、Microsoft Azure と ASP.NET に関する記事を執筆しています。

この記事のレビューに協力してくれた技術スタッフの Michael Collier、John de Havilland、Brady Gaster、Ryan Jones、Vijay Ramakrishnan、および Pranav Rastogi に心より感謝いたします。