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

    Вступление


    Некоторое время назад мне была поставлена задача найти оптимальный способ программного управления удалённым 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 сервера.


    Разработка


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

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

    Похожие публикации

    Комментарии 8

      0
      Вы сейчас, по сути, изобрели Puppet/Chef, но только для IIS.
        0
        Да, очень похоже на правду.
          0
          Очень интересно было бы узнать мотивацию.

          Для Puppet есть вот такие штуки:
          forge.puppetlabs.com/simondean/iis
          forge.puppetlabs.com/opentable/iis

          Даже для MSBuild есть вот такая штука:
          github.com/mikefourie/MSBuildExtensionPack/tree/master/Solutions/Main/IIS7

          Был какой-то смысл своё делать?
            0
            Да, системы хорошие. Я посмотрю более подробно позже. Тогда смогу дать детальный ответ.

            В то же время стоит отметить, что на первый взгляд — это такие же обёртки для конкретных операций. Т.е. точно так же нужно было бы их встраивать в систему, поскольку там методы, которые выполняют атомарные операции на подобии Microsoft.Web.Administration. При этом последняя — это инструмент, используемый IIS Manager'ом.

            Думаю, в последней версии статьи убрана информация о том, что управление исключительно кодом из C#.
          0
          Я возможно поздно пишу, но блин, рецепт простой.
          Погуглите про powershell и модуль WebAdminisration, там всё на раз делается

          technet.microsoft.com/en-us/library/ee790599.aspx
            0
            Думаю, повторюсь, все эти модули выполняют атомарные операции с IIS. Т.е. всё то же самое, что Microsoft.Web.Administration. Но при этом сложнее встраиваются в код C# (если вообще встраиваются). При этом MWA развивается вместе с IIS (поскольку IIS Manager использует эту библиотеку), что даёт дополнительное преимущество по стабильности, позволяет реализовывать все возможности IIS Manager из кода C#.
              0
              Думаю, для решения задач администрирования и управления инфраструктурой язык C# плохо подходит.
              Убеждён, что язык powershell был изобретён в Microsoft именно для решения таких задач, как раз потому что C# плохо подходит.

              Там есть много встроенных вещей которые облегчают жизнь — powershell remote management, desired state configuration, например.

              Я сам разработчик на C#, но я убеждён что пихать C# везде не надо, есть задачи где powershell справляется лучше.
              И ваша задача как раз из таких.
                0
                Суть дела в том, что этот модуль используется внутри сайта управления пользователями и позволяет создавать для пользователей нашего заказчика сайты, разворачивая IIS сайты, базы данных, выгружая и обновляя необходимые файлы сайтов, пользовательских настроек, изменяет hosts файл и многое другое. При этом работу по разворачиванию сайта для конечного пользователя делают люди далёкие от разработки (и уж тем более консольных приложений). Поэтому вариант использования подобных инструментов отпадал. Нужен был сайт, который может многое, включая выполнение IIS операций.

                Поэтому возможности отказа от C# не было.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое