Если говорить о стратегиях высокого уровня, то существует три основных подхода к процедуре перемещения приложений в контейнеры: действия по принципу «взял и перенес» (lift and shift), перенос с дополнением (augment) и перенос с перекодированием (rewrite).

Но какой бы метод вы ни выбрали, важно отдавать себе отчет в том, что в большинстве своем компьютерные программы задумывались и создавались еще до появления современных технологий контейнеров на основе образов. Даже если вы решите воспользоваться методом «взял и перенес» — а в этом случае вам, возможно, придется запускать монолитное приложение внутри одного контейнера, — скорее всего, ваше приложение нужно будет модифицировать. Чтобы успешно выполнить переход к работе с контейнерами, вам придется разработать добротную стратегию миграции, в которой будут учтены потребности ваших приложений и природа контейнеров в операционной системе Linux.

В предлагаемой статье приводятся специфические технические рекомендации и общие соображения, касающиеся перевода программного обеспечения в контейнеры, от процедур построения образов до указаний на то, как эти средства должны выполняться в производственной среде. Процедура переноса в контейнеры того или иного приложения определяется его требованиями.

Некоторые предположения

В коммерческом отношении использование контейнеров дает не меньшие выгоды, чем в техническом. При построении и использовании контейнеров решающее значение имеет размещение их компонентов. Анализируя приложение, вы должны думать о каждой из его частей и о том, как они взаимодействуют друг с другом. Это похоже на то, как аналитики разбивают программу на наборы классов и функций. Контейнеры состоят из пакетов и сценариев, которые в сочетании с другими контейнерами составляют ваше приложение. Поэтому рассматривайте контейнеры как набор, состоящий из более мелких блоков, и имейте в виду, что, формируя из этих блоков легко управляемые структуры, вы облегчаете процесс понимания контейнеризованного приложения, а также процессы его развертывания и обслуживания.

Надлежащее разбиение по уровням

Цель разбиения по уровням заключается в том, чтобы сформировать условия для создания более сложной структуры посредством введения тонкого уровня абстракции над предшествующим уровнем. Уровни — это логические блоки, содержимое которых относится к тому же типу объектов или решает аналогичную задачу.

Выделяя в контейнере надлежащее число уровней, вы облегчаете процесс его эксплуатации. Если число уровней избыточно, контейнер будет слишком сложным в управлении. Число уровней в приложении должно отражать степень сложности последнего — чем сложнее приложение, тем больше в нем уровней. Так, если задача контейнера Hello World состоит в том, чтобы передавать в стандартный поток вывода (stdout) текст Hello World, в этот контейнер не нужно включать настройки, управление процессами или зависимости. Соответственно здесь нам понадобится один уровень. Но если мы захотим расширить задачу приложения Hello World так, чтобы оно обращалось с приветствием к конкретному пользователю, нам нужно будет ввести второй уровень, ответственный за сбор входных данных.

Стартовые сценарии

Стартовые сценарии весьма успешно справляются с задачей формирования тонкого уровня абстракции над уровнем обслуживания, который запускается при старте контейнера. Стартовые сценарии расширяют возможности средств базового уровня с помощью весьма простого интерфейса прикладного программирования (API), к которому подключается оператор. Чаще всего стартовые сценарии решают такие задачи, как предоставление разрешений, перемещение файлов настроек, изменение собственников файлов, очистка каталогов и запуск служб.

Повторный запуск

Каждое действие, влияющее на жизненный цикл приложения, предусматривает повторный запуск, поскольку это недорогой и эффективный способ извещения процесса о том, что выполняется некая операция. Что же касается операций, влияющих на жизненный цикл, то перезапуск в ходе их выполнения требуется для того, чтобы внести изменения в процесс.

Рассмотрим такой пример. Допустим, нам требуется перенастроить работу базы данных MariaDB. Возьмем файл JSON, простой удаленный вызов процедуры (Remote Procedure Call, RPC), которая сопоставляет места расположения файлов настроек и стартовой информации. Упомянутый файл JSON интерпретируется модулем set_configs.py, который при запуске контейнера копирует файлы настроек в места их размещения. Пользователь перенастраивает установки базы данных MariaDB, изменяя настройки на хосте и перезапуская компьютер.

