Август 2015

Том 30 выпуск 8

Мобильные приложения, подключенные к облаку - Создание веб-сервиса с помощью Azure Web Apps и WebJobs

Крейг Брокшмидт

В статье рассматриваются:

  • управление серверной стороной в облаке средствами Azure для мобильного приложения;
  • использование Azure WebJobs для выполнения постоянных фоновых задач;
  • автоматизация сборок и развертывания средствами Visual Studio Online;
  • разработка REST API для мобильных клиентов с помощью ASP.NET Web API.

Исходный код можно скачать по ссылке aka.ms/altostratusproject.

Сегодня многие мобильные приложения подключены к одному или более веб-сервисам, которые предоставляют ценные и интересные данные. При проектировании и разработке таких приложений самый простой подход — выдавать прямые вызовы REST API к этим сервисам, в затем обрабатывать ответ на клиентской стороне. Однако у этого подхода есть ряд недостатков. Например, каждый сетевой вызов и каждый бит, обрабатываемый на клиентской стороне, потребляет драгоценные ресурсы аккумулятора и полосы пропускания. Более того, большой объем обработки на клиентской стороне может занимать некоторое время, особенно на устройствах из низшего ценового диапазона, делая приложения менее отзывчивым. Веб-сервисы могут накладывать ограничения на пропускную способность, а значит, чисто клиентское решение будет плохо масштабироваться под большее количество пользователей.

В итоге во многих сценариях имеет смысл (особенно в тех, где данные извлекаются из множества источников) создавать собственную серверную часть, которая позволит вам разгрузить клиентскую сторону от некоторых задач. В процессе нашей работы в Microsoft как группы, которая разрабатывает контент для ASP.NET, Microsoft Azure и средств кросс-платформенной разработки в Visual Studio, мы создали конкретный пример этого подхода.

В этой статье из двух частей мы обсудим наш подход, некоторые проблемы, с которыми мы столкнулись, и уроки, которые мы извлекли при разработке нашего приложения. Это приложение — Altostratus (интересный тип облака) — ведет поиск в Stack Overflow и Twitter по конкретным темам, которые мы называем обсуждениями. Мы выбрали эти два провайдера, так как они предлагают хорошие Web API. Приложение содержит два основных компонента.

  • Облачная серверная часть, размещенная в Azure. Серверная часть периодически выдает запросы провайдерам и агрегирует данные в форму, которая лучше всего подходит клиенту. Это предотвращает проблемы ограничения пропускной полосы, ослабляет любые проблемы, связанные с задержками у провайдеров, минимизирует обработку на клиентской стороне и уменьшает количество сетевых запросов от клиента. Один из компромиссов состоит в том, что WebJob выполняется через каждые несколько часов, поэтому вы не получаете данные в реальном времени.
  • Облегченное клиентское мобильное приложение, созданное с помощью Xamarin для выполнения в Windows, Android и iOS (рис. 1). Мобильный клиент получает агрегированные данные от серверной части и предоставляет их пользователю. Он также хранил синхронизируемый кеш данных в локальной базе данных для удобства работы в автономном режиме и ускорения запуска.

Мобильный клиент Xamarin, выполняемый на планшете Android (слева), в Windows Phone (в середине) и iPhone (справа)
Рис. 1. Мобильный клиент Xamarin, выполняемый на планшете Android (слева), в Windows Phone (в середине) и iPhone (справа)

Дополнительно (но не обязательно) пользователи могут входить на мобильный клиент, используя провайдеры социальных сетей (Google или Facebook). После входа пользователи могут указывать предпочтения, что еще больше оптимизирует коммуникации с клиентом. Если конкретнее, то они могут выбирать, какие предметные области следует принимать, и устанавливать максимальное количество обсуждений в рамках каждой предметной области. Предпочтения пользователей хранятся в серверной части, поэтому пользователь может входить с любого клиента и получать ту же среду. Чтобы продемонстрировать эту идею, мы также создали простой веб-клиент, который взаимодействует с той же серверной частью.

В этом проекте мы также хотели задействовать средства управления жизненным циклом приложений (application lifecycle management, ALM), встроенные в Visual Studio и Visual Studio Online, чтобы управлять спринтами и незавершенными задачами и выполнять автоматизированное модульное тестирование, непрерывную интеграцию (continuous integration, CI) и непрерывную доставку (continuous delivery, CD).

