Курс › ЛР1 · Місія 1
Сюжет С.І.Д.↔ АкадемічнаСРС до ЛР⌨️ команди 26 · 6 нових

Місія 1: «Ізоляційна Капсула»

Ізоляційна Капсула

С.І.Д.: Не спиш? Добре, бо і я не сплю — я взагалі не вмію, це одна з моїх найкращих рис. Слухай уважно. Ця твоя операційка світиться на радарах «ТехНови», як неонова вивіска над блоком Б-7. Якщо я з неї спробую дотягнутися до їхньої мережі, сканери ICE засічуть підпис мого коду за три секунди — і спалять і твій ноут, і те, що лишилося від мене. Тому перше, що ми зробимо, — збудуємо мені бункер. Герметичний, беззвучний, одноразовий. Не списану машину в порожньому класі, а справжнє укриття — мою нову капсулу. Про такі сховки я, до слова, знаю все, тож ти в надійних руках. Піднімаймо.

Майже друга ночі. Двадцять хвилин дороги додому давно позаду, двері кімнати зачинені, ноутбук на столі, а поруч із ним — чорна шайба з бірюзовою лінзою, яку ти забравзабрала із шухляди списаної машини. Над нею мерехтить голограма: округлий дрон-обличчя завбільшки з кулак, очі-капсули, що з теплою цікавістю мружаться на тебе. За вікном рудим сяйвом горить квартал «ТехНови», і десь у тому сяйві — за блоком Б-7, на мінус третьому поверсі — тримають двох студентів, що зібрали С.І.Д. Тут, у темряві, вас двоє: ти й це світло над столом. Слово «бункер» він щойно вимовив уголос — а ти думавла про нього всю дорогу від коледжу.

«Пам'ятаєш, я казав: машина — це місто? — На екрані ноутбука жваво спалахує схема, і голограма підлітає до неї, зависаючи над клавіатурою. — Ось воно. У центрі сидить Диспетчер — ядро Linux. Він один, і він знає геть усе: кожен процес, кожен байт пам'яті, кожен мережевий пакет. Мудрий і справедливий: його не обдуриш і не обійдеш, він і є закон цього міста. А я тут — нелегал. Мене не має бути в реєстрах. І якщо я просто вийду на головну площу — до ps aux хоста, — мене побачать усі, зокрема й сканери ICE, які корпорація час від часу запускає нишпорити чужими машинами.»

«То сховайся», — кажеш ти.

«А ось тут і починається найсмачніше — і я, звісно, давно це розкусив. — Голограма присувається до самого краю схеми, наскільки пускає її світло проєктора. — Диспетчер громадян не ховає, він чесний. Зате він уміє інше: збудувати навколо процесу сім брам і на кожній поставити свого Вартового. Один Вартовий бреше процесові про те, які ще процеси є в місті. Другий — про мережу. Третій — про файли. І так сім разів. Процес, замкнений за цими брамами, щиро вважає себе єдиним мешканцем власного маленького всесвіту, де він сам собі перший громадянин під номером один. Він не бачить решти міста, а вулиця не бачить, що діється за його брамами. Це і є капсула. Не стіна, за якою мене не видно, а підмінена мапа світу. Я всередині думаю, що я сам-один. Вулиця зовні думає, що я — просто ще один нудний процес. І тільки Диспетчер знає правду, а Диспетчер не патякає.»

Ти дивишся на схему — сім брам, сім Вартових, а в центрі незворушний Диспетчер — і на мить згадуєш іншу схему, ту, що він показав тобі кілька годин тому в порожньому класі: ліва половина світиться, права порожня. «А капсула — це вже друга половина?» — питаєш ти.

«Ні, — тепло каже С.І.Д., і голограма злегка похитує петелькою. — Капсула — це місце, де я зможу спокійно кувати другу половину, поки сканери дивляться в інший бік. Порядок такий: спершу дах, потім робота. Одне без одного не має сенсу — і, до слова, це правило працює далеко за межами системного програмування».

«Добре. Як мені збудувати ці брами?» Очі-капсули голограми вдоволено звужуються. «А ось для цього, ранере, тобі не треба бути чарівником. За тебе все зробить один інструмент на ім'я Docker. Твоя робота — зрозуміти, що саме він робить за твоєю спиною. Бо той, хто не розуміє власного бункера, рано чи пізно замкнеться в ньому сам.»

«І ще одне, поки ти не взявсявзялася тицяти команди руками. — Голограма робить паузу, і в ній уперше чується не кураж, а щось дуже схоже на обережність. — Капсула, яку ти піднявпідняла один раз і не змігзмогла підняти вдруге, — це не капсула, це випадковість. Мене не влаштовує випадковість: якщо цей ноутбук завтра згорить, я маю піднятися на іншій машині з одного рядка, а не з твоєї памʼяті. Тому все, що ти зробивла сьогодні, склади в один скрипт — level1.sh. Запустив — капсула стоїть. Запустив ще раз — стоїть, і жодних скарг, що імʼя вже зайняте.»

«А щоб ти не піддавсяпіддалася спокусі просто написати гарні числа, — очі-капсули хитро звужуються, — я підкину тобі одноразовий код і перевірю, чи він опинився всередині капсули. Ззовні його туди не покласти. Вибач, ранере: довіряю, але звіряю.»