Положитесь на оркестровку

Не следует полагать, что каждый уровень вашего приложения должен располагаться именно в контейнере, поэтому не создавайте в нем слишком много уровней. В этом случае ваши конструкции начнут быстро «накладываться» на существующие инструменты, на другие уровни, и структура контейнера станет слишком сложной. Мы не хотим создавать дополнительную работу за счет встраивания в контейнеры инструментов, которые только и могут, что развертывать наше приложение и управлять им. Наша цель в том, чтобы облегчить решение задачи управления хорошо укомплектованными контейнерами, возложенной на средства оркестровки.

Требования к приложениям

К приложениям предъявляются вполне конкретные требования в отношении соответствия определенным архитектурным критериям, критериям безопасности и производительности, как показано на рисунке 1. Некоторые из этих требований влияют на объем усилий, необходимых для переноса приложения в контейнер или для разнесения его по нескольким контейнерам.

Требования к приложениям
Рисунок 1. Требования к приложениям

Архитектура

В архитектурном отношении перемещение приложений в контейнеры чем-то напоминает переход от Unix к системе Linux или обновление операционной системы. Часто речь идет о таком приложении, которое эксплуатируется уже в течение нескольких лет. А документации у подобных приложений часто не бывает вообще или она безнадежно устарела. Как это происходит в большинстве случаев при осуществлении миграции, совершающему данную операцию сотруднику потребуется выполнить всю работу, необходимую для понимания того, как функционирует приложение в конструктивном отношении. Ему нужно будет произвести реконструкцию и разобраться с тем, как приложение было настроено. По меньшей мере он должен подготовиться к тому, чтобы ответить на такие вопросы:

  1. Где расположены исполняемые файлы для данного приложения? Установлены ли они с помощью инсталлятора, который размещает все подобные файлы в одном месте, или разбросаны по всей файловой системе? Имеется ли один исполняемый файл, который с легкостью запускается, или мы можем задействовать простой файл systemd?
  2. Где размещаются данные, используемые этим приложением? Предназначены они только для чтения или допускают возможность как чтения, так и записи? Можно ли записывать их без риска с применением двух одновременно исполняемых процессов?
  3. Где размещаются все данные с настройками? В одном каталоге, в одном файле или в различных местах по всей файловой системе?
  4. Какого рода секретные данные обрабатывает приложение? Можно ли задавать местоположение секретов в приложении? Допустимо ли перемещение этих данных в отдельные каталоги или к ним можно обращаться с помощью того или иного ключа через сервер идентификации либо сервер сертификатов?
  5. Какого рода сетевой доступ требуется для приложения? Просто HTTP? Или это сервер имен, для доступа к которому необходим протокол пользовательских дейтаграмм (User Datagram Protocol, UDP)? А может быть, мы имеем дело со сложным приложением, предусматривающим двухточечное шифрование данных, передаваемых от одного контейнера к другому, с помощью одного из протоколов IPsec?
  6. Является ли программа установки сценарием оболочки, при использовании которого мы можем получить дополнительную информацию о схеме приложения методом реконструкции? Устанавливаются ли исполняемые файлы с помощью диспетчеров RPM или с помощью других диспетчеров пакетов?
  7. Получаете ли вы в соответствии с лицензионным соглашением об использовании приложения право на беспрепятственное размещение приложения внутри образа контейнера? Иногда условия лицензирования накладывают на пользователя весьма жесткие ограничения; с другой стороны, вы, возможно, приобрели лицензию на работу с сайтом.
  8. Легко ли перезапускается приложение? Решения Apache нередко перезапускаются без сбоев тысячи раз, однако таблицы базы данных могут в таких условиях быть повреждены. Может ли это осложнить процесс оркестровки и восстановления?

