В программировании на 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 перестал устраивать

Основная проблема была даже не в 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.

Три расписания вместо сложного cron

Для резервных копий предусмотрены три режима:

  • 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 запускай программу».

А уже программа решает:

«Сегодня мне работать или сегодня можно поспать».

run и run ‑now

Предусмотрено два режима запуска:

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.

Если пользователь явно попросил разовое резервное копирование, нет смысла требовать от него настроенное расписание.

Проверка конфигурации — ConfigValidate

На этом этапе проверяются:

  • обязательные параметры;

  • допустимые значения;

  • взаимосвязанные настройки;

  • корректность расписаний;

  • наличие необходимых параметров для файловых и клиент‑серверных баз;

  • наличие утилиты СУБД.

Например, если используются PostgreSQL‑базы, программа ищет psql:

exec.LookPath("psql")

Для MS SQL Server — sqlcmd.

Если утилита не найдена, программа сообщает об этом ещё до начала резервного копирования.

Почему я не стал искать ibcmd через PATH

С 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С, каждый раз нужно исправлять параметр, указав путь к новой платформе.

Проверка окружения — EnvironmentValidate

После проверки конфигурации программа проверяет уже сам сервер.

Например:

  • существуют ли каталоги;

  • являются ли они действительно каталогами;

  • можно ли в них писать;

  • доступен ли 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 начинаются с \\

Далее:

  1. создаётся каталог монтирования;

  2. создаётся временный файл с CIFS credentials;

  3. выполняется mount.cifs;

  4. проверяется результат;

  5. база добавляется в список смонтированных;

  6. после завершения работы каталог размонтируется.

Примечание. Для работы mount.cifs требуются права root

Почему пароль CIFS не передаётся прямо в командной строке

Сначала я рассматривал обычный вызов 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 оказались не такими одинаковыми

На уровне требований всё выглядело просто:

«Нужно поддержать PostgreSQL и MS SQL Server».

На практике CLI‑аргументы у них отличаются.

Например, для PostgreSQL сервер с нестандартным портом передаётся в формате:

localhost port=5433

А для MS SQL Server потребовался вариант:

localhost,1433

То есть даже такая небольшая деталь потребовала отдельной логики при формировании аргументов ibcmd.

И это хороший пример того, почему абстракция:

«Давайте просто сделаем одну универсальную строку подключения»

не всегда оказывается универсальной.

Резервная копия — это ещё не резервная копия

И здесь начинается, пожалуй, самая интересная часть проекта.

В реальной работе я сталкивался с ситуациями, когда .dt после выгрузки существовал, имел вполне приличный размер, но при восстановлении возникали проблемы.

Поэтому я решил:

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

Для каждой базы появился параметр:

validate_restore: true

Если он включён, после успешной выгрузки путь к .dt сохраняется, и после завершения выгрузки всех выбранных баз запускается дополнительная проверка.

Проверка № 1 — подозрительно маленький файл

Первая проверка очень простая.
Если .dt меньше 2 КБ, он считается невалидным. Почему именно 2 КБ? Это не число, полученное в результате многомесячного научного исследования. Я просто проверил размер выгрузки пустой базы 1С. Получилось примерно 3,6 КБ. Поэтому выбрал 2 КБ с некоторым запасом.
Самое научное место всего проекта))

Размер пустых баз без конфигурации filedb02 и test01 4,3 и 3,6 Килобайт соответственно
Размер пустых баз без конфигурации filedb02 и test01 4,3 и 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

Поскольку проект для меня ещё и учебный, некоторые решения стали хорошей практикой работы с Go.

В процессе разработки я по‑настоящему оценил несколько вещей. Например, errors.Join позволяет собрать несколько ошибок в одну, что оказалось удобно при проверке конфигурации: можно сообщить пользователю сразу обо всех найденных проблемах, а не заставлять его исправлять их по одной. context.WithTimeout и exec.CommandContext помогают ограничивать время выполнения внешних команд, а exec.LookPath — проверять наличие необходимых утилит в системе. В работе с файлами пригодились os.ReadDir, os.Stat и другие функции стандартной библиотеки.

Но, пожалуй, больше всего мне понравилось, как в Go можно организовать саму программу. Разделить чтение конфигурации, проверку окружения и выполнение задач, вынести общую логику в отдельные функции и пакеты, а особенности Windows и Linux учитывать там, где это действительно необходимо. При этом для решения большинства задач не пришлось искать сторонние библиотеки: многое уже есть в стандартной библиотеке Go. Для утилиты, которая должна работать в разных окружениях и запускать внешние программы, это оказалось особенно полезно.

Для меня это было существенно полезнее, чем написать ещё один учебный HTTP‑сервер.

Здесь каждая ошибка действительно имеет последствия: программа должна нормально работать на сервере и не потерять резервную копию из‑за какой‑нибудь мелочи.

Что оказалось неожиданно сложным

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

Например:

1. Разные аргументы для PostgreSQL и MSSQL

На уровне идеи:

«Передать сервер и порт».

На практике:

«А почему localhost port=1433 не работает?»

2. Сетевые файловые базы

Windows может работать с UNC‑путём напрямую.

Linux требует mount.

Значит, нужно:

смонтировать → проверить → использовать → размонтировать → удалить временные данные

3. Проверка резервной копии

Сначала кажется:

«Проверим размер файла».

Потом:

«А может, всё‑таки попробуем восстановить?»

А потом внезапно появляется:

«Тогда надо создать тестовую базу, потом удалить её даже в случае ошибки, а для MSSQL ещё и выкинуть оттуда все соединения».

Так простой backup постепенно превращается в полноценный orchestration‑процесс.

Что было в Bash и что получилось в Go

Например:

Было

Стало

Bash + отдельный PowerShell

одна Go‑утилита

настройки внутри скрипта

YAML

пароли в скрипте

отдельный secrets.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, я уже опоздал.

Комментарии (0)