Стоит ли переходить с Powershell DSC на Ansible и как это сделать

Об IaC под Windows пишут мало, потому что DevOps/SRE ассоциируется в основном c Linux и Kubernetes. Мы решили исправить эту ситуацию и сравнить инструменты, которыми можно управлять IaC на базе Windows. Статья будет полезна разработчикам, которые работают с Windows-инфраструктурой и выбирают способы управления, и тем, кто уже внедрил Powershell DSC или Ansible, но сомневается в своем решении. Ниже поделимся опытом и расскажем:
  • как устроен Powershell DSC и чем он отличается от Ansible при управлении инфраструктурой на Windows;
  • почему мы перешли на Ansible;
  • с какими проблемами столкнулись и как их решали;
  • как соотносятся ожидания и реальность после перехода на Ansible;
  • кому стоит выбрать Powershell DSC, а кому — Ansible.

Почему изначально выбрали PowerShell DSC

В Mindbox развита культура DevOps/SRE, несмотря на преимущественно Windows-инфраструктуру: Hyper-V, IIS, MS SQL Server. И хотя компания постепенно переходит на Linux и Open Source, Windows пока превалирует.
Чтобы управлять этой инфраструктурой, планировали использовать инфраструктурный код: писать его, сохранять в репозиторий, а затем с помощью какого-то инструмента превращать код в реальную инфраструктуру. В то время как Ansible — самый популярный инструмент управления инфраструктурой через код, он все-таки традиционно ассоциируется с Linux-миром. Нам хотелось что-то нативное и заточенное под Windows, поэтому выбрали PowerShell DSC.

Как работает Powershell DSC

PowerShell Desired State Configuration (DSC) — это сервис, который уже есть в Windows из коробки и помогает управлять инфраструктурой через конфигурационные файлы. На вход он принимает инфраструктурный код на PowerShell, а внутри преобразует его в команды, которые конфигурируют систему. Кроме тривиальных операций, например установки Windows-компонентов, изменения ключей реестра, создания файлов или конфигурирования служб, он умеет многое из того, что обычно выполняется PowerShell-скриптами. Например, полный цикл настройки DNS или высокодоступный инстанс MS SQL Server.

Устройство DSC

Полезные ссылки к схеме:

Чем DSC отличается от Ansible

Критерий
DSC
Ansible
Архитектура
Служба на каждом управляемом хосте. В случае pull-модели, отдельный управляющий хост и база данных, пререквизиты в виде .NET Framework 4.0 и WMF 5.1
Несколько исполняемых файлов, например ansible, ansible-playbook и ansible-inventory. Запускается с любого Linux-хоста, пререквизит у управляемых хостов один — python
Хранение состояния хостов
Можно хранить в базе данных
Нет
Кроссплатформен­ность
Да
Да, включая управление сетевыми устройствами
Pull/push-режимы
Pull и push
Только push
Устранение дрифта конфигурации
Есть в pull-режиме
Нет
Декларативность
Декларативно-процедурный: важна последовательность выполнения тасков, можно писать недекларативные конструкции в любом месте, а значит, больше шансов написать запутанный код
Декларативно-процедурный: важна последовательность выполнения тасков, при этом невозможно написать скриптовый код, не обернув в таск
Аудитория
~1300 ресурсов в Gallery
~20000 ролей в Ansible Galaxy
Используемый язык
PowerShell
YAML
Инвентаризация
Да, по удобству проигрывает Ansible
Да
Единица распространения
Ресурс (модуль)
Роль

Проблемы, которые возникли с DSC

