Практическое использование Desired State Configuration для Windows Server 2012 R2

7208d7fbaa264104afaffd5d00351469.png

Администраторам Linux: это статья о “Puppet” для Windows, и уже есть бета-версия DSC для Linux.
Для тех, кто в теме: не будет ничего о новинках PowerShell 5.0, только о том, что доступно из “коробки” Windows Server 2012 R2.

Преамбула


В 2013 году с выходом Windows Server 2012 R2 компания Microsoft сообщила о появлении Powershell Desired State Configuration (DSC).

К этому моменту я более или менее представлял, что делают подобные системы для Linux (например, уже упомянутый Puppet). Поэтому, предложенные возможности мне показались недостаточными для полной автоматической настройки системы. И только недавние сообщения о готовящемся Powershell 5.0 и о новых возможностях DSC побудили меня снова обратить внимание на эту технологию.

Чтобы разобраться, я выдумал задачку попроще:

  • Пусть, есть некий клиент, который хочет самостоятельно установить разработанное нами ASP.NET приложение на своем сервере. Кроме IIS, нам нужен MS SQL Server, а также требуется сделать некоторые настройки операционной системы и установить какие-нибудь важные утилиты.


Можно ли вместо инструкции по установке и настройке выдать некий конфигурационный скрипт, который сделает все что требуется на только что установленном Windows Server 2012 R2?

Для лучшего понимания этой статьи, наверное, предварительно стоит прочесть описание в блоге Microsoft — http://habrahabr.ru/company/microsoft/blog/253497/.

Исходное положение


Первоначально предполагалось, что где-то у хостинг-провайдера был заказан сервер. На нем установлен Windows Server 2012 R2 и нам только что пришло оповещение с паролем администратора и ip-адресом сервера.

А мы набираем одну единственную команду, например:

makemagic -server new.example.com


и спустя какое-то время получаем готовую к употреблению систему.

К сожалению, пока это невозможно. Но у меня есть хорошая новость — это будет уже в следующей версии Windows Server 2016. Пока я пишу эту статью, описанная ниже конфигурация (естественно, без установки обновлений) накатывается на только установленный Technical Preview 3.

Сервер


Если у вас есть образ Windows Server 2012 R2 с интегрированными обновлениями, который вы можете использовать — смело пропускайте этот раздел.

В нынешней версии (2012 R2) — проблема в цепочке обновлений:

  1. Одна из первых задач, которую я попробовал сделать — проверить/настроить часовой пояс. Для этого нужно установить последнее обновление часовых поясов.
  2. Это обновление не устанавливается, так как требует большое обновление KB2919355.
  3. Которое, в свою очередь хочет чтобы было обновление KB2975061 — это в моем случае.


Ни одно из этих обновлений недоступно для установки через Windows Update на только что установленной системе.

Поэтому есть два варианта: 1) установить все обновления через Windows Update, но это будет долго (этот процесс вполне можно выполнить позднее), или 2) поставить только пару самых необходимых.

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

Invoke-WebRequest -Uri http://download.microsoft.com/download/3/9/7/3971FEA1-C483-409E-BF13-219F8A6E907E/Windows8.1-KB2975061-x64.msu -OutFile .\Downloads\Windows8.1-KB2975061-x64.msu
.\Downloads\Windows8.1-KB2975061-x64.msu /quiet /norestart
Invoke-WebRequest -Uri http://download.microsoft.com/download/2/5/6/256CCCFB-5341-4A8D-A277-8A81B21A1E35/Windows8.1-KB2919355-x64.msu -OutFile .\Downloads\Windows8.1-KB2919355-x64.msu
.\Downloads\Windows8.1-KB2919355-x64.msu /quiet /promtrestart


После перезагрузки наш сервер будет готов к приему конфигурации.

Еще раз напоминаю, что Windows Server 2016 TP3 сразу готов к экспериментам с DSC.

Компьютер администратора