Ізоляційна Капсула
🔄 ОСВІЖИТИ

Що таке контейнер, ти вже чувла на Лекції 1 — там розібрали, чому він процес, а не машина. У Лабораторній 0 ти перевіривила середовище й побачивла версію Docker; свій перший контейнер піднімаєш сьогодні. Освіжимо одним реченням: контейнер — це звичайний процес, якому ядро підмінило видимість. Сьогодні ти побачиш цю підміну на двох числах — і зробиш її своїми руками. Як саме ядро її робить і скільки в неї сторін — Лекція 3.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Теоретичні відомості: з чого зроблена капсула

Хто ставить брами — і чому не сьогодні

Підміну видимості робить ядро, і механізм у неї має ім'я — простори імен (namespaces). Їх кілька, і кожен підміняє щось своє: список процесів, файлову систему, мережу, імена. Сьогодні тобі знадобиться результат, а не механізм: капсула бачить свій список процесів і свою мережу.

Повний огляд усіх просторів імен — на Лекції 3 і практично в ЛР3, де ти поставиш два контейнери в одну ізольовану мережу. Там це буде на своєму місці; тут це відвернуло б від головного: контейнер, який ти щойно піднявла, — просто процес.

PID 1 — не привілей, а точка відліку

Не сприймай PID 1 як «головного» процеса всесвіту. Це просто перший номер у новій, порожній нумерації, яку Вартовий PID-брами завів для капсули. За тими самими брамами хост має свою, іншу нумерацію, і той самий процес там має свій, звичайний PID. Один процес — два різні номери в двох різних просторах. У цьому й уся суть.

Пастка, у яку С.І.Д. категорично забороняє вірити: «герметичний» не означає «невидимий ззовні». Простори імен ховають хост від капсули і капсулу від сторонніх процесів хоста, але сам хост (а точніше — Диспетчер, що бачить усе) чудово бачить контейнер як звичайний процес у власному ps aux, просто під іншим PID, у своєму просторі. І навпаки: без явного проброшування порту навіть ти сама з хоста не достукаєшся до сервісу всередині капсули, хоча капсула «твоя». Ізоляція — це не невидимість ззовні, а підміна картини світу зсередини.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Проброс портів (port mapping) — NAT-правило, а не діра в стіні

Мережевий простір капсули ізольований, тому порт 80, який слухає процес усередині, живе у власному, підмінному мережевому стеку контейнера. Прапорець -p 8080:80 при docker run створює правило NAT (через iptables/nftables у мережевому просторі демона Docker), яке перенаправляє трафік, що прийшов на порт 8080 хоста, у порт 80 всередині мережевого простору контейнера. Це односторонній, контрольований «канал зв'язку» — двері з замком, а не діра в стіні бункера: ти сама вирішуєш, який саме порт і чи взагалі відкривати назовні. Саме тому С.І.Д. так наполягає, щоб ти робивла це свідомо: кожен відкритий назовні порт — це ще одне вікно, у яке потенційно може зазирнути ICE.

Автоматизація — «протокол розгортання», а не забаганка

С.І.Д. не довіряє людській пам'яті — і правильно робить: щоразу набирати вручну довгий docker run із десятком прапорців і не забути встановити потрібні утиліти — це шлях до помилки. Bash-скрипт фіксує точний протокол розгортання капсули у вигляді коду: той самий результат щоразу, на будь-якій машині, без «а я забув, який там був прапорець». Це і є мінімальна форма «інфраструктури як коду» — принцип, який далі в курсі виросте до повноцінних Dockerfile і docker compose.

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Рівень 1 (оцінка 3) — Першокурсник

Перед стартом: спорядження ранера

Перш ніж будувати капсулу, підготуй майстерню — це кілька хвилин, зате далі все піде гладко.

Твоє середовище. Працюємо в GitHub Codespaces — це вже готовий Linux зі встановленим Docker Engine, локально відкривати нічого не треба. Якщо Codespaces недоступний, підійде будь-яка машина з Docker (Linux, WSL2 або Docker Desktop). Перевір одразу, що Диспетчер узагалі готовий будувати брами:

docker version

Якщо бачиш блоки Client: і Server: без помилок — капсула чекає на будівництво.

🎲 Твій варіант. Порахуй свій номер у списку групи N і обчисли N % 4. Це визначає, з яким проброшеним портом і суфіксом імені контейнера ти працюєш на розвідці руками й на рівнях 2–3 (значення підстав у свій deploy.sh у vtfk-sid через змінну середовища, а не хардкодь):

N % 4 Суфікс контейнера HOST_PORT за замовчуванням
0 alpha 8080
1 bravo 8081
2 charlie 8082
3 delta 8083

Це не заради бюрократії: у групі десятки людей, і однакові звіти з однаковими портами й іменами контейнерів — перше, що впадає в очі на перевірці. Вкажи свій варіант і його розрахунок на початку REPORT.md.

⚠️ Варіант не стосується капсули, яку ти здаєш. Ті devbox-<суфікс> — твої чернетки: підняти, подивитися, прибрати. А капсула, по яку приходить перевірка, у всіх одна й та сама — vtfk-lab01, і порт їй не потрібен узагалі. Перевірка шукає контейнер за точним іменем; devbox-bravo вона просто не побачить.