Ожидания от DSC оправдались не во всём. Кроме этого, во время работы возникли новые потребности, которые не могли удовлетворить с помощью DSC.
Разработчики не могут использовать инструмент самостоятельно без помощи SRE. Хотя почти в каждой команде есть SRE, инструмент IaC должен быть достаточно простым, чтобы разработчик мог им пользоваться и тратить на это не больше получаса. DSC позволяет использовать не только декларативный код, но и любые конструкции на Powershell. Это значит, что высок шанс сделать код, который будет сложно сопровождать или который приведет к инфраструктурной аварии. Например, развертывание приложения с некорректными параметрами не в той среде.
Невозможно пропустить конфигурацию в режиме dry run перед прокаткой, чтобы увидеть, какие именно изменения будут применены, а какие — нет.
Для DSC трудно организовать синтаксические проверки и проверки стиля кода. Инструментов для проверки мало, и они не обновляются. Для Ansible мы это уже сделали.
В push-режиме DSC нет удобного способа отслеживать состояние тасков. В случае если конфигурация применилась с ошибкой, для диагностики следует совершать дополнительные действия: выполнять команды, чтобы получить статус применения конфигурации, смотреть журналы событий. Если ошибка произошла на нескольких серверах, то это отнимает много времени.
Pull-режим так и не стал преимуществом. В нем конфигурация применяется асинхронно — узнать, когда точно закончено применение новой конфигурации, невозможно без обвязок и костылей.
Избыточное использование двух отличных друг от друга инструментов IaC, которые конфигурируют серверы. Ansible может делать то же, что и DSC, а ведь мы уже используем Ansible для конфигурирования Linux-хостов и сетевого оборудования.

Как планировали перейти с DSC на Ansible

Сначала задача казалась простой, приблизительно на месяц. Мы выделили три этапа работ:
  • научиться подключаться к Windows-хостам с помощью Ansible;
  • переписать конфигурации DSC с помощью Ansible-модулей;
  • удалить DSC pull server, его базу данных и прочие артефакты.
Вот какой рабочий процесс был на DSC, и как планировали организовать в Ansible:
Как было на DSC
Что хотели получить на Ansible
Весь код DSC хранился в одном из наших закрытых репозиториев на Github вместе с ad-hoc скриптами для администрирования. Серверы были разделены на типы, например: Hyper-V, IIS, Influx. Для каждого типа — отдельная конфигурация DSC. Также существовала общая для всех типов конфигурация Standard.
Отдельный репозиторий, где хранятся:
— плейбуки, которые содержат специфические пре- или пост-таски, вызовы ролей с фиксированной версией роли;
— файлы инвентаризации, которые содержат управляемые хосты, иерархия их групп, а также переменные для групп или хостов.
Для каждой роли — отдельный репозиторий, где есть проверка синтаксиса, линтинг и тесты. Выполнение происходит через GitHub Actions.
Пайплайн, который отправлял конфигурации на pull server. Проводить тестирование было сложно, поэтому от него отказались. Конфигурация стягивалась с pull server управляемыми серверами в течение неопределенного времени, до 30 минут.
Пайплайн, который производит линтинг и проверку синтаксиса, а также публикацию релиза в нашу CD-систему (Octopus Deploy).
Отдельная CD-система нужна, потому что код храним в GitHub, а работать он должен внутри периметра нашей сети. Для этого через Github Actions публикуем релиз в Octopus Deploy и через него запускаем раскатку командой ansible-playbook.
Мы сами написали несколько модулей DSC. Они билдились CI-системой и публиковались в NuGet feed.
Переиспользовать самописные DSC-модули в коде Ansible.
При появлении нового сервера добавляли его в файл с данными о нодах, прописывали ему среду и площадку. От среды и площадки зависели некоторые параметры конфигурации — так мы обеспечивали гибкость настроек. Хранились они в виде многоуровневой хэш-карты в отдельном файле. При этом часть переменных объявлялась прямо «на ходу» перед вызовом ресурса.
Использовать инвентаризацию Ansible.
Обращение с данными DSC очень вольное: переменные там можно определять, задавать, ссылаться на них в любом месте. Это обязывает самостоятельно задавать логику обращения с переменными и следить за её единообразием во всём коде, а это сложно.
В Ansible заложена своя строгая логика обращения с переменными. Мы всегда знаем, где можно объявить переменную и что будет, если одну и ту же переменную определить в двух местах по-разному.
Хотя был настроен pull server, мы не хотели ждать неопределенное время, пока pull-конфигурация начнет применяться — поэтому сами пушили конфигурацию на сервер.
Всегда сами запускаем деплоймент Ansible-кода из CD-системы. В этой системе есть история запусков и изменений, а также возможность увидеть будущий результат перед применением.
Репозиторий DSC
Репозиторий Ansible
Картинка
Configurations — конфигурации DSC для каждого типа серверов.
ConfigFiles — конфигурации ПО, которое разворачивается с помощью DSC.
BuildSteps — шаги для билда кастомных DSC-модулей.
Modules — код кастомных DSC-модулей.
Картинка
Inventories — файлы с перечнями серверов, их групп и переменных.
Playbooks — плейбуки, которые вызывают роли, и файлы requirements.yml со списками требуемых ролей и их версии. Каждая роль вынесена в отдельный репозиторий. Структура ролей стандартная.
На Ansible мы планировали отделить сложный код, который что-то конфигурирует и устанавливает, в код ролей и разнести роли по отдельным репозиториям. В главном репозитории Ansible должны были остаться только вызовы ролей, переопределения параметров ролей и списки серверов по группам. Так не только SRE, но и любой разработчик мог бы развернуть роль на нужные серверы или подправить параметр, не углубляясь в логику инфраструктурного кода. Исправить же код роли разработчик сможет только после ревью SRE.

