Когда один и тот же облачный Mac последовательно выполняет несколько задач сборки, самой незаметной проблемой часто становится не ошибка компиляции, а доступ следующей задачи к исходному коду, временным сертификатам, тестовым данным или кешу, оставшимся после предыдущей. Полагаться только на rm -rf при каждом запуске недостаточно надёжно: процессы могут продолжать использовать файлы, скрытые каталоги легко пропустить, а при аварийном завершении этап очистки может не выполниться. Более чёткую границу можно создать, подключая для каждой задачи отдельный зашифрованный разреженный образ и размещая в его файловой системе весь извлечённый код, процесс сборки и временные артефакты.
Почему для изоляции подходят разреженные образы
Логический объём разреженного образа может быть большим, тогда как место на диске хоста расходуется только по мере фактической записи данных. При этом образ остаётся обычным файлом, который удобно находить, учитывать и удалять по идентификатору задачи. После подключения он выглядит как отдельный том APFS, поэтому в существующих сценариях сборки обычно достаточно изменить рабочий каталог.
Шифрование снижает риск раскрытия данных, когда образ не подключён, а отдельная точка монтирования создаёт границу путей между задачами. Ни то ни другое не заменяет изоляцию пользователей и принцип минимальных привилегий, однако такой подход легче контролировать и проверять, чем общий постоянно существующий рабочий каталог.
Не следует считать, что «зашифрованный образ» недоступен во время выполнения задачи. После подключения образа процессы с соответствующими файловыми правами по-прежнему могут читать его содержимое, поэтому необходимо одновременно контролировать учётную запись runner, журналы и способ передачи секретов.
| Подход | Остаточные данные после аварийного завершения | Граница между задачами | Когда применять |
|---|---|---|---|
| Общий каталог с последующим удалением | Легко пропустить используемые файлы и скрытые каталоги | Зависит от корректности сценария | Короткие задачи без конфиденциальных данных |
| Отдельный обычный каталог для каждой задачи | Каталог остаётся на томе хоста | Пути разделены, файловая система общая | Параллельные сборки с низким уровнем риска |
| Зашифрованный разреженный образ | Можно обнаружить оставшиеся подключения и файлы образов | Отдельная файловая система | CI, которому нужна чёткая граница очистки |
Создание рабочей области APFS с именем задачи
Сначала идентификатор задачи необходимо ограничить разрешённым набором символов, чтобы в путь не попали косые черты, пробелы или подстановка команд. Каталог образов должен находиться в месте, доступном для чтения и записи только пользователю runner, а точку монтирования следует создавать отдельно для каждой задачи.
set -euo pipefail
set +x
umask 077
JOB_KEY="$(printf '%s' "${CI_JOB_ID:?}" | tr -cd 'A-Za-z0-9._-')"
IMAGE_ROOT="$HOME/ci-images"
IMAGE_PATH="$IMAGE_ROOT/$JOB_KEY.sparsebundle"
MOUNT_PATH="/Volumes/ci-$JOB_KEY"
mkdir -p "$IMAGE_ROOT" "$MOUNT_PATH"
chmod 700 "$IMAGE_ROOT"
printf '%s' "${CI_VOLUME_PASSWORD:?}" |
hdiutil create \
-size 80g \
-type SPARSEBUNDLE \
-fs APFS \
-volname "ci-$JOB_KEY" \
-encryption AES-256 \
-stdinpass \
"$IMAGE_PATH"
80g — это логический предел, а не немедленно занимаемые 80GB. Лимит должен учитывать пиковый объём исходного кода, зависимостей, производных данных и архивов, а также оставлять запас для журналов ошибок. Не указывайте пароль в аргументах команды, имени файла или журнале сборки. Сначала отключите трассировку команд, а затем передайте пароль через стандартный ввод из защищённой переменной CI.
Проверка сразу после подключения
Успешное создание образа ещё не означает, что выбрана правильная точка монтирования. Сценарий должен проверить, что целевой путь действительно является подключённым томом, и подтвердить тип файловой системы.
printf '%s' "$CI_VOLUME_PASSWORD" |
hdiutil attach \
-stdinpass \
-nobrowse \
-mountpoint "$MOUNT_PATH" \
"$IMAGE_PATH"
mount | grep -F "on $MOUNT_PATH "
diskutil info "$MOUNT_PATH" | grep -E 'File System Personality|Volume Name'
mkdir -p "$MOUNT_PATH/src" "$MOUNT_PATH/output" "$MOUNT_PATH/tmp"
После этого направьте на данный том каталог извлечения исходного кода, выходные данные сборки и временный каталог задачи. Общий кеш менеджера пакетов, доступный только для чтения, можно оставить за пределами тома, однако любой кеш, который задача способна изменить, следует копировать внутрь тома, чтобы избежать повреждения данных при параллельной записи.
Отключение как часть жизненного цикла, а не финальная команда
Очистку нельзя размещать только в последней строке сценария: ошибка компиляции, тайм-аут или сигнал завершения могут остановить задачу раньше. Используйте обработчик выхода, который централизованно выполняет синхронизацию, проверяет занятые файлы и отключает том, сохраняя при сбое достаточно диагностических данных.
cleanup_workspace() {
set +e
sync
if mount | grep -Fq "on $MOUNT_PATH "; then
lsof +D "$MOUNT_PATH" > "$IMAGE_ROOT/$JOB_KEY.lsof.txt" 2>/dev/null
hdiutil detach "$MOUNT_PATH"
fi
rmdir "$MOUNT_PATH" 2>/dev/null
}
trap cleanup_workspace EXIT INT TERM
export TMPDIR="$MOUNT_PATH/tmp"
cd "$MOUNT_PATH/src"
Команда lsof +D может выполняться медленно для большого каталога. Сначала можно проверить известные процессы сборки, тестирования и упаковки, а полное сканирование запускать только при ошибке отключения. Не применяйте принудительное отключение сразу: оно может скрыть продолжающий запись процесс и оставить незавершённые артефакты.
Обработка параллельных запусков, ограничений объёма и остаточных данных
При повторном запуске той же задачи старый образ может всё ещё существовать. Безопасный вариант — не перезаписывать его сразу, а сначала проверить, подключён ли он. Если образ подключён, необходимо заблокировать новую задачу и зафиксировать конфликт. Если он не подключён, его следует архивировать или удалить в соответствии с политикой хранения. Идентификатор задачи также должен включать номер текущего запуска, чтобы два запуска не обращались к одному образу.
Три обязательные проверки
Во-первых, перед запуском задачи выведите данные hdiutil info и убедитесь, что тома с таким именем нет. Во-вторых, во время сборки одновременно отслеживайте свободное место на томе хоста и подключённом томе: разреженный образ увеличивается, поэтому наличие места на логическом томе не означает, что оно есть на томе хоста. В-третьих, после завершения задачи просканируйте каталог образов и разрешите сохранять только те неудачные образцы, которые явно помечены как диагностические.
Различить эти два уровня ёмкости можно следующими командами:
df -h "$MOUNT_PATH"
df -h "$IMAGE_ROOT"
du -sh "$IMAGE_PATH"
hdiutil info
Если задачи определённого типа регулярно достигают предела, сначала отделите промежуточные артефакты, которые не нужно хранить долго, и только затем увеличивайте логический объём. Бесконтрольное увеличение образа лишь отсрочит момент, когда закончится место на диске хоста.
Границы безопасности и контрольный список внедрения
Секрет должен передаваться процессу только на этапах создания и подключения, после чего его необходимо немедленно удалить из экспортированных переменных текущей оболочки. Сценарии сборки не должны выводить переменные окружения или копировать пароль внутрь тома. Для файла образа следует сохранять права 600 или более строгие, а для корневого каталога образов — 700.
Перед вводом в эксплуатацию проверьте каждый пункт:
- runner работает под отдельной учётной записью без прав администратора;
- идентификатор задачи отфильтрован по белому списку, поэтому обход каталогов через путь невозможен;
- создание, подключение, сборка и отключение могут завершаться независимо друг от друга с понятным статусом;
- сигналы
EXIT,INTиTERMобрабатываются одной функцией очистки; - при ошибке отключения сначала регистрируются процессы, использующие том, без немедленного принудительного отключения;
- одновременно отслеживается объём тома APFS, файла разреженного образа и тома хоста;
- для образцов неудачных сборок установлен срок хранения, после которого весь образ удаляется;
- журналы не содержат паролей, закрытых ключей, полных учётных данных или фрагментов конфиденциального исходного кода.
Сначала проведите испытание на небольшом проекте без конфиденциальных данных по четырём сценариям: штатное завершение, ошибка компиляции, ручная остановка и почти исчерпанное место на диске. Такая изоляция становится пригодной для эксплуатации только в том случае, если при каждом из четырёх сценариев образ можно найти, объяснить его состояние и полностью освободить ресурсы.
Часто задаваемые вопросы
Заменяет ли шифрованный образ разграничение системных прав?
Нет. Он защищает сохранённые данные рабочей области, но дополнительно нужны отдельный пользователь запуска, минимальные права, контролируемая передача секрета и надёжное отключение тома.
Что делать, если после сбоя образ остался подключённым?
Найдите удерживающие точку подключения процессы через lsof, остановите только процессы этой задачи, выполните sync и повторите обычное отключение. Принудительный режим оставьте на крайний случай.
Выберите выделенную физическую машину для разработки, сборки и удалённого рабочего стола
Сравните три конфигурации на Apple Silicon и при оформлении заказа выберите узел, срок аренды и дополнительные параметры хранилища.