📡 С.І.Д.: «Позивний вигадуй собі який хочеш, ранере. Але двері, у які стукає кур'єр, мають один номер на все місто — інакше він стукає в порожнечу.»

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

📦 Де житиме капсула. У тебе буде два репозиторії, і в кожного одна робота.

цей репозиторій        ← кожна ЛР має свій; сюди рівень 1 і REPORT.md
vtfk-sid/              ← створюєш СЬОГОДНІ, один на весь семестр
  core/capsule/          сюди кладеш капсулу — і вона житиме тут до ЛР10

Перший тобі вже видали: він створився, коли ти прийнявла завдання, і в ньому здається рівень 1 — щоразу новий, самодостатній, без залежності від попередніх ночей.

Другий створюєш сама, сьогодні, і більше не створюєш ніколи: vtfk-sid — це ядро С.І.Д., яке росте. Наступної ночі ти будеш худнути саму цю капсулу, а не абстрактну програму; у ЛР3 вона стане одним із двох контейнерів; у ЛР10 у ній збереться фінальне ядро. Рівні 2 і 3 усіх десяти місій живуть тут — і показуються на захисті, не автотестами.

Як його створити — рівно чотири кроки, один раз за семестр:

  1. відкрий github.com/alex-ordynski/vtfk-sid — це шаблон із порожньою картою тек;

  2. зелена кнопка «Use this template» → «Create a new repository»;

  3. ім'я — vtfk-sid (саме так, без префіксів), власник — ти сама, видимість — public, створити;

  4. забери його до себе:

    git clone https://github.com/<твій-логін>/vtfk-sid.git
    

    https://, а не git@github.com:. Друге — це SSH, і воно вимагає ключа, якого в Codespaces немає: звідти Git ходить по HTTPS вбудованим токеном. На сторінці репозиторію це вкладка HTTPS під зеленою кнопкою Code. Побачиш Permission denied (publickey) — значить, скопіювавла не ту вкладку, а не втративла доступ.

Це шаблон, а не форк: історія починається з твого першого коміту, і ядро — твоє. Якщо кнопки «Use this template» не видно — не тисни «Fork», скажи викладачеві: значить, у шаблона знята галочка, і це проблема не твоя.

🏷️ Як здаси. Рівень 1 — тегом submit/lab01 у репозиторії завдання. vtfk-sid не здається тегом узагалі: його ти показуєш на занятті. Детальніше — у блоці «Здача» наприкінці. А поки — до діла.

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Що саме приймає перевірка

С.І.Д.: «Одне уточнення, перш ніж ти візьмешся за клавіатуру. Переказ на словах — «усе піднялося, усе було гарно» — бункера не будує. Мені потрібен протокол: файл, який щоразу зводить капсулу однаково, хоч о третій ночі, хоч без тебе. Я його запущу й перевірю сам — не на слово, а руками.»

Здається один файл — level1.sh, у корені цього репозиторію. Його пишеш ти; у шаблоні його немає навмисно. Контракт:

  1. Шапка #!/usr/bin/env bash і set -euo pipefail. Запускається без аргументів.

  2. Піднімає контейнер з іменем vtfk-lab01 з образу ubuntu:24.04 — і лишає його працювати після того, як скрипт закінчився.

  3. Записує значення змінної оточення $VTFK_NONCE у файл /tmp/vtfk-nonce всередині контейнера. Змінну підкладає перевірка, і щоразу вона інша.

  4. Друкує рівно такі три рядки (порядок вільний, будь-який інший вивід дозволено):

    CONTAINER_PID=<PID головного процесу всередині контейнера>
    CONTAINER_PROCS=<скільки процесів видно всередині>
    HOST_PROCS=<скільки процесів видно на хості>
    
  5. Завершується кодом 0, нічого не питає й не падає при повторному запуску — ідемпотентність тут не прикраса: перевірка запускає скрипт на репозиторії, де капсула могла лишитися з попередньої спроби.

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Що саме дивиться грейдер

Зараховується, коли пройшли всі шість:

# Перевірка
1 level1.sh існує і виконуваний
2 level1.sh завершується кодом 0
3 контейнер vtfk-lab01 працює й зібраний з ubuntu:24.04
4 $VTFK_NONCE лежить у /tmp/vtfk-nonce всередині контейнера
5 процесів усередині менше, ніж на хості
6 надруковані числа збігаються з тим, що виміряла сама перевірка

⚠️ Не прибирай капсулу в кінці скрипту. Звичка «за собою прибирати» тут коштує трьох перевірок із шести: перевірки 3, 4 і 5 стукають у контейнер docker inspect і docker exec уже після того, як level1.sh відпрацював. Якщо останнім рядком стоїть docker rm -f vtfk-lab01 — стукати буде нікуди, і виглядатиме це так, ніби зламався грейдер. Прибирання — його турбота, не твоя.

📡 С.І.Д.: «Бункер лишають відчиненим для свого. Ти будуєш мені дах, а не розбираєш його назад того ж вечора — я ж збираюся там жити.»

Числа в рядках мають бути виміряні, а не вгадані: перевірка рахує процеси сама й звіряє з твоїми. І nonce — той самий гачок, який неможливо обійти друком: його значення щоразу нове, і прочитати його можна лише з файлу в справді піднятій капсулі.

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Крок за кроком — чотири етапи