В этой статье из двух частей исследуются детали нашего проекта. В первой части основное внимание уделяется серверной части (back end) и нашему применению средств ALM (DevOps). Мы также обсудим некоторые проблемы, с которыми мы столкнулись, и некоторые уроки, которые мы извлекли, например:

  • как безопасно автоматизировать применять пароли в программах, отличных от веб-приложений;
  • как обрабатывать интервалы ожидания автоматизации Azure;
  • эффективная и информативная фоновая обработка;
  • ограничения CI/CD и способы их обхода.

Во второй части мы рассмотрим, как использовать Xamarin для ориентации на несколько клиентских мобильных платформ, в том числе поговорим об аутентификации и поддержании кеша данных на клиентской стороне в синхронизированном состоянии.

Архитектура

На рис. 2 показана высокоуровневая архитектура решения Altostratus.

Azure SQL Database Azure SQL Database
Azure App Service Web App Azure App Service Web App
ASP.NET Web API ASP.NET Web API
Entity Framework Entity Framework
Azure WebJobs Azure WebJobs
Entity Framework Entity Framework
StackOverflow API StackOverflow API
Twitter API Twitter API
SocialLogin Вход через социальные сети
REST API REST API
Xamarin App Приложение Xamarin
Web Client Веб-клиент

 

  • На серверной стороне мы используем Azure App Service для хостинга веб-приложения и Azure SQL Database для хранения данных в реляционной базе данных. Доступ к данным осуществляется через Entity Framework (EF).
  • С помощью Azure WebJobs мы выполняем запланированную фоновую задачу, которая агрегирует данные от Stack Overflow и Twitter и записывает их в базу данных. WebJob можно легко расширить для агрегации данных из дополнительных источников.
  • Мобильный клиент создается с помощью Xamarin и взаимодействует с серверной частью, используя простой REST API.
  • REST API реализуется через ASP.NET Web API, которая является инфраструктурой для создания HTTP-сервисов в Microsoft .NET Framework.
  • Веб-клиент является сравнительно простым JavaScript-приложением. Мы использовали библиотеку KnockoutJS для связывания данных и jQuery для AJAX-вызовов.

Архитектура Altostratus
Рис. 2. Архитектура Altostratus

Схема базы данных