Ответы на эти вопросы дают возможность судить о том, насколько ваше приложение приспособлено для переноса в контейнер (таблица 1). Если этот процесс окажется слишком сложным, вы просто не получите достаточную отдачу от инвестиций.

Типичные рабочие нагрузки в центре обработки данных
 

Безопасность

Во многих отношениях затрагивающие проблемы безопасности решения, которые связаны с контейнеризованными приложениями, подобны обычным приложениям, выполняемым внутри процессов. Определение того, какой уровень изоляции достаточен для данного приложения, возлагается на администратора. Оценивая рабочие нагрузки в процессе миграции и принимая решение относительно того, предоставляют ли контейнеры достаточный уровень изоляции, оператор должен точно представлять уровень изоляции рабочей нагрузки, обеспечиваемый по месту ее текущего выполнения.

Рассматривая рисунок 2, мы отмечаем, что каждое последующее технологическое решение обеспечивает более высокий уровень изоляции. Для некоторых приложений достаточный уровень изоляции обеспечивают регулярные процессы Linux. Нередко MySQL и веб-сервер выполняются на одном экземпляре операционной системы Linux. На другом конце спектра — случаи, когда две копии того или иного приложения приходится размещать в двух отдельных центрах обработки данных, функционирующих в районах с различными погодными условиями и различным уровнем сейсмоопасности, что характерно для ситуаций восстановления после аварийного сбоя.

Последовательность вложения нагрузок
Рисунок 2. Последовательность вложения нагрузок

Возьмем для примера рабочие нагрузки при осуществлении высокопроизводительных вычислений (High-Performance Computing, HPC), которые выполняются сегодня в больших кластерах, где изоляция обеспечивается только средствами регулярных процессов Linux. Это решение несовершенно, ведь работающие в большом кластере исследователи могут попытаться «навести порядок» в процессах своих коллег, однако принято считать, что такой уровень риска является допустимым.

Еще один пример. Даже при использовании виртуальных машин широко распространена практика, когда обращенные во внешнюю среду службы, такие как серверы имен доменов (DNS), службы HTTP или виртуальных частных сетей (VPN), выполняются во внешней сети, то есть совершенно не в том кластере виртуализации, в котором действуют службы, обращенные во внутреннюю среду, скажем, базы данных Oracle или экземпляры SAP. Такую схему размещения можно считать эквивалентной изоляции на уровне серверной стойки.

Редкий эксперт по проблемам безопасности будет чувствовать себя, что называется, в своей тарелке, если ему доведется выполнять эти два типа рабочих нагрузок в одном и том же кластере виртуализации. То же можно сказать и о контейнерных платформах; обычно эти типы служб выполняются в различных кластерах.

Итак, когда вы задаете себе вопрос, обеспечивают ли контейнеры достаточный уровень изоляции, рассмотрите его в контексте требований к рабочим нагрузкам.

Производительность

Рабочие нагрузки необходимо анализировать и в контексте производительности. Контейнеры и виртуализация — технологии, дополняющие друг друга. Для получения новых функций их можно использовать в сочетании с «голым железом». В таблице 2 представлено краткое руководство, иллюстрирующее важные возможности, которые следует принимать во внимание при переносе приложений в контейнеры.

Сравнение платформ рабочих нагрузок
 

Контейнеры представляют собой процессы Linux, которые обеспечивают более высокий уровень изоляции приложений с помощью таких технологий, как контрольные группы (cgroups), Security-Enhanced Linux (SELinux) и пространства имен. Это дает им возможность выполняться на собственной или близкой скорости. При этом уровень абстракции, такой как виртуализация, отсутствует, так что все помещенные в контейнеры приложения кластера должны быть построены на одной и той же аппаратной архитектуре и операционной системе.

Оставаясь в рамках того же примера — в среде HPC, — отметим, что контейнеры можно строить на «голом железе». При этом будет обеспечиваться прежний уровень производительности, тогда как уровень изоляции будет возрастать. С другой стороны, если рабочая нагрузка представляет собой корпоративное приложение и для работы последнего требуются компоненты, функционирующие в средах Windows и Linux, возможно, предпочтение следует отдать сочетанию контейнеров и средств виртуализации. При выполнении контейнеров внутри виртуальной машины мы получаем сочетание аппаратной свободы и более высокого уровня изоляции и управляемости, обеспечиваемого с использованием образов контейнеров.

