Всем привет.
Все мы немного нервничаем при установке нового релиза на прод. Существует много различных технологий позволяющих нам облегчить этот процесс и сделать его чуть менее нервозным. Одна из таких технологий, которую уже довольно давно облюбовали UNIX-инженеры, это использование символических ссылок, позволяющая свести к минимуму время накатки релиза и откатки на предыдущий релиз если «что-то пошло не так»(с). Этот механизм так же присутствует и в Windows, однако почему-то активно не используется. А зря. Данная статья призвана поправить это недоразумение и сделать процесс накатки релиза более приятным.


Кадр из х/ф «Джентльмены удачи»

Эту технологию в общем можно применять не только к ASP.NET-приложениям, но и к любым деплоям (например, windows-сервисы). Здесь ASP.NET развернутое на IIS используется как пример, потому что есть несколько граблей на которые пришлось наступить прежде чем эта идея взлетела. С остальными приложениями должно быть попроще.
Для начала опишем в чем собственно заключается проблема. Обычно файлы web-приложения на IIS-е располагаются в папке c:\inetpub\wwwroot\{appName} и каждый раз при деплое (например при помощи msdeploy) мы перезатираем старые файлы новыми. Это прекрасно работает до тех пор пока у нас набор файлов от релиза к релизу не меняется, а меняется только их содержимое. Если же в следующем релизе, например, одна из библиотек заменяется на другую (с другим именем), то при деплое появляется новая библиотека, а старая не удаляется, что зачастую приводит к эпик фейлам. Опять же, если вдруг у вас что-то не заработало и вы захотели быстро откатиться на предыдущий релиз вас может ждать очередной провал, так как не затрутся файлы из нового релиза. Символьные ссылки элегантно решают эту проблему.

Опишем конкретный пример. Создадим на диске C: папку Releases и в нее сложим релизы нашего приложения:
C:\Releases\1.1\WebApp
C:\Releases\1.2\WebApp

А в папке c:\inetpub\wwwroot\ создадим символьную ссылку WebApp, которая будет смотреть на папку c:\Releases\1.1\WebApp и которую в любой момент можно будет переключать на c:\Releases\1.2\WebApp и обратно.

Стоит сразу предупредить, что ваше приложение должно соответствовать нескольким параметрам (которые прекрасно описаны в 12 факторах)
  1. Папка с релизом должна содержать весь необходимый набор библиотек и файлов что бы приложение было полностью работоспособным. Никаких неявных ссылок.
  2. Ваши релизы должны быть совместимы с внешними ресурсами (например с базой данных). Т.е. если вы внесли изменения в БД, то приложение должно оставаться работоспособным как в новом так и в старом релизе.

Итак, для создания символьной ссылки в Windows используется команда mklink. В нашем случае с параметром /D, что говорит о том, что это ссылка на директорию.

mklink /D  c:\inetpub\wwwroot\WebApp c:\Releases\1.1\WebApp

Собственно все. Вы можете убедиться что в папке wwwroot появилась директория WebApp со специальным значком, и если вы кликните дважды на нее то окажитесь в папке релиза 1.1. Осталось превратить эту папку в IIS-е в Web-application, нажав на нее правой кнопкой и выбрав соответствующий пункт. Зайдите в браузер и убедитесь что все работает.
С «переключением» релиза возникают некоторые трудности. Дело в том, что ASP.NET-приложения при первом запуске компилируются и записываются в кэш. В дальнейшем IIS смотрит на изменения в файле web.config и если они происходят, то «пересобирает» приложение. В случае с символьными ссылками IIS не видит этих изменений, поэтому если вы просто переключите символьную ссылку на новую директорию, то ничего не произойдет, будет работать старый релиз. Для того что бы все заработало нужно очистить кэш. Гугл рекомендует делать это командой iisreset, а это приводит к перезапуску сервиса W3SVC, что для нас слишком радикально — мы же не хотим что бы перезапускались другие приложения на этом сервере. Благо IIS позволяет перезапускать не весь сервис целиком а только сам сайт или его пул. Сразу скажу — опытным путем было установлено, что перезапуск сайта почему-то работает крайне нестабильно. Он то пересобирает приложение, то нет, то вообще встает в неведомую раскоряку. А вот перезапуск пула, очень даже хорошо работает.
И еще один нюанс. Команда mklink не позволяет «переключать» ссылку. Поэтому вначале ее нужно удалять командой rmdir, а потом создавать заново но уже с другим путем. Итак, для того что бы переключить ссылку нужно выполнить следующий порядок действий:
  1. Остановить пул;
  2. Удалить символьную ссылку;
  3. Создать ссылку с новым путем;
  4. Запустить пул.


