Вступление
Привет всем!
Это мой первый пост на Хабре. Идея написать статью пришла от коллеги, и я решил: почему бы не попробовать.
Во‑первых, это хороший способ собрать собственный опыт в одном месте и разложить его по полочкам. Во‑вторых, возможно, я получу обратную связь от людей, которые решали похожие задачи. Ну и, в‑третьих, кому‑то этот подход может пригодиться в текущих рабочих условиях.
Коротко о том, чем я занимаюсь:
создаю инфраструктуру мониторинга: Prometheus, Grafana;
деплою новые сервисы на серверы: Ubuntu, Fedora, Windows Server 2008 R2;
работаю с удаленными точками на Windows 10;
автоматизирую задачи на Python, Ansible, PowerShell, BAT и Bash;
разбираю инциденты в сервисах;
администрирую MySQL / PostgreSQL
О работе
Мы небольшая компания с распределенной сетью торговых точек. На каждой точке обычно есть одна касса и один ПК. Где‑то стоит MikroTik, но на большинстве точек используется мобильная SIM‑карта в роутере или USB‑модем.
Автоматизация задач начиналась ещё до моего прихода. Основной подход строился вокруг заранее подготовленных образов.
Как было раньше
Подготавливается образ, в котором обязательно закладывается выполнение батника с любым содержанием, но который соответствует определенному имени.
Для задач подготавливается код на bat или python
Осуществляется доставка посредством двух инструментов. Это достаточно хорошее решение в текущих условиях. Для самой доставки выбран Syncthing, основной плюс этого сервиса, в том, что даже если интернет начнут глушить или он просто пропадет по любым причинам, загрузка при следующем выходе на связь продолжится с того же места, на котором была прервана. Как это работает, можно прочитать тут. Повторяться не буду.
Раскидка по папкам может осуществляться, по сути, из чего угодно, но у нас используется инструмент из 1С
На самой точке уже, соответственно, запускается целевой батник, который может запустить что угодно и на что хватит фантазии автора.
Главная проблема такого подхода, на мой взгляд, в том, что для сложных задач приходится писать слишком много кода на BAT, PowerShell или Python.
Когда я сталкивался с такими задачами, я старался выдерживать структуру и модульность: разбивать код на логические блоки, добавлять проверки, логирование и повторно используемые фрагменты. Это позволяло в дальнейшем брать готовые куски и переносить их в другие скрипты.
Но со временем стало понятно, что хочется более современного и управляемого подхода. Хотелось добавить IaC‑подход, и первым кандидатом стал Ansible.
Active Directory в нашем случае использовать нельзя: добавить его в текущую инфраструктуру мы не можем, поэтому этот вариант я даже не рассматривал.
Ниже пример BAT‑файла для распаковки архива.
:: ================================================================= :: distributor.cmd — проверка/ожидание архива + тест + распаковка :: Устойчив для запуска из другого .bat и из планировщика :: ================================================================= @echo off setlocal EnableExtensions EnableDelayedExpansion :: ================================================================= :: НАСТРОЙКИ | КОНСТАНТЫ :: ================================================================= set "LOG_DIR=C:\sys\sync\logs" set "LOG_FILE=%LOG_DIR%\distributor_%ComputerName%.log" set "WINRAR_PATH=C:\sys\sync\rar.exe" set "SOURCE_ARCHIVE=C:\sys\sync\distributor.rar" set "DESTINATION_PATH=C:\sys\sync\TS_Piot" :: OLD FILES TO DELETE set "REGIME_MSI=regime*" set "ESMZIP=esmpack*" set "REMOVE_ARCHIVE_AFTER_UNPACK=1" set "SIGNAL_OK=C:\sys\sync\distributor.ok" set "SIGNAL_ERR=C:\sys\sync\distributor.err" set "MAX_RETRIES=5" set "RETRY_DELAY=180" set "WINRAR_LOG=%LOG_DIR%\winrar_%ComputerName%.log" :: ================================================================= :: ПОДГОТОВКА :: ================================================================= if not exist "%LOG_DIR%" mkdir "%LOG_DIR%" >nul 2>&1 call :LOG "-------------------- Script Started --------------------" if exist "%SIGNAL_OK%" del /q "%SIGNAL_OK%" >nul 2>&1 if exist "%SIGNAL_ERR%" del /q "%SIGNAL_ERR%" >nul 2>&1 if not exist "%WINRAR_PATH%" ( call :LOG "[ERROR] WinRAR not found at: %WINRAR_PATH%" call :MAKE_ERR_SIGNAL endlocal exit /b 1 ) call :LOG "[INFO] WinRAR found successfully: %WINRAR_PATH%" if not exist "%DESTINATION_PATH%" ( call :LOG "[ERROR] Destination path not found: %DESTINATION_PATH%" call :LOG "[INFO] Creating folder..." mkdir "%DESTINATION_PATH%" if exist %DESTINATION_PATH% ( call :LOG "[INFO] Destination folder was successfully created at path: %DESTINATION_PATH%" ) else ( call :LOG "[ERROR] Destination folder not created at path: %DESTINATION_PATH%" call :MAKE_ERR_SIGNAL endlocal exit /b 1 ) ) else ( call :DELETE_OLD_FILES ) call :LOG "[INFO] Destination path OK: %DESTINATION_PATH%" :: ================================================================= :: ОСНОВНОЙ СКРИПТ :: ================================================================= call :TEST_ARCHIVE if errorlevel 1 ( call :LOG "[ERROR] Archive test stage failed." call :repete endlocal exit /b 1 ) call :UNPACK_ARCHIVE if errorlevel 1 ( call :LOG "[ERROR] Unpack stage failed." call :MAKE_ERR_SIGNAL endlocal exit /b 1 ) :: ================================================================= :: УСПЕШНОЕ ЗАВЕРШЕНИЕ :: ================================================================= type nul > "%SIGNAL_OK%" call :LOG "-------------------- Script Finished Successfully --------------------" endlocal goto :EOF :: ================================================================= :: ФУНКЦИИ :: ================================================================= :TEST_ARCHIVE set /a attempt=1 call :LOG "[INFO] Starting archive test..." :RETRY_LOOP if not exist "%SOURCE_ARCHIVE%" ( call :LOG "[WARNING] Archive not found, waiting... (Attempt !attempt!/%MAX_RETRIES%)" timeout /t %RETRY_DELAY% /nobreak >nul set /a attempt+=1 if !attempt! leq %MAX_RETRIES% goto :RETRY_LOOP call :LOG "[ERROR] Archive file not found after %MAX_RETRIES% attempts!" exit /b 1 ) :: --- Проверка, не докачивается ли архив (размер стабилен) --- for %%A in ("%SOURCE_ARCHIVE%") do set "SIZE1=%%~zA" timeout /t 10 /nobreak >nul for %%A in ("%SOURCE_ARCHIVE%") do set "SIZE2=%%~zA" if "!SIZE1!" neq "!SIZE2!" ( call :LOG "[WARNING] Archive size is changing (!SIZE1! -> !SIZE2!), retrying... (Attempt !attempt!/%MAX_RETRIES%)" timeout /t %RETRY_DELAY% /nobreak >nul set /a attempt+=1 if !attempt! leq %MAX_RETRIES% goto :RETRY_LOOP call :LOG "[ERROR] Archive copy did not stabilize after %MAX_RETRIES% attempts!" exit /b 1 ) :: --- Тест архива: ВАЖНО — без START, чтобы получить правильный errorlevel --- "%WINRAR_PATH%" t -inul "%SOURCE_ARCHIVE%" >> "%WINRAR_LOG%" 2>&1 if !errorlevel! equ 0 ( call :LOG "[INFO] Archive test completed successfully (Attempt !attempt!/%MAX_RETRIES%)" exit /b 0 ) call :LOG "[WARNING] Archive test failed (errorlevel=!errorlevel!), retrying... (Attempt !attempt!/%MAX_RETRIES%)" timeout /t %RETRY_DELAY% /nobreak >nul set /a attempt+=1 if !attempt! leq %MAX_RETRIES% goto :RETRY_LOOP call :LOG "[ERROR] Archive test failed after %MAX_RETRIES% attempts! See: %WINRAR_LOG%" exit /b 1 :DELETE_OLD_FILES call :LOG "[INFO] Starting delete old files..." IF EXIST "%DESTINATION_PATH%\%REGIME_MSI%" ( call :LOG "[INFO] Trying to delete %DESTINATION_PATH%\%REGIME_MSI%" del /F /Q "%DESTINATION_PATH%\%REGIME_MSI%" ) IF EXIST "%DESTINATION_PATH%\%ESMZIP%" ( call :LOG "[INFO] Trying to delete %DESTINATION_PATH%\%ESMZIP%" del /F /Q "%DESTINATION_PATH%\%ESMZIP%" ) exit /b 0 :UNPACK_ARCHIVE call :LOG "[INFO] Starting unpack..." :: Распаковка (без пароля). Лог пишем в файл. "%WINRAR_PATH%" x -o+ -y -inul -ilog"%WINRAR_LOG%" "%SOURCE_ARCHIVE%" "%DESTINATION_PATH%" >nul 2>&1 if %errorlevel% EQU 0 ( call :LOG "[INFO] Unpacked successfully." if "%REMOVE_ARCHIVE_AFTER_UNPACK%"=="1" ( del /q "%SOURCE_ARCHIVE%" >nul 2>&1 if errorlevel 1 ( call :LOG "[WARN] Could not delete archive: %SOURCE_ARCHIVE%" ) else ( call :LOG "[INFO] Source archive deleted: %SOURCE_ARCHIVE%" ) ) exit /b 0 ) call :LOG "[ERROR] Unpacking FAILED (errorlevel=%errorlevel%). Check: %WINRAR_LOG%" exit /b 1 :LOG echo [%DATE% %TIME%] %~1>> "%LOG_FILE%" exit /b 0 :repete call :LOG "[WARNING] Repete func was called. Repeating replace licenses process..." copy /y "C:\sys\sync\automation.run.cmd" "C:\sys\sync\automation.cmd" >nul 2>&1 if exist "C:\sys\sync\automation.cmd.copy.lock" del /q "C:\sys\sync\automation.cmd.copy.lock" >nul 2>&1 call :MAKE_ERR_SIGNAL exit /b 0 :MAKE_ERR_SIGNAL type nul > "%SIGNAL_ERR%" call :LOG "[ERROR] Distributor process failed!" call :LOG "-------------------- Script Finished with Errors --------------------" exit /b 0
Этот скрипт можно было бы сократить почти в два раза, если убрать проверки и логирование. Но тогда, как мне кажется, он стал бы менее удобным для поддержки и повторного использования.
Логи были важны, потому что окружение на разных точках могло отличаться. Например, кто‑то из выездных специалистов мог что‑то удалить, не установить или настроить не до конца. В итоге точка формально есть, но работает неполноценно. Логи и сигнальный файл в таком случае дают более понятную картину происходящего.
И та же самая задача, только написанная через Ansible
--- - name: Ensure destination directory exists ansible.windows.win_file: path: "{{ distribute_ts_piot_destination_dir }}" state: directory - name: Ensure archive exists ansible.windows.win_stat: path: "{{ distribute_ts_piot_archive }}" register: distribute_ts_piot_archive_file - name: Find all files inside the target folder ansible.windows.win_find: paths: "{{ distribute_ts_piot_destination_dir }}" file_type: file register: files_to_delete - name: Delete the registered files ansible.windows.win_file: path: "{{ item.path }}" state: absent loop: "{{ files_to_delete.files }}" - name: Extract archive to destination dir community.windows.win_unzip: src: "{{ distribute_ts_piot_archive }}" dest: "{{ distribute_ts_piot_destination_dir }}" delete_archive: true when: distribute_ts_piot_archive_file.stat.exists - name: Print a status message ansible.builtin.debug: msg: "Archive is not exists: {{ distribute_ts_piot_archive }} Skipping extracting..." when: not distribute_ts_piot_archive_file.stat.exists
Разница, на мой взгляд, существенная. Плейбук получается лаконичным, читаемым и достаточно понятным даже без глубокого погружения в детали реализации.
В этот момент стало понятно: Ansible стоит добавлять в рабочий процесс.
Проблема с инвентарем
Главной проблемой стала актуализация инвентаря.
У нас точки могут закрываться и открываться несколько раз в месяц, а иногда и чаще. Вносить все изменения руками не хотелось: это долго, неудобно и неизбежно приводит к ошибкам.
В качестве источника актуальных данных выбрали 1С. С помощью 1С‑разработчиков и нейросети была написана обработка, которая запускается через планировщик и генерирует CSV‑файл по заранее заданному шаблону.
Дальше я написал парсер на Python и запускаю его через systemd‑таймер. На выходе получаю сгенерированный Ansible‑инвентарь с актуальными данными.
По похожему принципу генерируется и инвентарь для Prometheus job.
Почему Syncthing все еще нужен
После внедрения Ansible Syncthing никуда не исчез. Наоборот, он остался важной частью схемы.
Например, есть задача: доставить и распаковать архив с ПО для выездного специалиста. Архив весит около 300 МБ.
Можно было бы отправлять его напрямую через Ansible, но это заняло бы заметное время. Честно говоря, я даже не стал проверять этот вариант, потому что Syncthing уже хорошо решает такую задачу.
Поэтому схема такая:
Заранее доставляем архив через Syncthing.
В Ansible‑плейбуке используем уже локально лежащий файл.
Выполняем распаковку и дальнейшие действия.
В итоге Syncthing отвечает за надежную доставку больших файлов, а Ansible — за управление состоянием и выполнение логики.
Итоговая схема, за исключением остальных сервисов, стала такой:

Что осталось улучшить
Из нерешенных задач пока остается retry‑runner — оболочка над Ansible, которая будет повторно запускать задачи для точек, оказавшихся недоступными.
Сейчас если часть узлов недоступна, их нужно обрабатывать отдельно. Хочется сделать механизм, который будет продолжать попытки для таких точек и не требовать постоянного ручного контроля.
В идеале это должно выглядеть так:
Ansible запускается по актуальному инвентарю;
недоступные точки попадают в отдельный список;
runner периодически повторяет запуск только по ним;
после успешного выполнения точка исключается из очереди.
Пока это скорее ближайший план, чем готовое решение. Но направление кажется правильным: Ansible закрывает управление состоянием, Syncthing — доставку файлов, а динамический инвентарь из 1С помогает держать инфраструктуру в актуальном виде.
Итог
Получилась достаточно рабочая схема, где есть актуальный инвентарь, понятная инфраструктура управления Windows‑хостами без Active Directory.
Главным плюсом для меня остается то, что в любой момент можно вернуться к старым батникам и предыдущему сценарию, он до сих пор работает и часть работы действительно выполняется с их помощью.
