Деплой ASP.NET приложений при помощи символических ссылок
Всем привет.
Все мы немного нервничаем при установке нового релиза на прод. Существует много различных технологий позволяющих нам облегчить этот процесс и сделать его чуть менее нервозным. Одна из таких технологий, которую уже довольно давно облюбовали 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 факторах)
- Папка с релизом должна содержать весь необходимый набор библиотек и файлов что бы приложение было полностью работоспособным. Никаких неявных ссылок.
- Ваши релизы должны быть совместимы с внешними ресурсами (например с базой данных). Т.е. если вы внесли изменения в БД, то приложение должно оставаться работоспособным как в новом так и в старом релизе.
Итак, для создания символьной ссылки в 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, а потом создавать заново, но уже с другим путем. Итак, для того что бы переключить ссылку нужно выполнить следующий порядок действий:
- Остановить пул;
- Удалить символьную ссылку;
- Создать ссылку с новым путем;
- Запустить пул.
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.
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!"
}