Управление удалённым IIS

в 11:12, , рубрики: .net, C#, iis, microsoft, Web Administration, WMI, Блог компании Аркадия, разработка, метки:

Вступление

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

Вот список основных требований к реализуемому модулю:

  • Возможность выполнения основных операций с IIS:
    • создание сайта
    • создание virtual application
    • создание virtual directory
    • настройка bindings для сайтов, включая установку сертификатов SSL
    • создание пулов приложений с детальной настройкой
  • Поддержка параллельной работы с несколькими IIS на разных серверах фермы
  • Поддержка IIS версии 8.0 (более ранние версии поддерживать не нужно).

Одним словом, модуль должен был уметь практически всё, что можно сделать через IIS Manager.

Я нашёл и исследовал три инструмента, подходящих для решения задач:

  1. Windows Management Instrumentation (WMI)
  2. ASP.NET Configuration API
  3. Microsoft Web Administration

После создания тестовых приложений с каждым из рассмотренных вариантов, я выбрал Microsoft.Web.Administration как наиболее перспективное.

От первого варианта я отказался, поскольку довольно сложно разобраться в методах инструмента, что увеличивает шанс ошибки. При этом работа с ним напоминает работу с COM-компонентами.
Пример создания сайта с использованием WMI:

DirectoryEntry IIS = new DirectoryEntry("IIS://localhost/W3SVC");          
object[] bindings = new object[] { "127.0.0.1:4000:", ":8000:" };
IIS.Invoke("CreateNewSite", "TestSite", bindings, "C:\InetPub\WWWRoot");

Второй вариант – это работа с конфигурационными XML файлами. То есть предполагалось практически вручную изменять root web.config файл на web серверах. Как понимаете, данный вариант меня тоже не устроил.

Третий же вариант позволил создавать сайты, делая всю низкоуровневую работу за разработчика. Дополнительным преимуществом этого инструмента является то, что IIS Manager использует именно эту библиотеку для выполнения всех доступных операций.

Конечно трудности при реализации решения всё-таки имелись. Я потратил много гугл-часов, чтобы заставить эту Microsoft.Web.Administration работать так, как мне было нужно. В Интернете можно найти много информации о том, как работать с данной библиотекой, что с её помощью можно сделать и т.п. Однако, вся эта информация сильно разбросана по разным статьям. Это и побудило меня написать о своём опыте «погружения в мир подводных камней» Microsoft.Web.Administration. Я постарался собрать в ней все проблемы, с которыми я столкнулся, и их решения. Вдруг кому-то пригодится.

Итак, начнём техническую часть.

Конфигурация систем

У нас есть ферма web-серверов, каждый из которых управляется Windows Server 2012 Standard с IIS 8.0. Есть отдельный Application сервер, на котором запущен Windows service, использующий наш модуль. На этом сервере IIS не развернут. Нам нужно управлять web-серверами с Application сервера.
Управление удалённым IIS - 1

Разработка

Подключение Microsoft.Web.Administration

Первые проблемы появились сразу ещё на этапе испытания библиотеки. Подключил её к проекту, написал код, который должен был создать сайт и получил ошибку доступа Exception from HRESULT: 0x80070005 (E_ACCESSDENIED). Как выяснилось после прочтения ряда описаний подобной проблемы (например, stackoverflow.com/questions/8963641/permissions-required-to-use-microsoft-web-administration ), Microsoft.Web.Administration для доступа к root web.config файлу требует права администратора.

Хорошо, создаём пользователя на удалённом сервере с логином и паролем. Создаём такого же пользователя на локальном компьютере с такими же логином и паролем (это важно, иначе приложение не залогиниться на удалённую машину). Запускаем. Та же проблема!

Изучаем проблему доступа более детально. Выясняется, что недостаточно создать пользователя с правами администратора. Ведь начиная с Windows Vista, появилась система UAC. И даже администратор компьютера в понимании этой системы вовсе не администратор, а пользователь с расширенными правами. Получается, чтобы наше приложение заработало, нужно отключить UAC на удалённом сервере. Однако, отключения UAC через Администрирование Windows недостаточно, т.к. проблема остаётся. Нужно полностью отключать UAC. Я делал это через реестр, как описано в статье по ссылке. Да, согласен, это не безопасно. Но это единственное решение. Благо заказчик на это согласен.

