От Bash к Go: как «просто бэкап 1С» превратился в отдельную утилиту
- среда, 14 октября 2026 г. в 00:00:14
В программировании на Go я пока новичок.
Поэтому, как любой уважающий себя разработчик, я решил начать с самого безопасного занятия — переписать на Go то, что уже несколько лет нормально работает на Bash.
Зачем?
Потому что один сервер — это Bash‑скрипт.
Два сервера — уже Bash‑скрипт плюс его копия с немного другими настройками. А когда появляются Linux, Windows, PostgreSQL, MS SQL Server, файловые базы, сетевые файловые базы и разные требования к срокам хранения резервных копий, начинается самое интересное.
В какой‑то момент я понял, что вместо очередного варианта Bash‑скрипта хочется получить одну нормальную утилиту, которую можно положить на сервер, дать ей YAML‑конфигурацию — и забыть о ручной настройке.
Ну а дальше всё пошло немного не по плану.
Изначально планировалось:
«Просто переписать Bash‑скрипт на Go».
В итоге появились Windows, MSSQL, автоматическое монтирование CIFS, отдельные секреты, валидация конфигурации, проверка окружения, несколько расписаний, проверка восстановления резервных копий и ещё несколько идей, которые тоже очень хотелось добавить.
В этой статье расскажу, как из обычного Bash‑скрипта получилась кроссплатформенная Go‑утилита для резервного копирования информационных баз 1С.
У меня был вполне рабочий Bash‑скрипт.
Он использовал ibcmd для выгрузки информационных баз 1С в .dt, а после выгрузки мог выполнять обслуживание баз средствами PostgreSQL.
То есть задача была не только сделать резервную копию, но и, например, выполнить REINDEX, VACUUM и другие операции.
Типичный рабочий цикл выглядел примерно так:
запуск по запуск по расписанию(планировщиком) → удаление старых копий → выгрузка баз через ibcmd по условию → обслуживание СУБД
Для одного конкретного сервера это решение меня вполне устраивало.
Например, логика выгрузки была примерно такой:
day=$(date +%d) dow=$(date +%u) for database in $(cat "$data_bases"); do if [ "$day" = "01" ]; then "$ibcmd_path" infobase dump \ --dbms=PostgreSQL \ --db-server=$SrvName \ --db-user=$dbUser \ --db-pwd=$PGPASSWORD \ --db-name=$database \ --user=$usr1c \ --password=$pwd1c \ "$pathB/Monthly/$database-$(date +%Y-%m-%d).dt" elif [ "$dow" -ge 2 ] && [ "$dow" -le 6 ]; then "$ibcmd_path" infobase dump \ --dbms=PostgreSQL \ --db-server=$SrvName \ --db-user=$dbUser \ --db-pwd=$PGPASSWORD \ --db-name=$database \ --user=$usr1c \ --password=$pwd1c \ "$pathB/Daily/$database-$(date +%Y-%m-%d).dt" fi done
Плюс удаление старых копий:
find "$pathB/Daily/" -name "*.dt" -mtime +"$daysD" -delete find "$pathB/Monthly/" -name "*.dt" -mtime +"$daysM" -delete
И обслуживание PostgreSQL:
reindexdb -h "$SrvName" --username "$dbUser" --dbname "$database" --jobs=2 vacuumdb --host=localhost --username="$dbUser" --dbname="$database" --analyze --jobs=2
На этом месте всё выглядело вполне прилично.
Проблемы начинались позже.
Основная проблема была даже не в Bash.
Проблема была в количестве разных серверов и конфигураций.
Linux + PostgreSQL, Windows + MS SQL Server. На ряду с клиент‑серверными появились файловые базы 1С, в том числе и на сетевых папках. И еще несколько разных комбинаций.
И каждый раз скрипт приходилось адаптировать.
Даже довелось однажды написать подобный скрипт на Windows PowerShell.
Кроме того, в самом скрипте постепенно появлялась масса переменных (логины и пароли; пути; адреса серверов; правила хранения; списки баз; отдельные условия для разных баз; дополнительные проверки)
В какой‑то момент стало понятно, что Bash здесь уже используется не столько как средство автоматизации, сколько как язык программирования.
А если уж всё равно писать программу, почему бы не написать её на Go?
Изначально задача была:
«Переписать Bash‑скрипт на Go».
Потом захотелось:
кроссплатформенность (Linux и Windows);
конфиги в YAML и отдельный secrets.yaml;
валидация конфигурации;
проверка и подготовка окружения перед началом работы;
три типа расписания с разными сроками хранения;
проверка резервных копий.
На этом этапе стало понятно, что это уже не переписывание Bash‑скрипта.
Это классический случай:
«Раз уж мы всё равно здесь, давайте ещё вот это...»
И проект начал расти.
На данный момент утилита протестирована на 7 серверах и около 50 информационных базах. Проверена работа в связках Windows + MS SQL Server и Linux + PostgreSQL.
Настройки разделены на два файла:
config.yaml secrets.yaml
Это сделано специально.
В config.yaml нет паролей, а данные для авторизации находятся отдельно.
Структура secrets.yaml:
net: net_user: "" net_password: "" dbms: dbms_user: "" dbms_password: "" bases: base01: user: "" password: "" base02: user: "" password: ""
В секции net находятся данные для доступа к сетевым ресурсам.
Они нужны только при работе с сетевыми файловыми базами из Linux.
В секции dbms находятся данные пользователя СУБД, которые передаются ibcmd.
В bases находятся данные пользователей информационных баз.
Если в базе вообще не создано ни одного пользователя 1С, соответствующую запись можно не добавлять.
Основная конфигурация выглядит примерно так:
general: backups_path: '/home/admins/backup/' ibcmd_path: '/opt/1cv8/x86_64/8.5.1.1529/' log_path: '/home/admins/backup/log/' secrets_file: '/home/admin/secrets.yaml' mount_path: '/mnt/' server_settings: dbms: "PostgreSQL" srv: "localhost" port: 5433 bases: - mode: "file" dbdir: '//172.30.1.11/TestFileDB/' dbname: "base01" validate_restore: true schedules: - daily - weekly - mode: "dbms" dbname: "base02" validate_restore: true schedules: - daily - weekly
Полное описание всех параметров конфигурации вынесено в README проекта на Github.
Для резервных копий предусмотрены три режима:
daily;
weekly;
monthly.
Например:
schedule_settings: daily: schedule_backup_dir: "daily" schedule_days: - 1 - 3 - 5 delete_older: 20 weekly: schedule_backup_dir: "weekly" schedule_days: - 6 delete_older: 1464 monthly: schedule_backup_dir: "monthly" schedule_days: - 31 delete_older: 8760
delete_older задаётся в часах.
Таким образом, например: ежедневные копии можно хранить две недели; еженедельные — три месяца; ежемесячные — год.
schedule_days для daily и weekly указываются номера дней недели, где 1–6 это понедельник — суббота, а 0 — воскресенье.
Для monthly этот параметр указывает на дни месяца
При этом база может участвовать не во всех расписаниях:
schedules: - daily - weekly
Например, менее важную базу можно не выгружать ежемесячно.
По моему мнению, такие настройки расписаний покрывают большинство потребностей.
Здесь есть небольшой нюанс.
Если один день попадает сразу в несколько расписаний, копия не должна создаваться несколько раз.
Поэтому у расписаний есть приоритет:
monthly ↓ weekly ↓ daily
Например, если 31 число выпало на 5 день недели, оно одновременно попало в monthly и daily, тогда будет выбран monthly.
Это позволяет поставить утилиту в планировщик на каждый день и не заставлять сам планировщик знать о внутренней логике резервного копирования.
То есть планировщик говорит:
«Каждый день в 02:00 запускай программу».
А уже программа решает:
«Сегодня мне работать или сегодня можно поспать».
Предусмотрено два режима запуска:
backup-1c run
и
backup-1c run --now
В обычном режиме учитываются расписания.
В режиме --now расписания игнорируются, а программа последовательно обрабатывает все базы из конфигурации. Это удобно для разового резервного копирования.
Например, перед обновлением платформы 1С можно просто выполнить:
backup-1c run --now
Одна из вещей, которую я сознательно хотел сделать лучше, чем в первоначальном Bash‑скрипте, — проверять конфигурацию заранее.
Не хотелось получить ситуацию:
запустили ↓ первая база ↓ ошибка ↓ исправили ↓ запустили ↓ вторая ошибка ↓ исправили ↓ запустили ...
Если программа уже может обнаружить несколько проблем — почему бы не сообщить о них все сразу?
В Go для этого удобно использовать errors.Join.
Например:
var errs []error errs = append(errs, fmt.Errorf( "в секции bases в настройках базы под номером %d %w", i+1, ErrInvalidMode, ), ) ... if len(errs) > 0 { return errors.Join(errs...) }
В результате пользователь получает сразу список найденных проблем.
Мне такой подход оказался очень удобным, и я старался использовать его там, где это действительно имеет смысл.
Глобально проверки разделены на два этапа:
func (c *Config) ConfigValidate(now bool) error
и
func (c *Config) EnvironmentValidate(now bool) error
Первая проверяет саму конфигурацию.
Вторая — окружение, в котором программа должна работать.
Параметр now нужен для режима run --now.
Если пользователь явно попросил разовое резервное копирование, нет смысла требовать от него настроенное расписание.
На этом этапе проверяются:
обязательные параметры;
допустимые значения;
взаимосвязанные настройки;
корректность расписаний;
наличие необходимых параметров для файловых и клиент‑серверных баз;
наличие утилиты СУБД.
Например, если используются PostgreSQL‑базы, программа ищет psql:
exec.LookPath("psql")
Для MS SQL Server — sqlcmd.
Если утилита не найдена, программа сообщает об этом ещё до начала резервного копирования.
С ibcmd ситуация немного другая.
Технически можно было бы сделать:
exec.LookPath("ibcmd")
Но я решил этого не делать.
Причина довольно практическая: наличие ibcmd на сервере ещё не означает, что он находится в PATH.
Поэтому путь к каталогу платформы 1С задаётся явно:
general: ibcmd_path: '/opt/1cv8/x86_64/8.5.1.1529/'
Через runtime.GOOS на Linux программа ищет:
ibcmd
а на Windows:
ibcmd.exe
То есть вместо:
«Надеемся, что разработчики добавили всё нужное в PATH»
получаем:
«Вот каталог — там и ищи».
Недостаток этого способа в том, что после обновления версии платформы 1С, каждый раз нужно исправлять параметр, указав путь к новой платформе.
После проверки конфигурации программа проверяет уже сам сервер.
Например:
существуют ли каталоги;
являются ли они действительно каталогами;
можно ли в них писать;
доступен ли ibcmd;
доступны ли утилиты СУБД;
можно ли подготовить сетевые файловые базы.
Для проверки каталога используется простой тест:
os.Stat() ↓ проверка IsDir() ↓ создание test.txt ↓ закрытие ↓ удаление
При этом программа намеренно не создаёт отсутствующие каталоги из конфигурации.
Если в YAML написано:
backups_path: "/home/admins/backpu/"
я предпочитаю получить ошибку, а не автоматически создать каталог с опечаткой.
С секретами в Linux я решил не ограничиваться предупреждением.
Если файл содержит пароли, программа требует права доступа 0600.
Проверка выглядит следующим образом:
perm := info.Mode().Perm() if perm&0044 != 0 { return fmt.Errorf( "файл %s доступен для чтения не только владельцу", file, ) }
То есть в Linux файл с секретами должен быть доступен для чтения только владельцу.
Например:
chmod 600 secrets.yaml
Для Windows аналогичную проверку ACL я пока не реализовывал, но планирую в дальнейшем сделать.
С файловыми базами всё относительно просто, пока они локальные.
Например:
mode: "file" dbdir: "/home/admins/testfiledb/"
Но файловая база может находиться на сетевом ресурсе:
//172.30.1.11/TestFileDB/
В Windows такой путь можно использовать напрямую как UNC‑путь. В Linux его сначала нужно смонтировать. И здесь получилась интересная маленькая деталь. Я не стал отдельно определять операционную систему. В Linux префикс сетевого пути выглядит как “//”
Поэтому достаточно:
strings.HasPrefix(base.DBDir, "//")
Если путь начинается с //, программа считает базу сетевой. И это делает путь уникальным для этой ОС. Спасибо Биллу за обратные слэши) сетевые пути в Windows начинаются с \\
Далее:
создаётся каталог монтирования;
создаётся временный файл с CIFS credentials;
выполняется mount.cifs;
проверяется результат;
база добавляется в список смонтированных;
после завершения работы каталог размонтируется.
Примечание. Для работы
mount.cifsтребуются права root
Сначала я рассматривал обычный вызов mount.cifs.
Но передавать пароль непосредственно в аргументах процесса не хотелось.
Поэтому программа создаёт временный credentials‑файл:
username=... password=...
и передаёт mount.cifs путь к нему. После завершения работы временный файл удаляется. Так пароль не оказывается непосредственно в аргументах командной строки.
Это не абсолютная защита секретов от всех возможных способов наблюдения за процессами, но в данном случае это существенно лучше, чем передавать пароль открытым аргументом.
После всех проверок начинается основная работа.
Для каждой выбранной базы программа формирует аргументы для ibcmd.
Для файловой базы это примерно:
ibcmd infobase dump --db-path=... ...
Для клиент‑серверной добавляются параметры СУБД:
--dbms=PostgreSQL --db-server=... --db-user=... --db-pwd=... --db-name=...
А затем передаются пользователь и пароль информационной базы. Сама программа при этом не реализует формат .dt. Она выступает скорее как управляющий слой вокруг штатных инструментов 1С и СУБД.