Мы определяем схему базы данных и управляем базой данных SQL на серверной стороне с помощью EF Code First. Сущности, которые хранятся в базе данных, показаны на рис. 3.

  • Provider Источник данных вроде Twitter или Stack Overflow.
  • Conversation Элемент от провайдера. В случае Stack Overflow соответствует вопросу с ответами, а в случае Twitter — твиту. (Твит тоже может быть с ответами, но мы не реализовали эту функциональность.)
  • Category Предмет для обсуждения (conversation), такой как «Azure» или «ASP.NET».
  • Tag Строка поиска для конкретных категории и провайдера. Соответствует тегами в Stack Overflow («azure-web-sites») и хеш-тегам в Twitter («#azurewebsites»). Серверная часть использует их для запроса провайдеров. Конечный пользователь их не видит.
  • UserPreference Хранит предпочтения, индивидуальные для каждого пользователя.
  • UserCategory Определяет таблицу join для UserPreference и Category.

Модель данных Altostratus
Рис. 3. Модель данных Altostratus

В принципе, CRUD-код (Create, Read, Update, Delete) в приложении Altostratus является типичным для EF Code First. Одно особое соображение связано с обработкой инициализации базы данными и миграциями. Code First создает и инициализирует новую базу данных, если при первой попытке обращения программы к данным никакой базы данных нет. Поскольку первая попытка доступа к данным могла бы произойти в WebJob, мы поместили в метод Main в WebJob следующий код, который заставляет EF использовать инициализатор MigrateDatabaseToLatestVersion:

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

Без этого кода WebJob использовал бы по умолчанию CreateDatabaseIfNotExists, который не инициализирует базу данных. В результате мы получили бы пустую базу данных и ошибки, когда приложение попыталось бы считать данные из пустых таблиц.

Разработка WebJob

WebJobs — идеальное решение для выполнения фоновых задач, которые раньше требовали выделенной рабочей роли Azure. Вы можете запускать WebJobs в Azure Web App без дополнительной оплаты. О преимуществах WebJobs по сравнению с рабочими ролями см. в блоге Трой Хантс (Troy Hunts) статью «Azure WebJobs Are Awesome and You Should Start Using Them Right Now!» (bit.ly/1c28yAk).

{Для верстки: в следующем абзаце дана ошибочная ссылка на рис. 3 – на самом деле это рис. 5, но давать ссылки на иллюстрации с перескоком не принято, поэтому я просто убрал отсюда ссылку на рисунок}

WebJob в нашем решении периодически выполняет три функции: получает данные Twitter, получает данные Stack Overflow и очищает старые данные. Эти функции независимы, но должны выполняться последовательно, так как используют один контекст EF. По завершении WebJob на Azure Portal показывается статус каждой функции. Если функция завершена, она помечается зеленым сообщением «Success», а если генерируется исключение — помечается красным сообщением «Failed».

Неудачи отнюдь не редки в Altostratus, потому что мы используем бесплатные API провайдеров Stack Overflow и Twitter. В частности, запросы ограниченны: если вы превышаете лимит запросов, эти провайдеры возвращают ошибку регулирования нагрузки (throttling). Изначально это было основной причиной создания серверной части. Хотя было бы проще, если бы мобильное приложение напрямую делало запросы к провайдерам, растущее количество пользователей быстро исчерпало бы лимит регулирования нагрузки. Вместо этого серверная часть может просто выдавать несколько периодических запросов для сбора и агрегации данных.

Одна из проблем возникла с обработкой ошибок WebJob. Обычно, если какая-то функция в WebJob генерирует исключение, экземпляр WebJob завершается, оставшиеся функции не обрабатываются, и выполнение WebJob в целом помечается как неудачное. Чтобы выполнять все задачи и показывать сбой на уровне функций, WebJob должен захватывать исключения. Если какая-либо функция в Main генерирует исключение, мы фиксируем последнее исключение и генерируем его заново, чтобы WebJob помечался как Failed. Этот подход демонстрируется псевдокодом на рис. 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 на уровне WebJob показывается только последнее исключение. На уровне функций протоколируется каждое исключение, поэтому никакие исключения не теряются. На рис. 5 представлена информационная панель для WebJob, который был выполнен успешно. Вы можете изучить подробности по работе каждой функции, просмотрев диагностический вывод.

Успешное выполнение WebJob
Рис. 5. Успешное выполнение WebJob

Разработка REST API

Мобильное приложение взаимодействует с серверной частью через простой REST API, который мы реализовали на основе ASP.NET Web API 2. (Заметьте, что Web API в ASP.NET 5 интегрирован в инфраструктуру MVC 6, что упрощает их включение в веб-приложение.)

Наш REST API кратко описан в табл. 1.

Табл. 1. Краткое описание REST API

GET api/categories Позволяет получить категории
GET api/conversations?from=iso-8601-date Позволяет получить обсуждения
GET api/userpreferences Позволяет получить предпочтения пользователя
PUT api/userpreferences Позволяет обновить предпочтения пользователя

Все ответы имеют формат JSON. Например, на рис. 6 показан HTTP-ответ для обсуждений (conversations).

Рис. 6. 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"
  },
  //...часть результатов удалена для экономии места
]

В случае обсуждений аутентифицировать запрос не требуется. Это означает, что мобильное приложение всегда может показать осмысленные данные без предварительного входа пользователя. Но мы также хотели продемонстрировать, как наличие серверной части позволяет выполнять дополнительные оптимизации при входе пользователя. Наше клиентское приложение дает возможность пользователю выбрать категории отображаемых обсуждений наряду с лимитом обсуждений. Поэтому, если запрос аутентифицируется (т. е. пользователь вошел в приложение), серверная часть автоматически фильтрует ответ на основе указанных предпочтений. Это ограничивает объем данных, которые приходится обрабатывать клиенту, что со временем уменьшает потребности в пропускной способности, памяти, хранилище и энергопотреблении..

API обсуждений также принимает необязательный параметр from в строке запроса. Если он указан, результаты фильтруются так, чтобы включать только обсуждения, обновляемые после этой даты:

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

Это сводит размер ответа к минимуму. Наш мобильный клиент использует этот параметр, чтобы запрашивать только данные, которые нужны для синхронизации его кеша.