Пробуем запустить наше приложение. Эврика! Сайт создан. Значит, можно двигаться дальше.
После того, как приложение было развёрнуто на тестовом сервере, проявилась вторая проблема конфигурирования: модуль не мог найти библиотеку Microsoft.Web.Administration и падал с ошибкой. Выяснилось, что в GAC на сервере лежала сборка другой версии. Чтобы справиться с данной трудностью, было принято решение включить копирование нужной библиотеки в Bin проекта.

Последняя сложность, связанная с подключением библиотеки, также возникла из-за версионности сборок. На этапе активной разработки были найдены библиотеки с версиями 7.0.0.0 и 7.5.0.0 Вторая упрощала реализацию некоторых нетривиальных вещей, например, установку AlwaysRunning для пула приложений. Поэтому я сначала подключил её. Но после выгрузки на тестовый сервер, приложение снова упало. Оказывается, Microsoft.Web.Administration 7.5.0.0 работает только с IIS Express. Поэтому если вы планируете управлять полноценным IIS, используйте версию 7.0.0.0.

Это были основные проблемы при подключении библиотеки Microsoft.Web.Administration и при её подготовке к решению поставленных задач. Впереди была реализация функциональности согласно требованиям заказчика. О них речь пойдёт дальше.

Реализация требований

Среди методов, которые я реализовал в модуле, есть и банальные, и такие, над которыми пришлось поломать голову. Я не буду описывать те вещи, которые можно легко найти в Интернете, и всё то, что и так понятно из названий методов библиотеки Microsoft.Web.Administration, например, создание веб сайта и байндингов для него. Вместо этого я сконцентрируюсь на проблемах, над которыми пришлось подумать, и для которых было сложно найти решение в Интернете (или вообще не удалось это сделать).

Многопоточность и многозадачность

Согласно требованиям к модулю, необходимо было предусмотреть возможность параллельного создания нескольких веб сайтов на одном или нескольких удалённых серверах. При этом два потока не могут управлять одним удалённым IIS, т.к. по факту это означает изменение одного и того же root web.config файла. Поэтому было принято решение сделать Lock потоков по имени веб сервера. Сделано это следующим образом:

private ServerManager _server;
private static readonly ConcurrentDictionary<string, object> LockersByServerName = new ConcurrentDictionary<string, object>();

private object Locker
{
    get { return LockersByServerName.GetOrAdd(ServerName, new object()); }
}

private void ConnectAndProcess(Action handler, CancellationToken token)
{
    token.ThrowIfCancellationRequested();

    lock (Locker)
    {
        try
        {
             _server = ServerManager.OpenRemote(ServerName);
             token.ThrowIfCancellationRequested();

             try
             {
                 handler();
             }
             catch (FileLoadException)
             {
                 // try again

                  token.ThrowIfCancellationRequested();
                  if (_server != null) _server.Dispose();
                  _server = ServerManager.OpenRemote(ServerName);
                  token.ThrowIfCancellationRequested();

                  handler();
             }
        }
        finally
        {
             if(_server != null)
                 _server.Dispose();
             _server = null;
        }
    }
}

В этом примере ServerName — это NetBIOS имя компьютера в локальной сети.

Каждый метод разрабатываемого модуля оборачивается в данный handler. Например, проверка существования веб сайта:

public bool WebSiteExists(string name, CancellationToken token)
{
    return ConnectAndGetValue(() =>
    {
        Site site = _server.Sites[name];
        return site != null;
    }, token);
}

private TValue ConnectAndGetValue<TValue>(Func<TValue> func, CancellationToken token)
{
        TValue result = default(TValue);
        ConnectAndProcess(() => { result = func(); }, token);
        return result;
}