C:\> C:\Windows\System32\inetsrv\appcmd stop apppool DefaultAppPool
C:\> rmdir c:\inetpub\wwwroot\WebApp
C:\> mklink /D c:\inetpub\wwwroot\WebApp c:\Releases\1.2\WebApp
C:\> C:\Windows\System32\inetsrv\appcmd start apppool DefaultAppPool


Такой подход дает еще одну вкусную плюшку. К примеру, на IIS вы можете поднять еще один сайт, допустим на порту 8080. Затем создать для него символьную ссылку с вашим новым релизом. Таким образом у вас будет старый релиз на 80, а новый на 8080. Теперь вы можете в спокойной обстановке проверить работоспособность нового релиза, а затем просто переключить ваш основной сайт (что на 80 порту) на новую папку.

В заключении хочу привести скрипт PowerShell, который автоматизирует данный процесс:
Текст скрипта
param([string]$Release=$null,[string]$AppName=$null,[string]$AppPath="C:\inetpub\wwwroot\",[string]$ReleasesPath="C:\Releases\",[string]$PoolName="DefaultAppPool");
if ($Release)
{
    if($AppName)
    {
        Write-Output "Старт!";
        Write-Output "Останавливаем пул...";
        cmd /c "C:\Windows\System32\inetsrv\appcmd stop apppool $PoolName";        
        Write-Output "Обновление приложения $AppName.";
        $link = $AppPath+$AppName;
        Write-Output "Link: $link";
        $target=$ReleasesPath+$Release+"\"+$AppName;
        Write-Output "Target: $target";
        if(Test-Path $target)
        {
            # если символическая ссылку существует, то удаляем ее
            if (Test-Path $link)
            {
                cmd /c "rmdir $link"
                Write-Output "Удалили директорию $link";
            };
            # Создаем символическую ссылку
            cmd /c "mklink /D $link $target" 
        }
        else
        {
            Write-Error "Не найден путь: $traget";
        }
        Write-Output "Запускаем пул..."
        cmd /c "C:\Windows\System32\inetsrv\appcmd start apppool $PoolName";
        Write-Output "Ok!"          
    }
    else
    {
        Write-Error "Не задан параметр AppName";
    }
}
else
{
    Write-Error "Не задан параметр Release!"
}


В качестве параметров туда передаются:
  • Release — название папки с релизом;
  • AppName — название приложения;
  • AppPath — путь к приложению (где находится символическая ссылка);
  • ReleasesPath — путь где лежат релизы;
  • PoolName — имя пула.


И еще…
PowerShell 5.0 теперь поддерживает работу с символьными ссылками. Так что если вы установите пятую версию и powershell-модуль управления IIS-ом (PS> Import-Module WebAdministration), то можно обойтись вообще без команд cmd, сделав все на чистом powershell.

Текст скрипта Powershell 5.0
param([string]$Release=$null,[string]$AppName=$null,[string]$AppPath="C:\inetpub\wwwroot\",[string]$ReleasesPath="C:\Releases\",[string]$PoolName="DefaultAppPool");
if ($Release)
{
    if($AppName)
    {
        Write-Output "Старт!";
        Write-Output "Останавливаем пул $PoolName...";
        Stop-WebAppPool $PoolName -Passthru;
        Write-Output "Обновляем приложения $AppName.";
        $link = $AppPath+$AppName;
        Write-Output "Link: $link";
        $target=$ReleasesPath+$Release+"\"+$AppName;
        Write-Output "Target: $target";
        if(Test-Path $target)
        {
            #создаем ссылку
            New-Item -ItemType SymbolicLink -Path $AppPath -Name $AppName -Value $target -Force
        }
        else
        {
            Write-Error "Не найден путь: $traget";
        }
        Write-Output "Запускаем пул..."
        Start-Sleep 3; #нужно немного подождать перед запуском :)
        Start-WebAppPool $PoolName -Passthru;
        Write-Output "Ok!"          
    }
    else
    {
        Write-Error "Не задан параметр AppName";
    }
}
else
{
    Write-Error "Не задан параметр Release!"
}

Комментарии (10)


  1. Permyakov
    11.04.2016 15:37

    Если линковать на шару, то могут быть проблемы с доступностью приложения.


    1. lepism
      11.04.2016 16:17

      Если честно на шару линковать не пробовал. Но спасибо за предупреждение.


  1. agorshkov23
    11.04.2016 19:00

    Спасибо, интересный подход, надо взять на вооружение.
    У меня на данный момент сделано следующим образом: есть каталог в Program Files и в нем лежат различные версии сборки (т.е. Site-2.1.10.0, Site-2.2.0.0 и т.п.), и я в IIS (в свойствах сайта) просто меняю путь на новую версию. При таком способе в IIS всё автоматически подхватывается, но есть проблема в том, что если в предыдущей версии были изменения в конфигурационном файле, то их нужно вручную редактировать в новой версии.


    1. darkdaskin
      11.04.2016 21:13

      Тоже делаю так на тестовых серверах. Не вижу у подхода с симлинками особых преимуществ.

      Для любителей консоли всё одной строчкой настраивается.

      PowerShell:
      Import-Module WebAdministration
      Set-ItemProperty 'IIS:\Sites\Default Web Site\MyApp' -Name PhysicalPath -Value 'C:\Dev\MyApp\1.1\'

      То же в обычном cmd:
      %systemroot%\System32\inetsrv\appcmd.exe set vdir "Default Web Site/MyApp/" -physicalPath:"C:\Dev\MyApp\1.1"

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

      Вносить изменения в конфиг можно так же через PowerShell или appcmd.


      1. lepism
        12.04.2016 10:20
        +1

        Не знаю… Переконфигурирование сервера при каждом деплое звучит страшновато.
        А с win-сервисами как быть? Тоже пути переписывать каждый раз?


  1. return_true
    11.04.2016 19:59
    +1

    Очень хрупкое решение, если честно.


    Есть же Octopus Deploy, который скачает артефакт билда, вытащит одну машину из лоад-балансера, удалит текущую версию сайта, распакует новую версию, заменит плейс-холдеры в конфигурационных файлах, запустит сайт и если сайт ответит на простой heals check, то машина вернётся в лоад-балансер.


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


    1. lepism
      12.04.2016 10:25

      Ну во-первых это решение бесплатнее Октопуса.
      Во-вторых для стабильной повторяемости это решение нужно использовать как оконечник для Bamboo, а не само по себе.
      А вообще спасибо, сейчас поставлю Октопус, посмотрю, может правда им лучше.


      1. return_true
        12.04.2016 12:10

        Октопус бесплатен для маленьких команд (а большие зарабатывают достаточно). И да, вам нужно хранилище артефактов, но если у вас уже есть билд-сервер, то добавить ему степ Octopack и публикацию артефакта не очень трудно. Бесплатный ProGet или любой другой in-house сервис подойдёт.


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


        Посмотрите на их дэмо сервис, поиграйте. Оно того стоит.


  1. Lirick
    12.04.2016 12:14

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


  1. adoconnection
    13.04.2016 13:16

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

    Кстати в тему деплоя.
    Два года назад, когда мы страдали при деплое нашего asp.net приложения, я долго смотрел на Octopus, TeamCity, Bamboo и т.п.
    Мне всё не понравилось. Билд серверы были чем-то вроде обертки над командной строкой с планировщиком, а октопус работал с версиями не так, как нам хотелось, интеграция тоже была так себе и не была из коробки. (У нас хитрый кейс, когда на живых серверах одновременно работает сразу несколько версий, как минимум текущая и следующая)

    Было принято волевое решение написать свой CI сервер с деплоем и куртизанками (как бы это адово не звучало).
    Исходники доступны: github.com/adoconnection/AspNetDeploy
    Основные идеи:

    нет скриптам, нет командной строке, все должно быть красиво
    правильный CI это и сборка и публикация в одном флаконе (continuous delivery, если угодно)
    в интерфейсе я должен оперировать проектами, указывая только куда и как их деплоить, а как оно будет собираться и паковаться — проблемы CI сервера
    программисты не должны мешать QA команде, когда дело доходит до публикации на тестовые сервера, именно тестировщики инициируют деплой, тогда, когда им удобно начать тестировать следующую фичу
    не должно получиться случайного деплоя на живой, поэтому релиз запускает старший разработчик, но обязательно с отмашки qa, которые должны зааппрувить один из билдов, как пригодный для заливки на живой.

    Что умеет:

    доставать исходники из SVN
    парсить все проекты солюшена, понимать их тип (WebProject, Tests, ConsolApp, DatabaseProject)
    из проектов собираются пакеты – то, что должно быть опубликовано за один раз. Для каждого типа проекта AspNetDeploy предлагает релевантный вариант публикации: если сайт — как его опубликовать в IIS, как настроить конфиги, если DatabaseProject — как его задеплоить в MSSQL
    много серверов, у каждого одна или несколько ролей, сервера объединяются в среды, а среды связаны между собой. Например test -> staging -> live
    удобные переменные, можно сразу увидеть и отредактировать значение переменной на всех средах
    и все это имеет версионность

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