Мы могли бы разработать API так, чтобы использовать строки запросов для получения предпочтений пользователя индивидуально для каждого запроса, что потребовало бы поддерживать эти предпочтения только на клиенте. И позволило бы обойтись без аутентификации. Хотя такой подход прекрасно работал бы в базовых сценариях, мы хотели предоставить пример, который можно было бы расширить для более сложных случаев, где только строк запросов было бы недостаточно. Хранение предпочтений на серверной стороне также означает, что они автоматически применяются к каждому клиенту, где пользователь осуществляет вход.

Data Transfer Objects (DTO)

REST API — это граница между схемой базы данных и сетевым представлением (wire representation). Мы не хотели напрямую сериализовать EF-модели по нескольким причинам.

  • Они содержат информацию, не нужную клиенту, например внешние ключи.
  • Они делают API уязвимым перед оверпостингом (over-posting). (Оверпостинг возникает, когда клиент обновляет поля базы данных, которые вы не предполагали делать доступными для обновлений. Такое возможно, когда вы преобразуете полезные данные HTTP-запроса напрямую в EF-модель без достаточной проверки ввода. Детали см. по ссылке bit.ly/1It1wl2.)
  • «Форма» EF-моделей предназначена для создания таблиц базы данных и не является оптимальной для клиента.

Ввиду этого мы создали набор объектов передачи данных (data transfer objects, DTO), которые являются просто C#-классами, определяющими формат для ответов REST API. Например, ниже приведена наша EF-модель для категорий:

public class Category
{
  public int CategoryID { get; set; }
  [StringLength(100)]
  public string Name { get; set; }  // например: Azure, ASP.NET
  public ICollection<Tag> Tags { get; set; }
  public ICollection<Conversation> Conversations { get; set; }
}

Сущность category имеет основной ключ (CategoryID) и навигационные свойства (Tags и Conversations). Навигационные свойства упрощают следование отношениям в EF, скажем, чтобы найти все теги для какой-то категории.

Когда клиент запрашивает категории, ему нужен просто список названий категорий:

[ "Azure", "ASP.NET" ]

Это преобразование легко выполнить с помощью LINQ-выражения Select в методе контроллера Web API:

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

Сущность UserPreference немного сложнее:

public class UserPreference
{
  // Внешний ключ в Id таблицы AspNetUser   
  [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 является внешним ключом для таблицы user. UserCategory указывает на таблицу junction, которая создает отношение «многие ко многим» между предпочтениями пользователя и категориями.

Вот как мы хотели, чтобы это выглядело для клиента:

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

Это скрывает детали реализации схемы базы данных вроде внешних ключей и таблиц junction и преобразует названия категорий в список строк.

LINQ-выражение для преобразования UserPreference в UserPreferenceDTO довольно сложное, поэтому вместо него мы использовали 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()));
               // Этот блок преобразует
               // ICollection<UserCategory> в плоский
               // список названий категорий

Первый вызов CreateMap сопоставляет Conversation с ConversationDTO, используя соглашения сопоставления AutoMapper по умолчанию. В случае 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). (Процесс аутентификации будет подробно описан во второй части этой статьи.) После того как Web API аутентифицирует запрос, контроллер Web API может использовать эту информацию для авторизации запросов и поиска пользователя в базе данных пользователей.

Чтобы ограничить доступ к 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), или реализовать свой фильтр авторизации для более сложных сценариев авторизации.

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. Один из наших проектировочных принципов заключался в том, чтобы брать данные из нескольких разных источников и агрегировать их в единый нормализованный источник. Это упрощает взаимодействие между клиентами и серверной частью, поскольку клиентам не нужно знать формат данных для каждого конкретного провайдера. На серверной стороне мы реализовали модель провайдеров, которая облегчает агрегацию дополнительных источников, не требуя никаких изменений в Web API или клиентах. Провайдеры предоставляют согласованный интерфейс:

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

Stack Overflow API и Twitter API предоставляют массу возможностей, но большой количество возможностей увеличивает сложность. Мы обнаружили, что NuGet-пакеты StacMan и LINQtoTwitter позволяют значительно упростить работу с этими API. LINQtoTwitter и StacMan хорошо документированы, активно поддерживаются, имеют открытый исходный код и легко используются в C#.

Обработка паролей и других секретов