Почему мы каждый раз заново подключаемся к серверу?
Во-первых, система, в рамках которой выполнялось создание данного модуля, свободно конфигурируемая. Поэтому мы не знаем заранее, какие методы будут вызваны, в каком порядке и как долго будет нужен экземпляр класса модуля (назовём его MWAConnector).
Во-вторых, коннектор может понадобиться другому потоку. А если у нас открыто подключение одного коннектора, то мы не можем позволить подключение второго, т.к. иначе будет ошибка параллельного доступа к файлу на редактирование.
Исходя из этих соображений, в коде держится один экземпляр класса MWAConnector для нескольких операций, каждая из которых будет выполняться в независимом контексте отдельного подключения.
Недостаток такого подхода — затраты ресурсов на создание подключений. Этими издержками было решено пренебречь, т.к. они не являются узким местом модуля: непосредственное выполнение операции занимает в несколько раз больше процессорного времени, чем создание подключения.

Установка AlwaysRunning

Одной из задач было создание пула приложения с флагом AlwaysRunning. В свойствах класса ApplicationPool из библиотеки Microsoft.Web.Administration 7.0.0.0 можно найти многое: AutoStart, Enable32BitAppOnWin64, ManagedRuntimeVersion, QueueLength. Но там нет RunningMode. В сборке версии 7.5.0.0 это свойство есть, но, как было отмечено выше, эта версия работает только с IIS Express.
Решение проблемы нашлось. Делается это так:

ApplicationPool pool = _server.ApplicationPools.Add(name);

//some code to set pool properties

if (alwaysRunning)
{
  pool["startMode"] = "AlwaysRunning";
}

Для сохранения изменений необходимо вызывать метод CommitChanges().

_server.CommitChanges();

Установка PreloadEnabled

Другой проблемой, с которой я столкнулся, было отсутствие встроенного свойства для установки флага PreloadEnabled для веб приложений и сайта. Этот флаг отвечает за уменьшение времени первичной загрузки сайта после рестарта. Он полезен, когда сайт долго «прогревается». А некоторые из развёртываемых заказчиком сайтов именно такие.
В качестве решения я приведу фрагмент кода, который создаёт веб приложение для сайта:

Site site = _server.Sites[siteName];
string path = string.Format("/{0}", applicationName);
Application app = site.Applications.Add(path, physicalPath);
app.ApplicationPoolName = applicationPoolName;
if (preload)
    app.SetAttributeValue("preloadEnabled", true);
_server.CommitChanges();

Отметим, что имя веб-приложения должно начинаться с «/». Это необходимо, т.к. иначе возникнет ошибка в методе получения, создания или удаления приложения.

Изменение параметров сайта, как веб приложения

Иногда возникает необходимость изменить пул приложений для самого сайта. Проблема в том, что в классе Site нет такого свойства. Его можно найти только у экземпляра класса Application.
Решение – получить веб приложение сайта:

Site site = _server.Sites[siteName];
Application app = site.Applications["/"];

Удаление сайта

Удаление сайта, казалось бы, — простая задача, и достаточно вызвать _server.Sites.Remove(site). Однако, недавно возникла проблема при удалении сайта, имеющего https байндинг. Дело в том, что Microsoft.Web.Administration удаляя сайт, удаляет и информацию о байндингах, что логично. При этом, библиотека также удаляет запись в системном кофиге о соответствии IP:port:SSL. Таким образом, если несколько сайтов имеют байндинги, использующие один и тот же сертификат, то при удалении любого из этих сайтов, все остальные теряют связку байндинга и сертификата.

Более свежая библиотека Microsoft.Web.Administration содержит метод для удаления байндинга, принимающий вторым параметром флаг о необходимости удаления записи из системного конфига. Поэтому решение проблемы выглядит следующим образом:

Site site = _server.Sites[name];
if (site == null)
    return;

var bindings = site.Bindings.ToList();
foreach (Binding binding in bindings)
{
    site.Bindings.Remove(binding, true);
}
_server.Sites.Remove(site);

_server.CommitChanges();

Заключение

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

Я привёл основные проблемы, с которыми столкнулся во время работы над модулем, и найденные мной решения. Надеюсь, этот материал будет кому-нибудь полезен. Если у кого-либо есть вопросы или предложения, задавайте – постараюсь ответить.

Автор: Alived

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js