В этой статье я хотел бы рассказать о том, как управляю конфигурацией системных папок (/etc
, /usr
, etc) на своих машинах.
Для таких целей часто советуют применять инструменты вроде stow, ansible или разнообразных Docker Swarm с Kubernetes (для этих не хочу даже ссылки давать), но я их не использую.
Я создаю свои пакеты (для пакетного менеджера дистрибутива) и публикую их в своих репозиториях. Можно конечно заливать все нужные файлы руками по ssh (раньше я так и делал), но у пакетов есть важное преимущество: с их помощью легко не только добавлять или обновлять файлы, но и удалять их, и в целом контролировать, что есть в системе. Это предотвращает её захламление. В статье описано, как это делается для Alpine Linux (которым я пользуюсь на домашнем ПК) и, кратко, для Debian/Ubuntu.
Репозиторий в Alpine Linux
Этот раздел посвящен тому, как собрать свой пакет для Alpine Linux, опубликовать в локальном репозитории и установить его оттуда. В качестве подопытного использую dbin – менеджер пользовательских пакетов с ориентацией на статическую линковку и AppImage.
Сборка бинарника
Для этого надо перейти на самый свежий тег в склонированном репозитории и, собственно, собрать софт по инструкции от разработчика. Структура сборочного хозяйства у меня выглядит так:
$ tree -L 1 ./dbin
./dbin
├── cache
├── dbin
├── install
├── nfpm.yaml
├── nfpm_template.yaml
└── package.sh
dbin/cache
– кэш сборки; dbin/dbin
– склонированный репозиторий; dbin/package.sh
– скрипт, который собирает пакет и добавляет его в локальный репозиторий пакетов. Основная магия происходит в этом скрипте. Рассмотрим его.
Для начала надо установить переменные:
export REPO="${HOME}/packages/$( uname -m )"
export PROJ="$( basename ${PWD} )" # dbin
export PREFIX="${PWD}/install"
export GOPATH="${PWD}/cache"
export GOBIN="${PREFIX}/bin"
mkdir -p "${GOPATH}" "${GOBIN}"
REPO
– расположение локального репозитория пакетов; PREFIX
– папка, в которой будет сформировано дерево устанавливаемых файлов; GOPATH
– путь к локальному кэшу (dbin
написан на Go); GOBIN
– папка, в которую команда go install
устанавливает скомпилированные бинарники.
Далее переходим на актуальный тег в репозитории и, собственно, собираем dbin
:
cd ${PROJ}
git fetch --tags
export TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
git switch --detach ${TAG}
go mod download -x
go install -ldflags "-s -w" -trimpath
cd ..
git switch
– современная альтернатива git checkout
для случаев, когда нужно перейти на ветку или коммит.
После этого в папке ./install
появятся нужные файлы:
$ tree ./install
./install
└── bin
└── dbin
и можно приступать к сборке собственно пакета.
Side note. Слежение за тегами.
Как узнать, когда надо обновляться? GitHub формирует для проектов ленту с тегами в формате Atom
. Для её получения надо просто прибавить tags.atom
к адресу репозитория, например
https://github.com/xplshn/dbin/tags.atom
Далее эту ленту можно загрузить в читалку RSS, я пользуюсь Feeder для Android.
Сборка пакета
Для сборки пакета я пользуюсь nfpm. Во-первых им можно делать единообразные конфиги для нескольких дистрибутивов, во-вторых тулинг Debian и Ubuntu просто отвратителен. Конфиг nfpm для dbin выглядит примерно так:
name: "dbin"
arch: "amd64"
platform: "linux"
version: "${TAG}"
section: "default"
priority: "extra"
maintainer: "me"
description: |
The easy to use, easy to get, suckless software distribution system.
vendor: "me"
homepage: "https://github.com/xplshn/dbin"
license: "ISC"
contents:
- src: ${PREFIX}
dst: /
type: tree
file_info:
mode: 0755
apk:
signature:
key_file: ${PACKAGER_PRIVKEY}
key_name: ${PACKAGER_PRIVKEY_NAME}
Наиболее важные метаданные здесь – это contents
(содержимое пакета) и signature
. Хоть это и не обязательно, но я подписываю пакет своим ключом, который сгенерирован с помощью утилиты abuild-keygen
из состава Alpine Linux SDK. Это обычный RSA-ключ в формате PKCS#8
. Его открытую часть нужно положить в /etc/apk/keys
. Переменные PACKAGER_PRIVKEY*
можно установить так:
source "${HOME}/.abuild/abuild.conf" # устанавливает ${PACKAGER_PRIVKEY}
export PACKAGER_PRIVKEY_NAME="$( basename ${PACKAGER_PRIVKEY} .rsa )"
Файл abuild.conf
автоматически обновляется abuild-keygen
при генерации ключа, редактировать его руками не нужно.
Открытая версия nfpm
не поддерживает шаблоны. Поэтому я пользуюсь сторонним шаблонизатором, в данном случае envsubst
из состава gettext. Эта утилита подставляет вместо имён переменных среды их значения, что мне и нужно. См. как устанавливается ${TAG}
выше.
envsubst < nfpm_template.yaml > nfpm.yaml
Получив nfpm.yaml
, генерируем apk-пакет:
nfpm pkg --packager apk --target $REPO/${PROJ}-${TAG}.0.apk
.0
в конце из-за того что Alpine ожидает, что у всех пакетов версии будут в формате x.y.z
.
После того как пакет попал в репозиторий, остаётся только обновить индекс пакетов и подписать этот индекс своим ключом:
apk index -vU -o APKINDEX.tar.gz *.apk
abuild-sign -k ${PACKAGER_PRIVKEY} APKINDEX.tar.gz
Установка пакета
Для установки пакета нужно, во-первых, добавить в систему использованный для подписи ключ. Это делается опять же с помощью abuild-keygen
, но никто не запрещает скопировать файл .rsa.pub
в /etc/apk/keys
руками.
Во-вторых, нужно добавить локальный репозиторий в список репозиториев:
echo @my ${HOME}/packages/ >> /etc/apk/repositories
маска @my
тут применяется, чтобы не было конфликтов имен с пакетами из уже имеющихся репозиториев. Обновляем кэш пакетов и смотрим информацию:
$ doas apk update
$ apk info dbin
dbin-1.5.0 description:
The easy to use, easy to get, suckless software distribution system.
dbin-1.5.0 webpage:
https://github.com/xplshn/dbin
dbin-1.5.0 installed size:
10 MiB
$ doas apk add dbin@my
$ cat /etc/apk/world | grep dbin # проверяем в списке пакетов
dbin@my
$ dbin list | wc -l
4053
С помощью этой штуки можно поставить 4053 пакета, на данный момент. Среди которых, впрочем, куча всяких AppImage из Nix и в целом нет ничего нужного мне. Так что…
$ doas apk del dbin
$ rm -rf ${PROJ}
Репозиторий в Debian/Ubuntu
В целом процедура его поднятия аналогична репозиториям Alpine: мы создаем с помощью nfpm набор deb-пакетов, генерируем индекс с помощью утилиты dpkg-scanpackages
, записываем хэш этого индекса в файл с метаданными репозитория (файл Release
), и подписываем, храня подпись либо в отдельном файле (Release.gpg
), либо прямо по месту (файл InRelease
) – последний способ рекомендуется.
Но дальше начинаются детали, расписывать которые у меня нет никакого желания. Например, оно использует GnuPG вместо PKCS#8
и допускает дублирование имён пакетов в разных репозиториях. Для разрешения конфликтов введён механизм приоритетов, при этом APT плевать хотел на этот механизм, когда дело касается репозиториев из /etc/apt/sources.list.d/ubuntu.sources
, пакеты оттуда всегда выбираются первыми.
Уже жду, как они гордо заменят GnuPG (разработчики которого кстати выпилили возможность запускаться как пользовательский сервис под супервизором) на безопасно переписанный на Rust Sequoia. Не ну а что, sudo-rs у них уже есть.
В результате получается набор статических файлов, на который я натравливаю nginx
(не вижу, почему бы благородным донам не использовать nginx), после чего пробрасываю получившийся порт при подключении к серверу по ssh:
# ~/.ssh/config
RemoteForward 7777 localhost:7777
и прописываю на сервере в качестве адреса репозитория http://localhost:7777
. Дальше – всё по классике, apt install/update/remove
.
Заключение
Всё это может показаться сложным, но на самом деле эти скрипты нужно написать только один раз, и потом просто копировать с минимальными изменениями. Которые заключаются по сути только в сборке и преобразовании тегов репозитория в правильный номер версии для каждого нового пакета.
Также, надеюсь, стало понятно, почему я пользуюсь Alpine Linux. При работе с ним постоянно натыкаешься на простые и понятные решения, сделанные для людей, а не для корпораций, которым важно чтобы пользователи скачивали пакеты из каких надо репозиториев.