С какими сложностями столкнулись при переходе на Ansible и как их решали

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

WinRM или SSH

Первый сюрприз состоял в выборе типа подключения. В случае Windows их два — WinRM и SSH. Оказалось, что Ansible медленно работает через WinRM. При этом Ansible не рекомендует использовать OpenSSH из коробки для Windows Server 2019. И мы нашли новое решение:
  1. Форкнули и переделали под себя роль из Galaxy.
  2. Написали плейбук, в котором есть только вызов этой роли. Это единственный плейбук, при котором идет подключение к хостам по WinRM.
  3. Стандартными средствами Prometheus Blackbox Exporter сделали мониторинг порта 22/tcp и версии OpenSSH.
- alert: SSHPortDown
  expr: probe_success{job=~".*-servers-ssh",instance!~".*domain..ru"} == 0
  for: 1d
  annotations:
   summary: "Cannot reach {{`{{ $labels.instance }}`}} with SSH"
  1. Выбрали и настроили LDAP-плагин для инвентаризации, чтобы не вписывать вручную все Windows-серверы из домена в статическую инвентаризацию.
plugin: ldap_inventory
domain: 'ldaps://domain:636'
search_ou: "DC=domain,DC=ru"
ldap_filter: "(&(objectCategory=computer)(operatingSystem=*server*)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
validate_certs: False
exclude_hosts:
- db-
account_age: 15
fqdn_format: True
  1. Развернули везде OpenSSH с нужными ключами и убедились, что ни одного алерта о недоступности Windows-серверов по SSH больше нет.
  2. Чуть позже интегрировали установку OpenSSH в стандартный образ. Наши образы готовятся с помощью Packer, который также умеет вызывать Ansible.
"type": "shell-local",
"tempfile_extension": ".ps1",
"execute_command": ["powershell.exe", "{{.Vars}} {{.Script}}"],
"env_var_format": "$env:%s=\"%s\"; ",
"environment_vars": [
"packer_directory={{ pwd }}",
"ldap_machine_name={{user `ldap_machine_name`}}",
"ldap_username={{user `ldap_username`}}",
"ldap_password={{user `ldap_password`}}",
"ansible_playbooks={{user `ansible_playbooks`}}",
"github_token={{user `github_token`}}"
],
"script": "./scripts/run-ansiblewithdocker.ps1"

Рефакторинг

Когда мы переписывали код под Ansible, то периодически натыкались на дублирование кода. Например, почти все конфигурации DSC содержали установку windows_exporter. Единственное, что отличалось — это коллекторы, которые экспортер должен был использовать.
Картинка
Чтобы избавиться от дублированного кода, вынесли windows_exporter в отдельную Ansible-роль, а параметры этой установки — в переменные групп хостов.

Second hop authentication

Наверное, second hop authentication — самая распространенная проблема, с которой сталкиваются те, кто начал использовать Ansible под Windows.
- name: Custom modules loaded into module directory
 win_copy:
 src: '\\share\dsc\modules'
 dest: 'C:\Program Files\WindowsPowerShell\Modules'
 remote_src: yes