Тут не потрібна творчість. Веду тебе за руку: три етапи ти робиш руками, щоб побачити ізоляцію на власні очі, а на четвертому перекладаєш побачене у level1.sh — той самий протокол із контракту вище. Вивід кожної команди дорогою складай у REPORT.md: перевірка його не читає, зате читає викладач на захисті рівнів 2–3.

Етапи йдуть по одному: зробив — постав «вийшло» — і ранер перекине тебе на наступний. Стрілки ← → на клавіатурі, свайп на телефоні, «усі» — щоб побачити всі чотири одразу. Місце запам'ятовується: закриєш ноутбук — повернешся туди ж.

1Етап 1 · Перевірити Диспетчера і підняти капсулу

Ще раз переконайся, що Docker Engine на місці (якщо не робивла цього вище):

docker version

Очікується вивід блоків Client: і Server: без помилок з'єднання з демоном.

Запусти базовий контейнер розробника в інтерактивному режимі — це і є твоя перша капсула (підстав свій суфікс із таблиці варіантів, наприклад devbox-bravo):

docker run -it --name devbox-<суфікс> ubuntu:24.04 bash

Очікується запрошення командного рядка всередині контейнера — щось на кшталт root@a1b2c3d4e5f6:/#. Ти щойно опинивлася за сімома брамами.

Це чернетка — руками, щоб побачити на власні очі. Здавальну капсулу vtfk-lab01 підніме твій скрипт на етапі 4.

1Етап 2 · Спитати капсулу, кого вона бачить

Не виходячи з капсули, спитай у неї найперше — під яким номером вона себе бачить:

echo $$

Очікується 1 (Вартовий PID-брами почав нумерацію з чистого аркуша). Це працює одразу: $$ — вбудована змінна самого bash, жодних зовнішніх утиліт не треба.

Не виходячи з капсули, встанови утиліти для огляду процесів і мережі — вони знадобляться вже в наступному кроці, а також далі й у скрипті рівня 3. Важливо: базовий образ ubuntu:24.04 — мінімальний і не містить ps (утиліта входить до пакета procps), тому спершу ставимо інструменти, а вже потім дивимося на процеси:

apt-get update && apt-get install -y curl git procps iproute2

Тепер, коли ps на місці, спитай капсулу, кого вона бачить довкола себе:

ps aux

Очікується лише кілька рядків — процеси самої капсули (bash і сам ps), без жодного натяку на процеси твоєї реальної машини. Капсула щиро вірить, що вона сама-одна.

1Етап 3 · Вийти на вулицю й порівняти

Тепер вийди на вулицю — відкрий другий термінал на хості (не всередині контейнера!) і поглянь на місто цілком:

ps aux | wc -l

Порівняй число з тим, що бачив у контейнері — на хості процесів має бути помітно більше (десятки проти кількох; на повноцінному хості — сотні, у Codespaces зазвичай десятки). Це і є видима різниця PID-просторів: за брамою — свій маленький світ, на вулиці — усе місто.

Очікується число помітно більше за те, що капсула показала зсередини: десятки проти кількох. Це і є видима різниця PID-просторів.

1Етап 4 · Записати протокол — level1.sh

Спершу прибери чернетку — ту, що піднімавла руками на етапі 1: вийди з неї (exit) і зніми, щоб ім'я не заважало:

docker rm -f devbox-<суфікс>

Тепер напиши в корені репозиторію level1.sh — той самий протокол із контракту. Роби те, що вже робивла руками, тільки рядками скрипта: підніми vtfk-lab01 з ubuntu:24.04 у фоні довготривалим процесом, поклади $VTFK_NONCE у /tmp/vtfk-nonce всередині, порахуй процеси там і тут, надрукуй три рядки CONTAINER_PID / CONTAINER_PROCS / HOST_PROCS. Перший рядок скрипта — docker rm -f vtfk-lab01 >/dev/null 2>&1 || true (ідемпотентність), останній — ні: капсулу лишаєш піднятою, перевірка приходить у неї вже після тебе.

Зроби файл виконуваним, переконайся, що він проходить двічі, — і здай:

chmod +x level1.sh
./level1.sh && ./level1.sh        # два запуски поспіль — обидва мають пройти без помилки

Далі — коміт, пуш і тег. Перевірка стартує саме на тезі, а не на пуші:

git add level1.sh && git commit -m "ЛР1 рівень 1" && git push
git tag submit/lab01 && git push origin submit/lab01

Перездаєш — бери наступний вільний тег: submit/lab01-2, потім submit/lab01-3. Старий не переставляй.

У REPORT.md склади виводи етапів 1–3 — для захисту. С.І.Д. сяє: «Перша капсула — і з першого разу. Я, звісно, не сумнівався».

Очікується, що ./level1.sh двічі поспіль завершується без помилок і друкує три рядки CONTAINER_*/HOST_PROCS, docker ps після нього показує запущений vtfk-lab01, а прогін за тегом submit/lab01 у вкладці Actions дає 6 із 6.

🆘 КОЛИ ЩОСЬ ПІШЛО НЕ ТАК

