Вы можете спросить, зачем разбираться с терминологией, если концепция контейнеров выглядит вполне простой и понятной? Однако, довольно часто неверное использование терминов создает препятствия на пути к освоению контейнеров. Например, люди часто считают, что термины «контейнеры» и «образы» взаимозаменяемы, хотя на самом деле между ними есть важные концептуальные различия. Другой пример: в мире контейнеров «репозиторий» означает вовсе не то, что вы думаете. Кроме того, контейнерные технологии – это гораздо больше, чем только docker.
Так что, не владея терминологией, будет сложно понять, чем docker отличается от CRI-O, rkt или lxc/lxd; либо оценить роль Open Container Initiative в деле стандартизации контейнерных технологий.
Начать работать с Linux-контейнерами очень просто, но вскоре выясняется, что эта простота обманчива. Обычно это происходит так: потратив всего пару минут на установку docker или другого контейнерного движка, вы уже вводите свои первые команды. Еще пара минут – и вы уже создали свой первый образ контейнера и выложили его в общий доступ. Затем вы привычно переходите к архитектуре продакшн-среды, и тут вдруг понимаете, что для этого надо сперва разобраться с массой терминов и технологий, которые за всем этим стоят. Хуже того, многие из перечисленных ниже терминов используются взаимозаменяемо, что создает большую путаницу для новичков.
Усвоив изложенную в этом документе терминологию, вы будете лучше понимать технологический базис контейнеров. Кроме того, это поможет вам и вашим коллегам говорить на одном языке, а также осознанно и целенаправленно проектировать архитектуру контейнерных сред согласно специфике решаемых задач. В свою очередь, с точки зрения ИТ-сообщества и отрасли в целом, общий рост понимания контейнерных технологий способствует появлению новых архитектур и решений. Обратите внимание, что эта статья рассчитана на читателя, который уже имеет представление о том, как запускать контейнеры.
Прежде чем переходить к терминологии контейнеров, определимся, что же такое, собственно, сам контейнер. Термин «контейнер» обозначает сразу две вещи. Как и обычная Linux-программа, контейнер может находиться в одном из двух состояний: работающем и неработающем. В неработающем состоянии контейнер представляет собой файл или набор файлов, хранящихся на диске. Именно к этому состоянию относятся термины Образ контейнера и Контейнерный репозиторий. Когда вы вводите команду запуска контейнера, контейнерный движок распаковывает нужные файлы и метаданные и передает их ядру Linux. Запуск контейнера очень похож на запуск обычного Linux-процесса и требует API-обращения к ядру Linux. Этот API-вызов обычно инициирует дополнительную изоляцию и монтирует копию файлов, которые находятся в образе контейнера. После того, как контейнер запущен, это всего лишь Linux-процесс. Процедура запуска контейнеров, а также формат образов контейнеров, хранящихся на диске, определяются и регулируются стандартами.
Существует несколько форматов образов контейнеров (Docker, Appc, LXD), однако отрасль постепенно движется к единому стандарту Open Container Initiative, который иногда называют Open Containers или просто OCI. Этот стандарт задает спецификацию формата образов контейнеров, которая определяет дисковый формат хранения образов контейнеров, а также метаданных, которые, в свою очередь, определяют такие вещи, как аппаратная архитектура и операционная система (Linux, Windows и т. п). Единый отраслевой формат образов – это ключ к появлению программной экосистемы, позволяющей разработчикам, проектам Open Source и поставщикам ПО создавать совместимые друг с другом образы и различные инструменты, такие как средства электронной подписи, сканирования, сборки, запуска, перемещения и управления образами контейнеров.
Кроме того, существует несколько контейнерных движков, таких как Docker, CRI-O, Railcar, RKT, LXC. Контейнерный движок берет образ контейнера и превращает его в контейнер (т.е. в запущенный процесс). Процедура такого превращения также определяется стандартом OCI, который включает в себя спецификацию выполнения контейнера и эталонную реализацию среды выполнения, которая называется RunC и представляет собой модель с открытым исходным кодом, которая регулируется соответствующим сообществом разработки. Многие контейнерные движки используют эту модель для взаимодействия с ядром хоста при создании контейнеров.
Инструменты, поддерживающие спецификации формата образов контейнеров и среды выполнения контейнеров стандарта OCI, обеспечивают переносимость в рамках экосистемы различных контейнерных платформ, контейнерных движков и вспомогательных инструментов на различных облачных платформах и локальных архитектурах. Понимание терминологии, стандартов и архитектуры контейнерных систем позволит вам плодотворно общаться с другими специалистами и проектировать масштабируемые и поддерживаемые контейнеризованные приложения и среды, обеспечивающие продуктивное использование контейнеров на годы вперед.
В простейшем определении образ контейнера – это файл, который скачивается с сервера реестра и локально используется в качестве точки монтирования при запуске контейнера. Несмотря на то, что термин «образ контейнера» используется довольно часто, он может обозначать разные вещи. Дело в том, что хотя Docker, RKT и даже LXD работают по только что описанному принципу – то есть скачивают удаленные файлы и запускают их в виде контейнеров, – каждая из этих технологий трактует образ контейнера по-своему. LXD оперирует с монолитными (однослойными) образами, а docker и RKT используют OCI-образы, которые могут содержать несколько слоев.
Строго говоря, образ контейнера на сервере реестра – это далеко не один файл. Когда люди используют термин «образ контейнера», они часто имеют в виду репозиторий и подразумевают набор из нескольких слоев образа контейнера, а также метаданные, которые содержат дополнительные сведения об этих слоях.
Кроме того, понятие образа контейнера неявным образом подразумевает наличие формата такого образа.
Изначально каждый контейнерный движок, включая LXD, RKT и Docker, имел свой формат образов. Одни из этих форматов допускают наличие только одного слоя, другие поддерживают древовидную структуру из несколько слоев. Сегодня почти все основные контейнерные инструменты и движки перешли на формат OCI, который определяет, как должны быть устроены слои и метаданные в образе контейнера. По сути, OCI-формат определяет образ контейнера, который состоит из отдельных tar-файлов для каждого слоя и общего файла manifest.json, содержащего метаданные.
Стандарт Open Container Initiative (OCI), который изначально базировался на формате образов Docker V2, успешно объединил большую экосистему контейнерных движков, облачных платформ и инструментальных средств (сканеры безопасности, средства подписи, создания и перемещения контейнеров) и позволяет вам защитить свои инвестиции в знания и инструментальные средства.
Контейнерный движок – это та часть программного обеспечения, которая принимает запросы пользователя, включая параметры командной строки, скачивает образы и, с точки зрения конечного пользователя, запускает контейнеры. Есть множество контейнерных движков, включая docker, RKT, CRI-O и LXD. Кроме того, многие облачные платформы, PaaS-сервисы и контейнерные платформы имеют собственные движки, которые понимают образы в формате Docker или OCI. Наличие отраслевого стандарта для формата образов обеспечивает интероперабельность всех этих платформ.
Спускаясь на уровень ниже, можно сказать, что большинство контейнерных движков на самом деле запускают контейнеры не сами, а через OCI-совместимую среду выполнения, наподобие runc. Обычно, среда выполнения контейнеров делает следующие вещи:
Контейнеры существуют в операционных системах уже довольно давно, ведь на самом деле это просто запущенный экземпляр контейнерного образа. Контейнер представляет собой стандартный Linux-процесс, который обычно создается с помощью системного вызова clone () вместо fork () или exec (). Кроме того, к контейнерам зачастую применяется дополнительные меры изоляции средствами cgroups, SELinux или AppArmor.
Контейнерный хост – это система, на который выполняются контейнеризованые процессы, которые для простоты зачастую называются контейнерами. Это может быть, например, виртуальная машина RHEL Atomic Host, находящаяся в публичном облаке или работающая на голом железе в корпоративном ЦОД. Когда образ контейнера (иначе говоря, репозиторий) с сервера реестра скачивается на локальный контейнерный хост, говорят, что он попадает в локальный кэш.
Определить, какие репозитории синхронизированы с локальным кэшем, можно с помощью следующей команды:
Сервер реестра – это, по сути, файловый сервер, который используется для хранения репозиториев docker. Как правило, сервер реестра задается по DNS-имени и, опционально, номеру порта. Большая часть преимуществ экосистемы docker обуславливается возможностью скачивать и загружать репозиториии на серверы реестра.
Если демон docker не находит копии репозитория в локальном кэше, он автоматически скачивает ее с сервера реестра. В большинстве дистрибутивов Linux демон docker будет использовать для этого сайт docker.io, однако в некоторых дистрибутивах его можно настроить по-своему. Например, Red Hat Enterprise Linux вначале пытается выполнить загрузку с сайта registry.access.redhat.com, и лишь затем с docker.io (Docker Hub).
Здесь надо подчеркнуть, что сервер реестра неявно считается доверенным. Поэтому вы должны сами решить, насколько доверяете содержимому того или иного реестра и, соответственно, разрешить или запретить его. Помимо безопасности есть и другие аспекты, которыми следует озаботиться заранее, например, вопросы лицензирования ПО или контроль соответствия требованиям. Простота, с которой docker позволяет пользователям скачать ПО, делает вопрос доверия чрезвычайно важным.
Red Hat Enterprise Linux позволяет настроить docker-реестр по умолчанию. Кроме того, RHEL7 и RHEL7 Atomic позволяют добавить или заблокировать серверы реестра через конфигурационный файл:
В RHEL7 и RHEL 7 Atomic по умолчанию используется сервер реестра Red Hat:
В некоторых случаях по соображениям безопасности имеет смысл заблокировать общедоступные реестры docker, такие как DockerHub:
Red Hat также предлагает свой интегрированный сервер реестра в составе OpenShift Container Platform, a также автономный корпоративный сервер реестра Quay Enterprise и облачные, частные и общедоступные репозитории Quay.io.
Люди обычно начинают с того, что устанавливают контейнерный хост и сначала просто скачивают нужные образы контейнеров. Затем они переходят к созданию собственных образов и загружают их на сервер реестра, чтобы сделать доступными остальным участникам команды. Через какое-то время возникает потребность объединить несколько контейнеров, чтобы их можно было развертывать как один юнит. И, наконец, в определенный момент эти юниты надо сделать частью производственного конвейера (разработка-QA-продакшн). Именно так люди обычно и приходят к осознанию того, что им нужна система оркестрации.
Система оркестрации контейнеров реализует всего две вещи:
Две эти вещи на самом деле обеспечивают целый ряд преимуществ:
Сообщества Open Source и поставщики ПО предлагают множество различных средств оркестрации. Изначально большая тройка таких инструментов включала в себя Swarm, Mesos и Kubernetes, однако сегодня Kubernetes фактически стал отраслевым стандартом, поскольку о его поддержке заявили даже Docker и Mesosphere, не говоря уже почти обо всех крупных поставщиках услуг. Однако если вы ищете корпоративную систему оркестрации, рекомендуем присмотреться к Red Hat OpenShift.
Среда выполнения контейнеров – это низкоуровневый компонент, который обычно используется в составе контейнерного движка, но также может применяться и в ручном режиме для тестирования контейнеров. Стандарт OCI задает эталонную реализацию среды выполнения, известную как runc. Это наиболее широко используемая реализация, однако есть и другие OCI-совместимые среды выполнения, такие как crun, railcar и katacontainers. Docker, CRI-O и многие другие контейнерные движки используют runc.
Среда выполнения контейнеров отвечает за следующие вещи:
Маленький исторический экскурс: когда движок Docker только появился, он использовал в качестве среды выполнения LXC. Затем разработчики Docker написали собственную библиотеку для запуска контейнеров под названием libcontainer. Она была написана на языке Golang и вошла в состав движка Docker. После учреждения организации OCI фирма Docker внесла исходный код libcontainer в этот проект и выпустила эту библиотеку в виде отдельной утилиты под названием runc, которая затем и стала эталонной реализацией среды выполнения контейнеров в рамках стандарта OCI и применяется в других контейнерах движках, таких как CRI-O. Runc – это очень простая утилита, которая просто ждет, когда ей передадут точку монтирования (каталог) и метаданные (config.json). Дополнительные сведения о runc можно найти здесь.
Для более глубокого понимания см. Общие сведения о контейнерных стандартах, а также Среда выполнения контейнеров.
Репозитории часто называют образами или образами контейнеров, хотя на самом деле репозитории состоят из одного или нескольких слоев. Слои образа в репозитории связаны между собой отношениями «родитель-потомок», и каждый слой образа содержит в себе отличия от родительского слоя.
Давайте просмотрим слои репозитория на локальном контейнерном хосте. Поскольку начиная с версии 1.7 в Docker нет встроенного инструмента для просмотра слоев образа в локальном репозитории (но есть инструменты для онлайн-реестров), мы будем использовать утилиту Dockviz. Обратите внимание, что каждый слой имеет тег и универсальный уникальный идентификатор (UUID). Чтобы просмотреть сокращенные идентификаторы UUID, которые обычно уникальны в пределах одной машины, мы используем следующую команду (если вам нужен полный UUID, используйте ту же команду с опцией -no-trunc):
docker run --rm --privileged -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz images -t
Как видите, репозиторий docker.io/registry фактически состоит из множества слоев. Однако, что гораздо важнее, пользователь в принципе может «запустить» контейнер с любой ступеньки в этой лесенке слоев, например, введя приведенную ниже команду (она полностью корректна, однако никто не сможет гарантировать, что она была протестирована или вообще будет работать правильно). Как правило, сборщик образов помечает тегом (создает имена) те слои, которые стоит использовать в качестве отправной точки:
Репозитории устроены подобным образом потому, что всякий раз, когда сборщик создает новый образ, отличия сохраняются в виде еще одного слоя. Существует два основных способа создания новых слоев в репозитории. Во-первых, при создании образа вручную каждое подтверждение изменений создает новый слой. Если сборщик создает образ с помощью файла Docker, каждая директива в файле создает новый слой. Поэтому всегда полезно иметь возможность посмотреть, что изменилось в репозитории между слоями.
Хотя пользователь может и сам указать стартовый слой для монтирования и запуска контейнера в репозитории, ему вовсе не обязательно это делать. Когда сборщик образов создает новый репозиторий, он, как правило, маркируют наиболее подходящие на эту роль слои. Эти маркеры называются тегами и представляют собой инструмент, с помощью которого сборщик образов может сообщить потребителю образов, какие слои лучше использовать. Обычно теги используются для обозначения версий ПО внутри репозитория. Однако ни OCI, ни какой-либо другой стандарт никак не регламентируют использование тегов, что открывает неограниченный простор для путаницы в ходе совместной работы. Поэтому мы советуем тщательно документировать теги, если они используются не только для маркировки версий ПО.
Кроме того, есть один особый тег – latest, который обычно указывает на слой, содержащий последнюю версию ПО в репозитории. Этот тег просто указывает на слой образа, как и любой другой тег, и поэтому тоже может использоваться неправильно.
Чтобы удаленно просмотреть доступные в репозитории теги, выполните следующую команду (утилита jq делает вывод гораздо читабельнее):
При использовании команды docker в командной строке указывается репозиторий, а не образ. Например, в приведенной ниже команде «rhel7» – это репозиторий.
На самом деле эта команда автоматически развертывается в следующую:
Однако многие считают, что это образ или образ контейнера. На самом же деле, для того, чтобы получить список локально доступных репозиториев, используется подкоманда docker images. Формально эти репозитории можно рассматривать как образы контейнеров, но при этом важно четко понимать, что эти репозитории на самом деле состоят из слоев и включают метаданные, содержащиеся в файле под названием «манифест» (manifest.json):
Указывая в командной строке репозиторий, мы на самом деле просим контейнерный движок выполнить за нас часть работы. В приведенном выше примере демон docker (именно демон, а не клиентский инструмент) имеет сконфигурированный список серверов для поиска, и поэтому будет искать репозиторий «rhel7» на каждом из них.
В приведенном выше примере мы указали только имя репозитория, но в клиенте docker можно указать и полный URL. Чтобы понять, как это сделать, разобьем полный URL на составные части.
Иначе говоря, все сводится к виду:
Полный URL состоит из имени сервера, пространства имен и, опционально, тега. На самом деле при указании URL есть масса нюансов, и по мере изучения экосистемы docker вы увидите, что многие вещи указывать не обязательно. В частности, посмотрите на команды ниже: все они являются корректными и приводят к одному и тому же результату:
Пространства имен – это инструмент для разделения репозиториев на группы. В общедоступном реестре DockerHub пространство имен обычно представляет собой имя пользователя, предоставившего образ в общий доступ, но может быть и именем группы или логическим именем.
Red Hat использует пространства имен для разделения групп репозиториев по продуктам, перечисленным на сервере Red Hat Federated Registry. Пример с результатами опроса registry.access.redhat.com приводится ниже. Обратите внимание, что последняя строка в этом примере фактически указывает на другой сервер реестра. Это связано с тем, что Red Hat работает над тем, чтобы отображать репозитории на реестр-серверах наших партнеров:
Обратите внимание, что иногда полный URL можно и не указывать. В примере выше для каждого пространства имен есть репозиторий по умолчанию. Если пользователь указывает только пространство имен fedora, то на локальный сервер скачается репозиторий с тегом latest. Поэтому приведенные ниже команды приводят к одному и тому же результату:
Пространства имен ядра кардинально отличаются от пространств имен, которые мы обсуждали выше, когда говорили о репозиториях и серверах реестра. При обсуждении контейнеров пространства имен ядра являются, пожалуй, самой важной структурой данных, благодаря которой и существуют контейнеры, по крайне мере, в их сегодняшнем виде. Пространства имен ядра позволяют каждому контейнеру иметь собственные точки монтирования, сетевые интерфейсы, идентификаторы пользователей, идентификаторы процессов и т. п.
Когда вы вводите команду в оболочке Bash и нажимаете Enter, Bash просит ядро создать обычный Linux-процесс с помощью системного вызова exec(). Контейнер же отличается тем, что когда вы отправляете запрос контейнерному движку, например docker, то демон docker просит ядро создать контейнеризованный процесс с помощью другого системного вызова, который называется clone(). Системный вызов clone () является особенным в том плане, что он может создавать процесс со своими виртуальными точками монтирования, идентификаторами процессов, идентификаторами пользователей, сетевыми интерфейсами, именем хоста и т.п.
Поэтому, хотя в Linux и нет какой-то одной структуры данных для представления контейнеров, ближе всего на роль подходят пространства имен ядра и системный вызов clone ().
Продолжение следует…
Так что, не владея терминологией, будет сложно понять, чем docker отличается от CRI-O, rkt или lxc/lxd; либо оценить роль Open Container Initiative в деле стандартизации контейнерных технологий.
Введение
Начать работать с Linux-контейнерами очень просто, но вскоре выясняется, что эта простота обманчива. Обычно это происходит так: потратив всего пару минут на установку docker или другого контейнерного движка, вы уже вводите свои первые команды. Еще пара минут – и вы уже создали свой первый образ контейнера и выложили его в общий доступ. Затем вы привычно переходите к архитектуре продакшн-среды, и тут вдруг понимаете, что для этого надо сперва разобраться с массой терминов и технологий, которые за всем этим стоят. Хуже того, многие из перечисленных ниже терминов используются взаимозаменяемо, что создает большую путаницу для новичков.
- Контейнер (Container)
- Образ (Image)
- Образ контейнера (Container Image)
- Слой образа (Image Layer)
- Реестр (Registry)
- Репозиторий (Repository)
- Тег (Tag)
- Базовый образ (Base Image)
- Образ платформы (Platform Image)
- Слой (Layer)
Усвоив изложенную в этом документе терминологию, вы будете лучше понимать технологический базис контейнеров. Кроме того, это поможет вам и вашим коллегам говорить на одном языке, а также осознанно и целенаправленно проектировать архитектуру контейнерных сред согласно специфике решаемых задач. В свою очередь, с точки зрения ИТ-сообщества и отрасли в целом, общий рост понимания контейнерных технологий способствует появлению новых архитектур и решений. Обратите внимание, что эта статья рассчитана на читателя, который уже имеет представление о том, как запускать контейнеры.
Контейнеры: основы
Прежде чем переходить к терминологии контейнеров, определимся, что же такое, собственно, сам контейнер. Термин «контейнер» обозначает сразу две вещи. Как и обычная Linux-программа, контейнер может находиться в одном из двух состояний: работающем и неработающем. В неработающем состоянии контейнер представляет собой файл или набор файлов, хранящихся на диске. Именно к этому состоянию относятся термины Образ контейнера и Контейнерный репозиторий. Когда вы вводите команду запуска контейнера, контейнерный движок распаковывает нужные файлы и метаданные и передает их ядру Linux. Запуск контейнера очень похож на запуск обычного Linux-процесса и требует API-обращения к ядру Linux. Этот API-вызов обычно инициирует дополнительную изоляцию и монтирует копию файлов, которые находятся в образе контейнера. После того, как контейнер запущен, это всего лишь Linux-процесс. Процедура запуска контейнеров, а также формат образов контейнеров, хранящихся на диске, определяются и регулируются стандартами.
Существует несколько форматов образов контейнеров (Docker, Appc, LXD), однако отрасль постепенно движется к единому стандарту Open Container Initiative, который иногда называют Open Containers или просто OCI. Этот стандарт задает спецификацию формата образов контейнеров, которая определяет дисковый формат хранения образов контейнеров, а также метаданных, которые, в свою очередь, определяют такие вещи, как аппаратная архитектура и операционная система (Linux, Windows и т. п). Единый отраслевой формат образов – это ключ к появлению программной экосистемы, позволяющей разработчикам, проектам Open Source и поставщикам ПО создавать совместимые друг с другом образы и различные инструменты, такие как средства электронной подписи, сканирования, сборки, запуска, перемещения и управления образами контейнеров.
Кроме того, существует несколько контейнерных движков, таких как Docker, CRI-O, Railcar, RKT, LXC. Контейнерный движок берет образ контейнера и превращает его в контейнер (т.е. в запущенный процесс). Процедура такого превращения также определяется стандартом OCI, который включает в себя спецификацию выполнения контейнера и эталонную реализацию среды выполнения, которая называется RunC и представляет собой модель с открытым исходным кодом, которая регулируется соответствующим сообществом разработки. Многие контейнерные движки используют эту модель для взаимодействия с ядром хоста при создании контейнеров.
Инструменты, поддерживающие спецификации формата образов контейнеров и среды выполнения контейнеров стандарта OCI, обеспечивают переносимость в рамках экосистемы различных контейнерных платформ, контейнерных движков и вспомогательных инструментов на различных облачных платформах и локальных архитектурах. Понимание терминологии, стандартов и архитектуры контейнерных систем позволит вам плодотворно общаться с другими специалистами и проектировать масштабируемые и поддерживаемые контейнеризованные приложения и среды, обеспечивающие продуктивное использование контейнеров на годы вперед.
Базовый словарь
Образ контейнера
В простейшем определении образ контейнера – это файл, который скачивается с сервера реестра и локально используется в качестве точки монтирования при запуске контейнера. Несмотря на то, что термин «образ контейнера» используется довольно часто, он может обозначать разные вещи. Дело в том, что хотя Docker, RKT и даже LXD работают по только что описанному принципу – то есть скачивают удаленные файлы и запускают их в виде контейнеров, – каждая из этих технологий трактует образ контейнера по-своему. LXD оперирует с монолитными (однослойными) образами, а docker и RKT используют OCI-образы, которые могут содержать несколько слоев.
Строго говоря, образ контейнера на сервере реестра – это далеко не один файл. Когда люди используют термин «образ контейнера», они часто имеют в виду репозиторий и подразумевают набор из нескольких слоев образа контейнера, а также метаданные, которые содержат дополнительные сведения об этих слоях.
Кроме того, понятие образа контейнера неявным образом подразумевает наличие формата такого образа.
Формат образа контейнера
Изначально каждый контейнерный движок, включая LXD, RKT и Docker, имел свой формат образов. Одни из этих форматов допускают наличие только одного слоя, другие поддерживают древовидную структуру из несколько слоев. Сегодня почти все основные контейнерные инструменты и движки перешли на формат OCI, который определяет, как должны быть устроены слои и метаданные в образе контейнера. По сути, OCI-формат определяет образ контейнера, который состоит из отдельных tar-файлов для каждого слоя и общего файла manifest.json, содержащего метаданные.
Стандарт Open Container Initiative (OCI), который изначально базировался на формате образов Docker V2, успешно объединил большую экосистему контейнерных движков, облачных платформ и инструментальных средств (сканеры безопасности, средства подписи, создания и перемещения контейнеров) и позволяет вам защитить свои инвестиции в знания и инструментальные средства.
Контейнерный движок
Контейнерный движок – это та часть программного обеспечения, которая принимает запросы пользователя, включая параметры командной строки, скачивает образы и, с точки зрения конечного пользователя, запускает контейнеры. Есть множество контейнерных движков, включая docker, RKT, CRI-O и LXD. Кроме того, многие облачные платформы, PaaS-сервисы и контейнерные платформы имеют собственные движки, которые понимают образы в формате Docker или OCI. Наличие отраслевого стандарта для формата образов обеспечивает интероперабельность всех этих платформ.
Спускаясь на уровень ниже, можно сказать, что большинство контейнерных движков на самом деле запускают контейнеры не сами, а через OCI-совместимую среду выполнения, наподобие runc. Обычно, среда выполнения контейнеров делает следующие вещи:
- Обрабатывает параметры, введение пользователем
- Обрабатывает параметры, переданные через API (чаще всего системой оркестрации контейнеров)
- Скачивает образы контейнеров с сервера реестра
- Распаковывает и сохраняет образ контейнера на диск с использованием драйвера Graph Driver (блочного или файлового, в зависимости от драйвера)
- Подготавливает точку монтирования контейнера, обычно в хранилище copy-on-write (опять же, блочном или файловом, в зависимости от драйвера)
- Подготавливает метаданные, которые будут переданы среде выполнения для корректного запуска контейнера, используя:
- Определенные параметры по умолчанию, которые подразумеваются для образа контейнера (например, ArchX86)
- Пользовательский ввод для переопределения содержащихся в образе контейнера значений по умолчанию (например, CMD, ENTRYPOINT)
- Параметры по умолчанию, заданные образом контейнера (например, правила SECCOM)
- Вызывает среду выполнения контейнеров
Контейнер
Контейнеры существуют в операционных системах уже довольно давно, ведь на самом деле это просто запущенный экземпляр контейнерного образа. Контейнер представляет собой стандартный Linux-процесс, который обычно создается с помощью системного вызова clone () вместо fork () или exec (). Кроме того, к контейнерам зачастую применяется дополнительные меры изоляции средствами cgroups, SELinux или AppArmor.
Контейнерный хост
Контейнерный хост – это система, на который выполняются контейнеризованые процессы, которые для простоты зачастую называются контейнерами. Это может быть, например, виртуальная машина RHEL Atomic Host, находящаяся в публичном облаке или работающая на голом железе в корпоративном ЦОД. Когда образ контейнера (иначе говоря, репозиторий) с сервера реестра скачивается на локальный контейнерный хост, говорят, что он попадает в локальный кэш.
Определить, какие репозитории синхронизированы с локальным кэшем, можно с помощью следующей команды:
[root@rhel7 ~]# docker images -a REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE registry.access.redhat.com/rhel7 latest 6883d5422f4e 3 weeks ago 201.7 MB
Сервер реестра
Сервер реестра – это, по сути, файловый сервер, который используется для хранения репозиториев docker. Как правило, сервер реестра задается по DNS-имени и, опционально, номеру порта. Большая часть преимуществ экосистемы docker обуславливается возможностью скачивать и загружать репозиториии на серверы реестра.
Если демон docker не находит копии репозитория в локальном кэше, он автоматически скачивает ее с сервера реестра. В большинстве дистрибутивов Linux демон docker будет использовать для этого сайт docker.io, однако в некоторых дистрибутивах его можно настроить по-своему. Например, Red Hat Enterprise Linux вначале пытается выполнить загрузку с сайта registry.access.redhat.com, и лишь затем с docker.io (Docker Hub).
Здесь надо подчеркнуть, что сервер реестра неявно считается доверенным. Поэтому вы должны сами решить, насколько доверяете содержимому того или иного реестра и, соответственно, разрешить или запретить его. Помимо безопасности есть и другие аспекты, которыми следует озаботиться заранее, например, вопросы лицензирования ПО или контроль соответствия требованиям. Простота, с которой docker позволяет пользователям скачать ПО, делает вопрос доверия чрезвычайно важным.
Red Hat Enterprise Linux позволяет настроить docker-реестр по умолчанию. Кроме того, RHEL7 и RHEL7 Atomic позволяют добавить или заблокировать серверы реестра через конфигурационный файл:
vi /etc/sysconfig/docker
В RHEL7 и RHEL 7 Atomic по умолчанию используется сервер реестра Red Hat:
ADD_REGISTRY='--add-registry registry.access.redhat.com'
В некоторых случаях по соображениям безопасности имеет смысл заблокировать общедоступные реестры docker, такие как DockerHub:
# BLOCK_REGISTRY='--block-registry'
Red Hat также предлагает свой интегрированный сервер реестра в составе OpenShift Container Platform, a также автономный корпоративный сервер реестра Quay Enterprise и облачные, частные и общедоступные репозитории Quay.io.
Оркестрация контейнеров
Люди обычно начинают с того, что устанавливают контейнерный хост и сначала просто скачивают нужные образы контейнеров. Затем они переходят к созданию собственных образов и загружают их на сервер реестра, чтобы сделать доступными остальным участникам команды. Через какое-то время возникает потребность объединить несколько контейнеров, чтобы их можно было развертывать как один юнит. И, наконец, в определенный момент эти юниты надо сделать частью производственного конвейера (разработка-QA-продакшн). Именно так люди обычно и приходят к осознанию того, что им нужна система оркестрации.
Система оркестрации контейнеров реализует всего две вещи:
- Динамически диспетчеризирует контейнерные нагрузки по компьютерам кластера (это часто называется «распределенные вычисления»)
- Предоставляет файл стандартного описания приложения (kube yaml, docker compose, и т. п.)
Две эти вещи на самом деле обеспечивают целый ряд преимуществ:
- Возможность управлять контейнерами, из которых состоит приложение, независимо друг от друга, что позволяет эффективно решать следующие задачи:
- Утилизация больших кластеров контейнерных хостов
- Отработка отказов на уровне отдельных контейнеров (переставшие отвечать процессы, исчерпание памяти)
- Отработка отказов на уровне контейнерных хостов (диски, сеть, перезагрузка)
- Отработка отказов на уровне контейнерного движка (повреждение, перезапуск)
- Индивидуальное масштабирование контейнеров вверх и вниз
- Простота развертывания новых экземпляров одного и того же приложения в новых средах, как облачных, так и традиционных, например:
- На машинах разработчиков, управляемых системой оркестрации
- В общей среде разработки в частном пространстве имен
- В общей среде разработки во внутреннем общедоступном пространстве имен для обеспечения видимости и выполнения тестирования
- Во внутренней среде QA
- В тестовой нагрузочной среде, динамически предоставляемой и отзываемой в облаке
- В эталонной среде для проверки совместимости с продакшн-средой
- В продакшн-среде
- В среде аварийного восстановления
- В новой продакшн-среде, содержащей обновленные контейнерные хосты, контейнерные движки или средства оркестрации
- В новой продакшн-среде, которая ничем не отличается от основной, но расположена в другом регионе
Сообщества Open Source и поставщики ПО предлагают множество различных средств оркестрации. Изначально большая тройка таких инструментов включала в себя Swarm, Mesos и Kubernetes, однако сегодня Kubernetes фактически стал отраслевым стандартом, поскольку о его поддержке заявили даже Docker и Mesosphere, не говоря уже почти обо всех крупных поставщиках услуг. Однако если вы ищете корпоративную систему оркестрации, рекомендуем присмотреться к Red Hat OpenShift.
Расширенный словарь
Среда выполнения контейнеров
Среда выполнения контейнеров – это низкоуровневый компонент, который обычно используется в составе контейнерного движка, но также может применяться и в ручном режиме для тестирования контейнеров. Стандарт OCI задает эталонную реализацию среды выполнения, известную как runc. Это наиболее широко используемая реализация, однако есть и другие OCI-совместимые среды выполнения, такие как crun, railcar и katacontainers. Docker, CRI-O и многие другие контейнерные движки используют runc.
Среда выполнения контейнеров отвечает за следующие вещи:
- Получает точку монтирования контейнера, предоставленную контейнерным движком (при тестировании это может быть просто каталог)
- Получает метаданные контейнера, предоставленные контейнерным движком (при тестировании этот может быть собранный вручную файл config.json)
- Связывается с ядром ОС для запуска контейнеризованных процессов (через системный вызов clone)
- Настраивает cgroups
- Настраивает SELinux Policy
- Настраивает правила App Armor
Маленький исторический экскурс: когда движок Docker только появился, он использовал в качестве среды выполнения LXC. Затем разработчики Docker написали собственную библиотеку для запуска контейнеров под названием libcontainer. Она была написана на языке Golang и вошла в состав движка Docker. После учреждения организации OCI фирма Docker внесла исходный код libcontainer в этот проект и выпустила эту библиотеку в виде отдельной утилиты под названием runc, которая затем и стала эталонной реализацией среды выполнения контейнеров в рамках стандарта OCI и применяется в других контейнерах движках, таких как CRI-O. Runc – это очень простая утилита, которая просто ждет, когда ей передадут точку монтирования (каталог) и метаданные (config.json). Дополнительные сведения о runc можно найти здесь.
Для более глубокого понимания см. Общие сведения о контейнерных стандартах, а также Среда выполнения контейнеров.
Слои образа
Репозитории часто называют образами или образами контейнеров, хотя на самом деле репозитории состоят из одного или нескольких слоев. Слои образа в репозитории связаны между собой отношениями «родитель-потомок», и каждый слой образа содержит в себе отличия от родительского слоя.
Давайте просмотрим слои репозитория на локальном контейнерном хосте. Поскольку начиная с версии 1.7 в Docker нет встроенного инструмента для просмотра слоев образа в локальном репозитории (но есть инструменты для онлайн-реестров), мы будем использовать утилиту Dockviz. Обратите внимание, что каждый слой имеет тег и универсальный уникальный идентификатор (UUID). Чтобы просмотреть сокращенные идентификаторы UUID, которые обычно уникальны в пределах одной машины, мы используем следующую команду (если вам нужен полный UUID, используйте ту же команду с опцией -no-trunc):
docker run --rm --privileged -v /var/run/docker.sock:/var/run/docker.sock nate/dockviz images -t
├─2332d8973c93 Virtual Size: 187.7 MB │ └─ea358092da77 Virtual Size: 187.9 MB │ └─a467a7c6794f Virtual Size: 187.9 MB │ └─ca4d7b1b9a51 Virtual Size: 187.9 MB │ └─4084976dd96d Virtual Size: 384.2 MB │ └─943128b20e28 Virtual Size: 386.7 MB │ └─db20cc018f56 Virtual Size: 386.7 MB │ └─45b3c59b9130 Virtual Size: 398.2 MB │ └─91275de1a5d7 Virtual Size: 422.8 MB │ └─e7a97058d51f Virtual Size: 422.8 MB │ └─d5c963edfcb2 Virtual Size: 422.8 MB │ └─5cfc0ce98e02 Virtual Size: 422.8 MB │ └─7728f71a4bcd Virtual Size: 422.8 MB │ └─0542f67da01b Virtual Size: 422.8 MB Tags: docker.io/registry:latest
Как видите, репозиторий docker.io/registry фактически состоит из множества слоев. Однако, что гораздо важнее, пользователь в принципе может «запустить» контейнер с любой ступеньки в этой лесенке слоев, например, введя приведенную ниже команду (она полностью корректна, однако никто не сможет гарантировать, что она была протестирована или вообще будет работать правильно). Как правило, сборщик образов помечает тегом (создает имена) те слои, которые стоит использовать в качестве отправной точки:
docker run -it 45b3c59b9130 bash
Репозитории устроены подобным образом потому, что всякий раз, когда сборщик создает новый образ, отличия сохраняются в виде еще одного слоя. Существует два основных способа создания новых слоев в репозитории. Во-первых, при создании образа вручную каждое подтверждение изменений создает новый слой. Если сборщик создает образ с помощью файла Docker, каждая директива в файле создает новый слой. Поэтому всегда полезно иметь возможность посмотреть, что изменилось в репозитории между слоями.
Теги
Хотя пользователь может и сам указать стартовый слой для монтирования и запуска контейнера в репозитории, ему вовсе не обязательно это делать. Когда сборщик образов создает новый репозиторий, он, как правило, маркируют наиболее подходящие на эту роль слои. Эти маркеры называются тегами и представляют собой инструмент, с помощью которого сборщик образов может сообщить потребителю образов, какие слои лучше использовать. Обычно теги используются для обозначения версий ПО внутри репозитория. Однако ни OCI, ни какой-либо другой стандарт никак не регламентируют использование тегов, что открывает неограниченный простор для путаницы в ходе совместной работы. Поэтому мы советуем тщательно документировать теги, если они используются не только для маркировки версий ПО.
Кроме того, есть один особый тег – latest, который обычно указывает на слой, содержащий последнюю версию ПО в репозитории. Этот тег просто указывает на слой образа, как и любой другой тег, и поэтому тоже может использоваться неправильно.
Чтобы удаленно просмотреть доступные в репозитории теги, выполните следующую команду (утилита jq делает вывод гораздо читабельнее):
curl -s registry.access.redhat.com/v1/repositories/rhel7/tags | jq { "7.0-21": "e1f5733f050b2488a17b7630cb038bfbea8b7bdfa9bdfb99e63a33117e28d02f", "7.0-23": "bef54b8f8a2fdd221734f1da404d4c0a7d07ee9169b1443a338ab54236c8c91a", "7.0-27": "8e6704f39a3d4a0c82ec7262ad683a9d1d9a281e3c1ebbb64c045b9af39b3940", "7.1-11": "d0a516b529ab1adda28429cae5985cab9db93bfd8d301b3a94d22299af72914b", "7.1-12": "275be1d3d0709a06ff1ae38d0d5402bc8f0eeac44812e5ec1df4a9e99214eb9a", "7.1-16": "82ad5fa11820c2889c60f7f748d67aab04400700c581843db0d1e68735327443", "7.1-24": "c4f590bbcbe329a77c00fea33a3a960063072041489012061ec3a134baba50d6", "7.1-4": "10acc31def5d6f249b548e01e8ffbaccfd61af0240c17315a7ad393d022c5ca2", "7.1-6": "65de4a13fc7cf28b4376e65efa31c5c3805e18da4eb01ad0c8b8801f4a10bc16", "7.1-9": "e3c92c6cff3543d19d0c9a24c72cd3840f8ba3ee00357f997b786e8939efef2f", "7.2": "6c3a84d798dc449313787502060b6d5b4694d7527d64a7c99ba199e3b2df834e", "7.2-2": "58958c7fafb7e1a71650bc7bdbb9f5fd634f3545b00ec7d390b2075db511327d", "7.2-35": "6883d5422f4ec2810e1312c0e3e5a902142e2a8185cd3a1124b459a7c38dc55b", "7.2-38": "6c3a84d798dc449313787502060b6d5b4694d7527d64a7c99ba199e3b2df834e", "latest": "6c3a84d798dc449313787502060b6d5b4694d7527d64a7c99ba199e3b2df834e" }
Репозиторий
При использовании команды docker в командной строке указывается репозиторий, а не образ. Например, в приведенной ниже команде «rhel7» – это репозиторий.
docker pull rhel7
На самом деле эта команда автоматически развертывается в следующую:
docker pull registry.access.redhat.com/rhel7:latest
Однако многие считают, что это образ или образ контейнера. На самом же деле, для того, чтобы получить список локально доступных репозиториев, используется подкоманда docker images. Формально эти репозитории можно рассматривать как образы контейнеров, но при этом важно четко понимать, что эти репозитории на самом деле состоят из слоев и включают метаданные, содержащиеся в файле под названием «манифест» (manifest.json):
docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE registry.access.redhat.com/rhel7 latest 6883d5422f4e 4 weeks ago 201.7 MB registry.access.redhat.com/rhel latest 6883d5422f4e 4 weeks ago 201.7 MB registry.access.redhat.com/rhel6 latest 05c3d56ba777 4 weeks ago 166.1 MB registry.access.redhat.com/rhel6/rhel latest 05c3d56ba777 4 weeks ago 166.1 MB ...
Указывая в командной строке репозиторий, мы на самом деле просим контейнерный движок выполнить за нас часть работы. В приведенном выше примере демон docker (именно демон, а не клиентский инструмент) имеет сконфигурированный список серверов для поиска, и поэтому будет искать репозиторий «rhel7» на каждом из них.
В приведенном выше примере мы указали только имя репозитория, но в клиенте docker можно указать и полный URL. Чтобы понять, как это сделать, разобьем полный URL на составные части.
Иначе говоря, все сводится к виду:
REGISTRY/NAMESPACE/REPOSITORY[:TAG]
Полный URL состоит из имени сервера, пространства имен и, опционально, тега. На самом деле при указании URL есть масса нюансов, и по мере изучения экосистемы docker вы увидите, что многие вещи указывать не обязательно. В частности, посмотрите на команды ниже: все они являются корректными и приводят к одному и тому же результату:
docker pull registry.access.redhat.com/rhel7/rhel:latest docker pull registry.access.redhat.com/rhel7/rhel docker pull registry.access.redhat.com/rhel7 docker pull rhel7/rhel:latest
Пространства имен
Пространства имен – это инструмент для разделения репозиториев на группы. В общедоступном реестре DockerHub пространство имен обычно представляет собой имя пользователя, предоставившего образ в общий доступ, но может быть и именем группы или логическим именем.
Red Hat использует пространства имен для разделения групп репозиториев по продуктам, перечисленным на сервере Red Hat Federated Registry. Пример с результатами опроса registry.access.redhat.com приводится ниже. Обратите внимание, что последняя строка в этом примере фактически указывает на другой сервер реестра. Это связано с тем, что Red Hat работает над тем, чтобы отображать репозитории на реестр-серверах наших партнеров:
registry.access.redhat.com/rhel7/rhel registry.access.redhat.com/openshift3/mongodb-24-rhel7 registry.access.redhat.com/rhscl/mongodb-26-rhel7 registry.access.redhat.com/rhscl_beta/mongodb-26-rhel7 registry-mariadbcorp.rhcloud.com/rhel7/mariadb-enterprise-server:10.0
Обратите внимание, что иногда полный URL можно и не указывать. В примере выше для каждого пространства имен есть репозиторий по умолчанию. Если пользователь указывает только пространство имен fedora, то на локальный сервер скачается репозиторий с тегом latest. Поэтому приведенные ниже команды приводят к одному и тому же результату:
docker pull fedora docker pull docker.io/fedora docker pull docker.io/library/fedora:latest
Пространства имен ядра
Пространства имен ядра кардинально отличаются от пространств имен, которые мы обсуждали выше, когда говорили о репозиториях и серверах реестра. При обсуждении контейнеров пространства имен ядра являются, пожалуй, самой важной структурой данных, благодаря которой и существуют контейнеры, по крайне мере, в их сегодняшнем виде. Пространства имен ядра позволяют каждому контейнеру иметь собственные точки монтирования, сетевые интерфейсы, идентификаторы пользователей, идентификаторы процессов и т. п.
Когда вы вводите команду в оболочке Bash и нажимаете Enter, Bash просит ядро создать обычный Linux-процесс с помощью системного вызова exec(). Контейнер же отличается тем, что когда вы отправляете запрос контейнерному движку, например docker, то демон docker просит ядро создать контейнеризованный процесс с помощью другого системного вызова, который называется clone(). Системный вызов clone () является особенным в том плане, что он может создавать процесс со своими виртуальными точками монтирования, идентификаторами процессов, идентификаторами пользователей, сетевыми интерфейсами, именем хоста и т.п.
Поэтому, хотя в Linux и нет какой-то одной структуры данных для представления контейнеров, ближе всего на роль подходят пространства имен ядра и системный вызов clone ().
Продолжение следует…