Таблица 2 поможет вам уяснить плюсы и минусы сочетания различных технологий.

Перечень технических требований

Рекомендации по перемещению приложений в контейнеры можно свести к приведенному ниже списку требований.

  • Разделяйте приложения на уровни.
  • Число уровней должно отражать степень сложности приложения.
  • Уровень абстракции контейнеров должен быть чуть выше, чем у пакетов установочных файлов RPM.
  • Не старайтесь решать все проблемы внутри контейнера.
  • Для обеспечения простого извлечения в среде исполнения используйте уровень стартовых сценариев.
  • Встраивайте в контейнеры четкие и лаконичные операции, которыми будут управлять инструменты, расположенные во внешней среде.
  • Идентифицируйте и разделяйте код, параметры настройки и данные.
  • Код должен функционировать на уровнях образов.
  • Настройки, данные и секретные сведения должны поступать из среды.
  • Контейнеры следует перезагружать.
  • Не восстанавливайте процессы.
  • Никогда не продолжайте разработку кода с последнего тега, иначе через некоторое время ваши сборки станут невоспроизводимыми.
  • Используйте проверки на существование и готовность.

Разработка архитектуры

В процессе разработки архитектуры обращайтесь к этому перечню при создании образов контейнеризованных приложений. Важно руководствоваться следующими рекомендациями:

1. Определите местоположение всех исполняемых файлов, необходимых для того, чтобы приложение могло выполняться в контейнере.

а) Используйте уровни — помните о базовых сборках и уровнях среды исполнения приложений.

б) Установите зависимости и определите, должны ли предшествующие уровни содержать эти зависимости, особенно если они могут быть объектами совместного использования или применяться другими приложениями.

в) Установите, какие механизмы запускают исполняемые файлы: сценарий, процесс systemd и т. д.

2. Определите файлы и каталоги, содержащие данные по настройкам для приложения. Настройка должна осуществляться из среды (разработка/тестирование/эксплуатация) и не встраиваться в образ контейнера.

3. Определите файлы и каталоги, которые будут содержать данные для приложения. Эти данные должны быть смонтированы к приложению во время исполнения. Данные приложения должны поступать из среды разработка/тестирование/эксплуатация и не встраиваться в образ контейнера.

4. Определите, какие сетевые протоколы потребуются для приложения. Это позволит понять, предназначены ли данные службы для работы с внутренними контрагентами (кластером) или с внешними (заказчиком).

5. Можно ли реконструировать программу установки, с тем чтобы точнее представить, как она работает?

а) Определите, какие изменения она вносит в настройки; попытайтесь определить, существует ли возможность вместо сценария установки использовать сценарий или средства управления настройками. Можно ли передать параметры образу контейнера во время исполнения, чтобы параметры были установлены динамически?

б) Определите, где хранятся данные; попытайтесь выяснить, создается ли схема данных во время установки и можно ли задать ее в сценарии в процессе исполнения приложения.

6. Определите, легко ли перезапускается служба. Если она чувствительна к перезапускам, с помощью проверок на существование и готовность выясните, требуется ли вмешательство со стороны оператора. Чрезвычайно важно максимально автоматизировать содержимое среды оркестровки контейнера.

7. Определите, куда должны поступать данные регистрации и выходные данные. Как правило, журналы регистрации приложений, установленных с помощью пакетов RPM, поступают в каталог /var/log или в другие известные местоположения. В контейнеризованной среде это может привести к сбросу больших объемов данных на уровень чтения-записи.

8. Установите, есть ли потребность в масштабировании индивидуальных процессов. При необходимости распределите приложение по нескольким контейнерам.

Обеспечение безопасности