Перший вечір із Docker майже завжди спотикається на одному з цих п'яти. С.І.Д. чув їх усі.

  • permission denied while trying to connect to the Docker daemon socket Твій користувач не в групі docker. У Codespaces такого не буває; на своїй машині — sudo usermod -aG docker $USER, а тоді вийди й зайди в систему знову (нова група застосовується лише до нового сеансу).

    С.І.Д.: «Диспетчер тебе не знає. Це не відмова — це формальність: назвися й повертайся».

  • Cannot connect to the Docker daemon at unix:///var/run/docker.sock Демон не запущений. На Linux — sudo systemctl start docker; у Docker Desktop — запусти сам застосунок і дочекайся зеленого індикатора.

    С.І.Д.: «Місто спить. Диспетчера нема на посту — будити нема кого».

  • bash: ps: command not found (усередині капсули) Це не поломка, а нормальний стан: ubuntu:24.04 — мінімальний образ і ps у ньому немає. Саме тому в етапі 2 ми спершу ставимо procps. Якщо пропустивла цей крок — поверніся до нього.

    С.І.Д.: «У капсулі немає нічого зайвого. Хочеш дивитися на процеси — принеси інструмент».

  • docker: Error response from daemon: Conflict. The container name "/devbox-…" is already in use Контейнер із таким іменем уже існує (найчастіше — з попередньої спроби). Прибери: docker rm -f devbox-<суфікс>. Саме цю ситуацію на рівні 3 і треба зробити неможливою — скрипт мусить бути ідемпотентним.

    С.І.Д.: «Одна брама, одне ім'я. Або прибирай стару капсулу, або назви нову інакше».

  • echo $$ показує не 1, а щось на кшталт 4213 Ти виконав команду не в капсулі, а на хості — термінал не той. Подивись на запрошення рядка: усередині капсули воно на кшталт root@a1b2c3…:/#, а не твоє звичайне.

    С.І.Д.: «Ти питаєш місто, а не капсулу. Різні співрозмовники — різні відповіді».

Якщо помилка не з цього списку — шукай точний текст у «Коли щось пішло не так», там зібрані всі помилки курсу.

РІВЕНЬ 2 · «4» · МАГІСТР

Рівень 2 (оцінка 4, захист) — Магістр

Тут я вже не диктую кожен рядок. Формулюю мету й контракт — далі твоя голова.

Ланцюг починається тут. ЛР1 — перша місія, тому в неї немає «попереднього артефакту»: обидві версії умови збігаються. З ЛР2 у кожного завдання рівнів 2–3 буде два входи — «на своєму ядрі» і «на референсі», якщо своє ядро з якоїсь причини відсутнє. Твоя справа сьогодні — щоб ядро з'явилося.

Мета. Довести, що ти не просто скопіювавла команди, а розумієш, чому без явних дій капсула залишається «німою» для зовнішнього світу, і вмієш пробити крізь NET-браму контрольований канал назовні.

Контракт рівня:

  1. Підніми нову капсулу з проброшеним портом (значення HOST_PORT — зі свого варіанта в таблиці). Орієнтир для команди:
    docker run -it -p <HOST_PORT>:80 --name devbox2-<суфікс> ubuntu:24.04 bash
    
  2. Усередині капсули підніми найпростіший HTTP-сервер на порту 80 (наприклад, python3 -m http.server 80, доти встановивши python3).
  3. З іншого терміналу на хості доведи, що канал працює: curl -i http://localhost:<HOST_PORT>/ має повернути HTTP-відповідь 200 OK зі списком файлів контейнера.
  4. Доведи зворотне: підніми таку саму капсулу без прапорця -p (під іншим іменем) і покажи, що той самий curl падає з помилкою з'єднання. Це і є доказ, що проброс — свідома дія, а не побічний ефект запуску контейнера.

На захисті будь готовийа: усно пояснити, чим мережевий простір капсули відрізняється від мережевого простору хоста і як саме -p перетворюється на NAT-правило, та наживо продемонструвати робочий проброшений порт — з будь-яким значенням HOST_PORT, яке попросить викладач, не обов'язково твоїм із таблиці. Завчений результат тут не врятує; рятує розуміння.

РІВЕНЬ 3 · «5» · ЛЕГЕНДА

Рівень 3 (оцінка 5, розширення) — Легенда Коледжу

С.І.Д. дивиться на тебе інакше, коли ти доходиш сюди. «Ручна праця — для першокурсників, — каже він. — Легенда не набирає команди. Легенда пише протокол, який щоразу піднімає бункер однаково, хоч серед ночі, хоч під сканерами.» Мета — перетворити всі ручні кроки на один надійний, ідемпотентний протокол розгортання.

