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

Начнём с cамого простого примера. Загрузим 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 описываются cмонтированные в контейнер директории:

"mounts": [
    {
        "destination": "/tmp",
        "type": "tmpfs",
        "source": "tmpfs",
        "options": ["nosuid","strictatime","mode=755","size=65536k"]
    },
    {
        "destination": "/data",
        "type": "bind",
        "source": "/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

#остановить процесс в контейнерe
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. Для желающих узнать больше приводим небольшую подборку полезных ссылок:


Если вы уже экспериментировали с runС — приглашаем поделиться опытом в комментариях.
Поделиться с друзьями
-->

Комментарии (3)


  1. coolder
    30.11.2016 16:04

    Если это серия статей, не могли бы вы привести ссылки на предыдущие статьи серии?


  1. AndreiYemelianov
    30.11.2016 16:08

    Мы постоянно пишем о контейнеризации, стараясь затрагивать интересные аспекты темы. Вот другие статьи из цикла:

    https://habrahabr.ru/company/selectel/blog/308208/
    https://habrahabr.ru/company/selectel/blog/303190/
    https://habrahabr.ru/company/selectel/blog/271957/


  1. rumkin
    04.12.2016 19:40

    Спасибо за материал. Одна из немногих статей, которая все-таки перечитывается после недели зависания в табах.


    Правда хочется увидеть больше развернутых комментариев к настройкам сети или даже отдельную статью с таким же простым и доступным описанием.