Такая конструкция вызывает ошибку Access Denied из-за того, что по умолчанию делегировать учетные данные для авторизации на удаленном ресурсе невозможно без дополнительных настроек. Обойти ошибку помогает, например, new_credentials. Но мы предпочли воспользоваться тем, что Ansible умеет вызывать ресурсы DSC через модуль win_dsc. Вызываем DSC-ресурс File, который по умолчанию выполняется под учетной записью компьютера. Делегация Kerberos в этом случае не нужна:
- name: Custom modules loaded into module directory
 win_dsc:
  resource_name: File
  SourcePath: '\\share\dsc\modules'
  DestinationPath: 'C:\Program Files\WindowsPowerShell\Modules'
  Type: Directory
  Recurse: true
  Force: true
  MatchSource: true
При этом нет противоречия в том, чтобы отказываться от DSC, но использовать его ресурсы, если они лучше решают задачу, чем модуль Ansible. Главная цель — прекратить использовать DSC-конфигурации, потому что нас не устраивала именно экосистема DSC, а не сами ресурсы. Например, если нужно создать виртуальный свитч Hyper-V, то придется использовать ресурс DSC — в Ansible пока нет средств по управлению конфигурацией Hyper-V.

Сетевой дисконнект

Некоторые задачи вызывают отключение сети (дисконнект) на конфигурируемых серверах. Например, создание виртуального свитча Hyper-V из примера выше:
- name: External switch present
 win_dsc:
  resource_name: xVMSwitch
  Ensure: 'Present'
  Name: 'Virtual Network'
  Type: 'External'
  NetAdapterName: 'TEAM_LAN'
  AllowManagementOS: True
Проблема в том, что в DSC такой вызов работает, а в Ansible завершается с ошибкой, так как управляемый хост дисконнектнул. Это происходит потому, что Windows всегда дисконнектит при создании виртуального экстернал-свитча. Решение — добавить к таску аргумент async.
async: 10
Так Ansible отправляет таск на хост, ждет заданное время и только потом запрашивает состояние.

Дрифт инфраструктуры

Когда мы стали переносить код, обнаружили дрифт конфигураций. Это фактические различия между тем, что описано в коде, и реальной конфигурацией сервера или ПО. Причина в том, что в некоторых случаях DSC выполнял только часть работы, а остальное делали либо скриптами, либо вручную по инструкции.
Чтобы облегчить работу с IaC, мы собрали все скрипты и документы и сделали единые однозначные инструкции. Кроме этого, организовали процесс так, чтобы никто не внес случайные изменения в Ansible. Мы храним весь инфраструктурный код в GitHub, а задачи инженерам ставим через GitHub Projects, поэтому у нас есть возможность связывать изменения инфраструктурного кода (pull requests) с задачами. Так мы можем посмотреть изменения по каждой выполненной задаче. Если у задачи не будет изменений, то её не примут и вернут на доработку.

Баги сбора фактов

В отличие от DSC, Ansible при запуске собирает факты об управляемых хостах, чтобы у разработчика была возможность определить поведение тасков в зависимости от состояния хоста. При сборе фактов с Windows-хостов Ansible может выдавать ошибку, из-за некорректного кода модуля. Чтобы её исправить, нужно подключить коллекцию ansible.windows.
[WARNING]: Error when collecting bios facts: New-Object : Exception calling ".ctor" with "0" argument(s): "Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index" At line:2
char:21 + ... $bios = New-Object -TypeName
Пайплайн для Ansible перед запуском каждого плейбука проверяет наличие файлов requirements.yml со списком необходимых ролей и коллекций, а затем устанавливает их. Туда мы и добавили коллекцию ansible.windows.
Коллекции — это новый концепт развития Ansible. Если раньше в Galaxy распространялись только роли, то теперь там можно найти подборки различных плагинов и модулей, плейбуков и ролей.

Тесты

