Управління контейнерами з runC

runc

Продовжуємо цикл статей про контейнеризації. Сьогодні ми поговоримо про runC — інструмент для запуску контейнерів, що розробляється в рамках проекту Open Containers. Мета цього проекту полягає в розробці єдиного стандарту в області контейнерних технологій. Проект підтримують такі компанії, як Facebook, Google, Microsoft, Oracle, EMC, Docker. Влітку 2015 року був опублікований чорновий варіант специфікації під назвою Open Container Initiative (OCI).

RunC вже активно використовується в сучасних інструментах контейнеризації. Так, останні версії Docker (починаючи з 1.11, вийшла навесні цього року) створені у відповідності зі специфікаціями OCI і працюють на базі runC. А бібліотека libcontainer, яка по суті є частиною runc, використовується в Docker замість LXC починаючи ще з версії 1.8.

У цій статті ми покажемо, як можна створювати контейнери і управляти ними за допомогою runC.

Установка
Ми будемо описувати установку runc для Ubuntu 16.04. В цій операційній системі остання на поточний момент стабільна версія Go (1.6) вже включена в офіційні репозиторії і встановлюється стандартним способом:

$ sudo apt-get install golang-go

Runc теж включений в репозиторії Ubuntu 16.04, але далеко не самої свіжої версії. Останню на сьогоднішній день версію (1.0.0) потрібно збирати з вихідного коду. Для цього спочатку потрібно встановити необхідні залежності:

sudo apt-get install build-essential make libseccomp-dev

Ось і все, можна приступати до складання runC:

$ git clone https://github.com/opencontainers/runc
$ cd runc
$ make
$ sudo make install 

В результаті виконання цих команд runc буде встановлений в директорію /usr/local/bin/runc.

Створюємо перший контейнер
Отже, до створення першого контейнера все готово.

Перше, що нам потрібно зробити — це створити окрему директорію під новий контейнер, а всередині її — директорію rootfs:

$ mkdir /mycontainer
$ cd /mycontainer
$ mkdir rootfs

Почнемо з самого простого прикладу. Завантажимо docker-образ memcached, перетворимо його в архів *.tar і распакуем в директорію rootfs:

$ docker export $(docker create memcached) | tar -C rootfs -xvf -

В результаті виконання цієї команди в директорію rootfs будуть поміщені системні файли для майбутнього контейнера:

$ ls rootfs
bin dev etc lib media opt root sbin sys usr
boot entrypoint.sh home lib64 mnt proc run srv tmp var

Після цього ми можемо запускати контейнери і керувати ними, не вдаючись до допомоги Docker. Створимо конфігураційний файл, в якому будуть прописані налаштування нового контейнера:

$ sudo runc spec

Після цього в директорії rootfs з'явиться новий файл — config.json. До запуску нового контейнера все готово. Виконаємо:

$ sudo runc run mycontainer

Всередині контейнера буде запущена командна оболонка.

Ми розглянули самий елементарний приклад, в якому контейнер був запущений автоматично згенерованими налаштуваннями. Для більш тонкої настройки контейнерів згаданий вище файл config.json доведеться редагувати вручну. Розберемо його структуру більш докладно.

Конфігураційний файл config.json
У першій частині конфігураційного файлу описуються загальні характеристики контейнера: версія OCI, операційна система і її архітектура, параметри терміналу:

"ociVersion": "1.0.0-rc1",
"platform": {
"os": "linux",
"arch": "amd64"
},
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],

У наступному розділі наводяться налаштування для директорії, в якій працює контейнер:

"cwd": "/",
"capabilities": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],

Абревіатура CWD означає current working directory. У нашому випадку це директорія «/». Далі вказується набір capabilities (на російську мову цей термін не зовсім вдало перекладають як «можливості») — дозволів для файлів, що виконуються на використання певних підсистем без права root. CAP_AUDIT_WRITE дозволяє робити записи в журнал аудиту, CAP_KILL — відправляти процесів сигнали, а CAP_NET_BIND_SERVICE — дозволяє прив'язку сокетів до привілейованих портів (тобто портам з номерами менше 1024).

Наступний розділ — rlimits:

"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"noNewPrivileges": true
},

Тут ми встановлюємо для контейнера ліміт ресурсів, а саме — максимальна кількість одночасно відкритих файлів (RLIMIT_NOFILE), яке становить 1024.

Далі йде опис налаштувань кореневої файлової системи:

"root": {
"path": "rootfs",
"readonly": true
}

У розділі mounts описуються змонтовані в контейнер директорії:

"mounts": [
{
"destination": "/tmp",
"type": "tmpfs",
"джерело": "tmpfs",
"options": ["nosuid","strictatime","mode=755","size=65536k"]
},
{
"destination": "/data",
"type": "bind",
"джерело": "/volumes/testing",
"options": ["rbind","rw"]
}
]