На уровне требований всё выглядело просто:
«Нужно поддержать PostgreSQL и MS SQL Server».
На практике CLI‑аргументы у них отличаются.
Например, для PostgreSQL сервер с нестандартным портом передаётся в формате:
localhost port=5433
А для MS SQL Server потребовался вариант:
localhost,1433
То есть даже такая небольшая деталь потребовала отдельной логики при формировании аргументов ibcmd.
И это хороший пример того, почему абстракция:
«Давайте просто сделаем одну универсальную строку подключения»
не всегда оказывается универсальной.
И здесь начинается, пожалуй, самая интересная часть проекта.
В реальной работе я сталкивался с ситуациями, когда .dt после выгрузки существовал, имел вполне приличный размер, но при восстановлении возникали проблемы.
Поэтому я решил:
Если мы действительно хотим проверить резервную копию, нужно не только посмотреть, создался ли файл. Нужно попробовать его восстановить.
Для каждой базы появился параметр:
validate_restore: true
Если он включён, после успешной выгрузки путь к .dt сохраняется, и после завершения выгрузки всех выбранных баз запускается дополнительная проверка.
Первая проверка очень простая.
Если .dt меньше 2 КБ, он считается невалидным. Почему именно 2 КБ? Это не число, полученное в результате многомесячного научного исследования. Я просто проверил размер выгрузки пустой базы 1С. Получилось примерно 3,6 КБ. Поэтому выбрал 2 КБ с некоторым запасом.
Самое научное место всего проекта))