Мы следовали рекомендациям из статьи «Best Practices for Deploying Passwords and Other Sensitive Data to ASP.NET and Azure App Service» (bit.ly/1zlNiQI), в которой предписывается никогда не проверять пароли в исходном коде. Мы храним секреты только во вспомогательных конфигурационных файлах на локальных компьютерах для разработки. Чтобы развернуть приложение в Azure, мы используем Windows PowerShell или Azure Portal.

Этот подход отлично работает для веб-приложения. Мы переместили секреты из файла web.config, используя такую разметку:

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

Перемещение файла на два уровня вверх из исходного каталога означает, что он оказывается полностью вне каталога решения и не будет добавляться в систему контроля версий.

Файл app.config, используемый консольным приложением (WebJobs), не поддерживает относительные пути, но поддерживает абсолютные пути. Перемещая свои секреты из каталога проекта вы тоже можете использовать абсолютный путь. В следующей разметке секреты добавляются в файл 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, необходимых нашему приложению (веб-приложению, базе данных, хранилищу), и для подключения всех ресурсов.

Visual Studio Deployment Wizard делает неплохую работу по автоматизации развертывания в Azure, но все равно остается несколько этапов в развертывании и конфигурировании приложения, выполняемых вручную:

  • ввод пароля для учетной записи администратора в Azure SQL Database;
  • ввод секретов в параметры приложения для веб-приложения и WebJob;
  • ввод строк учетной записи хранилища WebJob для подключения мониторинга WebJob;
  • обновление URL развертывания в консолях разработчика Facebook и Google, чтобы разрешить вход через социальные сети с применением OAuth.

URL нового развертывания требует обновления URL провайдера аутентификации OAuth, поэтому нет никакого способа автоматизировать последний этап без использования собственного доменного имени. Наши скрипты Windows PowerShell создают все необходимые нам ресурсы Azure и подключают их, поэтому все, что можно было автоматизировать, автоматизировано.

При первом развертывании WebJob из Visual Studio вам предлагается настроить расписание выполнения этого WebJob. (Мы выполняем WebJob через каждые три часа.) На момент написания этой статьи отсутствует какой-либо способ задать расписание для WebJob через Windows PowerShell. После выполнения скриптов Windows PowerShell для создания и развертывания нужен разовый дополнительный этап развертывания WebJob из Visual Studio для настройки расписания (или сделать то же самое через портал).

Вскоре мы обнаружили, что при попытке создать ресурс в скриптах Windows PowerShell иногда истекают периоды ожидания. Используя традиционный подход с Azure PowerShell, трудно иметь дело со случайными сбоями в создании ресурсов. Удаление любых успешно созданных ресурсов требует написания нетривиального скрипта, который, возможно, удалил бы и те ресурсы, которые вы не собирались удалять. Даже когда ваш скрипт успешно отрабатывает, по окончании тестирования нужно удалять все ресурсы, созданные этим скриптом. Отслеживание ресурсов, подлежащих удалению после прогона теста, — дело не тривиальное и подверженное ошибкам.

Заметьте, что истечение периодов ожидания при создании ресурсов в Azure не является каким-то дефектом. Удаленное создание сложных ресурсов вроде сервера данных или веб-приложения по определению занимает некое время. Архитектуру облачных приложений с самого начала следует рассчитывать на обработку тайм-аутов и сбоев.

Azure Resource Manager (ARM) спешит на помощь

ARM позволяет создавать ресурсы как группу. Это дает возможность создавать все ресурсы и обрабатывать промежуточные отказы (transient faults), так что, если скрипту не удается создать какой-то ресурс, можно попытаться снова. Кроме того, очистка весьма проста. Вы просто удаляете свою группу ресурсов, и все зависимые объекты автоматически удаляются.

Промежуточные отказы встречаются нечасто, и обычно для успешного выполнения операции достаточно всего одной повторной попытки. Следующий фрагмент из скрипта 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 — это написанная нами функция Windows PowerShell, которая обертывает вызов командлета New-AzureResourceGroup, использующего шаблонARM для создания ресурсов Azure. Вызов New-AzureResourceGroup будет почти всегда успешным, но создание ресурсов, указанных шаблоном, может давать тайм-ауты.