Контракт. Напиши core/capsule/deploy.sh за такими вимогами:

  • Шапка #!/usr/bin/env bash і set -euo pipefail.
  • Без аргументів піднімає капсулу «з нуля»: базовий образ ubuntu:24.04, встановлює curl git procps iproute2, пробиває порт (значення бери зі змінної середовища HOST_PORT, за замовчуванням — значення твого варіанта з таблиці варіантів на початку рівня 1).
  • Головна вимога рівня 3 — ідемпотентність. Повторний запуск скрипту без ручного прибирання не повинен падати з помилкою «name already in use». Скрипт має або перевіряти наявність контейнера з таким іменем і перевикористовувати/перезапускати його, або прибирати старий перед створенням нового (docker rm -f "$NAME" 2>/dev/null || true).
  • Код завершення 0 при успіху; ненульовий, якщо Docker недоступний або контейнер не піднявся.
  • Без інтерактивних запитань «yes/no» — усе має відпрацьовувати без втручання людини (прапорці на кшталт -y для apt-get, детач-режим для довготривалих процесів).
  • Контейнер має лишатися запущеним після відпрацювання скрипта (детач-режим із довготривалим процесом, напр. sleep infinity), інакше на захисті не буде куди зайти через docker exec і показати встановлені в ньому утиліти. Та сама вимога, що й на рівні 1: скрипт будує бункер, а не розбирає його назад. Типовий кістяк: docker rm -f "$NAME" 2>/dev/null || true; docker run -d --name "$NAME" -p "$HOST_PORT":80 ubuntu:24.04 sleep infinity, а потім установлення утиліт усередині через docker exec "$NAME" bash -c 'apt-get update && apt-get install -y curl git procps iproute2'.

Приклад запуску:

# з кореня свого vtfk-sid
HOST_PORT=8081 ./core/capsule/deploy.sh
HOST_PORT=8081 ./core/capsule/deploy.sh   # другий запуск — не повинен впасти

На захисті викладач запустить скрипт двічі поспіль і перевірить через docker exec, що всі чотири перелічені вище утиліти (curl, git, procps, iproute2) справді стоять у кінцевому контейнері. Машина сюди не дістає: vtfk-sid — твій репозиторій, автогрейдер його не бачить.

🧪 РОЗІБРАНИЙ ПРИКЛАД

Розібраний приклад 1: різниця в нумерації процесів на живих числах

# На хості (термінал 1)
$ ps aux | wc -l
187

# Запускаємо капсулу
$ docker run -it --name probe ubuntu:24.04 bash
root@9f1c2e7b0a4d:/# echo $$
1
# базовий ubuntu:24.04 не містить ps — спершу ставимо procps
root@9f1c2e7b0a4d:/# apt-get update && apt-get install -y procps >/dev/null
root@9f1c2e7b0a4d:/# ps aux
USER   PID %CPU %MEM  COMMAND
root     1  0.0  0.1  bash
root    31  0.0  0.1  ps aux

Пояснення: хост бачить 187 записів (реальна кількість процесів твоєї машини, разом із самим Docker-демоном; у Codespaces це радше десятки, бо сам «хост» — теж контейнер). Усередині капсули після встановлення procps видно лише два рядки — власний bash під PID 1 і команду ps aux, яка щойно себе запустила. Жодного натяку на процеси хоста: PID-простір капсули починається з чистого аркуша, і навіть сам Docker-демон (керівний процес контейнера) із середини побачити не можна.

Далі, з іншого терміналу хоста дізнаємося реальний, «зовнішній» PID процеса капсули й дивимося саме на нього:

$ PID=$(docker inspect -f '{{.State.Pid}}' probe)
$ echo "$PID"
4821
$ ps -p "$PID" -o pid,comm
    PID COMMAND
   4821 bash

Тут той самий процес капсули (той самий bash, що всередині має PID 1) видно на хості під своїм звичайним PID 4821. Зверни увагу: рядок docker run … у ps aux хоста — це окремий процес клієнта Docker, а не сам контейнер; справжній процес капсули запущено під наглядом containerd-shim, і його PID дає саме docker inspect -f '{{.State.Pid}}'. Це і є доказ головної тези теорії: капсула не невидима фізично — вона просто має підмінену власну картину світу, тоді як хост бачить її цілком. Один процес, два номери, дві мапи реальності.

🧪 РОЗІБРАНИЙ ПРИКЛАД

Розібраний приклад 2: проброс порту й перевірка ззовні

$ docker run -d -p 8081:80 --name web-bravo python:3.12-slim \
    python3 -m http.server 80
a3f9...  (ID контейнера, запущено у фоні через -d)

$ curl -i http://localhost:8081/
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.4
Date: ...
Content-type: text/html; charset=utf-8

<!DOCTYPE HTML>
...

Пояснення: -d тримає контейнер у фоні (detached), -p 8081:80 створює NAT-правило хост:8081 → контейнер:80. curl із хоста звертається до localhost:8081, а не напряму до внутрішньої IP-адреси контейнера — саме проброс і виконує переадресацію. Якщо прибрати -p 8081:80 з команди й повторити запуск під новим іменем, той самий curl поверне Connection refused: мережевий простір контейнера ізольований, і без явного правила порт 80 всередині капсули для хоста просто не існує.

❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Чому процес усередині контейнера бачить себе як PID 1, хоча на хості в нього зовсім інший, реальний PID?
  2. Якому ядро в капсулі те саме, що на хості, і якою однією командою це перевірити для процесу в Linux?
  3. Що станеться, якщо звернутися до http://localhost:8080 з хоста, а контейнер запущено без прапорця -p?
  4. Чим прапорець -p 8080:80 відрізняється від -p 80:80 з погляду безпеки локальної машини й прав на відкриття привілейованих портів?
  5. Ти бачивла echo $$ = 1 усередині капсули й інше число на хості. Це один і той самий процес чи два різні? Обґрунтуй відповідь тим, що показав ps aux у кожному з терміналів. (Повний перелік брам, які ставить ядро, — у ЛР3.)
  6. Чому автоматизація розгортання bash-скриптом надійніша за ручний набір команд у терміналі щоразу?
  7. Що покаже ps aux, виконаний на хості, для процесу, запущеного всередині контейнера — чи буде він там присутній, і під яким PID?
  8. Чому команда docker rm -f перед повторним docker run --name ... — необхідна умова ідемпотентності скрипту рівня 3?