В дальнейшем этот порог можно будет вынести в настройки.
Размер файла — это только предварительная проверка.
Основная проверка выглядит так:
backup.dt │ ▼ создать тестовую базу │ ▼ restore backup.dt │ ▼ проверить результат │ ▼ удалить тестовую базу
Для клиент‑серверной базы тестовая база создаётся в СУБД.
Для файловой — в каталоге, предназначенном для резервных копий.
После завершения проверки тестовая база удаляется независимо от результата восстановления.
Это особенно важно: если restore завершился ошибкой, программа всё равно должна попытаться убрать за собой временные объекты.
Для PostgreSQL используется psql.
Например:
DROP DATABASE IF EXISTS tmptestdb WITH (FORCE);
Для MS SQL Server используется sqlcmd.
Там перед удалением тестовая база переводится в SINGLE_USER:
ALTER DATABASE [tmptestdb] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [tmptestdb];
Пароль СУБД при этом не передаётся непосредственно аргументом.
Для PostgreSQL используется:
PGPASSWORD
для MS SQL Server:
SQLCMDPASSWORD
Есть важный нюанс.
Восстановление большой базы — это полноценная операция. Она может серьёзно нагрузить: процессор; оперативную память; дисковую подсистему; СУБД; файловую систему; сеть. Кроме того, нужно место на диске. Минимально разумно иметь свободное пространство хотя бы сопоставимое с размером самой большой базы, а лучше больше.
Поэтому validate_restore — это не просто ещё один флажок — это компромисс
На небольших базах я предпочитаю проверять восстановление всегда.
Для больших — уже зависит от инфраструктуры.

