Как мы управляем удаленными Windows 10-точками без Active Directory: Syncthing, Ansible и 1С

от автора

Вступление

Привет всем!

Это мой первый пост на Хабре. Идея написать статью пришла от коллеги, и я решил: почему бы не попробовать.

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

Коротко о том, чем я занимаюсь:

  • создаю инфраструктуру мониторинга: Prometheus, Grafana;

  • деплою новые сервисы на серверы: Ubuntu, Fedora, Windows Server 2008 R2;

  • работаю с удаленными точками на Windows 10;

  • автоматизирую задачи на Python, Ansible, PowerShell, BAT и Bash;

  • разбираю инциденты в сервисах;

  • администрирую MySQL / PostgreSQL

О работе

Мы небольшая компания с распределенной сетью торговых точек. На каждой точке обычно есть одна касса и один ПК. Где‑то стоит MikroTik, но на большинстве точек используется мобильная SIM‑карта в роутере или USB‑модем.

Автоматизация задач начиналась ещё до моего прихода. Основной подход строился вокруг заранее подготовленных образов.

Как было раньше

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

  2. Для задач подготавливается код на bat или python

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

    Раскидка по папкам может осуществляться, по сути, из чего угодно, но у нас используется инструмент из 1С

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

Главная проблема такого подхода, на мой взгляд, в том, что для сложных задач приходится писать слишком много кода на BAT, PowerShell или Python.

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

Но со временем стало понятно, что хочется более современного и управляемого подхода. Хотелось добавить IaC‑подход, и первым кандидатом стал Ansible.

Active Directory в нашем случае использовать нельзя: добавить его в текущую инфраструктуру мы не можем, поэтому этот вариант я даже не рассматривал.

Ниже пример BAT‑файла для распаковки архива.

:: =================================================================:: distributor.cmd — проверка/ожидание архива + тест + распаковка:: Устойчив для запуска из другого .bat и из планировщика:: =================================================================@echo offsetlocal 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 DELETEset "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>&1call :LOG "-------------------- Script Started --------------------"if exist "%SIGNAL_OK%" del /q "%SIGNAL_OK%" >nul 2>&1if exist "%SIGNAL_ERR%" del /q "%SIGNAL_ERR%" >nul 2>&1if 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_ARCHIVEif errorlevel 1 (    call :LOG "[ERROR] Archive test stage failed."    call :repete    endlocal    exit /b 1)call :UNPACK_ARCHIVEif 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 --------------------"endlocalgoto :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 уже хорошо решает такую задачу.

Поэтому схема такая:

  1. Заранее доставляем архив через Syncthing.

  2. В Ansible‑плейбуке используем уже локально лежащий файл.

  3. Выполняем распаковку и дальнейшие действия.

В итоге Syncthing отвечает за надежную доставку больших файлов, а Ansible — за управление состоянием и выполнение логики.

Итоговая схема, за исключением остальных сервисов, стала такой:

Общая схема получилась такой: 1С отдает актуальный список точек, Python генерирует инвентарь для Ansible и Prometheus, Syncthing отвечает за доставку больших файлов, а Ansible — за выполнение задач на Windows-узлах.

Общая схема получилась такой: 1С отдает актуальный список точек, Python генерирует инвентарь для Ansible и Prometheus, Syncthing отвечает за доставку больших файлов, а Ansible — за выполнение задач на Windows‑узлах.

Что осталось улучшить

Из нерешенных задач пока остается retry‑runner — оболочка над Ansible, которая будет повторно запускать задачи для точек, оказавшихся недоступными.

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

В идеале это должно выглядеть так:

  • Ansible запускается по актуальному инвентарю;

  • недоступные точки попадают в отдельный список;

  • runner периодически повторяет запуск только по ним;

  • после успешного выполнения точка исключается из очереди.

Пока это скорее ближайший план, чем готовое решение. Но направление кажется правильным: Ansible закрывает управление состоянием, Syncthing — доставку файлов, а динамический инвентарь из 1С помогает держать инфраструктуру в актуальном виде.

Итог

Получилась достаточно рабочая схема, где есть актуальный инвентарь, понятная инфраструктура управления Windows‑хостами без Active Directory.

Главным плюсом для меня остается то, что в любой момент можно вернуться к старым батникам и предыдущему сценарию, он до сих пор работает и часть работы действительно выполняется с их помощью.

ссылка на оригинал статьи https://habr.com/ru/articles/1057308/