Обращайтесь к этому перечню при создании образов контейнеризованных приложений. Важно учитывать следующие рекомендации:

1. При любой возможности используйте политики. Для определения политик применяйте ограничения контекста безопасности и учетные записи служб. Специалисты Red Hat пришли к заключению, что элементы управления на базе профилей обычно функционируют лучше, а также поддерживаются более широко, чем индивидуальные правила для каждого приложения.

2. Определите файлы, содержащие секретные сведения, которые используются приложением. К ним относятся, в частности, сертификаты и пароли. Подобные объекты никогда не следует размещать в образе контейнера, поскольку доступ к ним получает любой пользователь, загрузивший такой образ. Эти данные должны быть защищены и предоставлены средой контейнера.

3. Не запускайте контейнеры на низких портах, где требуется наличие привилегий. Пользуйтесь встроенными ограничениями контекста безопасности, которые препятствуют реализации этого варианта.

4. Избегайте выполнения контейнеров в качестве корневых. Даже в случае применения пользовательского пространства имен в ядре существует опасность выхода за границы контейнера, что может привести к компрометации базового хоста контейнера. Чтобы не допустить подобного развития событий, используйте встроенные ограничения контекста безопасности.

5. По возможности запускайте контейнеры с корневой файловой системой в режиме «только чтение». С целью принудительного перехода в этот режим используйте ограничения контекста безопасности.

a) При работе с веб-контентом запускайте службу установки тома тоже в режиме «только чтение».

6. Определите необходимый уровень изоляции. Руководствуйтесь принципами наименьших привилегий и глубокой обороны. Например, в среде Red Hat OpenShift Container Platform применяются следующие технологии с ограничениями контекста безопасности и учетными записями служб:

a) SELinux — системы Red Hat Enterprise Linux поставляются с профилями, готовыми к работе без предварительной настройки. Динамические контексты генерируются для каждого контейнера с использованием технологии безопасной виртуализации (sVirt).

б) Безопасные вычисления (secure computing, seccomp). Контейнеры запускаются без использования стандартного профиля seccomp. Пользователи должны определять и настраивать профиль самостоятельно.

в) Возможности Linux:

— стандартный контекст безопасности в OpenShift ограничен; возможности KILL, MKNOD, SYS_CHROOT, SETUID, SETGID не предусмотрены;

— возможно, у администраторов возникнет потребность в создании особых ограничений, исключающих следующие функции, в первую очередь для процессов, которым будут предоставляться корневые привилегии (полезно для администраторов): AUDIT_CONTROL, BLOCK_SUSPEND, DAC_READ_SEARCH, IPC_LOCK, IPC_OWNER, LEASE, LINUX_IMMUTABLE, MAC_OVERRIDE, а также MAC_ADMIN;

— возможно, у администраторов возникнет потребность в создании особых ограничений, предусматривающих следующие функции, в первую очередь для процессов, которым будут предоставляться корневые привилегии (полезно при выполнении административных задач): NET_ADMIN, NET_BROADCAST, SYS_ADMIN, SYS_BOOT, SYS_MODULE, SYS_NICE, SYS_PTRACE, SYS_PACCT, SYS_RAWIO, SYS_RESOURCE, SYS_TIME, SYS_TTY_CONFIG, SYSLOG, а также WAKE_ALARM.

г) Ограничения контекста безопасности включаются в платформу Red Hat OpenShift Container и регулируются правилами, применяемыми по умолчанию.

Обеспечение производительности

Обращайтесь к этому перечню при создании образов контейнеризованных приложений. Важно учитывать приведенные ниже рекомендации.

1. Определите, обращаются ли приложения к временным файлам или создают их. По умолчанию эти файловые системы монтируются как нередактируемые. Их можно подключать как тома, но следует проявлять осторожность при предоставлении нескольким процессам права доступа с возможностью чтения и записи.

a) /sys;

б) /proc:

— псевдофайловая система, предоставляющая доступ к структурам данных ядра;

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

— записи в каталоге /proc/[pid]ns представляют пространства имен ядра, созданные для размещенного в контейнере процесса id;