Ми розглянули лише самі основні розділи файлу config.json. Про деяких інших його розділах ми поговоримо нижче. З докладним описом цього файлу можна ознайомитися тут.

Хуки
Ще одна цікава можливість runc — настройка хуків: ми можемо прописати в конфігураційному файлі конкретні дії, які будуть виконані перед запуском користувацького процесу в контейнері (prestart), після запуску користувацького процесу (poststart) і після його зупинки (poststop).

Наведемо кілька прикладів, щоб краще зрозуміти, навіщо потрібні хуки і як прописуються відповідні налаштування в конфігураційному файлі. Уявімо собі таку ситуацію цілком: перш ніж запускати деяку програму в контейнері, нам потрібно налаштувати мережу. Для цього додамо в конфігураційний файл такий хук (приклад взято з офіційної документації):

"hooks": {
"prestart": [
{
"path": "/path/to/script"
}

У розділі path прописується шлях до програми, яка і виконає налаштування мережі. Готові програмні інструменти подібного роду можна знайти на Github — див., наприклад, тут.

Розглянемо тепер приклад poststart-хука:

"poststart":[
{
"path": "/usr/bin/notify-start",
"timeout": 5
}

Коли цей хук спрацює, буде запущений скрипт (в нашому прикладі вона називається notify-start), який запише в логи інформацію про події, пов'язані з запуском контейнера.

Poststop-хуки ініціюють дії, які будуть виконані після завершення користувацького процесу в контейнері. Вони можуть знадобитися, наприклад, у випадку, коли нам потрібно видалити логи і сесійні файли, залишені контейнером в системі, а заодно і сам контейнер. Ось простий приклад:

"poststop": [
{
"path": "/usr/sbin/cleanup.sh",
"args": ["cleanup.sh", "-f"]
}

При спрацьовуванні цього хука буде запущений скрипт cleanup.sh, який і виконати всі згадані вище дії.

Управління контейнерами: основні команди
Для управління контейнерами в runc використовуються прості і звичні команди. Ось їх короткий перелік:

#переглянути список контейнерів та інформацію про їх стан
runc list

#запустити процес у контейнері
runc start mycontainerid

#зупинити процес у контейнері
runc stop mycontainerid

#видалити контейнер
runc delete mycontainerid

Налаштування мережі
З базовими операціями з управління контейнерами ми розібралися. Спробуємо налаштувати в контейнері мережу. Це не найпростіше завдання. Всі операції потрібно здійснювати вручну.

Для початку виконаємо наступну послідовність команд (взято з цієї статті):

$ sudo brctl addbr runc0
$ sudo ip link set runc0 up
$ sudo ip addr add 192.168.10.1/24 dev runc0
$ sudo ip link add name veth-host type veth peer name veth-guest
$ sudo ip link set veth-host up
$ sudo brctl addif runc0 veth-host
$ sudo ip netns add runc
$ sudo ip link set veth-guest netns runc
$ sudo ip netns exec runc ip link set veth-guest name eth1
$ sudo ip netns exec runc ip addr add 192.168.10.101/24 dev eth1
$ sudo ip netns exec runc ip link set eth1 up
$ sudo ip netns exec runc ip route add default via 192.168.10.1

Наведені команди говорять самі за себе. Спочатку ми створюємо міст для зв'язку між контейнером і інтерфейсом на основному хості. Потім «піднімаємо» віртуальний інтерфейс і додаємо його в міст. Після цього ми створюємо мережеве простір імен (неймспейс) з ім'ям runc і призначаємо в ньому IP-адреса інтерфейсу eth1.

Щоб налаштувати в контейнері мережа, ми повинні асоціювати з ним неймспейс runc. Для цього внесемо невеликі зміни в файл config.json:

......

"root": {
"path": "rootfs",
"readonly": false
}

У розділі namespaces вкажемо шлях до простору імен runc:

{
"type": "network",
"path": "/var/run/netns/runc"
},


Ось і все: усі необхідні налаштування прописані. Зберігаємо зміни і перезапускаємо контейнер. Виконаємо на основному хості команду:

$ ping 192.168.10.101

PING 192.168.10.101 (192.168.10.101) 56(84) bytes of data.
64 bytes from 192.168.10.101: icmp_seq=2 ttl=64 time=0.070 ms
64 bytes from 192.168.10.101: icmp_seq=3 ttl=64 time=0.090 ms
64 bytes from 192.168.10.101: icmp_seq=4 ttl=64 time=0.106 ms
64 bytes from 192.168.10.101: icmp_seq=5 ttl=64 time=0.091 ms
64 bytes from 192.168.10.101: icmp_seq=6 ttl=64 time=0.097 ms

Її висновок свідчить про те, що контейнер приймає ping з основного хоста.

Висновок
Ця стаття представляє собою лише короткий вступ в runC. Для бажаючих дізнатися більше наводимо невелику підбірку корисних посилань:

Якщо ви вже експериментували з гіпс — запрошуємо поділитися досвідом в коментарях.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.