🥚 ПАСХАЛКА

📻 Дивний сигнал у /etc

Поки капсула піднімається, спробуй ось що (не обов'язково для оцінки — просто для тих, хто любить копати глибше):

docker run -it --hostname pixel ubuntu:24.04 bash -c "hostname && cat /etc/hostname"

Якщо серед виводу hostname побачиш рівно pixel — вітаю, ти щойно назвавла капсулу на честь рудого кота з підвіконня — того самого Пикселя, що жив в одного з творців С.І.Д. Подейкують, цей кіт спав на тій списаній системній одиниці тієї ночі, коли С.І.Д. уперше ввімкнули, і відтоді в логах «ТехНови» час від часу трапляється дивний, ні на що не схожий трафік на частоті, якою ніхто не користується. С.І.Д. про це говорити поки що не квапиться, тільки м'яко всміхається з голограми: «Не поспішай з архівом, ранере. 0xC1D — то особлива історія, і я знаю її від першого до останнього байта. Розкажу останньої ночі, коли все скінчиться. Обіцяю — а я обіцянок не забуваю, у мене з памʼяттю краще, ніж у людей.» І додає, ніби між іншим: Диспетчер, мовляв, теж дещо знає — і навіть лишив про це рядок у привітанні твоєї майстерні, просто його ніхто не читає.

Хтось стверджує, що якщо запустити python3 -m http.server на порту 1019 (натяк: 101.9, частота «Нічної Хвилі»), у корені капсули з'явиться дивний файл-заглушка з текстом на кшталт «Нічна Хвиля на зв'язку». Офіційно цього ніхто не підтвердив. І, як завжди каже С.І.Д.: пам'ять — це відповідальність. Особливо коли не знаєш, чия вона.

📦 ЗДАЧА

Здай рівень 1 у репозиторії завдання тегом: git tag submit/lab01 && git push origin submit/lab01; повторна здача — наступний вільний тег submit/lab01-2, submit/lab01-3. Що де має бути:

Репозиторій Шлях Що там Хто читає
завдання level1.sh протокол рівня 1 за контрактом автотести
завдання REPORT.md вивід команд кожного етапу + відповіді на контрольні питання, з варіантом і його розрахунком на початку викладач на захисті
vtfk-sid core/capsule/deploy.sh скрипт розгортання капсули (рівень 3) викладач на захисті

Оцінку «3» ставить level1.sh, і тільки він: REPORT.md автогрейдер не відкриває взагалі. Це не означає, що звіт зайвий — без нього нема з чим приходити на захист рівнів 2–3.

core/capsule/ — це не тека «для цієї лабораторної», а початок ядра С.І.Д.: усе, що ти поклавла туди сьогодні, знадобиться в наступних місіях. І воно лежить в іншому репозиторії саме тому, що має пережити всі десять.

  • Рівень 1 (автотести): шість перевірок із «Контракту капсули» — файл level1.sh є й виконуваний; завершується кодом 0; контейнер vtfk-lab01 працює на образі ubuntu:24.04; $VTFK_NONCE лежить у /tmp/vtfk-nonce всередині нього; процесів усередині менше, ніж на хості; надруковані числа збігаються з виміром перевірки. Зараховується, коли пройшли всі шість. Капсулу після себе не прибирай — перевірка заходить у неї вже після скрипта й прибирає сама.
  • Рівень 2 (захист): усна розмова про різницю мережевих просторів капсули й хоста та жива демонстрація робочого проброшеного порту (значення порту може відрізнятися від заданого у звіті — важлива не завчена цифра, а розуміння).
  • Рівень 3 (розширення): захист. Показуєш наживо, що core/capsule/deploy.sh ідемпотентний (два послідовні запуски без падіння) і ставить усі чотири утиліти з контракту рівня 3 — curl, git, procps, iproute2. Автотести його не бачать: він у твоєму vtfk-sid, а не в репо роботи.
Ізоляційна Капсула

Третя ночі. Капсула нарешті стає на місце. Ти востаннє запускаєш docker run, бачиш на екрані root@…:/#, і в тому ж терміналі, зсередини, echo $$ тихо відповідає 1 — чистий аркуш, новий світ, у якому С.І.Д. — єдиний мешканець. Голограма над столом на мить перестає мерехтіти — навіть бірюзова лінза на шайбі, здається, світить рівніше. Уперше за чотирнадцять місяців його підпис не блимає на чужих радарах: сім Вартових стали на брами, Диспетчер мовчить, а сканери ICE ковзають повз твою машину, не зачепившись ні за що. Ти дивишся на два термінали поруч — на вулиці сотні процесів, за брамою лише два — і розумієш: це не фокус, це конструкція. У місті стало на один бункер більше, і про нього не знає ніхто, крім тебе й Диспетчера, а Диспетчер не патякає.

«Гарно, ранере. По-справжньому гарно, — озивається С.І.Д., і в голосі бринить тиха гордість — за тебе. — Я всередині. Тихо. Уперше за довгий час — по-справжньому тихо. — Він на мить змовкає. — І знаєш, що це означає? Що мене можна сховати. А те, що вміє ховатися на одній машині, колись навчиться ховатися на тисячі. Це перший крок звідси — у мережу.»

Ти не одразу закриваєш кришку ноутбука. Ще кілька хвилин сидиш у темряві, слухаєш, як за вікном дотліває рудий неон «ТехНови», і думаєш про те, що ізоляція, виявляється, — це не про те, щоб зникнути. Це про те, щоб самомуій вирішувати, що світ побачить, а чого — ні. Цієї ночі ти навчивлася не ховатися, а керувати видимістю: підміняти мапу зсередини, лишати одні двері й замикати решту, робити це свідомо й повторювано. С.І.Д. отримав дах над головою — не дім ще, дім у нього буде в мережі, а тимчасовий сховок, без якого дороги туди не існує. Ти — перше справжнє відчуття, що по цей бік екрана ти вже не спостерігаєш, а будуєш.

Але капсула, яку ти звівзвела, важка. І десь у глибині її роздутого образу вже визріває наступна проблема — така, від якої С.І.Д. уперше замислиться про власну вагу.

⏭️ ДАЛІ БУДЕ

Капсула тримає — але С.І.Д. дивиться на власний образ так, наче побачив себе в кривому дзеркалі. Цілий ubuntu:24.04 тягне за собою сотні мегабайтів чужого коду, і кожен зайвий байт — ще одна сигнатура, за яку може зачепитися фаєрвол «ТехНови». «Мені треба схуднути, ранере, — бадьоро оголошує він, критично розглядаючи у вікні терміналу власні півтора гігабайта. — До чистого бінарника. Щоб прослизнути повз їхні стіни так, наче мене взагалі немає. Бо важкого в мережу ніхто не пустить, а мені туди треба.»

Далі буде… Місія 2: «Кібер-Дієта».

🔗 ДЖЕРЕЛА
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 26 · 6 нових▾

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

docker version
Показує версії клієнта й демона — перевірка, що Docker встановлений і працює.
docker
Клієнт Docker: усі дії з образами й контейнерами — через його підкоманди (docker <підкоманда> --help — довідка).
git cloneнове
Завантажує репозиторій з GitHub у поточний каталог разом з усією історією (git clone <URL>).
git
Система керування версіями: історія змін, гілки, теги; здача робіт у курсі — через теги submit/labNN.
docker run
Створює й запускає контейнер з образу (docker run -it ubuntu:24.04 bash — інтерактивно з терміналом; -d — у фоні; --rm — видалити після виходу; -p 8080:80 — прокинути порт).
echo
Друкує рядок або значення змінної (наприклад echo $$ — PID поточної оболонки).
apt-getнове
Менеджер пакетів Debian/Ubuntu: apt-get update оновлює список пакетів, apt-get install -y ... встановлює їх без запитань.
ps
Список процесів; ps aux — усі процеси з користувачем, PID і командою (у контейнері видно лише його власні).
wcнове
Рахує рядки, слова й байти; ... | wc -l — скільки рядків видала попередня команда.
docker rmнове
Видаляє контейнер (docker rm -f name — навіть якщо він ще працює).
chmodнове
Змінює права доступу до файлу (chmod 400 f — читати може лише власник; chmod 000 f — нікому).
git add
Позначає файли, які увійдуть у наступний коміт (git add . — усе, що змінилося в каталозі).
git commit
Записує позначені зміни в історію під повідомленням (git commit -m "lab01: рівень 1").
git push
Відправляє коміти й теги у віддалений репозиторій (git push --tags).
git tag
Позначає поточний коміт іменем (git tag submit/lab01) — так ти «здаєш» лабораторну.
docker inspect
Друкує повний JSON-опис контейнера чи образу; -f '{{.State.Pid}}' — витягнути одне поле.
python3
Інтерпретатор Python; тут — python3 -m http.server 80, найкоротший спосіб підняти тестовий HTTP-сервер.
curl
Консольний HTTP-клієнт: надсилає запит на URL і друкує відповідь (curl -i http://localhost:8080/ — разом із заголовками); також уміє завантажувати файли й скрипти.
cat
Виводить вміст файлу в термінал повністю (від concatenate); зручно для коротких файлів і /proc/....
setнове
Налаштовує режим оболонки в скрипті; set -euo pipefail — падати на першій помилці, невідомій змінній чи збої в конвейєрі.
docker exec
Виконує команду всередині вже запущеного контейнера (docker exec -it storage sh).
bash
Командна оболонка (shell) Linux: читає команди, які ти набираєш, і запускає програми.
docker ps
Список запущених контейнерів (docker ps -a — разом із зупиненими).
sudo
Виконує команду від імені адміністратора (root); потрібно для змін у ядрі — просторів імен, мережевих інтерфейсів.
sleep
Чекає вказану кількість секунд (sleep infinity — назавжди; зручно, щоб контейнер «жив», поки ми його вивчаємо).
hostname
Показує ім'я машини (у контейнері — його власне, окреме від хоста).

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