— к числу примеров относятся /proc/asound, /proc/bus, /proc/fs, /proc/irq, /proc/sys и /proc/sysrq-trigger.

в) /dev:

— внутри контейнера приложение может взаимодействовать с ограниченным числом файлов устройств, таких как /dev/null и / dev/zero. Контейнеризованные приложения могут обращаться к файлам устройств хоста, таким как /dev/sdX и /dev/ttySX, только если функционируют в привилегированном режиме.

г) /run:

— после подключения программы docker v1.10 пользователь может передавать параметр -tmpfs для выполнения этой программы; далее /run подключается как tmpfs внутри контейнера;

— в системах Red Hat /run/secrets всегда подключается как tmpfs, с тем чтобы обеспечить место для введения сведений о подписке;

— на главной системе Linux /run подключается как tmpfs для сохранения временных данных процесса (например, pid системного процесса). После перезагрузки сервера они будут удалены. Внутри контейнера только каталог /run/secrets подключается как tmpfs, тогда как каталог /run включается в файловую систему /(root). В результате файлы, входящие в каталог /run, не удаляются даже в случае перезагрузки контейнера.

2. Определите, нужно ли для работы приложения вносить изменения в параметры ядра (/proc/sys) или предоставлять доступ к специальным аппаратным средствам.

a) По умолчанию размещенные в каталоге /proc/sys переменные для настройки ядра могут использоваться контейнеризованным процессом в режиме только для чтения.

б) Возможно, для запуска приложений этого типа потребуется внести в настройки определенных узлов корректные параметры ядра или аппаратные средства и использовать селекторы узлов для закрепления на узлах с особой архитектурой либо ресурсами. Приведем в качестве примеров /proc/sys/fs/mqueue, /proc/sys/kernel/{msgmax, msgmnb, msgmni, sem, shmall, shmmax, shmmni, and shm_rmid_forced}.

в) Если необходимо изменить параметры ядра в самом приложении (например, через sysctl), его следует запускать в изолированном кластере с привилегированными контейнерами.

3. Дата, время и локальные настройки.

a) Определите, нужно ли вводить в приложение другой набор локальных настроек (например, JST). Введите их в образ на этапе его создания — перестройте образ.

б) Определите, нужно ли в данном приложении изменить настройку даты.

4. Определите, предполагается ли присвоить приложению фиксированный IP-адрес.

a) По возможности не используйте статическую IP-адресацию.

б) В интерфейсе контейнерной сети изменение IP-адресов невозможно.

в) Где это возможно, используйте имена главных систем, поскольку заботу о средствах для работы в сети возьмет на себя служебная сеть Kubernetes.

г) Если в параметры или настройки приложения включен IP-адрес, отключите ENTRYPOINT, чтобы файл настроек динамически изменялся при запуске. Для выполнения этой операции используются такие инструменты, как SED или Ansible.

5. Выясните, нужно ли для работы приложения задействовать несколько сетевых интерфейсов.

a) На сегодня технология Kubernetes не поддерживает использование сетевых интерфейсных плат (Network Interface Controllers, NICs). Сетевые резервные функции, такие как связывание, не могут использоваться внутри контейнеров; такая возможность реализуется на уровне хостов. Контейнеры следует проектировать так, чтобы сбои и перезапуски осуществлялись на узлах с работающей сетью. Проведите необходимые проверки на интенсивность и готовность.

б) Возможность использования интерфейсов многоканальных сетей может быть обеспечена с помощью инструментов сторонних производителей.

Таким образом, для успешного переноса приложения в контейнер или в несколько контейнеров необходимо разобраться в особенностях этого приложения и разработать подробный план. Такому переносу можно подвергнуть практически любое приложение, но важно иметь ясное представление о том объеме усилий, которые придется приложить для решения этой задачи. Кроме того, необходимо, чтобы миграция обеспечивала по меньшей мере сохранение прежних показателей производительности и приводила не к снижению, а желательно к повышению уровня обеспечения безопасности.