🌌 Підняти капсулу двічі — і не розбудити сусідів — С.І.Д. ставиться до цього завдання серйозніше, ніж воно виглядає. «Скрипт, який працює лише з чистого столу, — це не скрипт, ранере, це щасливий випадок. Мене доведеться піднімати ще багато разів, і не завжди з першої спроби. Зроби так, щоб другий запуск був нудним». Саме тут народжується перший файл ядра — `core/capsule/deploy.sh`, який доживе до останньої ночі.
Операція ідемпотентна, якщо її повторний запуск не змінює результату: після другого,
третього й десятого разу система в тому самому стані, що після першого, і код виходу
лишається 0. Це не педантизм, а єдиний спосіб мати скрипт, якому можна довіряти: скрипт
запускають після збою, після перезавантаження, посеред налагодження — і майже ніколи з
чистого столу.
Найчастіша неідемпотентна команда цієї роботи — docker run --name. Друга спроба падає з
Conflict. The container name "/capsule" is already in use, і разом із нею падає весь
скрипт.
set -euo pipefail
| Прапорець | Що робить | Чому без нього гірше |
|---|---|---|
-e |
уриває скрипт на першій невдалій команді | інакше скрипт бадьоро йде далі після провалу й «успішно» завершується |
-u |
помилка на зверненні до неоголошеної змінної | rm -rf "$DIR/" з порожнім DIR — класика, яку -u ловить |
-o pipefail |
конвеєр невдалий, якщо впала будь-яка його ланка | без нього false | tee log вважається успіхом |
Ідемпотентність робиться не || true, а питанням про стан. || true глушить помилку
разом із її причиною; перевірка стану дає скрипту гілку.
if docker ps -aq --filter name="^capsule$" | grep -q .; then
echo "[deploy] капсула вже піднята — нічого не роблю"
else
docker run -d --name capsule -p "${HOST_PORT}:80" ...
fi
Два дрібні місця, де легко помилитися:
--filter name=capsule знаходить і capsule, і capsule-old, і my-capsule — фільтр
працює підрядком. Потрібне саме ^capsule$;docker ps без -a не бачить зупинених контейнерів, а ім'я вони тримають. Тому
перевірка на існування — з -a.| Шлях | Коли доречний | Ціна |
|---|---|---|
docker rm -f capsule перед запуском |
капсула одноразова, стану всередині немає | знищує дані всередині контейнера |
| перевірити й не створювати вдруге | усередині є стан, який має пережити запуск | складніше: треба вирішити, що робити зі зміненою конфігурацією |
Для deploy.sh цієї роботи підходить будь-який — але вибір треба проговорити на
захисті, бо в ЛР3 у капсулі з'явиться том із даними, і перший шлях почне їх стирати.
Якщо скрипт створює тимчасове (файл, мережу, проміжний контейнер), прибирання чіпляють до виходу — тоді воно спрацює і на помилці:
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
Скрипт має завершуватися 0, коли стан досягнутий (незалежно від того, чи довелося щось
робити), і ненульовим — коли досягнути не вдалося. Перевірка проста:
./deploy.sh && ./deploy.sh && echo "обидва запуски успішні"
Коди виходу в оболонці — це один байт: exit 3101 дасть 29, бо 3101 % 256 == 29.
Осмислені коди тримають у діапазоні 1–125.
Прочитати про set -e, set -u і set -o pipefail у bash(1) (розділ SHELL BUILTIN COMMANDS → set) і про коди виходу; довідку docker ps --filter і docker inspect (обидва вміють відповідати кодом виходу, а не лише текстом). Прогнати власний скрипт через ShellCheck і прочитати кожне його зауваження.
core/capsule/deploy.sh, який піднімає капсулу з проброшеним портом зі змінної середовища HOST_PORT (без хардкоду), і запустити його двічі підряд; обидва запуски мусять завершитися кодом 0, а другий — повідомити, що робити нічого не треба.set -euo pipefail: зламати скрипт тричі (невдала команда, неоголошена змінна, впала ланка конвеєра) і зафіксувати поведінку з прапорцем і без нього.|| true і письмово показати, що саме при цьому втрачається: навести випадок, коли скрипт із || true завершується успішно, хоча капсула не піднялася.--filter name=capsule знаходить зайве: створити контейнер capsule-old, показати хибне спрацювання й полагодити фільтр на ^capsule$. Окремо показати, що без -a зупинена капсула «не існує» для перевірки, хоч ім'я тримає.trap на прибирання тимчасового файлу, навмисно урвати скрипт помилкою посеред роботи й показати, що прибирання все одно відбулося. Прогнати підсумковий скрипт через ShellCheck і закрити всі зауваження або обґрунтувати кожне залишене.`docker run --name capsule ...` на другому запуску падає з `name is already in use`. Полагодити це можна щонайменше двома способами: видалити наявний контейнер або перевірити його наявність і не створювати вдруге. Чим ці два шляхи відрізняються за наслідками для даних усередині контейнера?
deploy.sh, який С.І.Д. пам'ятає, виходив з кодом 29. «Не питай, ранере, чому саме двадцять дев'ять. Порахуй: коди виходу в оболонці живуть за модулем 256, а 3101 за модулем 256 — це рівно 29. Я тоді вважав це підписом. Тепер вважаю це помилкою, яку ти більше не повториш: код виходу — це відповідь, а не автограф».Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.
echoecho $$ — PID поточної оболонки).docker psdocker ps -a — разом із зупиненими).dockerdocker <підкоманда> --help — довідка).bashdocker inspect-f '{{.State.Pid}}' — витягнути одне поле.setset -euo pipefail — падати на першій помилці, невідомій змінній чи збої в конвейєрі.grepps aux | grep bash).docker rundocker run -it ubuntu:24.04 bash — інтерактивно з терміналом; -d — у фоні; --rm — видалити після виходу; -p 8080:80 — прокинути порт).rmrm -rf dir — каталог рекурсивно і без запитань (обережно: відміни немає).docker rmdocker rm -f name — навіть якщо він ще працює).💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.