Если какой-то ресурс не был создан, состояние подготовки (provisioning state) становится Failed, после чего скрипт переходит на какое-то время в спящее состояние, а затем повторяет попытку. В ходе повторной попытки ресурсы, которые уже были успешно созданы, не создаются заново. Предыдущий скрипт выполняет три повторные попытки. (Четыре отказа подряд почти наверняка указывают на постоянную ошибку.)

Идемпотентность, обеспечиваемая ARM, крайне полезна в скриптах, создающих множество ресурсов. Мы не предполагаем, что вам понадобится эта логика повторных попыток во всех ваших процессах развертывания, но ARM дает такую возможность (см. «Using Azure PowerShell with Azure Resource Manager», bit.ly/1GyaMzv).

Интеграция сборки и автоматизированное развертывание

Visual Studio Online облегчает настройку непрерывной интеграции (CI). Всякий раз, когда код ставится на учет в Team Foundation Server (TFS), это автоматически инициирует сборку и запуск модульных тестов. Мы также применили непрерывную доставку (continuous delivery, CD). Если сборка и автоматизированное модульное тестирование проходят успешно, приложение автоматически развертывается на ваш тестовый сайт Azure. Подробнее о настройке CI/CD в Azure см. на странице документации «Continuous Delivery to Azure Using Visual Studio Online» (bit.ly/1OkMkaW).

На ранних этапах цикла разработки, когда трое из нас (или более) активно фиксировали исходный код в системе контроля версий, мы использовали триггер определения сборки по умолчанию, чтобы запускать цикл сборки/тестирования/развертывания в 3 часа ночи, с понедельника по пятницу. Это прекрасно работало, предоставляя всем нам шанс быстро проверять веб-сайт каждое утро при начале работы. По мере стабилизации кодовой базы и менее частых фиксациях исходного кода, но, возможно, более важных мы перевели триггер в режим CI, чтобы каждая фиксация инициировала процесс. Когда мы дошли до этапа «Code clean up» с частыми изменениями низкого риска, мы перевели триггер в режим Rolling builds, где цикл сборки/тестирования/развертывания запускается не чаще, чем раз в час. На рис. 7 показана сводка сборки, включающая информацию о развертывании и охвате тестами.

Сводная информация о процессе сборки
Рис. 7. Сводная информация о процессе сборки

Заключение

В этой статье мы обсудили некоторые соображения по созданию серверной части в облаке, которая агрегирует, обрабатывает и доставляет данные мобильным клиентам. Ни одна из частей нашего приложения-примера не является ужасно сложной, но подвижных деталей в нем много, что типично для облачных решений. Мы также рассмотрели, как Visual Studio Online позволяет небольшой группе выполнять непрерывные сборки и развертывания без выделенного диспетчера DevOps.

Во второй части мы подробно опишем клиентское приложение и расскажем, как Xamarin Forms упрощают разработку с ориентацией на несколько платформ с минимумом специфичного для конкретной платформы кода. Мы также углубимся в таинства входа через социальные сети с применением OAuth2.


Рик Андерсон (Rick Anderson) — старший редактор документации разработок в Microsoft по ASP.NET MVC, Microsoft Azure и Entity Framework. Следите за его заметками в twitter.com/RickAndMSFT.

Крейг Брокшмидт (Kraig Brockschmidt) — старший редактор документации разработок в Microsoft по кросс-платформенным мобильным приложениям. Автор книги «Programming Windows Store Apps with HTML, CSS and JavaScript» (два издания) от Microsoft Press, ведет блог на сайте kraigbrockschmidt.com.

Том Дейкстра (Tom Dykstra) — старший разработчик контента в Microsoft, уделяет основное внимание Microsoft Azure и ASP.NET.

Эрик Райтан (Erik Reitan) — старший разработчик контента в Microsoft, уделяет основное внимание Microsoft Azure и ASP.NET. Следите за его заметками в twitter.com/ReitanErik.

Майк Уоссон (Mike Wasson) — разработчик контента в Microsoft. В течение многих лет писал документацию по мультимедийным Win32 API. В настоящее время пишет о Microsoft Azure и ASP.NET.

Выражаем благодарность за рецензирование статьи экспертам Микелю Колье (Michael Collier), Джону де Хэвилленду (John de Havilland), Брейди Гастеру (Brady Gaster), Райену Джонсу (Ryan Jones), Виджею Рамакришнану (Vijay Ramakrishnan) и Пранаву Растоги (Pranav Rastogi).