Мы небольшая компания с распределенной сетью торговых точек. На каждой точке обычно есть одна касса и один ПК. Где‑то стоит MikroTik, но на большинстве точек используется мобильная SIM‑карта в роутере или USB‑модем.Главная проблема такого подхода, на мой взгляд, в том, что для сложных задач приходится писать слишком много кода на BAT, PowerShell или Python. Хотелось добавить IaC‑подход, и первым кандидатом стал Ansible. Что из этого вышло
Привет всем!
Это мой первый пост на Хабре. Идея написать статью пришла от коллеги, и я решил: почему бы не попробовать.
Во‑первых, это хороший способ собрать собственный опыт в одном месте и разложить его по полочкам. Во‑вторых, возможно, я получу обратную связь от людей, которые решали похожие задачи. Ну и, в‑третьих, кому‑то этот подход может пригодиться в текущих рабочих условиях.
Коротко о том, чем я занимаюсь:
создаю инфраструктуру мониторинга: 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.
После внедрения Ansible Syncthing никуда не исчез. Наоборот, он остался важной частью схемы.
Например, есть задача: доставить и распаковать архив с ПО для выездного специалиста. Архив весит около 300 МБ.
Можно было бы отправлять его напрямую через Ansible, но это заняло бы заметное время. Честно говоря, я даже не стал проверять этот вариант, потому что Syncthing уже хорошо решает такую задачу.
Поэтому схема такая:
Заранее доставляем архив через Syncthing.
В Ansible‑плейбуке используем уже локально лежащий файл.
Выполняем распаковку и дальнейшие действия.
В итоге Syncthing отвечает за надежную доставку больших файлов, а Ansible — за управление состоянием и выполнение логики.
Итоговая схема, за исключением остальных сервисов, стала такой:

Общая схема получилась такой: 1С отдает актуальный список точек, Python генерирует инвентарь для Ansible и Prometheus, Syncthing отвечает за доставку больших файлов, а Ansible — за выполнение задач на Windows‑узлах.
Из нерешенных задач пока остается retry‑runner — оболочка над Ansible, которая будет повторно запускать задачи для точек, оказавшихся недоступными.
Сейчас если часть узлов недоступна, их нужно обрабатывать отдельно. Хочется сделать механизм, который будет продолжать попытки для таких точек и не требовать постоянного ручного контроля.
В идеале это должно выглядеть так:
Ansible запускается по актуальному инвентарю;
недоступные точки попадают в отдельный список;
runner периодически повторяет запуск только по ним;
после успешного выполнения точка исключается из очереди.
Пока это скорее ближайший план, чем готовое решение. Но направление кажется правильным: Ansible закрывает управление состоянием, Syncthing — доставку файлов, а динамический инвентарь из 1С помогает держать инфраструктуру в актуальном виде.
Получилась достаточно рабочая схема, где есть актуальный инвентарь, понятная инфраструктура управления Windows‑хостами без Active Directory.
Главным плюсом для меня остается то, что в любой момент можно вернуться к старым батникам и предыдущему сценарию, он до сих пор работает и часть работы действительно выполняется с их помощью.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Своя self-hosted двухсерверная VPN-архитектура. Подробный путь разработки и ошибки, с которыми вы можете столкнуться | 7 | 8 | 13-07-2026 |
| 2 | Режим киоска в «Аврора Центр»: устройство под рабочую задачу | 5 | 7 | 08-07-2026 |
| 3 | Как я уведомления мониторинга в мессенджер национальный переносил | 0 | 5 | 14-07-2026 |
| 4 | Памятник kubelet, Kubernetes != CRI | 0 | 7 | 13-07-2026 |
| 5 | Как я сделал аналог Hamachi для геймеров: история PortPlay | 2 | 7 | 09-07-2026 |
| 6 | Оптимизация без AI: как я автоматизировал API-ручки и типы | -2 | 5 | 29-06-2026 |
| 7 | Два учета, одна правда: как я перестал сверять 1С и ITAM руками | 0 | 5 | 08-07-2026 |
| 8 | Вам не нужен BloodHound | 0 | 6 | 11-06-2026 |
| 9 | Свой VPN на Rust: как я спорил с сетью, TLS и самим собой | 7 | 8 | 27-06-2026 |
| 10 | Две недели я делал народную карту заправок. Написать код оказалось самой скучной частью работы | 0 | 5 | 07-07-2026 |