Важно: Так некоторые из вас могли пропустить предыдущий раздел: Для тестирования этого примера конфигурации любым доступным способом подключите на сервере образ MS SQL Server 2014, я выбрал Express Edition и подключил как R:.

Должен огорчить тех, кто уже обновился до Windows 10 — есть нюансы, которые не позволят использовать созданные в этой системе конфигурации. Тоже самое относится к тем, кто поставил Powershell 5.0.

Создание и применение конфигураций выполнялось на Windows 8.1. Версия Powershell:

PS C:\Users\nelsh> $PSVersionTable

Name                           Value                                                                                                       
----                           -----                                                                                                       
PSVersion                      4.0                                                                                                         
WSManStackVersion              3.0                                                                                                         
SerializationVersion           1.1.0.1                                                                                                     
CLRVersion                     4.0.30319.34209                                                                                             
BuildVersion                   6.3.9600.17400                                                                                              
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0}                                                                                        
PSRemotingProtocolVersion      2.2                   


Но и этого еще недостаточно. На компьютере администратора необходимо включить Powershell Remoting (на сервере, после установки двух обновлений, PSRemoting уже включен). Это выполняется в консоли Powershell с повышенными привилегиями:

Enable-PSRemoting -Force


Кроме того, нужно разрешить (!) с компьютера администратора подключаться к другим компьютерам.

Set-Item WSMan:\localhost\Client\TrustedHosts -Value *


Для удобства, я также добавил в файл hosts строку c ip-адресом созданного сервера.

<ip-address> cs1.example.com


Исходный код примера доступен на Github: https://github.com/nelsh/DSC-WS2012R2.

Первая конфигурация


Это скриншот первой версии файла DSC-W2012R2.ps1 (в репозитории, для удобства, он лежит под именем DSC-W2012R2-First.ps1).

5111cd86146f4dbcba32e486ef99073b.png

  • 1-21 строки — собственно сама конфигурация, названная DSCW2012R2.
    • В ней на 3-7 строке мы сообщаем, что будет всего один параметр — массив имен серверов.
    • На 8 строке мы подключаем необходимый модуль PowerShell
    • 10-20 строки — список используемых ресурсов в конфигурации. В нашем случае — только один ресурс “Script”, о нем чуть ниже.
  • 23-25 строки — так как одним из шагов будет добавление пользователя, то нам потребуется создать для него пароль. Чтобы не разбираться с шифрованием — разрешим хранить пароли в конфигурации в открытом виде.
  • 27 строка — создание конфигурации. В результате выполнения этого скрипта у нас появится файл DSCW2012R2\cs1.example.com.mof — что-то типа скомпилированной конфигурации.
  • 29-31 строки — запускает применение конфигурации к серверу с именем cs1.example.com. Предварительно появиться стандартное окошко для запроса имени и пароля для доступа к серверу.
  • Начиная с 33 строки — пример для одновременной настройки пары серверов.


Возвращаемся к ресурсам — Powershell DSC имеет 12 встроенных ресурсов. Большинство (а точнее 11 из 12) из них просты и понятны. Но для полноценной настройки системы их явно недостаточно. Microsoft предлагает самостоятельно создавать необходимые ресурсы. Но, честно признаюсь, даже сейчас у меня нет особого желания разбираться с этим.

