Курс › СРС › SRS02
до ЛР1 рівень 3 → «5»↔ Лабораторна 1↔ Лекція 1⌨️ команди 10
Самостійна робота · ~3 год

Ідемпотентність скрипта розгортання: запустити двічі й не зламати

🌌 Підняти капсулу двічі — і не розбудити сусідів — С.І.Д. ставиться до цього завдання серйозніше, ніж воно виглядає. «Скрипт, який працює лише з чистого столу, — це не скрипт, ранере, це щасливий випадок. Мене доведеться піднімати ще багато разів, і не завжди з першої спроби. Зроби так, щоб другий запуск був нудним». Саме тут народжується перший файл ядра — `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 і прочитати кожне його зауваження.

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. Написати core/capsule/deploy.sh, який піднімає капсулу з проброшеним портом зі змінної середовища HOST_PORT (без хардкоду), і запустити його двічі підряд; обидва запуски мусять завершитися кодом 0, а другий — повідомити, що робити нічого не треба.
  2. Показати на власному прикладі, що робить кожен із трьох прапорців set -euo pipefail: зламати скрипт тричі (невдала команда, неоголошена змінна, впала ланка конвеєра) і зафіксувати поведінку з прапорцем і без нього.
  3. Замінити перевірку стану на || true і письмово показати, що саме при цьому втрачається: навести випадок, коли скрипт із || true завершується успішно, хоча капсула не піднялася.
  4. Довести, що --filter name=capsule знаходить зайве: створити контейнер capsule-old, показати хибне спрацювання й полагодити фільтр на ^capsule$. Окремо показати, що без -a зупинена капсула «не існує» для перевірки, хоч ім'я тримає.
  5. Додати trap на прибирання тимчасового файлу, навмисно урвати скрипт помилкою посеред роботи й показати, що прибирання все одно відбулося. Прогнати підсумковий скрипт через ShellCheck і закрити всі зауваження або обґрунтувати кожне залишене.
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Що таке ідемпотентність і чому для скрипта розгортання вона важливіша за лаконічність?
  2. Що робить кожен із трьох прапорців у `set -euo pipefail` і що ламається без кожного з них?
  3. Чому `docker run --name` неідемпотентний, і які два способи це виправити?
  4. Чим перевірка стану краща за `|| true`?
  5. Чому `--filter name=capsule` — не те саме, що `--filter name=^capsule$`, і навіщо тут `-a`?
  6. Що станеться з даними в контейнері за підходу «спершу `docker rm -f`», і чому це питання стане критичним уже в ЛР3?
  7. Яким кодом виходу мусить завершитися скрипт, якщо потрібний стан уже досягнутий, і чому?
✅ САМОПЕРЕВІРКА

`docker run --name capsule ...` на другому запуску падає з `name is already in use`. Полагодити це можна щонайменше двома способами: видалити наявний контейнер або перевірити його наявність і не створювати вдруге. Чим ці два шляхи відрізняються за наслідками для даних усередині контейнера?

🔗 ДЖЕРЕЛА
🥚 ПАСХАЛКА
Перший deploy.sh, який С.І.Д. пам'ятає, виходив з кодом 29. «Не питай, ранере, чому саме двадцять дев'ять. Порахуй: коди виходу в оболонці живуть за модулем 256, а 3101 за модулем 256 — це рівно 29. Я тоді вважав це підписом. Тепер вважаю це помилкою, яку ти більше не повториш: код виходу — це відповідь, а не автограф».
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 10▾

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

echo
Друкує рядок або значення змінної (наприклад echo $$ — PID поточної оболонки).
docker ps
Список запущених контейнерів (docker ps -a — разом із зупиненими).
docker
Клієнт Docker: усі дії з образами й контейнерами — через його підкоманди (docker <підкоманда> --help — довідка).
bash
Командна оболонка (shell) Linux: читає команди, які ти набираєш, і запускає програми.
docker inspect
Друкує повний JSON-опис контейнера чи образу; -f '{{.State.Pid}}' — витягнути одне поле.
set
Налаштовує режим оболонки в скрипті; set -euo pipefail — падати на першій помилці, невідомій змінній чи збої в конвейєрі.
grep
Фільтрує текст: виводить лише рядки, що містять шаблон (ps aux | grep bash).
docker run
Створює й запускає контейнер з образу (docker run -it ubuntu:24.04 bash — інтерактивно з терміналом; -d — у фоні; --rm — видалити після виходу; -p 8080:80 — прокинути порт).
rm
Видаляє файли; rm -rf dir — каталог рекурсивно і без запитань (обережно: відміни немає).
docker rm
Видаляє контейнер (docker rm -f name — навіть якщо він ще працює).

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.

← SRS01усі темиSRS03 →