Прежде чем передать IaC-инструментарий разработчикам, мы хотели быть уверенными, что Ansible-код будет надежным и ничего не сломает. В случае с DSC никаких специальных тестов не было, хотя существует специальный фреймворк для этой задачи. Конфигурации обычно валидировались на стейджинг-серверах, поломка которых не приводила к дефектам.
Для тестирования Ansible обычно используют инструмент molecule как обертку для запуска тестов. Это удобный инструмент для Linux-ролей, но в случае с Windows есть проблема. Раньше molecule умела поднимать инфраструктуру сама, но сейчас разработчики убрали такую возможность. Теперь инфраструктура поднимается либо с помощью molecule в Docker, либо вне molecule. Протестировать Windows-роли в Docker чаще всего невозможно: Hyper-V и большинство других Windows-фич в Docker-контейнере не установятся. Придется разворачивать инфраструктуру под тесты вне molecule и использовать delegated driver в molecule.
Эту задачу мы еще не решили, но нашли инструменты, которые обнаружат самые очевидные ошибки:
Проверка
Функционал
Комментарий
Проверяет синтаксис и возможность запуска кода
Используем синтаксическую проверку и линтинг локально и в репозитории. Например, встраиваем в pre-commit check и настраиваем GitHub Action, который будет запускаться при каждом pull request
Проверяет код на логические ошибки
Позволяет до запуска плейбука узнать, что он сделает
Используем в пайплайне раскатки кода: запускаем ansible-playbook с флагами check и diff, затем оцениваем изменения и подтверждаем раскатку. Когда пишем роли, учитываем, что для некоторых тасков необходимо явно указывать, что именно они должны поменять. Например, win_command и win_shell

Как устроена работа с Ansible

После того как мы внедрили Ansible и преодолели все сложности, сформировался процесс действий инженеров и автоматических запусков:
  1. Инженер пишет код роли и тесты к ней, если это роль для Linux-серверов. Как только инженер решит, что роль готова, он делает pull request в отдельный бранч в GitHub-репозитории, созданном специально для роли.
  2. При создании pull request автоматически запускается воркфлоу GitHub Actions, который выполняет синтаксическую проверку и линтинг роли. Если это Linux-роль, то запускаются еще и тесты. Инженер проверяет, что всё хорошо, и при необходимости исправляет.
  3. Другой инженер делает ревью кода из pull request. После того как автор роли исправляет все замечания, код вливается в мастер-бранч, а версия роли автоматически повышается.
  4. Теперь нужно развернуть новую версию роли. Версии перечислены в специальных файлах requirements.yml, которые лежат в GitHub-репозитории с плейбуками. Для каждого плейбука — отдельный такой файл. Автор роли изменяет версию в таком файле. Если нужно развернуть роль на серверы, которых нет в инвентаризации Ansible, автор дополняет инвентаризацию. Потом автор снова создает pull request, но уже в репозиторий с плейбуками.
  5. После подтверждения pull request снова запускается GitHub Actions, который создает новый релиз в Octopus Deploy. Роль готова к развертыванию.
  6. Инженер заходит в Octopus Deploy и запускает развертывание. Процесс развертывания позволяет инженеру ограничить теги и хосты, а также переопределить переменные аналогично опциям команды ansible-playbook: --tags, --limit и --extra-vars.
  7. Процесс развертывания сначала запускает режим проверки, который показывает, какие изменения будут сделаны. Инженер оценивает результат проверки и либо подтверждает развертывание кода на целевую инфраструктуру, либо сначала устраняет обнаруженные недостатки.

Организация работы с Ansible

Что выбрать: DSC или Ansible

Перейти с DSC на Ansible
Если важно:
— отслеживать состояние тасков;
— иметь возможность пропустить конфигурацию в режиме dry run перед прокаткой;
— модифицировать инфраструктурный код;
— делать синтаксические и логические проверки.
Если Linux-хосты или сетевое оборудование уже управляются с помощью Ansible.
Если не боитесь работать с Linux, потому что Ansible нужно централизованно запускать на Linux, будь то агент CI/CD системы или Docker-контейнер.
Внедрить с нуля или остаться на DSC
Если инфраструктура только на Windows и вы не хотите работать с Linux.
Если готовы дописывать свои ресурсы для DSC.
Нужно хранить состояние инфраструктуры, а также исправлять её дрифт.
Внедрить с нуля Ansible
Если управляете смешанной Windows/Linux средой и хотите переделать существующие скрипты в инфраструктурный код и разворачивать его с помощью CI/CD систем.