Однако, при первом знакомстве я не обратил внимание на ресурс Script. Раcсмотрим пример подробнее:

        Script First {
            TestScript = { if ( "Test script content" ) { $true } else { $false } }
            SetScript = { "Set script content" }
            GetScript = { return @{ Result = "Result for GetScript"
                GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
                }
            }


Это самый простой вариант, который ничего не делает. Как он работает:

  • Для применения конфигурации мы вызываем команду Start-DSCConfiguration (строка 29).
  • Когда в процессе обработки списка ресурсов скрипт доходит до ресурса “Script First”, то вначале вызывает код из переменной TestScript. В нашем примере он всегда возвращет $true.
  • А вот если бы он вернул $false, тогда вызвался бы код из переменной SetScript.


В этих переменных может быть любой код на Powershell — масштабы зависят только от наших фантазий. В идеальном варианте, код в TestScipt должен проверять правильность всех настроек, которые выполняются в коде SetScript.

Попробуем запустить нашу первую конфигурацию (по старой привычке я запускаю из Far Manager):

powershell.exe -ExecutionPolicy RemoteSigned .\DSC-WS2012R2.ps1


49b7d59b9dd34d08a168edf104992091.png

Завершено без ошибок.

Чтобы не объяснять как запустить проверку с компьютера администратора, я зашел на наш сервер и выполнил пару команд.

d0991ce1ce8e4dd9bd35e3371f66f069.png

Первая: Test-DSCConfiguration — проверяет конфигурацию. Обратите внимание на последнее сообщение (после желтого текста) — True. Т.е. конфигурация проверена и ошибок не обнаружено.

Следующая команда: Get-DSCConfiguration — сообщает подробности о текущей конфигурации. Достаточно сверить код нашего ресурса “Script First” с этим скриншотом, чтобы понять что и откуда берется.

В этот самый момент я понял, что, пожалуй, все получится.

Итак. Переходим к…

Продвинутая конфигурация


Начинаем добавлять реальные задачи в конфигурацию узлов.

Первое, в чем я хотел бы быть уверен — имя компьютера и основной dns-суффикс. Если в нашей DNS-зоне этот компьютер будет называться cs1.example.com, то имя — cs1, а dns-суффикс — example.com

Начнем с имени. Вначале я написал такой код:

       $shortName = $Server.Split(".")[0].ToLower()
        Script ComputerName {
            SetScript = { Rename-Computer -NewName $shortName }
            GetScript = { return @{ Result = $env:computerName
                GetScript = $GetScript.Trim(); SetScript = $SetScript.Trim(); TestScript = $TestScript.Trim() } }
            TestScript = { $env:computerName.ToLower() -eq $shortName }
        }


Но он не работает. SetScript, GetScript и TestScript ничего не знают о переменных вне своей зоны видимости. Передать можно только используя форматирование строк. Вот так:

       $shortName = $Server.Split(".")[0].ToLower()
        Script ComputerName {
            SetScript = ({
                Rename-Computer -NewName "{0}"
            } -f @($shortName))
            GetScript = { return @{ Result = $env:computerName
                GetScript = $GetScript.Trim(); SetScript = $SetScript.Trim(); TestScript = $TestScript.Trim()
                }
            }
            TestScript = ({ 
                $env:computerName.ToLower() -eq "{0}" 
            } -f @($shortName))
        }


С проверкой dns-суффикса все оказалось проще — это параметр в реестре, поэтому используем стандартный ресурс Registry:

       Registry PrimaryDomainSuffix {
            Key = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\"
            ValueName = "NV Domain"
            Ensure = "Present"
            ValueData = "example.com"
            ValueType = "String"
        }


Чтобы во время экспериментов обновления не ставились автоматически — настроим Windows Update. Только получать уведомления о доступных обновлениях:

       Script WindowsUpdateSettings {
            SetScript = {
                $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
                $WUSettings.NotificationLevel=2
                $WUSettings.IncludeRecommendedUpdates=$true
                $WUSettings.Save()
            }
            GetScript = { return @{ Result = ''
                    GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
                }
            }
            TestScript = { 
                $WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings; $WUSettings.NotificationLevel -eq 2 -and $WUSettings.IncludeRecommendedUpdates -eq $true
            }
        }


Получаем пару важных обновления и все-таки настраиваем часовой пояс


Следующий важный параметр — временная зона. И зачем только я предположил, что московское время подходит не всем? Видимо, для этого:

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

Создаем каталог для утилиты:

       $abcUpdatePath = "C:\UTILS\ABC-Update"
        $abcUpdateZip  = Join-Path $abcUpdatePath "ABC-Update.zip"
        File AbcUpdateDir {
            Ensure          = "present"
            DestinationPath = $abcUpdatePath
            Type            = "Directory"
        }


Скачиваем:

       Script AbcUpdateDownload {
            DependsOn = "[File]AbcUpdateDir"
            SetScript =  ({
                Invoke-WebRequest -Uri http://abc-deploy.com/Files/ABC-Update.zip -OutFile {0}
                } -f @($abcUpdateZip)) 
            GetScript = { 
                return @{ Result = $TestScript
                    GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
                    }
                }
            TestScript = ({
                Test-Path {0} 
                } -f @($abcUpdateZip)) 
        }


Распаковываем, используя встроенный ресурс Archive:

       Archive AbcUpdateUnpack {
            Ensure = "Present"
            DependsOn = "[Script]AbcUpdateDownload"
            Path = $abcUpdateZip
            Destination = $abcUpdatePath
        }


Запускаем, и, кроме обновления часовых поясов, сразу же обновим .NET Framework до версии 4.5.2:

        Script AbcUpdateNet452Install {
            DependsOn = "[Archive]AbcUpdateUnpack"
            SetScript = { C:\UTILS\ABC-Update\ABC-Update.exe /a:install /k:2934520 }
            GetScript = { return @{ Result = if ( Get-HotFix -Id KB2934520 -ErrorAction SilentlyContinue ) { "KB2934520: Installed" } else { "KB2934520: Not Found" }
                GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
                }
            }
            TestScript = { if ( Get-HotFix -Id KB2934520 -ErrorAction SilentlyContinue ) { $true } else { $false } }
        }

        Script AbcUpdateTimeZoneInstall {
            DependsOn = "[Archive]AbcUpdateUnpack"
            SetScript = { C:\UTILS\ABC-Update\ABC-Update.exe /a:install /k:3013410 }
            GetScript = { return @{ Result = if ( Get-HotFix -Id KB3013410 -ErrorAction SilentlyContinue ) { "KB3013410: Installed" } else { "KB3013410: Not Found" }
                GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
                }
            }
            TestScript = { if ( Get-HotFix -Id KB3013410 -ErrorAction SilentlyContinue ) { $true } else { $false } }
        }


Наконец-то мы добрались до часовых поясов. Методом научного тыка я выбрал часовой пояс около озера Байкал.

Я нашел такой вариант: изменить часовой пояс можно только с помощью утилиты командной строки tzutil.exe, а проверить только с помощью Powershell. Но этот случай особенный — при установке используется одно значение «North Asia East Standard Time», а проверяется совершенно другое «Russia TZ 7 Standard Time»:

       Script TimeZoneSettings {
            SetScript = { tzutil.exe /s "North Asia East Standard Time" }
            GetScript = { return @{ Result = [System.TimeZone]::CurrentTimeZone.StandardName
                GetScript = $GetScript.Trim(); SetScript = $SetScript.Trim(); TestScript = $TestScript.Trim()
                }
            }
            TestScript = { [System.TimeZone]::CurrentTimeZone.StandardName -eq "Russia TZ 7 Standard Time" }
        }


Похоже, такая беда со всеми часовыми поясами России.

Компоненты Windows


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

       WindowsFeature offFSSMB1 {
            Ensure = "Absent"
            Name   = "FS-SMB1"
        }
        WindowsFeature WebAspNet45 {
            Ensure = "Present"
            Name   = "Web-Asp-Net45"
            IncludeAllSubFeature = $True
        }


В первом случае компонент удаляется, во втором ставится вместе со всеми зависимостями.

Установка пакетов


На примере Far Manager. Во-первых, пакет нужно скачать уже известным нам способом:

       Script FarDownLoad {
            SetScript = { Invoke-WebRequest -Uri http://www.farmanager.com/files/Far30b4400.x64.20150709.msi -OutFile C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi }
            GetScript = { return @{ Result = Test-Path C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi
                GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
                }
            }
            TestScript = { Test-Path C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi }
        }


В параметрах ресурса Package есть «ProductId». И вроде есть даже программа, которая анализирует msi-файл и сообщает этот самый «ProductId». Я пошел напролом: сразу же попробовал применить конфигурацию без этого параметра и тексте ошибки обнаружил «ProductId», а также правильный «Name». Описание ресурса получилось следующее:

       Package FarInstall {
            Ensure    = "Present"
            DependsOn = "[Script]FarDownLoad"
            Name      = "Far Manager 3 x64"
            ProductId = 'E5512F32-B7C1-48E3-B6AF-E5F962F99ED6'
            Path      = "C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi"
            Arguments = ''
            LogPath   = "C:\Users\Public\Downloads\FarInstall.log"
        }


Пользователи и права


По постановке задачи, сервер находится под управлением заказчика, но тем не менее я допустил, что будет существовать возможность обновления web-приложения с помощью нашего сервера непрерывной интеграции. Мы используем Jenkins CI (кстати, все задачи в нем тоже реализованы на Powershell).

В минимальном варианте нам нужен пользователь Jenkins в группе Users и с правом записи в каталог, где размещается web-приложение. Пусть это будет c:\web.

Пользователь создается таким образом:

       $JenkinsCredential = New-Object System.Management.Automation.PSCredential(`
            "Jenkins", ("Pa`$`$w0rd" | ConvertTo-SecureString -asPlainText -Force)`
        )
        User JenkinsUser {
            UserName = "Jenkins"
            Ensure = "Present"
            Password = $JenkinsCredential
            PasswordChangeNotAllowed = $true
            PasswordNeverExpires = $true
        }


Есть способ использовать в конфигурации зашифрованные пароли, но мы пойдем простым путем. В данном случае будет создан пользователь «Jenkins» с паролем «Pa$$w0rd».

Создание каталога делается уже привычным образом. А вот с назначением прав на каталоги и проверкой пришлось повозится:

       $AccessStringTmpl = "NT AUTHORITY\SYSTEM Allow  FullControl`nBUILTIN\Administrators Allow  FullControl`nBUILTIN\Users Allow  ReadAndExecute, Synchronize`nCS1\Jenkins Allow  Modify, Synchronize"
        File DirDweb {
            Ensure          = "present"
            DestinationPath = "c:\web"
            Type            = "Directory"
        }
        Script AclsDweb
        {
            DependsOn = "[File]DirDweb"
            SetScript = {
                icacls c:\web /reset /t /q
                takeown.exe /f c:\web /r /a /d y
                icacls.exe c:\web /inheritance:r
                icacls.exe c:\web /grant:r "Administrators:(OI)(CI)(F)" "System:(OI)(CI)(F)" "Users:(OI)(CI)(RX)" "Jenkins:(OI)(CI)(M)" /t /q
            }
            GetScript = { return @{ Result = (get-acl c:\web).AccessToString
                GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
                }
            }
            TestScript = ({ (get-acl c:\web).AccessToString -eq "{0}"  
            } -f @($AccessStringTmpl))
        }


Проще все назначить права с помощью icacls.exe. В данном случае выполняется по порядку:

  1. в первой строке: сброс всех прав и включение наследования от родительского каталога
  2. во второй: владельцем назначается встроенная группа Administrators
  3. в третьей: отменяется наследование и удаляются все права
  4. в четвертой: назначение полных прав для Administrators и SYSTEM, чтение для пользователей и изменение для Jenkins.


Для проверки используется метод (get-acl c:\web).AccessToString — полученная строка должна совпадать с переменной $AccessStringTmpl. Кстати, в примере ошибка — в строке явным образом указано имя сервера “CS1” — а должно подставляться значение $Server.Split(".")[0].ToUpper().

MS SQL


Я несколько пожалел, что решил не использовать сторонних модулей. Так как уже есть модуль для установки и настройки MS SQL Server. Но у меня есть конфигурационный файл для автоматической установки и я решил попробовать.

Во-первых, нам потребуется еще один компонент Windows — ресурс “WindowsFeature NetFrameworkCore”:

       WindowsFeature NetFrameworkCore {
            Ensure = "Present"
            Name   = "Net-Framework-Core"
            IncludeAllSubFeature = $True
        }


Во-вторых, конфигурационный файл для установщика — ресурс “Script MSSQLConfigDownLoad”:

        Script MSSQLConfigDownLoad {
            SetScript = { Invoke-WebRequest -Uri https://raw.githubusercontent.com/nelsh/DSC-WS2012R2/master/SQL2014-Setup.ini -OutFile C:\Users\Public\Downloads\SQL2014-Setup.ini }
            GetScript = { return @{ Result = Test-Path C:\Users\Public\Downloads\SQL2014-Setup.ini
                GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
                }
            }
            TestScript = { Test-Path C:\Users\Public\Downloads\SQL2014-Setup.ini }
        }


В-третьих, вы не забыли подключить образ с дистрибутивом какой-нибудь редакции MS SQL Server 2014?

        Script MSSQL {
            SetScript = { r:\setup.exe /configurationfile=C:\Users\Public\Downloads\SQL2014-Setup.ini /SAPWD=1q@w3e }
            GetScript = { return @{ Result = if ( Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue ) { "Servise MSSQLSERVER is exist" } else { "Servise MSSQLSERVER not found" }
                GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
                }
            }
            TestScript = { if ( Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue ) { $true } else { $false } }
        }


Я воспользовался диском с Express Edition, который подключен как R:. В процессе будут установлены Database Engine плюс FullSearch, а также средства администрирования. Проверка в TestScript самая простая — есть сервис MSSQLSERVER или нет.

… И когда по списку процессов на сервере я понял, что установка запустилась — стало понятно, что эксперимент можно считать завершенным.

Дальнейшая настройка может зависеть от ситуации: если требуется срочно-срочно показать рабочее приложение, то дальше каким-то образом получаем и устанавливаем наше приложение.

Если же у нас плановая установка (без авралов), то предварительно можно настроить брандмауэр и установить служебные программы (мониторинг, резервное копирование) — у всех могут быть свои варианты. Из того, что используется у нас, наибольшую трудность может вызвать только AWStats: код в TestScript будет напоминать небольшую программу, но это тоже решаемо.

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

Предварительные итоги


На мой взгляд, DSC можно брать на вооружение, не дожидаясь следующей версии Windows Server.

В доменной инфраструктуре полностью заменить групповые политики эта технология не сможет, но у нее есть определенные плюсы:

  • Может использоваться как в ручном режиме, так и в автоматическом с сервером конфигураций.
  • Может использоваться независимо от наличия Active Directory, а может и вместе с групповыми политиками.
  • Каталог с конфигурациями можно положить в систему контроля версий.
  • С выходом Powershell 5.0 мы получаем удобный способ использования дополнительных модулей — см. powershellgallery.com. Там уже есть несколько десятков модулей, созданных сообществом. Возможно, среди них уже есть такие, которыми можно заменить скрипты из моего примера.


Прошу учесть, что в примере из этой статьи возможны ошибки, а также, вероятно, есть более эффективные решения.

Последнее важное примечание: насколько я понял, вопрос с необходимой перезагрузкой в процессе настройки даже в новых версиях не имеет решения. Поэтому описанная выше конфигурация применяется за два прохода — причем и на 2012R2 и на 2016 — прерывается на установке компонентов и просит перезагрузку. После чего необходимо снова запустить применение конфигурации.

Полезные ссылки


© Habrahabr.ru