Лабораторна робота № 3. Два контейнери разом і ізольована мережа
Мета роботи
Навчитися піднімати кілька взаємодіючих контейнерів у власній ізольованій мережі Docker з DNS-резолвінгом за іменем, обмінюватися даними через спільний том (volume), незалежний від життєвого циклу контейнерів, і безпечно передавати секретні ключі між контейнерами через змінні середовища, мінімізуючи слід секрету в місцях, доступних стороннім.
📚Теоретичні відомості510 слів · ~3 хв
1. Простори імен: що використовується в цій роботі
Повний перелік просторів імен, прапорці clone() і механізм підміни видимості розглянуті на
лекції 3 (того самого дня). У цій роботі використовується один простір — мережевий (NET):
кожен контейнер отримує власні інтерфейси, таблицю маршрутизації та діапазон портів.
Належність процесу до простору імен перевіряється через каталог /proc/<pid>/ns/, у якому
кожен простір представлений символьним посиланням вигляду net:[4026532008]. Число в дужках —
inode простору: однаковий inode означає той самий простір. Це дає пряму перевірку, яка
виконується в роботі: два контейнери в спільній user-defined мережі мають однаковий inode
net, а контейнер і хост — різні.
2. Мережева ізоляція контейнерів і потреба у спільній мережі
Кожен контейнер за замовчуванням отримує власний NET-простір імен (лабораторна робота
№ 1), тому два довільні контейнери не бачать мереж один одного, доки не приєднані до
спільної мережі Docker. Дефолтна мережа bridge, у яку контейнери потрапляють без
явного --network, не веде записів DNS для контейнерів за іменем — доступ можливий лише
за внутрішньою IP-адресою, яка змінюється при кожному перезапуску контейнера.
3. User-defined bridge мережа і вбудований DNS
Користувацька (user-defined) bridge-мережа, створена командою docker network create,
дає контейнерам, приєднаним до неї з іменем (--name), вбудований DNS-резолвінг: звернення
до імені контейнера всередині мережі автоматично резолвиться у поточну IP-адресу того
контейнера, без ручного оновлення /etc/hosts чи будь-якого зовнішнього DNS-сервера. Це
принципова відмінність від дефолтної мережі bridge, де такого сервісного DNS немає.
4. Том (volume) як незалежна одиниця зберігання
Файлова система контейнера — тимчасова: видалення контейнера (docker rm) знищує все, що
контейнер записав понад базові шари образу. Том (volume), створений командою docker volume create, — окрема сутність, керована Docker і не прив'язана до життєвого циклу
конкретного контейнера. Кілька контейнерів можуть монтувати той самий том одночасно
(-v <ім'я-тому>:<шлях>) і обмінюватися файлами через файлову систему, не відкриваючи для
цього мережевий порт. На відміну від bind mount (монтування конкретної теки хоста), том
керується самим Docker і не залежить від структури файлів на конкретному хості, що робить
його переносним.
5. Секрети: змінні середовища, --env-file, межі приховування
Секрети (ключі доступу, паролі) не повинні лишати слідів у місцях, доступних стороннім:
у команді запуску контейнера (видима через docker inspect, поле Config.Cmd), в історії
процесів термінала, у логах CI/CD чи безпосередньо в Dockerfile. Передача через змінні
середовища при запуску контейнера можлива двома способами:
docker run -e KEY=value— значення потрапляє і вConfig.Envконтейнера, і дублюється в команді запуску, видимій в історії шелла та журналах.docker run --env-file secrets.env— значення потрапляє вConfig.Env, але не з'являється в команді запуску й не лишається в історії шелла; сам файл секретів не комітиться в репозиторій (.gitignore).
Важливо розуміти межу цього підходу: значення все одно залишається доступним через
docker inspect у полі Config.Env — повністю приховати його від будь-якого доступу цей
механізм не може. Мета навчального завдання — не лишити слід у Dockerfile, історії команд
збірки/запуску й репозиторії, а не досягти криптографічно строгого приховування (для
цього існують docker secret у режимі Swarm або секрети, змонтовані як файли в
Kubernetes/Compose — тема розширення поза межами базового завдання).
Обладнання та програмне забезпечення
- GitHub Codespaces або локальний devcontainer із Docker Engine.
docker network,docker volume,docker run --env-file.- Два прості образи/сервіси: умовний «сервіс» (клієнт, що звертається до сховища за обраним протоколом) і умовне «сховище» (сервер або процес, що надає доступ до даних).
Порядок виконання
- Створити user-defined bridge-мережу:
docker network create mission-net. Очікуваний результат: мережаmission-netз'являється у спискуdocker network ls. - Підняти контейнер-сховище в цій мережі з іменем (
--name storage --network mission-net). Очікуваний результат: стан контейнераrunning. - Підняти контейнер-сервіс у тій самій мережі (
--name agent --network mission-net) і перевірити резолвінг іменіstorage(ping storageабоgetent hosts storage). Очікуваний результат: успішна відповідь за іменем, без явного вказання IP. - Створити том:
docker volume create mission-logsі змонтувати його в обидва контейнери (-v mission-logs:/data). Очікуваний результат: файл, записаний у/dataодним контейнером, видно в іншому. - Записати тестовий «зашифрований лог» у спільний том із контейнера-сервісу і прочитати його з контейнера-сховища. Очікуваний результат: вміст файлу ідентичний у обох контейнерах.
- Видалити обидва контейнери й підняти їх заново з тим самим томом. Очікуваний результат: дані в томі збереглися, незважаючи на видалення контейнерів.
- Передати секретний ключ у контейнер-сервіс через
--env-file secrets.env(файл не комітиться в репозиторій), перевірити доступність змінної всередині контейнера й переконатися, що ключ не видно вdocker inspect agentяк частина команди запуску. Очікуваний результат: усередині контейнераecho "$STORAGE_ACCESS_KEY"виводить значення;docker inspect agent --format '{{.Config.Cmd}}'значення не містить.
Завдання за рівнями
Два репозиторії. Рівень 1 виконується в репозиторії завдання — його створює Classroom 50 на прийняття, окремий на кожну роботу, і саме там працюють автотести (здача тегом
submit/labNN). Рівні 2 і 3 виконуються в наскрізному репозиторіїvtfk-sid, який студент створює один раз у роботі № 1 і веде до кінця семестру; вони оцінюються на захисті, не автотестами. Шляхи видуcore/...у тексті нижче — це шляхи вvtfk-sid.Тег ставить скрипт здачі, а не студент. У робочому просторі — Ctrl+Shift+B (меню
Terminal → Run Build Task), або те саме з терміналу:bash .devcontainer/submit.sh. Скрипт комітить незбережене, показує, який тег створить, і чекає підтвердження; відмова нічого не здає. Ручніgit tag/git pushлишаються запасним шляхом — вони працюють і поза Codespaces.
Рівень 1 (оцінка 3, автотести). Підняти два контейнери (сервіс і сховище) й налаштувати ізольовану user-defined мережу між ними з резолвінгом за іменем. Відповідає крокам 1–3 порядку виконання.
Здається скриптом scripts/up.sh у корені репозиторію завдання — кроки 1–3, записані
так, щоб їх можна було відтворити однією командою. Контракт скрипту:
- Шапка
#!/usr/bin/env bashіset -euo pipefail; запуск без аргументів (bash scripts/up.sh), без інтерактивного вводу. - Створити user-defined bridge-мережу
mission-net. - Підняти в ній контейнер
storage— HTTP-сервер на порту8000, що віддає свою робочу теку (python3 -m http.server 8000на образіpython:3.12-slim). - Підняти в ній контейнер
agentдовготривалим процесом (-d … sleep infinity, не-it); в образі мають бутиgetentі HTTP-клієнт (python3,curlабоwget) —python:3.12-slimмає обидва. - Не прописувати адресу сховища явно: жодних
--add-host,--ipчи записів в/etc/hosts— ім'яstorageрезолвить вбудований DNS мережі. - Бути ідемпотентним: повторний запуск поспіль завершується кодом
0попри вже наявні мережу й контейнери. - Лишити обидва контейнери працювати після завершення скрипту.
Спільного тому й секретів (secrets.env) у скрипті рівня 1 немає — це рівні 2–3.
Що саме перевіряють автотести:
| # | Перевірка |
|---|---|
| 1 | scripts/up.sh існує і завершується кодом 0 |
| 2 | контейнери agent і storage працюють |
| 3 | обидва приєднані до спільної user-defined bridge-мережі mission-net |
| 4 | getent hosts storage усередині agent повертає IP сховища в mission-net, і ім'я не прописане в /etc/hosts |
| 5 | HTTP-запит з agent на http://storage:8000/<файл> повертає вміст файлу, який перевірка поклала в робочу теку storage |
| 6 | повторний запуск scripts/up.sh теж завершується кодом 0 |
Перевірки 2–5 виконуються після завершення скрипту (docker inspect, docker exec),
тому прибирання контейнерів у кінці up.sh знімає чотири перевірки з шести; прибирає
автотест сам. Ім'я й вміст файлу п'ятої перевірки випадкові й щоразу нові, тож пройти її
можна лише справжнім запитом агента до сховища за іменем. Рівень зараховується, коли
пройшли всі шість перевірок.
Рівень 2 (оцінка 4, захист). Підключити спільний том (volume) для обміну даними між контейнерами; на захисті продемонструвати запис і читання даних обома контейнерами. Відповідає крокам 4–6 порядку виконання.
Два рівноцінні варіанти виконання (оцінка однакова):
- A — на власному наскрізному артефакті: роль контейнера
agentвиконує образcore/capsule/, зібраний у роботах № 1–2. Том, підключений до нього в цій роботі, використовується далі — у роботі № 4 у ньому зберігається згенерований ключ. - B — на референсному знімку: якщо власного образу немає, береться
vtfk-sid-reference@after-lab02.
Допуск до захисту. Перед захистом рівня 2 треба залишити три предметні зауваги до роботи № 2 іншого студента — в issue або коментарі до комміта його репозиторію завдання. Кого рецензувати: студента, чий номер у списку групи на 3 більший за власний (по колу — номер цієї роботи задає зсув, тому пари щоразу різні). Заувага предметна, якщо називає конкретний рядок, команду або крок і пояснює, у чому проблема; загальні оцінки («усе правильно», «гарний код») не враховуються. Це елемент ЗК7 — здатності працювати в команді: рівень 1 доводить, що код збирається, рецензія — що студент уміє його читати.
Рівень 3 (оцінка 5, розширення). Налаштувати передачу секретних ключів між контейнерами через змінні середовища так, щоб значення не з'являлося в команді запуску контейнера. Відповідає кроку 7 порядку виконання.
Ті самі два варіанти: A — конфігурація секрету лягає в core/capsule/ власного
репозиторію; B — той самий результат від референсного знімка.
Приклад 1. DNS-резолвінг у user-defined мережі
$ docker network create mission-net
$ docker run -d --name storage --network mission-net nginx:1.27-alpine
$ docker run -it --rm --network mission-net alpine:3.20 sh
/ # getent hosts storage
172.19.0.2 storage
Резолвінг storage → 172.19.0.2 відбувається автоматично через вбудований DNS-сервер
Docker у межах мережі mission-net, без попереднього вказання IP. У дефолтній мережі
bridge (без явного docker network create) та сама команда завершилася б помилкою —
дефолтна мережа не веде записів DNS для контейнерів за іменем.
Приклад 2. -e проти --env-file
$ docker run -d --name bad-agent -e STORAGE_ACCESS_KEY=0xC1D-secret alpine sleep 1000
$ docker inspect bad-agent --format '{{.Config.Env}}'
[PATH=/usr/local/sbin:... STORAGE_ACCESS_KEY=0xC1D-secret]
$ echo "STORAGE_ACCESS_KEY=0xC1D-secret" > secrets.env
$ docker run -d --name agent --env-file secrets.env alpine sleep 1000
$ docker inspect agent --format '{{.Config.Cmd}}'
[sleep 1000]
В обох випадках значення технічно опиняється в Config.Env контейнера — це властивість
самого механізму змінних середовища. Різниця в тому, де ще значення лишає слід: -e
дублює його в команді запуску (видно в Config.Cmd/аргументах та в історії шелла), тоді
як --env-file тримає значення виключно у виключеному з репозиторію файлі.
- Чому два контейнери в дефолтній bridge-мережі не резолвлять один одного за іменем, а в user-defined bridge — резолвлять?
- Що станеться з даними в томі (volume) після видалення обох контейнерів, які його монтували?
- Чим том (volume) відрізняється від bind mount з погляду керування Docker?
- Чому передавати секрет як аргумент команди в Dockerfile (
ARG/ENVіз хардкодом) небезпечніше, ніж через--env-fileпід час запуску? - Як мережева ізоляція (
--network mission-net) захищає обмін даними між сервісом і сховищем від зовнішнього доступу? - Що покаже
docker inspect agentщодо переданих через-eзмінних середовища? - Чому DNS-резолвінг за іменем контейнера важливіший, ніж здається, якщо контейнер може бути перезапущений з новою IP-адресою?
- У чому принципова межа приховування секрету через
--env-fileпорівняно зdocker secretу режимі Swarm?
Парні теми СРС
- Рівень 2: srs05 — «Мережі Docker (bridge/none/host) і службове DNS між контейнерами».
- Рівень 3: srs06 — «Керування секретами: env проти файлів проти docker secrets».
Критерії оцінювання та форма звіту
Здача відбувається тегом submit/lab03. Звіт REPORT.md містить: вивід резолвінгу
agent → storage за іменем, вміст файлу зі спільного тому, записаного одним
контейнером і прочитаного іншим, фрагмент docker inspect agent (зі значенням секрету,
видаленим уручну зі звіту, якщо воно там присутнє в незашифрованому вигляді), і відповіді
на контрольні питання.
Автотести (рівень 1) перевіряють скрипт scripts/up.sh: він проходить двічі поспіль,
обидва контейнери піднято, приєднано до спільної user-defined мережі mission-net, і
контейнер-сервіс успішно резолвить та опитує контейнер-сховище за іменем (без явного IP). Захист (рівень 2) — усне пояснення механізму DNS-резолвінгу в
мережі Docker і демонстрація запису/читання даних через спільний том обома контейнерами.
Розширення (рівень 3) перевіряється на захисті: демонструється відсутність секретного
значення у виводі docker inspect (у полі команди запуску чи аргументів) і коректну доступність
значення змінної всередині контейнера під час виконання.