Для длительных операций добавлена возможность задать таймаут:
advanced: ctx_timeout: 120
Значение задаётся в минутах.
В Go для запуска внешних процессов используется context.Context, поэтому по истечении таймаута процесс может быть прерван.
Поскольку проект для меня ещё и учебный, некоторые решения стали хорошей практикой работы с Go.
В процессе разработки я по‑настоящему оценил несколько вещей. Например, errors.Join позволяет собрать несколько ошибок в одну, что оказалось удобно при проверке конфигурации: можно сообщить пользователю сразу обо всех найденных проблемах, а не заставлять его исправлять их по одной. context.WithTimeout и exec.CommandContext помогают ограничивать время выполнения внешних команд, а exec.LookPath — проверять наличие необходимых утилит в системе. В работе с файлами пригодились os.ReadDir, os.Stat и другие функции стандартной библиотеки.
Но, пожалуй, больше всего мне понравилось, как в Go можно организовать саму программу. Разделить чтение конфигурации, проверку окружения и выполнение задач, вынести общую логику в отдельные функции и пакеты, а особенности Windows и Linux учитывать там, где это действительно необходимо. При этом для решения большинства задач не пришлось искать сторонние библиотеки: многое уже есть в стандартной библиотеке Go. Для утилиты, которая должна работать в разных окружениях и запускать внешние программы, это оказалось особенно полезно.
Для меня это было существенно полезнее, чем написать ещё один учебный HTTP‑сервер.
Здесь каждая ошибка действительно имеет последствия: программа должна нормально работать на сервере и не потерять резервную копию из‑за какой‑нибудь мелочи.
Некоторые задачи, которые на бумаге выглядели элементарными, оказались отдельными мини‑проектами и требовали дополнительного изучения предметных областей.
Например:
На уровне идеи:
«Передать сервер и порт».
На практике:
«А почему
localhost port=1433не работает?»
Windows может работать с UNC‑путём напрямую.
Linux требует mount.
Значит, нужно:
смонтировать → проверить → использовать → размонтировать → удалить временные данные
Сначала кажется:
«Проверим размер файла».
Потом:
«А может, всё‑таки попробуем восстановить?»
А потом внезапно появляется:
«Тогда надо создать тестовую базу, потом удалить её даже в случае ошибки, а для MSSQL ещё и выкинуть оттуда все соединения».
Так простой backup постепенно превращается в полноценный orchestration‑процесс.
Например:
Было | Стало |
|---|---|
Bash + отдельный PowerShell | одна Go‑утилита |
настройки внутри скрипта | YAML |
пароли в скрипте | отдельный |
ручная проверка параметров | автоматическая валидация |
PostgreSQL | PostgreSQL + MS SQL Server |
Linux | Linux + Windows |
обычный dump | dump + опциональный restore |
ручная работа с сетевыми базами | автоматический CIFS mount |
сложная настройка cron | расписание в YAML |
Проект пока нельзя назвать законченным.
Есть несколько направлений, которые хочется развить и это планируется разработать в следующих релизах.
Во‑первых, обслуживание баз средствами СУБД.
Для PostgreSQL планируется:
ANALYZE REINDEX VACUUM VACUUM FULL
Для MS SQL Server:
UPDATE STATISTICS REORGANIZE REBUILD DBCC CHECKDB
Во‑вторых, управление службой сервера 1С:
остановка → выполнение резервного копирования → запуск обратно
В‑третьих, уведомления: электронная почта; мессенджеры.
И, наконец, возможность запуска самой утилиты как службы:
systemd в Linux;
Windows Service в Windows.
Наверное, главный результат этого проекта для меня даже не сама утилита.
Я получил нормальный учебный проект на Go, в котором пришлось работать не с абстрактной задачей, а с реальной инфраструктурой.
Здесь есть:
процессы / файловая система / сеть
Linux / Windows
PostgreSQL / MS SQL Server
YAML;
ошибки / таймауты;
безопасность;
внешние CLI‑программы;
И главное — у программы есть практическая задача.
Если она работает хорошо, я могу поставить её на сервер и реально использовать.
Всё начиналось с довольно простой мысли:
«Почему бы не переписать мой Bash‑скрипт на Go?»
В результате получилась утилита, которая умеет немного больше, чем просто сделать .dt
Проект бесплатный, исходный код и готовые исполняемые файлы доступны на GitHub.
https://github.com/Dr‑Mikegreen/maintenance_1C
Если кому‑то окажется полезной сама утилита — отлично.
Если кто‑то найдёт в коде ужасное Go‑решение и расскажет, как сделать лучше — тоже отлично.
Я пока только учусь. А переписывать работающий Bash‑скрипт на Go, как выяснилось, довольно неплохой способ учиться.
Главное — вовремя остановиться, когда появляется мысль:
«А давайте ещё одну небольшую функцию...»
Хотя, судя по текущему TODO, я уже опоздал.