Русский
Русский
English
Статистика
Реклама

Контейнеры

Перевод Восторг безопасника технология для шифрования образов контейнеров

18.08.2020 18:15:11 | Автор: admin
На днях поступила интересная задачка необходимо найти способ защитить исходные данные контейнера (читай: не иметь возможности прочитать, что лежит внутри), когда он остановлен. В голову сразу пришла мысль про шифрование, а пальцы начали набирать в гугле заветные слова. Пока разбирался в теме, наткнулся на достаточно интересную статью, которую с удовольствием привожу вам.



Обеспечение конфиденциальности данных и кода в образах контейнеров


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

Успех технологии контейнеризации в основном зависит от безопасности контейнеров на различных этапах их жизненного цикла. Одна из проблем безопасности наличие уязвимостей внутри отдельных контейнеров. Для их выявления пайплайны DevOps, используемые для создания контейнеров, дополняют сканерами, которые ищут в контейнерах пакеты с возможными уязвимостями и предупреждают их владельцев или технических специалистов в случае их обнаружения. Vulnerability Advisor в IBM Cloud является примером такой утилиты.

Еще один аспект безопасности заключается в том, что нужно убедиться, что запускаемый контейнер именно тот контейнер, который вам нужен, и он не был изменен. Этот вопрос решается путем использования хранящихся в Notary цифровых подписей, которые защитят контейнеры от каких-либо модификаций. Docker Notary это пример публичного репозитория, в котором хранятся подписи образов. Используя Notary, клиент может проверить подпись образа контейнера, чтобы убедиться, что образ контейнера не был изменен с момента его подписания ключом владельца или специалиста по обслуживанию.

Еще одна потенциальная проблема безопасности это изоляция контейнера. Технологии безопасности среды исполнения Linux, такие как пространство имен, контрольные группы (cgroups), возможности Linux, а также профили SELinux, AppArmor и Seccomp, помогают ограничить процессы контейнеров и изолировать контейнеры друг от друга во время исполнения.

В этой статье рассматривается все еще актуальная проблема безопасности предприятий в отношении конфиденциальности данных и кода в образах контейнеров. Основная цель безопасности при работе с образами контейнеров позволить создавать и распространять зашифрованные образы контейнеров, чтобы сделать их доступными только определенному кругу получателей. В этом случае другие могут иметь доступ к этим образам, но они не смогут запускать их или видеть конфиденциальные данные внутри них. Шифрование контейнеров основано на существующей криптографии, такой как технологии шифрования Ривеста-Шамира-Адлемана (RSA), эллиптической кривой и Advanced Encryption Standard (AES), также известный как Rijndael симметричный алгоритм блочного шифрования.

Вводные


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

Смежные работы по шифрованию и контейнерам


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

Зашифрованные файловые системы существуют во многих операционных системах на предприятиях и могут поддерживать монтирование зашифрованных разделов и каталогов. Зашифрованные файловые системы могут даже поддерживать загрузку с зашифрованного загрузочного диска. Linux поддерживает шифрование на уровне блочного устройства с помощью драйвера dm-encrypt; ecryptfs является одним из примеров зашифрованной файловой системы. Для Linux доступны другие решения для шифрования файлов с открытым исходным кодом. В ОС Windows шифрование поддерживает файловая система NTFS v3.0. Кроме того, многие производители создают диски с самошифрованием. Для образов виртуальных машин существует решение, подобное зашифрованным дискам. Эмулятор машины (ПК) QEMU с открытым исходным кодом и продукты виртуализации VMware поддерживают зашифрованные образы виртуальных машин.

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

Структура


Экосистема Docker сформировалась для того, чтобы стандартизировать форматы образов контейнеров с помощью группы стандартов Open Container Initiative (OCI), которая теперь контролирует формат времени исполнения контейнера (runtime-spec) и формат образа контейнера (image-spec). Поскольку работа команды требовала расширения существующего формата образа контейнера, мы выделили расширение стандарта для поддержки зашифрованных образов. В следующих разделах описывается существующий формат образа контейнера и расширения.

На верхнем уровне контейнер может состоять из документа в Java Script Object Notation (JSON), который представляет собой список манифестов образов. Например, вы можете использовать этот список манифестов, когда для образа контейнера используются несколько архитектур или платформ. Список манифестов содержит ссылки на манифесты контейнеров, по одной для каждой комбинации архитектуры и операционной системы. Например, поддерживаемые архитектуры включают amd64, arm и ppc64le, а к поддерживаемым операционным системам относится Linux или Windows. Пример списка манифестов показан на скриншоте ниже:



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

Уровень ниже списка манифестов это манифест. Манифест также является документом JSON и содержит упорядоченный список ссылок на слои образов. Эти ссылки содержат mediaType, описывающий формат слоя. Формат может описывать, сжимается ли слой, и если да, то каким образом. Например, каждый уровень может быть сохранен в виде файла .tar, содержащего файлы, которые были добавлены на определенном этапе сборки при выполнении docker build в файле Docker. Для повышения эффективности хранения слои часто также упаковываются с использованием сжатых файлов .gzip. Пример документа манифеста показан на следующем скриншоте:



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

В рамках проекта нашей команды мы создали шифрование образов на основе гибридной схемы шифрования с использованием открытого и симметричного ключей. Симметричные ключи используются для массового шифрования данных (применяются для многоуровневого шифрования), а открытые ключи используются для упаковки симметричных ключей. Мы использовали три различные технологии шифрования на основе открытых ключей: OpenPGP, JSON Web Encryption (JWE) и PKCS#7.

OpenPGP


OpenPGP это технология шифрования и подписи, которая обычно используется для шифрования и подписи сообщений электронной почты. Сообщества с открытым исходным кодом также часто используют его для подписания коммитов (тегов) исходного кода в репозиториях git. Это интернет-стандарт, определенный IETF в RFC480, и его можно рассматривать как открытую версию предыдущей проприетарной технологии PGP.

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

Можно использовать формат зашифрованного сообщения OpenPGP для шифрования массовой рассылки сообщения для нескольких получателей. Заголовок сообщения OpenPGP содержит по одному блоку для каждого получателя. Каждый блок содержит 64-битный идентификатор ключа, который указывает алгоритму дешифрования, где необходимо пытаться дешифровать соответствующий закрытый ключ. После того как зашифрованный большой двоичный объект внутри блока расшифрован, он показывает симметричный ключ, который можно использовать для дешифрования массовой рассылки сообщения. Зашифрованный большой двоичный объект с открытым ключом каждого получателя показывает один и тот же симметричный ключ.

Мы использовали OpenPGP аналогичным образом, но в данном случае зашифрованное сообщение, которое он передает, не является слоем. Вместо этого оно содержит документ JSON, который, в свою очередь, содержит симметричный ключ, используемый для шифрования и дешифрования как слоя, так и вектора инициализации. Мы называем этот ключ ключом шифрования слоя (LEK), он представляет собой форму ключа шифрования данных. Преимущество этого метода в том, что нам нужен только один LEK. С помощью LEK мы шифруем слой для одного или нескольких получателей. У каждого получателя (образа контейнера) может быть свой тип ключа, и это не обязательно должен быть ключ OpenPGP. Например, это может быть простой ключ RSA. Пока у нас есть возможность использовать этот ключ RSA для шифрования LEK, мы сможем работать с несколькими получателями с разными типами ключей.

JSON Web Encryption (JWE)


JSON Web Encryption, также известное как JWE, является еще одним интернет-стандартом IETF и определено в RFC7516. Это более новый стандарт шифрования, чем OpenPGP, поэтому в нем используются более свежие низкоуровневые шифры, предназначенные для удовлетворения более строгих требований к шифрованию.

Если смотреть укрупненно, JWE работает по тому же принципу, что и OpenPGP, поскольку он также поддерживает список получателей и массовую рассылку сообщения, зашифрованного с помощью симметричного ключа, к которому имеет доступ каждый получатель сообщения. Получатели сообщения JWE могут иметь разные типы ключей, такие как ключи RSA, определенные типы ключей эллиптической кривой, предназначенные для шифрования, и симметричные ключи. Поскольку это более новый стандарт, все еще есть возможность расширения JWE для поддержки ключей в аппаратных устройствах, таких как TPM или модули аппаратной защиты (HSM), с использованием интерфейсов PKCS#11 или Key Management and Interoperability Protocol (KMIP). Использование JWE происходит аналогичным OpenPGP образом, если получатели имеют ключи RSA или эллиптические кривые. В будущем мы могли бы расширить его для поддержки симметричных ключей, таких как KMIP внутри HSM.

PKCS#7


PKCS#7, также известный как синтаксис криптографических сообщений (CMS), определен в стандарте IEFT RFC5652. Согласно Википедии о CMS, его можно использовать для цифровой подписи, дайджеста, аутентификации или шифрования любых форм цифровых данных.

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

Для поддержки ранее описанных технологий шифрования мы расширили документ манифеста, добавив следующую информацию:
  • Сообщения OpenPGP, JWE и PKCS#7 хранятся в карте аннотаций, которая является частью манифеста.
  • В каждом указанном слое содержится по одной карте. Карта аннотаций в основном представляет собой словарь со строками в качестве ключей и строками в качестве значений (пары ключ-значение).

Для поддержки шифрования образов мы определили следующие ключи:
  • org.opencontainers.image.enc.keys.openpgp
  • org.opencontainers.image.enc.keys.jwe
  • org.opencontainers.image.enc.keys.pkcs7

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

Чтобы определить, что слой был зашифрован с помощью LEK, мы расширили существующие медиатипы суффиксом '+encrypted', как показано в следующих примерах:
  • application/vnd.docker.image.rootfs.diff.tar+encrypted
  • application/vnd.docker.image.rootfs.diff.tar.gzip+encrypted

Эти примеры показывают, что слой заархивирован в файле .tar и зашифрован или оба заархивированы в файле .tar и сжаты в файл .gzip и зашифрованы. На следующем скриншоте показан пример манифеста, который ссылается на зашифрованные слои. Он также показывает карту аннотаций, содержащую зашифрованное сообщение JWE.



Многослойное шифрование с использованием симметричных ключей


Для симметричного шифрования с помощью LEK наша команда выбрала шифр, который поддерживает аутентифицированное шифрование и основан на стандарте шифрования AES со 128- и 256-битными ключами.

Пример реализации: containerd


Мы реализовали наш вариант в новом проекте среды выполнения контейнеров под названием containerd. Его исходный код на golang можно посмотреть, перейдя по ссылке. Docker-демон использует containerd для запуска некоторых своих служб, а у Kubernetes есть плагин для прямого использования containerd. Поэтому мы надеемся, что наши расширения для поддержки зашифрованных образов контейнеров пригодятся обоим.

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

Поддержка алгоритмов аутентифицированного шифрования в Golang принимает байтовый массив в качестве входных данных и выполняет весь этап его шифрования (запечатывания) или дешифрования (открытия), не допуская передачи и добавления дополнительных массивов в поток. Поскольку этот криптографический API требовал загрузки всего уровня в память или изобретения некоторой схемы для изменения вектора инициализации (IV) для каждого блока, мы решили не использовать аутентифицированное шифрование golang с поддержкой связанных данных (AEAD). Вместо этого мы использовали крипто-библиотеку miscreant golang, которая поддерживает AEAD в потоках (блоках) и реализует собственную схему изменения IV в каждом блоке. В нашей реализации мы разбиваем уровень на блоки размером 1 МБ, которые и передаем один за другим для шифрования. Такой подход позволяет снизить объем памяти при использовании аутентифицированного шифра. На стороне дешифрования мы делаем обратное и обращаем внимание на ошибки, возвращаемые функцией Open (), чтобы убедиться, что блоки шифрования не были подделаны.

На уровне выше симметричного шифрования, асимметричные криптографические схемы шифруют LEK уровня и вектор инициализации (IV). Для добавления или удаления криптографических схем мы регистрируем каждую асимметричную криптографическую реализацию. Когда API асимметричного криптографического кода вызывается для шифрования уровня, мы вызываем один за другим зарегистрированные криптографические обработчики, передавая открытые ключи получателей. После того как все ключи получателей используются для шифрования, мы возвращаемся к карте аннотаций с идентификаторами асимметричных криптоалгоритмов в качестве ключей сопоставления и со значениями, содержащими сообщения в кодировке OpenPGP, JWE и PKCS#7. Каждое сообщение содержит упакованные LEK и IV. Карты аннотаций хранятся в документе манифеста, как показано на предыдущем скриншоте.

Мы также можем добавлять получателей к уже зашифрованному образу. Авторы образов добавляют получателей, если они есть в списке. Для списка получателей используется закрытый ключ, необходимый для распаковки LEK и IV уровней. Затем мы упаковываем LEK и IV в новое сообщение, используя ключ нового получателя, и добавляем это сообщение к карте аннотаций.

Мы использовали три типа асимметричных схем шифрования для разных типов ключей. Мы используем ключи OpenPGP для шифрования сообщений OpenPGP. PKCS#7, которую мы используем, требует сертификаты x.509 для ключей шифрования. JWE обрабатывает все остальные типы ключей, такие как простые ключи RSA, эллиптические кривые и симметричные ключи. Мы создали прототип расширения для JWE, который позволяет выполнять криптографические операции с использованием ключей, управляемых сервером KMIP.

Среда выполнения containerd включает клиентский инструмент ctr для взаимодействия с ней. Мы расширили ctr, чтобы включить тестирование наших изменений и предоставить доступ для пользователей контейнера. ctr уже реализует подкоманду, которая поддерживает операции с образами, такие как взаимодействие с реестром образов через их извлечение и отправку.

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

Точно также мы можем расшифровывать отдельные слои отдельных архитектур. Мы добавили подкоманду layerinfo, которая показывает статус шифрования каждого уровня и отображает технологии шифрования, используемые для него. Для OpenPGP мы также можем отображать идентификаторы ключей, необходимые для дешифрования, или преобразовывать их в адреса электронной почты их получателей с помощью keyring.

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

Когда простой (незашифрованный) образ извлекается из реестра, он автоматически распаковывается и разархивируется, чтобы из него можно было сразу создавать контейнеры. Чтобы упростить это действие для зашифрованных образов, мы предлагаем передавать закрытый ключ команде, которая занимается распаковкой образов, чтобы они могли расшифровать слои до распаковки. Если образ зашифрован с помощью нескольких ключей, команде pull можно передать несколько. Такая передача также поддерживается. После успешного извлечения зашифрованного образа из реестра любой пользователь, имеющий доступ к containerd, может создать контейнер из образа. Чтобы подтвердить, что пользователь имеет права на использование образа контейнера, мы предлагаем ему предоставить закрытые ключи, используемые для расшифровки контейнера. Мы используем ключи для проверки авторизации пользователя, могут ли они использоваться для расшифровки LEK каждого зашифрованного уровня, и если это подтверждается, разрешаем запуск контейнера.

Пошаговое руководство шифрования с использованием containerd


В этом разделе мы продемонстрируем шаги шифрования, которые применяются с containderd, используя ctr в командной строке. Мы покажем, как происходит шифрование и дешифрование образа контейнера.

В первую очередь, нужно клонировать репозиторий git containerd/imgcrypt, который является подпроектом и может шифровать/дешифровать образ контейнера. Затем необходимо собрать containerd и запустить его. Чтобы выполнить эти шаги, нужно знать, как настраивается среда разработки golang:

Для imgcrypt требуется использовать containerd версии не ниже 1.3.

Собираем и устанавливаем imgcrypt:

# make# sudo make install

Запустите containerd с файлом конфигурации, который можно увидеть на примере ниже. Чтобы избежать возникновения конфликта в containerd, используйте директорию /tmp для каталогов. Также соберите containerd версии 1.3 из исходника, но не устанавливайте его.

# cat config.tomldisable_plugins = ["cri"]root = "/tmp/var/lib/containerd"state = "/tmp/run/containerd"[grpc]  address = "/tmp/run/containerd/containerd.sock"  uid = 0  gid = 0[stream_processors]    [stream_processors."io.containerd.ocicrypt.decoder.v1.tar.gzip"]        accepts = ["application/vnd.oci.image.layer.v1.tar+gzip+encrypted"]        returns = "application/vnd.oci.image.layer.v1.tar+gzip"        path = "/usr/local/bin/ctd-decoder"    [stream_processors."io.containerd.ocicrypt.decoder.v1.tar"]        accepts = ["application/vnd.oci.image.layer.v1.tar+encrypted"]        returns = "application/vnd.oci.image.layer.v1.tar"        path = "/usr/local/bin/ctd-decoder"# sudo ~/src/github.com/containerd/containerd/bin/containerd -c config.toml

Создайте пару ключей RSA с помощью инструмента командной строки openssl и зашифруйте образ:

# openssl genrsa --out mykey.pemGenerating RSA private key, 2048 bit long modulus (2 primes)...............................................+++++............................+++++e is 65537 (0x010001)# openssl rsa -in mykey.pem -pubout -out mypubkey.pemwriting RSA key# sudo chmod 0666 /tmp/run/containerd/containerd.sock# CTR="/usr/local/bin/ctr-enc -a /tmp/run/containerd/containerd.sock"# $CTR images pull --all-platforms docker.io/library/bash:latest[...]# $CTR images layerinfo --platform linux/amd64 docker.io/library/bash:latest   #                                                                    DIGEST      PLATFORM      SIZE   ENCRYPTION   RECIPIENTS   0   sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609   linux/amd64   2789669                             1   sha256:7dd01fd971d4ec7058c5636a505327b24e5fc8bd7f62816a9d518472bd9b15c0   linux/amd64   3174665                             2   sha256:691cfbca522787898c8b37f063dd20e5524e7d103e1a3b298bd2e2b8da54faf5   linux/amd64       340                          # $CTR images encrypt --recipient jwe:mypubkey.pem --platform linux/amd64 docker.io/library/bash:latest bash.enc:latestEncrypting docker.io/library/bash:latest to bash.enc:latest$ $CTR images layerinfo --platform linux/amd64 bash.enc:latest   #                                                                    DIGEST      PLATFORM      SIZE   ENCRYPTION   RECIPIENTS   0   sha256:360be141b01f69b25427a9085b36ba8ad7d7a335449013fa6b32c1ecb894ab5b   linux/amd64   2789669          jwe        [jwe]   1   sha256:ac601e66cdd275ee0e10afead03a2722e153a60982122d2d369880ea54fe82f8   linux/amd64   3174665          jwe        [jwe]   2   sha256:41e47064fd00424e328915ad2f7f716bd86ea2d0d8315edaf33ecaa6a2464530   linux/amd64       340          jwe        [jwe]

Запустите локальный реестр образов, чтобы вы могли отправить в него зашифрованный образ. Для приема зашифрованных образов контейнеров нужны последние версии реестра.

# docker pull registry:latest# docker run -d -p 5000:5000 --restart=always --name registry registry

Отправьте зашифрованный образ в локальный реестр, извлеките его с помощью ctr-enc, а затем запустите образ:

# $CTR images tag bash.enc:latest localhost:5000/bash.enc:latest# $CTR images push localhost:5000/bash.enc:latest# $CTR images rm localhost:5000/bash.enc:latest bash.enc:latest# $CTR images pull localhost:5000/bash.enc:latest# sudo $CTR run --rm localhost:5000/bash.enc:latest test echo 'Hello World!'ctr: you are not authorized to use this image: missing private key needed for decryption# sudo $CTR run --rm --key mykey.pem localhost:5000/bash.enc:latest test echo 'Hello World!'Hello World!


Заключение


Шифрование образов контейнеров это хорошее дополнение к их безопасности, оно обеспечивает конфиденциальность данных и целостность образов контейнеров в месте хранения. Предложенная технология основана на общедоступных технологиях шифрования RSA, эллиптической кривой и AES. Она применяет ключи к схемам шифрования более высокого уровня, таким как OpenPGP, JWE и PKCS#7. Если вы умеете работать с OpenPGP, вы можете шифровать образы контейнеров для получателей OpenPGP, используя их адреса электронной почты, в то время как простые ключи RSA и эллиптические кривые используются для шифрования, например, JWE.
Подробнее..

Из песочницы Способы и примеры внедрения утилит для проверки безопасности Docker

21.10.2020 18:15:40 | Автор: admin

Привет, Хабр!

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

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

Утилиты проверки безопасности


Существует большое количество различных вспомогательных приложений и скриптов, которые выполняют проверки разнообразных аспектов Docker-инфраструктуры. Часть из них уже была описана в предыдущей статье (http://personeltest.ru/aways/habr.com/ru/company/swordfish_security/blog/518758/#docker-security), а в данном материале я бы хотел остановиться на трех из них, которые покрывают основную часть требований к безопасности Docker-образов, строящихся в процессе разработки. Помимо этого я также покажу пример как можно эти три утилиты соединить в один pipeline для осуществления проверок безопасности.

Hadolint
https://github.com/hadolint/hadolint

Довольно простая консольная утилита, которая помогает в первом приближении оценить корректность и безопасность инструкций Dockerfile-ов (например использование только разрешенных реестров образов или использование sudo).

Вывод утилиты Hadolint

Dockle
https://github.com/goodwithtech/dockle

Консольная утилита, работающая с образом (или с сохраненным tar-архивом образа), которая проверяет корректность и безопасность конкретного образа как такового, анализируя его слои и конфигурацию какие пользователи созданы, какие инструкции используются, какие тома подключены, присутствие пустого пароля и т. д. Пока количество проверок не очень большое и базируется на нескольких собственных проверках и рекомендациях CIS (Center for Internet Security) Benchmark для Docker.


Trivy
https://github.com/aquasecurity/trivy

Эта утилита нацелена на нахождение уязвимостей двух типов проблемы сборок ОС (поддерживаются Alpine, RedHat (EL), CentOS, Debian GNU, Ubuntu) и проблемы в зависимостях (Gemfile.lock, Pipfile.lock, composer.lock, package-lock.json, yarn.lock, Cargo.lock). Trivy умеет сканировать как образ в репозитории, так и локальный образ, а также проводить сканирование на основании переданного .tar файла с Docker-образом.



Варианты внедрения утилит


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

Основная идея состоит в том, чтобы продемонстрировать, как можно внедрить автоматическую проверку содержимого Dockerfile и Docker-образов, которые создаются в процессе разработки.

Сама проверка состоит из следующих шагов:
  1. Проверка корректности и безопасности инструкций Dockerfile утилитой линтером Hadolint
  2. Проверка корректности и безопасности конечного и промежуточных образов утилитой Dockle
  3. Проверка наличия общеизвестных уязвимостей (CVE) в базовом образе и ряде зависимостей утилитой Trivy

Дальше в статье я приведу три варианта внедрения этих шагов:
Первый путём конфигурации CI/CD pipeline на примере GitLab (с описанием процесса поднятия тестового инстанса).
Второй с использованием shell-скрипта.
Третий с построением Docker-образа для сканирования Docker-образов.
Вы можете выбрать вариант который больше вам подходит, перенести его на свою инфраструктуру и адаптировать под свои нужды.

Все необходимые файлы и дополнительные инструкции также находятся в репозитории: https://github.com/Swordfish-Security/docker_cicd

Интеграция в GitLab CI/CD


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

Установка GitLab
1. Ставим Docker:
sudo apt-get update && sudo apt-get install docker.io

2. Добавляем текущего пользователя в группу docker, чтобы можно было работать с докером не через sudo:
sudo addgroup <username> docker

3. Находим свой IP:
ip addr

4. Ставим и запускаем GitLab в контейнере, заменяя IP адрес в hostname на свой:
docker run --detach \--hostname 192.168.1.112 \--publish 443:443 --publish 80:80 \--name gitlab \--restart always \--volume /srv/gitlab/config:/etc/gitlab \--volume /srv/gitlab/logs:/var/log/gitlab \--volume /srv/gitlab/data:/var/opt/gitlab \gitlab/gitlab-ce:latest

Ждём, пока GitLab выполнит все необходимые процедуры по установке (можно следить за процессом через вывод лог-файла: docker logs -f gitlab).

5. Открываем в браузере свой локальный IP и видим страницу с предложением поменять пароль для пользователя root:

Задаём новый пароль и заходим в GitLab.

6. Создаём новый проект, например cicd-test и инициализируем его стартовым файлом README.md:

7. Теперь нам необходимо установить GitLab Runner: агента, который будет по запросу запускать все необходимые операции.
Скачиваем последнюю версию (в данном случае под Linux 64-bit):
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

8. Делаем его исполняемым:
sudo chmod +x /usr/local/bin/gitlab-runner

9. Добавляем пользователя ОС для Runner-а и запускаем сервис:
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bashsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runnersudo gitlab-runner start

Должно получиться примерно так:

local@osboxes:~$ sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runnerRuntime platform arch=amd64 os=linux pid=8438 revision=0e5417a3 version=12.0.1local@osboxes:~$ sudo gitlab-runner startRuntime platform arch=amd64 os=linux pid=8518 revision=0e5417a3 version=12.0.1

10. Теперь регистрируем Runner, чтобы он мог взаимодействовать с нашим инстансом GitLab.
Для этого открываем страницу Settings-CI/CD (http://personeltest.ru/away/OUR_ IP_ADDRESS/root/cicd-test/-/settings/ci_cd) и на вкладке Runners находим URL и Registration token:

11. Регистрируем Runner, подставляя URL и Registration token:
sudo gitlab-runner register \--non-interactive \--url "http://<URL>/" \--registration-token "<Registration Token>" \--executor "docker" \--docker-privileged \--docker-image alpine:latest \--description "docker-runner" \--tag-list "docker,privileged" \--run-untagged="true" \--locked="false" \--access-level="not_protected"

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

Конфигурация pipeline

1. Добавим в репозиторий файлы mydockerfile.df (это некий тестовый Dockerfile, который мы будем проверять) и конфигурационный файл GitLab CI/CD процесса .gitlab-cicd.yml, который перечисляет инструкции для сканеров (обратите внимание на точку в названии файла).

YAML-файл конфигурации содержит инструкции по запуску трех утилит (Hadolint, Dockle и Trivy), которые проанализируют выбранный Dockerfile и образ, заданный в переменной DOCKERFILE. Все необходимые файлы можно взять из репозитория: https://github.com/Swordfish-Security/docker_cicd/

Выдержка из mydockerfile.df (это абстрактный файл с набором произвольных инструкций только для демонстрации работы утилиты). Прямая ссылка на файл: mydockerfile.df

Содержимое mydockerfile.df
FROM amd64/node:10.16.0-alpine@sha256:f59303fb3248e5d992586c76cc83e1d3700f641cbcd7c0067bc7ad5bb2e5b489 AS tsbuildCOPY package.json .COPY yarn.lock .RUN yarn installCOPY lib libCOPY tsconfig.json tsconfig.jsonCOPY tsconfig.app.json tsconfig.app.jsonRUN yarn buildFROM amd64/ubuntu:18.04@sha256:eb70667a801686f914408558660da753cde27192cd036148e58258819b927395LABEL maintainer="Rhys Arkins <rhys@arkins.net>"LABEL name="renovate"...COPY php.ini /usr/local/etc/php/php.iniRUN cp -a /tmp/piik/* /var/www/html/RUN rm -rf /tmp/piwikRUN chown -R www-data /var/www/htmlADD piwik-cli-setup /piwik-cli-setupADD reset.php /var/www/html/## ENTRYPOINT ##ADD entrypoint.sh /entrypoint.shENTRYPOINT ["/entrypoint.sh"]USER root

Конфигурационный YAML выглядит таким образом (сам файл можно взять по прямой ссылке здесь: .gitlab-ci.yml):

Содержимое .gitlab-ci.yml
variables:    DOCKER_HOST: "tcp://docker:2375/"    DOCKERFILE: "mydockerfile.df" # name of the Dockerfile to analyse       DOCKERIMAGE: "bkimminich/juice-shop" # name of the Docker image to analyse    # DOCKERIMAGE: "knqyf263/cve-2018-11235" # test Docker image with several CRITICAL CVE    SHOWSTOPPER_PRIORITY: "CRITICAL" # what level of criticality will fail Trivy job    TRIVYCACHE: "$CI_PROJECT_DIR/.cache" # where to cache Trivy database of vulnerabilities for faster reuse    ARTIFACT_FOLDER: "$CI_PROJECT_DIR" services:    - docker:dind # to be able to build docker images inside the Runner stages:    - scan    - report    - publish HadoLint:    # Basic lint analysis of Dockerfile instructions    stage: scan    image: docker:git     after_script:    - cat $ARTIFACT_FOLDER/hadolint_results.json     script:    - export VERSION=$(wget -q -O - https://api.github.com/repos/hadolint/hadolint/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')    - wget https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64 && chmod +x hadolint-Linux-x86_64         # NB: hadolint will always exit with 0 exit code    - ./hadolint-Linux-x86_64 -f json $DOCKERFILE > $ARTIFACT_FOLDER/hadolint_results.json || exit 0     artifacts:        when: always # return artifacts even after job failure               paths:        - $ARTIFACT_FOLDER/hadolint_results.json Dockle:    # Analysing best practices about docker image (users permissions, instructions followed when image was built, etc.)    stage: scan       image: docker:git     after_script:    - cat $ARTIFACT_FOLDER/dockle_results.json     script:    - export VERSION=$(wget -q -O - https://api.github.com/repos/goodwithtech/dockle/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')    - wget https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.tar.gz && tar zxf dockle_${VERSION}_Linux-64bit.tar.gz    - ./dockle --exit-code 1 -f json --output $ARTIFACT_FOLDER/dockle_results.json $DOCKERIMAGE            artifacts:        when: always # return artifacts even after job failure               paths:        - $ARTIFACT_FOLDER/dockle_results.json Trivy:    # Analysing docker image and package dependencies against several CVE bases    stage: scan       image: docker:git     script:    # getting the latest Trivy    - apk add rpm    - export VERSION=$(wget -q -O - https://api.github.com/repos/knqyf263/trivy/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')    - wget https://github.com/knqyf263/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz && tar zxf trivy_${VERSION}_Linux-64bit.tar.gz         # displaying all vulnerabilities w/o failing the build    - ./trivy -d --cache-dir $TRIVYCACHE -f json -o $ARTIFACT_FOLDER/trivy_results.json --exit-code 0 $DOCKERIMAGE            # write vulnerabilities info to stdout in human readable format (reading pure json is not fun, eh?). You can remove this if you don't need this.    - ./trivy -d --cache-dir $TRIVYCACHE --exit-code 0 $DOCKERIMAGE         # failing the build if the SHOWSTOPPER priority is found    - ./trivy -d --cache-dir $TRIVYCACHE --exit-code 1 --severity $SHOWSTOPPER_PRIORITY --quiet $DOCKERIMAGE             artifacts:        when: always # return artifacts even after job failure        paths:        - $ARTIFACT_FOLDER/trivy_results.json     cache:        paths:        - .cache Report:    # combining tools outputs into one HTML    stage: report    when: always    image: python:3.5         script:    - mkdir json    - cp $ARTIFACT_FOLDER/*.json ./json/    - pip install json2html    - wget https://raw.githubusercontent.com/shad0wrunner/docker_cicd/master/convert_json_results.py    - python ./convert_json_results.py         artifacts:        paths:        - results.html

При необходимости также можно сканировать и сохраненные образы в виде .tar-архива (однако потребуется в YAML файле изменить входные параметры для утилит)

NB: Trivy требует для своего запуска установленные rpm и git. В противном случае он будет выдавать ошибки при сканировании RedHat-based образов и получении обновлений базы уязвимостей.

2. После добавления файлов в репозиторий, в соответствии с инструкциями в нашем конфигурационном файле, GitLab автоматически начнёт процесс сборки и сканирования. На вкладке CI/CD Pipelines можно будет увидеть ход выполнения инструкций.

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

По умолчанию Trivy останавливает своё выполнение, если были обнаружены CRITICAL уязвимости в образе или зависимостях. В то же время Hadolint всегда возвращает Success код выполнения, так как в результате его выполнения всегда есть замечания, что приводит к остановке сборки.

В зависимости от конкретных требований можно сконфигурировать код выхода, чтобы эти утилиты при обнаружении проблем определенной критичности, останавливали также и процесс сборки. В нашем случае сборка остановится, только если Trivy обнаружит уязвимость с критичностью, которую мы указали в переменной SHOWSTOPPER в .gitlab-ci.yml.


Результат работы каждой утилиты можно посмотреть в логе каждой сканирующей задачи, непосредственно в json-файлах в разделе artifacts или в простом HTML-отчёте (о нем чуть ниже):


3. Для представления отчётов утилит в чуть более человекочитаемом виде используется небольшой скрипт на Python для конвертации трёх json-файлов в один HTML-файл с таблицей дефектов.
Этот скрипт запускается отдельной задачей Report, а его итоговым артефактом является HTML-файл с отчётом. Исходник скрипта также лежит в репозитории и его можно адаптировать под свои нужды, цвета и т. п.


Shell-скрипт


Второй вариант подходит для случаев, когда необходимо проверять Docker-образы не в рамках CI/CD системы или необходимо иметь все инструкции в виде, который можно выполнить непосредственно на хосте. Этот вариант покрывается готовым shell-скриптом, который можно запустить на чистой виртуальной (или даже реальной) машине. Скрипт выполняет те же самые инструкции, что и вышеописанный gitlab-runner.

Для успешной работы скрипта в системе должен быть установлен Docker и текущий пользователь должен быть в группе docker.

Сам скрипт можно взять здесь: docker_sec_check.sh

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

В процессе выполнения скрипта все утилиты будут скачаны в директорию docker_tools, результаты их работы в директорию docker_tools/json, а HTML с отчётом будет находиться в файле results.html.

Пример вывода скрипта
~/docker_cicd$ ./docker_sec_check.sh[+] Setting environment variables[+] Installing required packages[+] Preparing necessary directories[+] Fetching sample Dockerfile2020-10-20 10:40:00 (45.3 MB/s) - Dockerfile saved [8071/8071][+] Pulling image to scanlatest: Pulling from bkimminich/juice-shop[+] Running Hadolint...Dockerfile:205 DL3015 Avoid additional packages by specifying `--no-install-recommends`Dockerfile:248 DL3002 Last USER should not be root...[+] Running Dockle...WARN    - DKL-DI-0006: Avoid latest tag        * Avoid 'latest' tagINFO    - CIS-DI-0005: Enable Content trust for Docker        * export DOCKER_CONTENT_TRUST=1 before docker pull/build...[+] Running Trivyjuice-shop/frontend/package-lock.json=====================================Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 2, CRITICAL: 0)+---------------------+------------------+----------+---------+-------------------------+|       LIBRARY       | VULNERABILITY ID | SEVERITY | VERSION |             TITLE       |+---------------------+------------------+----------+---------+-------------------------+| object-path         | CVE-2020-15256   | HIGH     | 0.11.4  | Prototype pollution in  ||                     |                  |          |         | object-path             |+---------------------+------------------+          +---------+-------------------------+| tree-kill           | CVE-2019-15599   |          | 1.2.2   | Code Injection          |+---------------------+------------------+----------+---------+-------------------------+| webpack-subresource | CVE-2020-15262   | LOW      | 1.4.1   | Unprotected dynamically ||                     |                  |          |         | loaded chunks           |+---------------------+------------------+----------+---------+-------------------------+juice-shop/package-lock.json============================Total: 20 (UNKNOWN: 0, LOW: 1, MEDIUM: 6, HIGH: 8, CRITICAL: 5)...juice-shop/package-lock.json============================Total: 5 (CRITICAL: 5)...[+] Removing left-overs[+] Making the output look pretty[+] Converting JSON results[+] Writing results HTML[+] Clean exit ============================================================[+] Everything is done. Find the resulting HTML report in results.html


Docker-образ со всеми утилитами


В качестве третьей альтернативы я составил два простых Dockerfile для создания образа с утилитами безопасности. Один Dockerfile поможет собрать набор для сканирования образа из репозитория, второй (Dockerfile_tar) собрать набор для сканирования tar-файла с образом.

1. Берем соответствующий Docker файл и скрипты из репозитория https://github.com/Swordfish-Security/docker_cicd/tree/master/Dockerfile.
2. Запускаем его на сборку:
docker build -t dscan:image -f docker_security.df .

3. После окончания сборки создаем контейнер из образа. При этом передаём переменную окружения DOCKERIMAGE с названием интересующего нас образа и монтируем Dockerfile, который хотим анализировать, с нашей машины на файл /Dockerfile (обратите внимание, что требуется абсолютный путь до этого файла):
docker run --rm -v $(pwd)/results:/results -v $(pwd)/docker_security.df:/Dockerfile -e DOCKERIMAGE="bkimminich/juice-shop" dscan:image

[+] Setting environment variables[+] Running Hadolint/Dockerfile:3 DL3006 Always tag the version of an image explicitly[+] Running DockleWARN    - DKL-DI-0006: Avoid latest tag        * Avoid 'latest' tagINFO    - CIS-DI-0005: Enable Content trust for Docker        * export DOCKER_CONTENT_TRUST=1 before docker pull/buildINFO    - CIS-DI-0006: Add HEALTHCHECK instruction to the container image        * not found HEALTHCHECK statementINFO    - DKL-LI-0003: Only put necessary files        * unnecessary file : juice-shop/node_modules/sqlite3/Dockerfile        * unnecessary file : juice-shop/node_modules/sqlite3/tools/docker/architecture/linux-arm64/Dockerfile        * unnecessary file : juice-shop/node_modules/sqlite3/tools/docker/architecture/linux-arm/Dockerfile[+] Running Trivy...juice-shop/package-lock.json============================Total: 20 (UNKNOWN: 0, LOW: 1, MEDIUM: 6, HIGH: 8, CRITICAL: 5)...[+] Making the output look pretty[+] Starting the main module ============================================================[+] Converting JSON results[+] Writing results HTML[+] Clean exit ============================================================[+] Everything is done. Find the resulting HTML report in results.html

Результаты


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

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

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

Альтернативы VirtualBox для любителей приватности и свободы. Гипервизоры и менеджеры виртуальных машин. Часть I

03.01.2021 22:21:10 | Автор: admin

Приветствую читателей.

Меня зовут Андреас, давно веду видеоблог (SunAndreas) на темы гражданской информационной безопасности и сегодня о том, какими гипервизорами и менеджерами виртуальных машин лучше пользоваться особым любителям приватности и свободы. Если вы не знакомы с иными материалами моего YouTube-канала, рекомендую ознакомиться с его содержимым.

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

В сети только и пестрит VirtualBox, да, VirtualBox. Конечно, это очень простой и удобный вариант для совсем новичков, а также и для пользователей виндоус, которые больше ценят удобство и популярность, но сознательным людям, использующим GNU/Linux системы, настоятельно рекомендую перейти, если вы всё ещё этого не сделали, на более этичные аналоги, после того как, вроде пару лет назад, Oracle, пересмотрела немного свои позиции в отношении VirtualBox.

Ранее, более 4 лет назад, когда ещё на канале публиковалось почти легендарное видео с названием SunbooK, тогда VirtualBox был большей мере открытым и свободным проектом, не было с моей стороны к нему никакой пренебрежительности, но ныне уже лучше всё же использовать иные гипервизоры, тем более, что они позволяют легко импортировать виртуальные диски, то есть по сути виртуальные машины, которые ранее были созданы с помощью VirtualBox. Таким образом переезд с VirtualBox на иные решения может обойтись вам даже без потери данных.

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

Далее я перечислю графические оболочки, то есть праграммы-менеджеры виртуальных машин, с которыми и будет работать рядовой пользователь не вникая, что такое QEMU, KVM и libvirt. Продвинутый пользователь может работать и напрямую через терминал с libvirt, но я озвучу программные решения для более широкого круга лиц.

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

AQEMU это более продвинутый вариант графической оболочки, но всё же не лучший, на мой взгляд. Менеджер позволяет осуществлять различные настройки, но по моему опыту немного уступает следующему менеджеру. Знайте о существовании AQEMU и фанаты тематики могут его опробывать самостоятельно, а мы двигаемся дальше.

Третий вариант. Virt-manager также, как и предыдущие менеджеры работает с QEMU и KVM через libvirt, но кроме того может работать и с иными вариантами виртуализации, не только с гипервизорами, но и с более лёгким вариантом виртуализации, а именно, с контейнерной виртуализацией,

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

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

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

Возвращаясь к Virt-manager, если вы пожелаете использовать с помощью него лёгкую контейнерную виртуализацию, то я могу порекомендовать не заморачиваться и не выискивать варианты, и использовать LXC. Если же вас интересуют иные варианты, ещё раз обращаю ваш взор к изучению отдельного видео на канале, ссылка под этим видео.

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

На этом завершаем теорию и в следующей публикации разберём работу с QEMU и KVM. Существует и ещё один добротный вариант, это Xen, с которым также работает virt-manager, и я уважаю этот гипервизор и регулярно использую его. О нём я рассказывал в отдельном выпуске о виртуализации. А связка QEMU и KVM является доступной, лёгкой в работе, пожалуй, чуть более удобнее для простых пользователей и не требуется при загрузке операционной системы в загрузочном GRUB-меню выбирать никакие варианты.

В последующих публикациях я покажу работу virt-manager с QEMU-KVM, которая будет понятна даже не продвинутому пользователю, что считаю особо общественно-полезным делом, тем более, что работе данного ПО даже в английском сегменте интернета не достаточно информации на мой взгляд. А в русско-язычной сфере, так я вообще не встречал ничего особо достойного по данной теме. Лишь малоценные поверхностные обзоры.

Всем развития и успехов.

Подробнее..

Стражи публичных облаков как мы внедряли анклавы Intel SGX для защиты чувствительных данных

14.01.2021 16:09:15 | Автор: admin
Как развеять предубеждения потенциальных пользователей относительно безопасности публичных облаков? На помощь приходит технология Intel Software Guard Extensions (Intel SGX). Рассказываем, как мы внедрили её в своём облаке и какие преимущества от нашего решения получила компания Aggregion.





Кратко об Intel SGX и его роли в облаке


Intel Software Guard Extensions (Intel SGX) набор инструкций ЦП, с помощью которых в адресном пространстве приложения создаются частные защищённые области (анклавы), где размещается код уровня пользователя. Технология обеспечивает конфиденциальность и целостность чувствительных данных. Путём изоляции в анклаве они получают дополнительную защиту как от несанкционированного внешнего доступа, в том числе со стороны провайдера облачных услуг, так и от внутренних угроз, включая атаки со стороны программного обеспечения привилегированного уровня.

Принципы работы. Для хранения кода и данных анклавов технология Intel SGX выделяет область памяти Processor Reserved Memory (PRM). ЦП защищает её от всех внешних обращений, в том числе от доступа со стороны ядра и гипервизора. В PRM содержится Enclave Page Cache (EPC), состоящий из блоков страниц объёмом 4 КиБ, при этом каждая страница должна принадлежать только одному анклаву, а их состояние фиксируется в Enclave Page Cache Metadata (EPCM) и контролируется ЦП.

Безопасность EPC обеспечивается за счёт Memory Encryption Engine (MEE), который генерирует ключи шифрования, хранящиеся в ЦП. Предполагается, что страницы могут быть расшифрованы только внутри физического ядра процессора.

Преимущества. Intel SGX позволяет повысить уровень доверия к публичному облаку со стороны организаций, использующих в своей работе чувствительные данные (пароли, ключи шифрования, идентификационные, биометрические, медицинские данные, а также информацию, относящуюся к интеллектуальной собственности). Речь идёт о представителях самых разных отраслей финансового сектора, медицины и здравоохранения, ритейла, геймдева, телекома, медиасферы.

Наш подход к внедрению Intel SGX


Чтобы в публичном облаке G-Core Labs появилась возможность выделять виртуальные машины с анклавами Intel SGX, нам пришлось пройти путь от компиляции патченного ядра KVM и QEMU до написания Python-скриптов в сервисах OpenStack Nova. Вычислительные узлы, которые планировалось использовать для выделения виртуальных машин повышенной безопасности, мы решили определить в отдельный агрегатор тип вычислительных ресурсов, требующий дополнительной настройки. На таких узлах было необходимо:

  • Включить поддержку Intel SGX на уровне BIOS.
  • Поставить патченные QEMU/KVM.

Изначально у нас не было понимания, как это должно работать и что в итоге мы должны прикрутить, чтобы получить ВМ нужной конфигурации. Разобраться с этим вопросом нам частично помогло руководство Intel для разработчиков. С его помощью мы узнали, как подготовить вычислительный узел для работы с SGX и какими дополнительными параметрами должен обладать конфигурационный XML-файл виртуальной машины. Здесь же мы нашли исчерпывающую информацию, как с помощью виртуализации KVM сделать гостевую машину с использованием Intel SGX. Чтобы убедиться, что мы смогли обеспечить поддержку данной технологии, мы использовали два способа:

  • Проверили секцию /dev/sgx/virt_epc в файле /proc/$PID/smaps:

    [root@compute-sgx ~]# grep -A22 epc /proc/$PID/smaps7f797affe000-7f797b7fe000 rw-s 00000000 00:97 57466526/dev/sgx/virt_epcSize:               8192 kBKernelPageSize:        4 kBMMUPageSize:           4 kBRss:                   0 kBPss:                   0 kBShared_Clean:          0 kBShared_Dirty:          0 kBPrivate_Clean:         0 kBPrivate_Dirty:         0 kBReferenced:            0 kBAnonymous:             0 kBLazyFree:              0 kBAnonHugePages:         0 kBShmemPmdMapped:        0 kBFilePmdMapped:         0 kBShared_Hugetlb:        0 kBPrivate_Hugetlb:       0 kBSwap:                  0 kBSwapPss:               0 kBLocked:                0 kBTHPeligible:0VmFlags: rd wr sh mr mw me ms pf io dc dd hg
    
  • И воспользовались данным shell-скриптом, предварительно поставив SGX-драйвер (все действия осуществлялись внутри ВМ):

    [root@sgx-vm ~]# cat check_sgx.sh#!/bin/bashMETRICS="sgx_nr_total_epc_pages \    sgx_nr_free_pages \    sgx_nr_low_pages \    sgx_nr_high_pages \    sgx_nr_marked_old \    sgx_nr_evicted \    sgx_nr_alloc_pages \    "MODPATH="/sys/module/isgx/parameters/"for metric in $METRICS ; do    echo "$metric= `cat $MODPATH/$metric`"done[root@sgx-vm ~]# curl -fsSL https://raw.githubusercontent.com/scontain/SH/master/install_sgx_driver.sh | bash -s - install -p metrics -p page0[root@sgx-vm ~]# ./check_sgx.shsgx_nr_total_epc_pages= 2048sgx_nr_free_pages= 2048sgx_nr_low_pages= 32sgx_nr_high_pages= 64sgx_nr_marked_old= 0sgx_nr_evicted= 0sgx_nr_alloc_pages= 0
    

    Стоит учитывать, что, если одна страница занимает 4 КиБ, то для 2048 страниц требуется 8 МиБ (2048 x 4 = 8192).

Трудности разработки и их преодоление


Отсутствие какой-либо технической документации по интеграции Intel SGX в OpenStack было нашей основной трудностью на момент внедрения. Поиск привел нас к статье проекта SecureCloud, где был представлен способ управления виртуальными машинами с анклавами SGX.

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

  1. Добиться от сервиса OpenStack Nova генерации XML-файла с дополнительными параметрами для виртуальных машин с поддержкой Intel SGX.
  2. Написать фильтр планировщика OpenStack Nova с целью определения доступной памяти для анклавов на вычислительных узлах и осуществления некоторых других проверок.

Их выполнения было достаточно для интеграции Intel SGX в наше публичное облако.

Кроме того, мы дописали сбор статистики с учетом EPC:

# openstack usage showUsage from 2020-11-04 to 2020-12-03 on project a968da75bcab4943a7beb4009b8ccb4a:+---------------+--------------+| Field         | Value        |+---------------+--------------+| CPU Hours     | 47157.6      || Disk GB-Hours | 251328.19    || EPC MB-Hours  | 26880.02     || RAM MB-Hours  | 117222622.62 || Servers       | 23           |+---------------+--------------+


Безопасная среда для запуска контейнеризированных приложений



Научившись выделять виртуальные машины с поддержкой Intel SGX, мы использовали платформу SCONE компании Scontain, чтобы обеспечить возможность безопасного запуска контейнеризированных приложений в случае угроз со стороны привилегированного программного обеспечения. При использовании данного решения для прозрачной защиты файловых систем в средах Docker, Kubernetes и Rancher достаточно наличия процессора Intel с поддержкой SGX и драйвера Linux SGX.

Запуск каждого из контейнеров возможен лишь при наличии файла конфигурации, создаваемого клиентским расширением платформы SCONE. В нём содержатся ключи шифрования, аргументы приложения и переменные среды. Файлы, сетевой трафик и стандартные потоки ввода/вывода (stdin/stdout) прозрачно зашифрованы и недоступны даже для пользователей с правами root.
Платформа SCONE оснащена встроенной службой аттестации и конфигурации, проверяющей приложения на соответствие принятой политике безопасности. Она генерирует приватные ключи и сертификаты, которые должны быть доступны только в пределах анклава. Конфиденциальность и целостность данных в процессе их передачи обеспечиваются криптографическим протоколом TLS.

С помощью драйвера SGX для каждого анклава в виртуальном адресном пространстве резервируется до 64 ГБ памяти. Платформа SCONE поддерживает языки программирования C/C++/C#/Rust/Go/Python/Java. За счёт специального компилятора исходный код автоматически (без необходимости дополнительных модификаций) подготавливается к использованию совместно с Intel SGX.

Кейс Aggregion


Завершив все необходимые работы по интеграции Intel SGX, мы подключили к нашему публичному облаку платформу управления распределёнными данными компании Aggregion.

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

Софт Aggregion целиком ставится в контур поставщика данных, что подразумевает наличие в его распоряжении инфраструктуры с поддержкой Intel SGX. Теперь клиенты компании могут рассматривать подключение к нашему публичному облаку в качестве альтернативы аренде или покупке физических серверов.

Принципы безопасной работы на платформе Aggregion. В контуре каждого поставщика чувствительные данные изолируются в анклавы Intel SGX, которые фактически представляют собой чёрные ящики: что происходит внутри, недоступно никому, в том числе и провайдеру облачной инфраструктуры. Проверка первоначального состояния анклава и возможности его использования для хранения конфиденциальной информации осуществляется за счёт удалённой аттестации, когда MrEnclave определяет хеш-значение.

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



Выводы


Мы понимаем, что Intel SGX не является панацеей по защите данных и можно найти ряд статей, порицающих эту технологию, в том числе и на Хабре. Периодически появляются сообщения об атаках, способных извлечь конфиденциальные данные из анклавов: так, в 2018 году бреши в SGX пробили Meltdown и Spectre, в 2020 году SGAxe и CrossTalk. В свою очередь компания Intel устраняет выявленные уязвимости с помощью обновлений микрокода процессоров.

Почему же мы всё-таки решили внедрить данную технологию? Мы видим в применении Intel SGX возможность сократить потенциальную область кибератак за счёт создания дополнительного контура защиты облачной инфраструктуры G-Core Labs наряду с уже задействованными технологиями информационной безопасности и тем самым повысить доверие наших пользователей к хранению и обработке конфиденциальных данных. Надеемся, что в будущем нам ещё предстоит поделиться с вами успешными клиентскими кейсами, хотя и не берёмся утверждать, что наши статьи не будут основаны на историях обнаружения и устранения новых уязвимостей. А пока предлагаем вам поделиться своими методами по защите чувствительных данных в комментариях.
Подробнее..

Перевод Docker vs Kubernetes

14.05.2021 20:06:19 | Автор: admin


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

Для этого сначала дадим определение термину контейнер в контексте Kubernetes (K8) и Docker. Это позволит понять основы обеих технологии, прежде чем мы углубимся в каждую из них.

Что такое контейнер


Допустим, вы хотите установить приложение в идеальной для максимальной производительности среде. Обычно параметры такой среды зависят от серверных стоек, сетевых переменных и других технических характеристик внешней инфраструктуры. Это означает, что в 100% случаев вы не достигнете максимальной производительности только если не создадите контейнер, чтобы абстрагировать приложение от его физического местоположения.
Представьте себе песочницу или виртуальную машину с указанными переменными (тип ОС, Compute и т. д.). Допустим, вам также нужно было установить другое приложение, контейнер на том же оборудовании, но с другой ОС и другими переменными, и это создало изолированную среду, идеально подходящую для тестирования и внедрения данного приложения.
Эти контейнеризированные приложения функционируют таким образом, как будто они находятся на разных компьютерах и даже в разных местах. Ключевое преимущество использования контейнеров заключается в том, что мы можем реплицировать их среды на любом устройстве, таким образом устраняя проблемы несоответствия, существовавшие в доконтейнерную эру разработки ПО.

Что представляет собой Kubernetes?


Kubernetes это инструмент, разработанный Google в 2014 году для оркестровки задач, связанных с контейнерами и платформами контейнеризации. Это система с открытым исходным кодом, которая может управлять несколькими контейнерами, расширяя свои возможности (см. функции ниже) для поддержания бесперебойной работы и доступности контейнеризованных приложений.

Ключевые функции Kubernetes


Одними из особенностей Kubernetes являются:
  1. Поддержание среды с заданными параметрами для разработки, тестирования и внедрения
  2. Предсказуемая и автоматически масштабируемая (горизонтально) инфраструктура
  3. Самовосстанавливающаяся (с возможностью отмены) среда с балансировкой нагрузки
  4. Широкие возможности для установки приложений
  5. Инструменты управления на уровне приложений

Это пять основных характерных особенностей, для которых разработчики создали Google Kubernetes Engine.

Что представляет собой Docker?


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

Ключевые функции Docker


Вот краткий список особенностей Docker:
  1. Совместное использование образов среды с помощью Docker Build
  2. Docker Assemble для распознавания языка программирования и речи при создании контейнеров
  3. Нативные и облачные инструменты для оптимизации производительности разработчиков
  4. Инструменты CI/CD для команд, работающих над развивающимися приложениями с системой контроля версий
  5. Высокая отказоустойчивость с надежной поддержкой больших кластеров


Docker или Kubernetes. Нужно ли выбирать между ними?




Не всегда.
Сам по себе Kubernetes не может запустить контейнер, когда вы начинаете свой проект. Для этого лучше использовать Docker (или его альтернативу). Но мы рекомендуем использовать их в синергии.
Docker создает контейнеры и управляет ими..., а затем Kubernetes управляет Docker.
В случае, если ваше приложение простое, Docker может также предоставить всю необходимую инфраструктуру для поддержания его работоспособности. По мере роста приложения, когда ему может потребоваться несколько кластеров и более сложное обслуживание, нужно использовать Kubernetes.
Вы стоите перед необходимостью выбора только в том случае, когда ваше приложение масштабируется.

Плюсы и минусы Docker: контейнеризация


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

Плюсы:


  • Легкость создания. Инициализация контейнеров в Docker выполняется быстро и требует минимальных технических навыков.
  • Инструменты Docker. Управлять контейнерами легко благодаря полному набору стартовых инструментов
  • Эффективная поддержка. У Docker есть активное сообщество разработчиков, которые предоставляют техническую поддержку и помогают устранить любые проблемы, с которыми вы можете столкнуться.


Минусы:


  • Отсутствие хранилища. При каждой перезагрузке контейнер теряет ранее созданные вами данные. Сохранение не настроено по умолчанию, и для начинающих разработчиков это может создавать сложности.
  • Низкая скорость. Контейнеры Docker никогда не достигнут уровня производительности выделенных серверов из-за сетевого уровня, на котором они теряют скорость. Нечто неслыханное при использовании инфраструктуры выделенных серверов для хостинга приложений.
  • Отсутствие кроссплатформенной поддержки. Изолированный характер контейнеров Docker делает невозможным установку приложений в средах, отличных от среды, в которой они были разработаны.


Плюсы и минусы Kubernetes: оркестровка контейнеров


Как и у Docker, у Kubernetes есть свои преимущества и недостатки, которые разработчики должны учитывать при его использовании. Давайте рассмотрим несколько плюсов и минусов для более глубокого понимания использования K8.

Плюсы:


  • Модули pod (поды). К8 поддерживает поды (контейнеры и инструменты контейнеризации) для сохранения с автовосстановлением (воссозданием) в случае неожиданного сбоя.
  • Разработка Google. Kubernetes вселяет уверенность (не всем, конечно) в своем качестве за счет известности разработчика и растущего (самого большого) сообщества.
  • Наличие хранилища по умолчанию. Для удобства разработчиков K8 поставляется с облачными хранилищами и хранилищами SAN.


Минусы:


  • Сложная установка. Требует значительных технических усилий, а для правильной установки и настройки нужно много времени.
  • Overkill простым приложениям не нужна сложность Kubernetes. Но кто из ваших разработчиков признается, что ваше приложение простое?
  • Технические возможности К8 обходятся недешево. Услуги разработчиков DevOps, способных создавать и поддерживать инструменты Kubernetes, стоят дорого.


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

Примеры использования Docker и Kubernetes





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

Когда нужно использовать Kubernetes


Если масштаб вашего приложения значительно вырос, возможно, вам пора переходить на К8:
  • Почти идеальное время безотказной работы. Функция самовосстановления Kubernetes позволяет ресурсоемким приложениям продолжать работу независимо от количества сбоев в системе.
  • При выборе между различными поставщиками услуг контейнеризации. Так как К8 сотрудничает (на разных уровнях сложности) почти со всеми поставщиками, использование К8 в качестве системы оркестровки дарит свободу выбора. Ни один поставщик не может претендовать на контракт с вашей компанией, если вы не будете довольны качеством услуг после пробного периода.
  • Если вы не уверены в потенциале роста. Во время горизонтального масштабирования К8 автоматически распределяет ресурсы по приложениям.


Когда нужно использовать Docker


В некоторых случаях для хостинга приложений лучше использовать Docker и его инструменты. Рассмотрим некоторые из них.
  • Если К8 не подходит. Недостаточные технические возможности, несовместимость с API и высокая стоимость услуг могут привести к необходимости использования Docker и его инструментов. Платформа оркестровки Docker Swarm может полностью заменить К8.
  • Когда вы только начинаете развитие приложения. Вам не нужно использовать Docker совместно с какой-либо другой системой оркестровки, когда приложения еще находятся в стадии роста. На данных этапах скорость важнее устойчивости.
  • При создании приложений CLI. При разработке Docker была предусмотрена его интеграция с приложениями CLI, благодаря эффективности которых повышается производительность.


Когда необходимо совместное использование


При совместном использовании Kubernetes и Docker дополняют друг друга. Во-первых, стоит отметить медленную пропускную способность, на которую мы жаловались при внедрении Kubernetes и проверке работоспособности контейнеров.
При наличии достаточного бюджета и технических возможностей для поддержки приложений, эти инструменты отлично работают. Вы не столкнетесь с проблемой простоя приложения, поскольку вам поможет сообщество.
Также нужно признать, что в каждом из данных инструментов остались пробелы и недостатки, поэтому они лучше функционируют вместе. Kompose от K8 это адаптация Docker Compose. Это означает, что использование обоих инструментов было и остается стандартом.
Таким образом, результат данного противостояния дружеская ничья. Случаи применения полностью зависят от ваших предпочтений. Однако лучше не использовать только Kubernetes.
Подробнее..

Перевод О растущей популярности Kubernetes

28.08.2020 12:23:00 | Автор: admin
Привет, Хабр!

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



Приятного чтения!

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

Контейнеры зародились как специальная конструкция для изоляции процессов в Linux; в состав контейнеров с 2007 входят cgroups, а с 2002 пространства имен. Контейнеры оформились еще лучше к 2008 году, когда стала доступна LXC, а в Google разработали свой внутрикорпоративный механизм под названием Borg, где вся работа ведется в контейнерах. Отсюда перенесемся в 2013, когда состоялся первый релиз Docker, и контейнеры окончательно перешли в разряд популярных массовых решений. На тот момент основным инструментом для оркестрации контейнеров был Mesos, хотя, бешеной популярностью он не пользовался. Первый релиз Kubernetes состоялся 2015 году, после чего этот инструмент де-факто стал стандартом в области оркестрации контейнеров.

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

Инфраструктура как YAML


В мире, который от Puppet и Chef пришел к Kubernetes, одна из самых больших перемен заключалась в переходе от инфраструктуры как код к инфраструктуре как данные конкретно, как YAML. Все ресурсы в Kubernetes, к которым относятся поды, конфигурации, развернутые экземпляры, тома и др. можно легко описать в файле YAML. Например:

apiVersion: v1kind: Podmetadata:  name: site  labels:    app: webspec:  containers:    - name: front-end      image: nginx      ports:        - containerPort: 80

С таким представлением специалистам по DevOps или SRE удобнее полностью выражать свои рабочие нагрузки, без необходимости писать программный код на таких языках как Python или Javascript.

Другие достоинства организации инфраструктуры как данных, в частности, таковы:

  • GitOps или контроль версий Git Operations Version. Такой подход позволяет держать все YAML-файлы Kubernetes в репозиториях git, благодаря чему вы в точности можете отследить, когда было внесено изменение, кто его внес, и что именно изменилось. Благодаря этому повышается прозрачность операций в пределах всей организации, повышается эффективность работы в силу устранения неоднозначности, в частности, в том, где сотрудники должны искать нужные им ресурсы. В то же время, становится проще автоматически вносить изменения в ресурсы Kubernetes путем обычного слияния pull-запроса.
  • Масштабируемость. Когда ресурсы определены в виде YAML, операторам кластера становится чрезвычайно просто изменить одно или два числа в ресурсе Kubernetes, изменив тем самым принципы его масштабирования. В Kubernetes предусмотрен механизм для горизонтального автомасштабирования подов, при помощи которого удобно определять, каково минимальное и максимальное количество подов, которое требуется в конкретной развернутой конфигурацией, чтобы справляться с низким и высоким уровнем трафика. Например, если вы развернули конфигурацию, для которой требуются дополнительные мощности из-за резкого всплеска трафика, то показатель maxReplicas можно изменить с 10 на 20:

apiVersion: autoscaling/v2beta2kind: HorizontalPodAutoscalermetadata:  name: myapp  namespace: defaultspec:  scaleTargetRef:    apiVersion: apps/v1    kind: Deployment    name: myapp-deployment  minReplicas: 1  maxReplicas: 20  metrics:  - type: Resource    resource:      name: cpu      target:        type: Utilization        averageUtilization: 50

  • Безопасность и управление. YAML отлично подходит для оценки того, как те или иные вещи развертываются в Kubernetes. Например, серьезная проблема, связанная с обеспечением безопасности, касается того, запускаются ли ваши рабочие нагрузки из-под пользователя, не обладающего правами администратора. В данном случае нам могут пригодиться такие инструменты как conftest, валидатор YAML/JSON, плюс Open Policy Agent, валидатор политик, позволяющий убедиться, что контекст SecurityContext ваших рабочих нагрузок не позволяет контейнеру работать с привилегиями администратора. Если требуется это обеспечить, пользователи могут применить простую политику rego, вот так:

package maindeny[msg] {  input.kind = "Deployment"  not input.spec.template.spec.securityContext.runAsNonRoot = true  msg = "Containers must not run as root"}

  • Варианты интеграции с облачным провайдером. Одна из наиболее заметных тенденций в современных высоких технологиях запускать рабочие нагрузки на мощностях общедоступных облачных провайдеров. При помощи компонента cloud-provider Kubernetes позволяет любому кластеру интегрироваться с тем облачным провайдером, на котором он работает. Например, если пользователь запустил приложение в Kubernetes на AWS и хочет открыть доступ к этому приложению через сервис, облачный провайдер помогает автоматически создать сервис LoadBalancer, который автоматически предоставит балансировщик нагрузки Amazon Elastic Load Balancer, чтобы перенаправить трафик в поды приложений.

Расширяемость


Kubernetes очень хорошо расширяется, и это нравится разработчикам. Существует набор имеющихся ресурсов, таких, как поды, развертки, StatefulSets, секреты, ConfigMaps, т.д. Правда, пользователи и разработчики могут добавлять и другие ресурсы в форме пользовательских определений ресурсов.

Например, если мы хотим определить ресурс CronTab, то могли бы сделать нечто подобное:

apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata:  name: crontabs.my.orgspec:  group: my.org  versions:    - name: v1      served: true      storage: true      Schema:        openAPIV3Schema:          type: object          properties:            spec:              type: object              properties:                cronSpec:                  type: string                  pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$'                replicas:                  type: integer                  minimum: 1                  maximum: 10  scope: Namespaced  names:    plural: crontabs    singular: crontab    kind: CronTab    shortNames:    - ct

Позже мы можем создать ресурс CronTab приблизительно таким образом:

apiVersion: "my.org/v1"kind: CronTabmetadata:  name: my-cron-objectspec:  cronSpec: "* * * * */5"  image: my-cron-image  replicas: 5

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

В сообществе есть несколько инструментов, при помощи которых разработчикам удобно создавать собственные операторы. Среди них Operator Framework и его Operator SDK. Этот SDK предоставляет основу, отталкиваясь от которой, разработчик может очень быстро приступить к созданию оператора. Скажем, можно начать с командной строки примерно таким образом:

$ operator-sdk new my-operator --repo github.com/myuser/my-operator


Так создается весь стереотипный код для вашего оператора, в том числе, файлы YAML и код на Golang:

.|____cmd| |____manager| | |____main.go|____go.mod|____deploy| |____role.yaml| |____role_binding.yaml| |____service_account.yaml| |____operator.yaml|____tools.go|____go.sum|____.gitignore|____version| |____version.go|____build| |____bin| | |____user_setup| | |____entrypoint| |____Dockerfile|____pkg| |____apis| | |____apis.go| |____controller| | |____controller.go

Затем можно добавлять нужные API и контроллер, вот так:

$ operator-sdk add api --api-version=myapp.com/v1alpha1 --kind=MyAppService$ operator-sdk add controller --api-version=myapp.com/v1alpha1 --kind=MyAppService

После чего, наконец, собрать оператор и отправить его в реестр вашего контейнера:

$ operator-sdk build your.container.registry/youruser/myapp-operator

Если разработчику требуется еще более полный контроль, то можно изменить стереотипный код в файлах на Go. Например, чтобы видоизменить специфику контроллера, можно внести изменения в файл controller.go.

Другой проект, KUDO, позволяет создавать операторы, пользуясь одними лишь декларативными YAML-файлами. Например, оператор для Apache Kafka будет определяться примерно так. С его помощью можно всего лишь парой команд установить кластер Kafka поверх Kubernetes:

$ kubectl kudo install zookeeper$ kubectl kudo install kafka


А затем настроить его при помощи еще одной команды:

$ kubectl kudo install kafka --instance=my-kafka-name \            -p ZOOKEEPER_URI=zk-zookeeper-0.zk-hs:2181 \            -p ZOOKEEPER_PATH=/my-path -p BROKER_CPUS=3000m \            -p BROKER_COUNT=5 -p BROKER_MEM=4096m \            -p DISK_SIZE=40Gi -p MIN_INSYNC_REPLICAS=3 \            -p NUM_NETWORK_THREADS=10 -p NUM_IO_THREADS=20

Инновации


В течение нескольких последних лет крупные релизы Kubernetes выходят раз в несколько месяцев то есть, по три-четыре крупных релиза в год. Количество новых фич, внедряемых в каждом из них, не уменьшается. Более того, признаков замедления не наблюдается даже в наши сложные времена посмотрите, какова сейчас активность проекта Kubernetes на Github.

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

Сообщество


Еще один серьезный аспект популярности Kubernetes заключается в силе его сообщества. В 2015 году, по достижении версии 1.0, Kubernetes спонсировался Cloud Native Computing Foundation.

Также существуют разнообразные сообщества SIG (специальные группы по интересам), нацеленные на проработку различных областей Kubernetes по мере того, как этот проект развивается. Эти группы постоянно добавляют все новые возможности, благодаря чему работать с Kubernetes становится удобнее и удобнее.

Фонд Cloud Native Foundation также проводит CloudNativeCon/KubeCon, которая, на момент написания этого текста, является крупнейшей опенсорсной конференцией в мире. Как правило, она проводится три раза в год и собирает тысячи профессионалов, желающих улучшить Kubernetes и его экосистему, а также освоить новые возможности, появляющиеся каждые три месяца.

Более того, в Cloud Native Foundation есть Комитет по техническому надзору, который, совместно с SIGs рассматривает новые и имеющиеся проекты фонда, ориентированные на облачную экосистему. Большинство из этих проектов помогают улучшить сильные стороны Kubernetes.

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

Будущее


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

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

Перевод Знакомство с Docker

01.06.2021 08:12:59 | Автор: admin

Это первая статья в цикле Знакомство с Docker. Если вы раньше не работали с Docker, мы расскажем, что он из себя представляет.

Что такое Docker?

Docker - это инструмент DevOps для контейнеризации сервисов и процессов... Подождите... Подождите... Подождите! Что такое DevOps? Что такое контейнеризация? Какие услуги и процессы я могу контейнеризовать? Начнём с самого начала.

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

Контейнер - это не более чем процесс, который выполняется изолированно в операционной системе. У него есть собственная сеть, собственная файловая система и выделенная память. Вы можете подумать, а почему бы просто не использовать виртуальную машину? Что ж, виртуальная машина - это отдельная ОС, сильно загруженная множеством других процессов, которые могут вам никогда не понадобиться, вместо виртуализации всей операционной системы для запуска одной службы вы можете виртуализировать службу. Точнее говоря, вы можете создать легкую виртуальную среду для одной службы. Этими службами могут быть серверы Nginx, NodeJS или приложения angular. И Docker помогает нам в этом.

Название Docker происходит от слова док (dock). Док используется для погрузки и разгрузки грузов на кораблях. Здесь можно провести простую аналогию, груз может быть контейнерами, а корабль может быть нашей операционной системой. Все товары в составе груза изолированы от товаров другого груза и самого корабля. Точно так же в Docker процесс одного Docker контейнера (Docker Container) изолирован от процесса другого контейнера и самой операционной системы.

Как работает контейнеризация

Docker использует технологию Linux Containers (LXC) и механизмы ядра Linux. Поскольку у docker-контейнера нет собственной операционной системы, он полагается на хостовую операционную систему. Контейнер, созданный в Linux, может быть запущен в любом дистрибутиве Linux, но не может работать в Windows, и то же самое касается образа, созданного в Windows. Docker расширяет возможности LXC, но также использует контрольные группы (cgroups), которые позволяют ядру хоста разделять использование ресурсов (ЦП, память, дисковый ввод-вывод, сеть и т. д.) на уровни изоляции, называемые пространствами имён (namespaces).

Как создать Docker контейнер?

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

Большое количество готовых образов можно найти в DockerHub, общедоступном репозитории Docker, который позволяет вам делиться своими образами или использовать образы, созданные другими людьми. Вы также можете создавать свои собственные образы и помещать их в свой частный репозиторий (например, Harbor). В дальнейшем этот образ будет использован для создания контейнеров. Один и тот же образ можно использовать для создания одного или нескольких контейнеров, используя Docker-CLI. А что такое Docker CLI спросите вы?

Рассмотрим архитектуру Docker,

  1. Docker демонслушает запросы Docker API и управляет всеми объектами Docker, такими как образы, контейнеры, сети и тома. Это основная служба Docker, которая необходима для работы контейнеров и других компонентов Docker. Если Docker демон перестанет работать, так же перестанут работать все запущенные контейнеры.

  2. Docker демон также предоставляет REST API. Различные инструменты могут использовать его для взаимодействия с демоном. Вы также можете создать приложение, для работы с Docker REST API.

  3. Docker-CLIэто инструмент командной строки, который позволяет вам общаться с демоном Docker черезREST API.

Сеть Docker

Docker предусматривает несколько режимов работы сети. Подробнее о работе сети можно прочитать в нашей статье Сеть контейнеров это не сложно.

  • Host networks -Контейнер Docker будет использовать сеть хоста, соответственно он не будет изолирован с точки зрения сети, это не коснётся изоляции контейнера в целом, например изоляции процессов и файловой системы.

  • Bridge networks - Позволяет изолировать ваши приложения, но они могут взаимодействовать между собой и принимать трафик снаружи, если включено сопоставления портов (port forwarding).

  • Overlay networks - Оверлейные сети соединяют вместе несколько демонов Docker и позволяют службам Docker Swarm взаимодействовать друг с другом. Docker Swarm (аналог Kubernetes) может использоваться, если у вас несколько серверов с Docker.

  • Macvlan networks - Позволяет назначить MAC-адрес контейнеру, чтобы он отображался как физическое устройство в вашей сети.

  • None -Сеть отсутствует, соответственно вы не сможете подключиться к контейнеру.

И всё же, почему стоит использовать Docker?

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

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

  • Вы можете ограничить потребляемые контейнером ресурсы - ЦП и память, это позволит запустить больше контейнеров на одном сервере.

  • Возможность запуска нескольких контейнеров из одного образа, позволит сэкономить место на диске и в репозитории образов.

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

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

Вместо заключения

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

Подробнее..

Перевод Слабо поднять такой крошечный контейнер? Создаем контейнеризованный HTTP-сервер на 6kB

25.04.2021 16:15:44 | Автор: admin
TL;DR я решил создать самый маленький образ контейнера, при помощи которого все-таки можно сделать что-нибудь полезное. Опираясь на преимущества многоступенчатых сборок, базового образаscratchи крошечного http-сервера на основе этой сборки, я смог ужать результат до 6.32kB!





Если предпочитаете видео, вот ролик по статье, выложенный на YouTube!

Раздутые контейнеры


Контейнеры часто превозносятся как панацея, позволяющая справиться с любыми вызовами, связанными с эксплуатацией ПО. Притом, как мне нравятся контейнеры, на практике мне часто попадаются контейнерные образы, отягощенные разнообразными проблемами. Распространенная беда размер контейнера; у некоторых образов он достигает многих гигабайт!

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

Задача


Правила довольно просты:

  • Контейнер должен выдавать содержимое файла по http на выбранный вами порт
  • Монтирование томов не допускается (так называемое Правило Марека )

Упрощенное решение


Чтобы узнать размер базового образа, можно воспользоваться node.js и создать простой серверindex.js:

const fs = require("fs");const http = require('http');const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'text/html' })fs.createReadStream('index.html').pipe(res)})server.listen(port, hostname, () => {console.log(`Server: http://0.0.0.0:8080/`);});

и сделать из него образ, запуская официальный базовый образ node:

FROM node:14COPY . .CMD ["node", "index.js"]

Этот завесил на943MB!

Уменьшенный базовый образ


Один из простейших и наиболее очевидных тактических подходов к уменьшению размера образа выбрать более компактный базовый образ. Официальный базовый образ node существует в варианте slim(по-прежнему на основе debian, но с меньшим количеством предустановленных зависимостей) и вариантalpineна основеAlpine Linux.

С применениемnode:14-slimиnode:14-alpineв качестве базового удается уменьшить размер образа до167MBи116MBсоответственно.

Поскольку образы docker аддитивны, где каждый уровень надстраивается над следующим, здесь уже практически нечего сделать, чтобы еще сильнее уменьшить решение с node.js.

Скомпилированные языки


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

Я создал простейший файловый серверserver.go:

package mainimport ("fmt""log""net/http")func main() {fileServer := http.FileServer(http.Dir("./"))http.Handle("/", fileServer)fmt.Printf("Starting server at port 8080\n")if err := http.ListenAndServe(":8080", nil); err != nil {log.Fatal(err)}}

И встроил его в контейнерный образ, воспользовавшись официальным базовым образом golang:

FROM golang:1.14COPY . .RUN go build -o server .CMD ["./server"]

Который завесил на818MB.

Здесь есть проблема: в базовом образе golang установлено много зависимостей, которые полезны при создании программ на go, но не нужны для запуска программ.

Многоступенчатые сборки


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

Это полезно по нескольким причинам, но одна из наиболее очевидных размер образа! Выполнив рефакторинг dockerfile вот так:

### этап сборки ###FROM golang:1.14-alpine AS builderCOPY . .RUN go build -o server .### этап запуска ###FROM alpine:3.12COPY --from=builder /go/server ./serverCOPY index.html index.htmlCMD ["./server"]

Размер полученного образа всего13.2MB!

Статическая компиляция + образ Scratch


13 MB совсем неплохо, но у нас в запасе осталась еще пара трюков, позволяющих еще сильнее ужать этот образ.

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

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

### этап сборки ###FROM golang:1.14 as builderCOPY . .RUN go build \-ldflags "-linkmode external -extldflags -static" \-a server.go### этап запуска ###FROM scratchCOPY --from=builder /go/server ./serverCOPY index.html index.htmlCMD ["./server"]

В частности, мы задаем externalв качестве режима линковки и передаем флаг-staticвнешнему линковщику.

Благодаря двум этим изменениям удается довести размер образа до 8.65MB

ASM как залог победы!


Образ размером менее 10MB, написанный на языке вроде Go, отчетливо миниатюрен почти для любых обстоятельств но можно сделать еще меньше! Пользовательnemasuвыложил на Github полноценный http-сервер, написанный на ассемблере. Он называется assmttpd.

Все, что потребовалось для его контейнеризации установить несколько зависимостей сборки в базовый образ Ubuntu, прежде, чем запустить предоставленный рецепт make release:

### этап сборки ###FROM ubuntu:18.04 as builderRUN apt updateRUN apt install -y make yasm as31 nasm binutilsCOPY . .RUN make release### этап запуска ###FROM scratchCOPY --from=builder /asmttpd /asmttpdCOPY /web_root/index.html /web_root/index.htmlCMD ["/asmttpd", "/web_root", "8080"]

Затем полученный в результате исполняемый файлasmttpdкопируется в scratch-образ и вызывается через командную строку. Размер полученного образа всего 6,34kB!
Подробнее..

Как ажиотажный спрос на туалетную бумагу привел к дефициту электроники

19.06.2021 12:05:20 | Автор: admin
Современный мир удивительное место. Глобальная экономика, производственные цепочки, разнесенные по всему миру, и связность, казалось бы, абсолютно несовместимых между собой вещей через общие точки соприкосновения. Хорошим примером такой связности является то, что ажиотажный спрос на туалетную бумагу в США привел к дефициту электронных товаров всех категорий, а в перспективе вовсе к глобальному сбою в мировой торговле. И это могло бы быть даже забавно, если бы не приводило к пустым полкам и значительному росту цен на те товары, которые до нас все же доезжают.



Все началось весной 2020 года, когда мир столкнулся с COVID-19. После первых сообщений о грядущей пандемии и возможном локдауне на длительный срок, весь мир охватила туалетная истерика, которая выражалась в покупке нетипично большого количества туалетной бумаги. Кто-то потешался над видео из Соединенных Штатов, где мужчины и женщины дрались за упаковки с ценным товаром, кто-то молча прикупал пару лишних рулонов или упаковку впрок. Вопрос лишь в том, что взрывной рост спроса на туалетную бумагу и прочие гигиенические товары в США, которые население сметало с полок в любых количествах, привели к масштабному кризису мировых грузоперевозок.

Локдаун Китая в марте 2020 года


На самом деле паника среди жителей США насчет того, что придется ходить с грязным задом, была не совсем безосновательна. Одним из катализаторов ажиотажа вокруг туалетной бумаги стали не только интернет-тролли, но и сообщения из Китая о закрытии большинства своих провинций на карантин в марте 2020 года.



Это мы привыкли к тому, что сидя на 1/4 мировых запасов леса, всегда имеем доступ к продукции целлюлозно-бумажной промышленности. Да, качество отечественных производителей не всегда высоко, но при желании Россия за счет запасов леса Сибири может обеспечивать бумагой или сырьем для ее производства весь евразийский континент. Сложнее дела обстоят в США: страна активно импортирует бумажную продукцию.

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

Объявление о закрытии Китая привело к тому, что в регионе скопилась серьезная часть контейнеров для морских грузоперевозок, что вызвало сбой в цепочке поставок товаров, которые были даже не завязаны на эту страну. Однако довольно быстро Китай взял ситуацию под контроль и открыл торговлю, а накопленные товары хлынули на Запад, в изнывающие от дефицита различных продуктов Соединенные Штаты. В том числе в Новый Свет поплыло огромное количество туалетной бумаги, которая стала в США откровенно заканчиваться.

Разворот ситуации


Разовая доставка огромного количества товаров в США из Юго-Восточной Азии привела к тому, что в порты страны прибыло большое число кораблей с еще большим числом контейнеров на борту. Мировая торговля устроена таким образом, что перегрузку судно будет ждать только в случае, если оператору выгодно придержать корабль в порту и дождаться, когда контейнеры освободят от товаров или на погрузку прибудут новые. Так как в США наблюдался дефицит товаров, суда из Китая прибывали на отгрузку и убывали вновь в Азию, уже порожняком. Это было намного прибыльнее, чем ждать поставку чего-либо из Штатов.

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



Через некоторое время о локдауне стали объявлять и в Соединенных Штатах и ситуация стала диаметрально противоположной: десятки тысяч 40-футовых контейнеров, которые являются основой морских грузоперевозок, застряли на территории США. Перевозить в них нечего, да и зайти в сам порт тоже нельзя.

Эффект домино


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

В мире насчитывается около 180 млн контейнеров, но все они находятся не в том месте, главный исполнительный директор логистической компании Redwood Logistics Марк Йегер

В общей сложности на апрель 2021 года в США наблюдался дисбаланс в размере 40% в плане прибывающих и убывающих из страны грузовых контейнеров.

Как это сказалось на рынке электроники


Ощутимее всего кризис контейнерных перевозок сказался на рынке электротехнических товаров, на который наложился еще и кризис полупроводниковой продукции. Ведь именно производство электроники наиболее глобальный и чувствительный к цепочке поставок сегмент. И да, на стоимость микрочипов повлиял не только рост спроса, но и ажиотаж вокруг туалетной бумаги в США весной 2020 года.

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

Если до пандемии COVID-19 стоимость транспортировки из Китая в Европу морем составляла около $1500, то сейчас она подскочила до $9000 и выше за 40-футовый контейнер. В случае перевозки тех же холодильников, которых в контейнер может войти не более 60-70 штук, мы сталкиваемся с ростом стоимости каждой единицы товара на ~$100. Это же касается любой габаритной техники и прочей продукции, которая раньше перевозилась морем.



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

Закрытые порты Китая


Стоимость на компьютерные комплектующие и технику и так уже вызывают желание пустить слезу, но ситуация может еще и ухудшиться. В провинции Гуандун зафиксирована новая вспышка коронавирусной инфекции, что привело к закрытию порта с 25 по 31 мая и формированию очереди на погрузку. Это затронуло работу терминала Яньтянь в порту Шэньчжэнь (оборот 13,3 млн контейнеров в год), и ближайшие терминалы Шэкоу в Шэньчжэне и Наньша в Гуанчжоу (15,6 млн контейнеров).

Порты Шэньчжэнь и Гуанчжоу находятся на третьем и пятом месте среди городов с самыми крупными контейнерными портами мира, также через них проходит огромное количество микроэлектроники и электротехнических товаров, производимых в Китае.

Сейчас порты открыли частично, они работают на 30% своей мощности, а очередь на погрузку уже растянулась почти на неделю.

Мы испытываем сложности с отправкой из портов Шэкоу и Наньша, так как помимо наличия свободного оборудования у линии необходимо найти ещё и машину с водителем, у которого будет отрицательный тест ПЦР, а также проверить, не пропускает ли линия судозаход, рассказала прессе руководитель отдела интермодальных перевозок Itella в России Юлия Никитина.

Перспективы


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

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

Второе контейнеры, которые застряли в США, никуда не денутся. Как считают логисты, рано или поздно баланс восстановится и при усиленном производстве 40-футовых и прочих контейнеров, в будущем рынок может столкнуться с их избытком, что выльется в прямые финансовые потери для тех, кто контейнеры заказал и оплатил их производство сейчас.

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



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

Ироничное послесловие


Во всей описанной выше ситуации с контейнерными перевозками есть один нюанс: США на самом деле может возникнуть дефицит туалетной бумаги.

Только причиной для этого станет не апокалипсис на улицах американских городов, а сбой в поставке целлюлозы из Бразилии в Китай. Сейчас логистическим компаниям выгоднее работать на рынках США и Европы, фрахтуя контейнеры в четыре-пять раз дороже, чем раньше. Насыпные же грузоперевозки из Южной Америки не столь выгодны, так что эта логистическая линия находится под угрозой. А не будет сырья для заводов в Китае не будет и бумажной продукции в США, нездоровый и необоснованный спрос на которую и запустил всю эту цепочку.



На правах рекламы


VDSina предлагает недорогие серверы в аренду на Linux или Windows выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа. И главное, всё в наличии и без дефицита!

Подписывайтесь на наш чат в Telegram.

Подробнее..

Из песочницы Хеш-таблицы

02.07.2020 14:05:51 | Автор: admin

Предисловие


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


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


image


Мотивация использовать хеш-таблицы


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


Контейнер \ операция insert remove find
Array O(N) O(N) O(N)
List O(1) O(1) O(N)
Sorted array O(N) O(N) O(logN)
Бинарное дерево поиска O(logN) O(logN) O(logN)
Хеш-таблица O(1) O(1) O(1)

Из этой таблицы очень хорошо понятно, почему же стоит использовать хеш-таблицы. Но тогда возникает противоположный вопрос: почему же тогда ими не пользуются постоянно?
Ответ очень прост: как и всегда, невозможно получить все сразу, а именно: и скорость, и память. Хеш-таблицы тяжеловесные, и, хоть они и быстро отвечают на вопросы основных операций, пользоваться ими все время очень затратно.


Понятие хеш-таблицы


Хеш-таблица это контейнер. Реализация у него, возможно, и не очевидная, но довольно простая.


Для начала объяснение в нескольких словах. Мы определяем функцию хеширования, которая по каждому входящему элементу будет определять натуральное число. А уже дальше по этому натуральному числу мы будем класть элемент в (допустим) массив. Тогда имея такую функцию мы можем за O(1) обработать элемент.


Теперь стало понятно, почему же это именно хеш-таблица.


Проблема коллизии


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


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


Решения проблемы коллизии методом двойного хеширования


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


Одна хеш-функция (при входе g) будет возвращать натуральное число s, которое будет для нас начальным. То есть первое, что мы сделаем, попробуем поставить элемент g на позицию s в нашем массиве. Но что, если это место уже занято? Именно здесь нам пригодится вторая хеш-функция, которая будет возвращать t шаг, с которым мы будем в дальнейшем искать место, куда бы поставить элемент g.


Мы будем рассматривать сначала элемент s, потом s + t, затем s + 2*t и т.д. Естественно, чтобы не выйти за границы массива, мы обязаны смотреть на номер элемента по модулю (остатку от деления на размер массива).


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




Реализация хеш-таблицы


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


int HashFunctionHorner(const std::string& s, int table_size, const int key){    int hash_result = 0;    for (int i = 0; s[i] != 0; ++i)        hash_result = (key * hash_result + s[i]) % table_size;    hash_result = (hash_result * 2 + 1) % table_size;    return hash_result;}struct HashFunction1 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size - 1); // ключи должны быть взаимопросты, используем числа <размер таблицы> плюс и минус один.    }};struct HashFunction2 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size + 1);    }};

Чтобы идти дальше, нам необходимо разобраться с проблемой: что же будет, если мы удалим элемент из таблицы? Так вот, его нужно пометить флагом deleted, но просто удалять его безвозвратно нельзя. Ведь если мы так сделаем, то при попытке найти элемент (значение хеш-функции которого совпадет с ее значением у нашего удаленного элемента) мы сразу наткнемся на пустую ячейку. А это значит, что такого элемента и не было никогда, хотя, он лежит, просто где-то дальше в массиве. Это основная сложность использования данного метода решения коллизий.


Помня о данной проблеме построим наш класс.


template <class T, class THash1 = HashFunction1, class THash2 = HashFunction2>class HashTable{    static const int default_size = 8; // начальный размер нашей таблицы    constexpr static const double rehash_size = 0.75; // коэффициент, при котором произойдет увеличение таблицы    struct Node    {        T value;        bool state; // если значение флага state = false, значит элемент массива был удален (deleted)        Node(const T& value_) : value(value_), state(true) {}    };    Node** arr; // соответственно в массиве будут хранится структуры Node*    int size; // сколько элементов у нас сейчас в массиве (без учета deleted)    int buffer_size; // размер самого массива, сколько памяти выделено под хранение нашей таблицы    int size_all_non_nullptr; // сколько элементов у нас сейчас в массиве (с учетом deleted)};

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


...public:    HashTable()    {        buffer_size = default_size;        size = 0;        size_all_non_nullptr = 0;        arr = new Node*[buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr[i] = nullptr; // заполняем nullptr - то есть если значение отсутствует, и никто раньше по этому адресу не обращался    }    ~HashTable()    {        for (int i = 0; i < buffer_size; ++i)            if (arr[i])                delete arr[i];        delete[] arr;    }

Из необходимых методов осталось еще реализовать динамическое увеличение, расширение массива метод Resize.
Увеличиваем размер мы стандартно вдвое.


void Resize()    {        int past_buffer_size = buffer_size;        buffer_size *= 2;        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < past_buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value); // добавляем элементы в новый массив        }        // удаление предыдущего массива        for (int i = 0; i < past_buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }

Немаловажным является поддержание асимптотики O(1) стандартных операций. Но что же может повлиять на скорость работы? Наши удаленные элементы (deleted). Ведь, как мы помним, мы ничего не можем с ними сделать, но и окончательно обнулить их не можем. Так что они тянутся за нами огромным балластом. Для ускорения работы нашей хеш-таблицы воспользуемся рехешом (как мы помним, мы уже выделяли под это очень странные переменные).


Теперь воспользуемся ими, если процент реальных элементов массива стало меньше 50 %, то мы производим Rehash, а именно делаем то же самое, что и при увеличении таблицы (resize), но не увеличиваем. Возможно, это звучит глуповато, но попробую сейчас объяснить. Мы вызовем наши хеш-функции от всех элементов, переместим их в новых массив. Но с deleted-элементами это не произойдет, мы не будем их перемещать, и они удалятся вместе со старой таблицей.


Но к чему слова, код все разъяснит:


void Rehash()    {        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        // удаление предыдущего массива        for (int i = 0; i < buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }

Ну теперь мы уже точно на финальной, хоть и длинной, и полной колючих кустарников, прямой. Нам необходимо реализовать вставку (Add), удаление (Remove) и поиск (Find) элемента.


Начнем с самого простого метод Find элемент по значению.


bool Find(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size); // значение, отвечающее за начальную позицию        int h2 = hash2(value, buffer_size); // значение, ответственное за "шаг" по таблице        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return true; // такой элемент есть            h1 = (h1 + h2) % buffer_size;            ++i; // если у нас i >=  buffer_size, значит мы уже обошли абсолютно все ячейки, именно для этого мы считаем i, иначе мы могли бы зациклиться.        }        return false;    }

Далее мы реализуем удаление элемента Remove. Как мы это делаем? Находим элемент (как в методе Find), а затем удаляем, то есть просто меняем значение state на false, но сам Node мы не удаляем.


bool Remove(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)            {                arr[h1]->state = false;                --size;                return true;            }            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }

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


Помимо этого в данном методе есть еще одна часть, поддерживающая правильную асимптотику. Это запоминание первого подходящего для вставки элемента (даже если он deleted). Именно туда мы вставим элемент, если в нашей хеш-таблицы нет такого же. Если ни одного deleted-элемента на нашем пути нет, мы создаем новый Node с нашим вставляемым значением.


bool Add(const T& value, const THash1& hash1 = THash1(),const THash2& hash2 = THash2())    {        if (size + 1 > int(rehash_size * buffer_size))            Resize();        else if (size_all_non_nullptr > 2 * size)            Rehash(); // происходит рехеш, так как слишком много deleted-элементов        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        int first_deleted = -1; // запоминаем первый подходящий (удаленный) элемент        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return false; // такой элемент уже есть, а значит его нельзя вставлять повторно            if (!arr[h1]->state && first_deleted == -1) // находим место для нового элемента                first_deleted = h1;            h1 = (h1 + h2) % buffer_size;            ++i;        }        if (first_deleted == -1) // если не нашлось подходящего места, создаем новый Node        {            arr[h1] = new Node(value);            ++size_all_non_nullptr; // так как мы заполнили один пробел, не забываем записать, что это место теперь занято        }        else        {            arr[first_deleted]->value = value;            arr[first_deleted]->state = true;        }        ++size; // и в любом случае мы увеличили количество элементов        return true;    }

В заключение приведу полную реализацию хеш-таблицы:


int HashFunctionHorner(const std::string& s, int table_size, const int key){    int hash_result = 0;    for (int i = 0; s[i] != 0; ++i)    {        hash_result = (key * hash_result + s[i]) % table_size;    }    hash_result = (hash_result * 2 + 1) % table_size;    return hash_result;}struct HashFunction1 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size - 1);    }};struct HashFunction2 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size + 1);    }};template <class T, class THash1 = HashFunction1, class THash2 = HashFunction2>class HashTable{    static const int default_size = 8;    constexpr static const double rehash_size = 0.75;    struct Node    {        T value;        bool state;        Node(const T& value_) : value(value_), state(true) {}    };    Node** arr;    int size;    int buffer_size;    int size_all_non_nullptr;    void Resize()    {        int past_buffer_size = buffer_size;        buffer_size *= 2;        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < past_buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        for (int i = 0; i < past_buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }    void Rehash()    {        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        for (int i = 0; i < buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }public:    HashTable()    {        buffer_size = default_size;        size = 0;        size_all_non_nullptr = 0;        arr = new Node*[buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr[i] = nullptr;    }    ~HashTable()    {        for (int i = 0; i < buffer_size; ++i)            if (arr[i])                delete arr[i];        delete[] arr;    }    bool Add(const T& value, const THash1& hash1 = THash1(),const THash2& hash2 = THash2())    {        if (size + 1 > int(rehash_size * buffer_size))            Resize();        else if (size_all_non_nullptr > 2 * size)            Rehash();        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        int first_deleted = -1;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return false;            if (!arr[h1]->state && first_deleted == -1)                first_deleted = h1;            h1 = (h1 + h2) % buffer_size;            ++i;        }        if (first_deleted == -1)        {            arr[h1] = new Node(value);            ++size_all_non_nullptr;        }        else        {            arr[first_deleted]->value = value;            arr[first_deleted]->state = true;        }        ++size;        return true;    }    bool Remove(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)            {                arr[h1]->state = false;                --size;                return true;            }            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }    bool Find(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return true;            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }
Подробнее..

Перевод Kubernetes ускорьте ваши сервисы через снятие процессорных ограничений

03.09.2020 12:19:11 | Автор: admin
Еще в 2016 году мы в Buffer перешли на Kubernetes, и сейчас около 60 нод (на AWS) и 1500 контейнеров трудятся на нашем k8s-кластере под управлением kops. Тем не менее, на микросервисы мы переходили методом проб и ошибок, и даже после нескольких лет нашей работы с k8s мы до сих пор сталкиваемся с новыми для себя проблемами. В этом посте мы поговорим про процессорные ограничения: почему мы считали их хорошей практикой и почему в итоге они оказались не столь хороши.

Процессорные ограничения и троттлинг


Как и многие другие пользователи Kubernetes, Google очень рекомендует настраивать процессорные ограничения. Без такой настройки контейнеры в ноде могут занять все мощности процессора, из-за чего, в свою очередь, важные Kubernetes-процессы (например kubelet) перестанут реагировать на запросы. Таким образом, настройка процессорных ограничений это хороший способ защиты ваших нод.

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

Что может случиться, если мы не зададим процессорные ограничения?


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

Проявление проблемы троттлинга и отклика


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



Как можно видеть ниже, мы задали ограничение в 800m (0.8 или 80% ядра), и пиковые значения в лучшем случае достигают 200m (20% ядра). Казалось бы, до троттлинга сервиса у нас еще полно процессорных мощностей, однако



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

Столкнувшись с этим, мы вскоре обнаружили несколько ресурсов (проблема на github, презентация на zadano, пост на omio) про падение производительности и времени отклика сервисов из-за троттлинга.

Почему мы наблюдаем троттлинг при низкой нагрузке процессора? Краткая версия звучит так: в ядре Linux есть баг, из-за которого срабатывает необязательный троттлинг контейнеров с заданными процессорными ограничениями. Если вас интересует природа проблемы, вы можете ознакомиться с презентацией (видео и текстовый варианты) за авторством Дейва Чилука (Dave Chiluk).

Снятие процессорных ограничений (с особой осторожностью)


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

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


Деловая переписка по насущному вопросу.

Как защитить ваши ноды при снятии ограничений?


Изолирование неограниченных сервисов:

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

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



Назначение корректного запроса процессора и памяти:

Больше всего мы опасались, что процесс сожрет слишком много ресурсов и нода перестанет отвечать на запросы. Так как теперь (благодаря Datadog) мы могли четко наблюдать за всеми сервисами на нашем кластере, я проанализировал несколько месяцев работы тех из них, которые мы планировали назначить несвязанными. Я попросту задал максимальное использование процессора с запасом в 20%, и таким образом выделил место в ноде на случай, если k8s будет пробовать назначать другие сервисы в ноду.



Как можно видеть на графике, максимальная нагрузка на процессор достигла 242m CPU ядер (0.242 ядра процессора). За запрос процессора достаточно взять число чуть большее от этого значения. Обратите внимание, что так как сервисы ориентированы на пользователей, пиковые значения нагрузки совпадают с трафиком.

Сделайте то же самое с использованием памяти и запросами, и вуаля вы все настроили! Для большей безопасности вы можете добавить горизонтальное автоскалирование подов. Таким образом каждый раз, когда нагрузка на ресурсы будет высока, автоскалирование создаст новые поды, и kubernetes распределит их в ноды со свободным местом. На случай если места не останется в самом кластере, вы можете задать себе оповещение или настроить добавление новых нод через их автоскалирование.

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

Результаты


Я рад опубликовать эти отличные результаты экспериментов последних нескольких недель, мы уже отметили значительные улучшения отклика среди всех модифицированных сервисов:



Наилучшего результата мы добились на нашей главной странице (buffer.com), там сервис ускорился в двадцать два раза!



Исправлен ли баг ядра Linux?


Да, баг уже исправлен, и фикс добавлен в ядро дистрибутивов версии 4.19 и выше.

Тем не менее, при прочтении проблемы kubernetes на github за второе сентября 2020 года мы все еще сталкиваемся с упоминаниями некоторых Linux-проектов с аналогичным багом. Я полагаю, что в некоторых дистрибутивах Linux все еще есть эта ошибка и сейчас только ведется работа над ее исправлением.

Если ваша версия дистрибутива ниже 4.19, я бы порекомендовал обновиться до последней, но вам в любом случае стоит попробовать снять процессорные ограничения и посмотреть сохранится ли троттлинг. Ниже можно ознакомиться с неполным списком управляющих Kubernetes сервисов и Linux дистрибутивов:

  • Debian: фикс интегрирован в последнюю версию дистрибутива, buster, и выглядит достаточно свежим (август 2020 года). Некоторые предыдущие версии тоже могут быть пофикшены.
  • Ubuntu: фикс интегрирован в последнюю версию Ubuntu Focal Fossa 20.04
  • EKS обзавелся фиксом еще в декабре 2019 года. Если ваша версия ниже этой, следует обновить AMI.
  • kops: С июня 2020 года у kops 1.18+ основным образом хоста станет Ubuntu 20.04. Если ваша версия kops старше, вам, вероятно, придется подождать фикса. Мы и сами сейчас ждем.
  • GKE (Google Cloud): Фикс интегрирован в январе 2020 года, однако проблемы с троттлингом все еще наблюдаются.

Что делать, если фикс исправил проблему с троттлингом?

Я не уверен, что проблема полностью решена. Когда мы доберемся до версии ядра с фиксом, я протестирую кластер и обновлю пост. Если кто-то уже обновился, я с интересом ознакомлюсь с вашими результатами.

Заключение


  • Если вы работаете с Docker-контейнерами под Linux (не важно Kubernetes, Mesos, Swarm или еще какими), ваши контейнеры могут терять в производительности из-за троттлинга;
  • Попробуйте обновиться до последней версии вашего дистрибутива в надежде, что баг уже пофиксили;
  • Снятие процессорных ограничений решит проблему, но это опасный прием, который следует применять с особой осторожностью (лучше сначала обновить ядро и сравнить результаты);
  • Если вы сняли процессорные ограничения, внимательно отслеживайте использование процессора и памяти, и убедитесь, что ваши ресурсы процессора превышают потребление;
  • Безопасным вариантом будет автоскалирование подов для создания новых подов в случае высокой нагрузки на железо, чтобы kubernetes назначал их в свободные ноды.

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

P.S. Здесь автор ведет переписку с читателями и комментаторами (на английском).
Подробнее..

Вышел минималистичный Linux-дистрибутив Bottlerocket для запуска контейнеров. Самое главное о нём

11.09.2020 12:04:11 | Автор: admin


Компания Amazon объявила о финальном релизе Bottlerocket специализированного дистрибутива для запуска контейнеров и эффективного управления ими.

Bottlerocket (кстати, так называют мелкие самодельные ракеты на дымном порохе) не первая ОС для контейнеров, но вполне вероятно, что она получит широкое распространение благодаря дефолтной интеграции с сервисами AWS. Хотя система ориентирована на облако Amazon, открытый исходный код позволяет собрать её где угодно: локально на сервере, на Raspberry Pi, в любом конкурирующем облаке и даже в среде без контейнеров.

Это вполне достойная замена дистрибутиву CoreOS, который похоронила Red Hat.

Вообще, у подразделения Amazon Web Services уже есть Amazon Linux, который недавно вышел во второй версии: это дистрибутив общего назначения, который можно запустить в контейнере Docker или с гипервизорами Linux KVM, Microsoft Hyper-V и VMware ESXi. Он был оптимизирован для работы в облаке AWS, но с выходом Bottlerocket всем рекомендуется сделать апгрейд на новую систему, которая более безопасная, современная и потребляет меньше ресурсов.

AWS анонсировала Bottlerocket в марте 2020 года. Она сразу признала, что это не первый Linux для контейнеров, упомянув в качестве источников вдохновения CoreOS, Rancher OS и Project Atomic. Разработчики написали, что операционная система является результатом уроков, которые мы извлекли за долгое время работы производственных служб в масштабе Amazon, и с учётом опыта, который мы получили за последние шесть лет о том, как запускать контейнеры.

Экстремальный минимализм


Linux очищен от всего, что не нужно для запуска контейнеров. Такой дизайн, по словам компании, сокращает поверхность атаки.

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

Amazon также удалила все оболочки и интерпретаторы, исключив риск их использования или случайного повышения привилегий пользователями. В базовом образе ради минимализма и безопасности отсутствует командная оболочка, сервер SSH и интерпретируемые языки типа Python. Инструменты для администратора вынесены в отдельный служебный контейнер, который отключён по умолчанию.

Управление системой предусмотрено двумя способами: через API и оркестровку.

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

Фреймворк TUF (The Update Framework) загружает обновления на основе образов в альтернативные или размонтированные разделы. Под систему выделяется два дисковых раздела, один из которых содержит активную систему, а на второй копируется обновление. При этом корневой раздел монтируется в режиме только для чтения, а раздел /etc монтируется с файловой системой в оперативной памяти tmpfs и восстанавливает исходное состояние после перезапуска. Прямое изменение конфигурационных файлов в /etc не поддерживается: для сохранения настроек следует использовать API или выносить функциональность в отдельные контейнеры.


Схема обновления через API

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


Контейнеры создаются штатными механизмами ядра Linux cgroups, пространства имён и seccomp, а в качестве системы принудительного контроля доступа, то есть для дополнительной изоляции используется SELinux в режиме "enforcing".

По умолчанию включены политики для разделения ресурсов между контейнерами и ядром. Бинарники защищены флагами, чтобы пользователи или программы не могли их выполнять. И если кто-то добрался до файловой системы, Bottlerocket предлагает инструмент для проверки и отслеживания любых внесённых изменений.

Режим проверенной загрузки реализован через функцию device-mapper-verity (dm-verity), которая проверяет целостность корневого раздела во время загрузки. AWS описывает dm-verity как функцию ядра Linux, обеспечивающую проверку целостности, чтобы предотвратить работу зловредов в ОС, таких как перезапись основного системного программного обеспечения.

Также в системе присутствует фильтр eBPF (extended BPF, разработка Алексея Старовойтова), который позволяет заменять модули ядра более безопасными программами BPF для низкоуровневых системных операций.

Модель выполнения Задаётся пользователем Компиляция Безопасность Режим сбоя Доступ к ресурсам
Юзер задача да любая права пользователей прерывание выполнения системный вызов, fault
Ядро задача нет статическая нет паника ядра прямой
BPF событие да JIT, CO-RE верификация, JIT сообщение об ошибке ограниченные хелперы

Отличие BPF от обычного кода уровня пользователя или ядра, источник

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

Для системных администраторов предусмотрен контейнер администратора. Но AWS не думает, что админу часто придется работать внутри Bottlerocket: Акт входа в отдельный инстанс Bottlerocket предназначен для нечастых операций: расширенной отладки и устранения неполадок, пишут разработчики.

Язык Rust


Инструментарий ОС поверх ядра в основном написан на Rust. Этот язык по своей природе уменьшает вероятность небезопасного доступа к памяти, а также устраняет условия гонки между потоками.

При сборке по умолчанию применяются флаги --enable-default-pie и --enable-default-ssp для включения рандомизации адресного пространства исполняемых файлов (position-independent executable, PIE) и защиты от переполнения стека.

Для пакетов на C/C++ дополнительно включаются флаги -Wall, -Werror=format-security, -Wp,-D_FORTIFY_SOURCE=2, -Wp,-D_GLIBCXX_ASSERTIONS и -fstack-clash-protection.

Кроме Rust и C/C++, некоторые пакеты написаны на языке Go.

Интеграция с сервисами AWS


Отличие от аналогичных контейнерных операционных систем заключается в том, что Amazon оптимизировала Bottlerocket для работы на AWS и интеграции с другими сервисами AWS.

Самым популярным оркестратором контейнеров является Kubernetes, поэтому AWS внедрила интеграцию с собственным Enterprise Kubernetes Service (EKS). Инструменты для оркестровки идут в отдельном управляющем контейнере bottlerocket-control-container, который включён по умолчанию и управляется через API и AWS SSM Agent.

Будет интересно посмотреть, взлетит ли Bottlerocket, учитывая провал некоторых подобных инициатив в прошлом. Например, PhotonOS от Vmware оказалась невостребованной, а RedHat купила CoreOS и закрыла проект, который считался пионером в данной области.

Интеграция Bottlerocket в сервисы AWS делает эту систему в своём роде уникальной. Возможно, это главная причина, почему некоторые пользователи могут предпочесть Bottlerocket другим дистрибутивам, таким как CoreOS или Alpine. Система изначально спроектирована для работы с EKS и ECS, но повторим, что это не обязательно. Во-первых, Bottlerocket можно собрать самостоятельно и использовать, например, как hosted-решение. Во-вторых, пользователи EKS и ECS по-прежнему сохранят возможность выбора ОС.

Исходный код Bottlerocket опубликован на GitHub под лицензией Apache 2.0. Разработчики уже реагируют на баг-репорты и запросы фич.



На правах рекламы


VDSina предлагает VDS с посуточной оплатой. Возможно установить любую операционную систему, в том числе из своего образа. Каждый сервер подключён к интернет-каналу в 500 Мегабит и бесплатно защищён от DDoS-атак!

Подробнее..

Наиболее интересные факты о Ceph по результатам опроса пользователей в 2019 году

23.09.2020 00:14:39 | Автор: admin
TL;DR: наиболее интересные факты о Ceph в таблицах и графиках, полученных из результатов опроса пользователей Ceph в 2019 году.



Какой у вас тип организации?


Ответили на вопрос: 405
Пропустили вопрос: 0
Ответ Ответили %
Коммерческая 257 63.46
Правительственная 19 4.69
Военная 0 0
Образовательная 57 14.07
Некоммерческая 16 3.95
Другая 56 13.82



Почему используете Ceph?


Ответили на вопрос: 405
Пропустили вопрос: 0
Ответ Ответили %
Открытый исходный код 367 90.62
Масштабируемость 326 80.49
Стоимость 247 60.99
Функционал 223 55.06
Отказоустойчивость 309 76.30
Надежность\живучесть\целостность данных 279 68.89
Производительность 125 30.86
Возможность внедрения со смежными технологиями 120 29.63
Другое 13 3.21



Как давно используете Ceph?


Ответили на вопрос: 405
Пропустили вопрос: 0
Ответ Ответили %
Менее года 72 17.78
1-2 года 65 16.05
2-5 лет 203 50.12
Более 5 лет 58 14.32
Не пользуюсь 7 1.73



В каких странах у вас развернут Ceph?


Ответили на вопрос: 405
Пропустили вопрос: 0
Ответ Ответили %
Другая 249 61.48
США 87 21.48
Германия 73 18.02
Китай 30 7.41
Великобритания 29 7.16
Россия 26 6.42
Франция 20 4.94



Как устанавливаете Ceph?


Ответили на вопрос: 344
Пропустили вопрос: 61
Ответ Ответили %
Пакеты от разработчиков 170 49.42
Пакеты из дистрибутивов 131 38.08
Пакеты от производителей 93 27.03
Собираем свои пакеты 26 7.56
Собираем свою версию 12 3.49



N.B. Если всё ещё немного плаваете в вопросах, как устанавливать и разворачивать правильно Ceph c 15 октября запускается курс по Ceph от практиков. Вы изначально получите системные знания по базовым понятиям и терминам, а по окончании курса научитесь полноценно устанавливать, настраивать и управлять Ceph.


Как разворачиваете?


Ответили на вопрос: 312
Пропустили вопрос: 93
Ответ Ответили %
Ansible 134 42.95
ceph-deploy 133 42.63
Другое (тут Proxmox + CLI) 75 24.04



Применяемая операционная система


Ответили на вопрос: 344
Пропустили вопрос: 61
Ответ Ответили %
Ubuntu 131 38.08
Debian 101 29.36
CentOS 125 36.34
RHEL 34 9.88
SLES\OpenSuse 21 6.10
Другая 55 15.99



Какое оборудование применяете?


Ответили на вопрос: 343
Пропустили вопрос: 62
Ответ Ответили %
Supermicro 171 50.00
Dell 131 38.30
HPE 89 26.02
Другое 162 47.23



Какие накопители применяете?


Ответили на вопрос: 342
Пропустили вопрос: 63
Ответ Ответили %
HDD 305 89.18
SSD (SAS, SATA) 261 76.32
NVMe 161 47.08
Другие 21 6.14



Используете отдельную сеть для OSD?


Ответили на вопрос: 342
Пропустили вопрос: 63
Ответ Ответили %
Да 249 72.81
Нет 93 27.19


Какое ПО используете совместно с Ceph?


Ответили на вопрос: 340
Пропустили вопрос: 65
Ответ Ответили %
RBD на Linux серверах 123 36.18
Proxmox 114 33.53
KVM 105 30.88
OpenStack 97 28.53
Kubernetes 88 25.88
Другое 178 52.35



Для чего используете RBD?


Ответили на вопрос: 295
Пропустили вопрос: 110
Ответ Ответили %
Виртуализация 232 78.64
Резервное копирование 133 45.08
Облака 122 41.36
Контейнеры 117 39.66
Архивное хранилище 94 31.86



Для чего используете RGW?


Ответили на вопрос: 163
Пропустили вопрос: 242
Ответ Ответили %
Архивное хранение 105 64.42
Резервное копирование 92 56.44
Big data и аналитика 61 37.42



Для чего используете CephFS?


Ответили на вопрос: 184
Пропустили вопрос: 221
Ответ Ответили %
NAS общего назначения 98 53.26
Резервное копирование 87 47.28
Домашние каталоги 63 34.24
Архивное хранение 54 29.35
Media\Streaming 44 23.91



Какой мониторинг используете?


Ответили на вопрос: 312
Пропустили вопрос: 93
Ответ Ответили %
Ceph Dashboard 170 54.49
Grafana (свои настройки) 135 43.27
Prometheus 126 40.38
Proxmox 91 29.17
Zabbix 60 19.23
Nagios\Icinga 54 17.31



N.B. Если есть необходимость подтянуть, а то и научиться правильному логированию и мониторингу, добро пожаловать на курс Мониторинг и логирование инфраструктуры в Kubernetes. Сейчас можно приобрести курс с существенной скидкой. На курсе узнаете Узнаете, что именно мониторить, какие метрики собирать и как настраивать алерты для оперативного поиска и устранения проблем в кластере. Какие метрики стоит собирать с помощью Prometheus? Как визуализировать мониторинг с помощью Grafana и как грамотно настроить алерты?
Данные для графиков взяты отсюда.
Подробнее..

Перевод Пять промахов при развертывании первого приложения на Kubernetes

23.09.2020 18:06:49 | Автор: admin
Fail by Aris-Dreamer

Многие считают, что достаточно перенести приложение на Kubernetes (либо с помощью Helm, либо вручную) и будет счастье. Но не всё так просто.

Команда Mail.ru Cloud Solutions перевела статью DevOps-инженера Джулиана Гинди. Он рассказывает, с какими подводными камнями его компания столкнулась в процессе миграции, чтобы вы не наступали на те же грабли.

Шаг первый: настройка запросов пода и лимитов


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

Запросы пода (pod requests) это основное значение, используемое планировщиком для оптимального размещения пода.

Из документации Kubernetes: на шаге фильтрации определяется набор узлов, где можно запланировать под. Например, фильтр PodFitsResources проверяет, достаточно ли на узле ресурсов для удовлетворения конкретных запросов пода на ресурсы.

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

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

Лимиты пода (pod limits) это более четкое ограничение для пода. Оно представляет собой максимальный объем ресурсов, который кластер выделит контейнеру.

Опять же, из официальной документации: если для контейнера установлен лимит памяти 4 ГиБ, то kubelet (и среда выполнения контейнера) введет его принудительно. Среда выполнения не позволяет контейнеру использовать больше заданного лимита ресурсов. Например, когда процесс в контейнере пытается использовать больше допустимого объема памяти, ядро системы завершает этот процесс с ошибкой out of memory (OOM).

Контейнер всегда может использовать больше ресурсов, чем указано в запросе на ресурсы, но никогда не может использовать больше, чем указано в ограничении. Это значение сложно установить правильно, но оно очень важно.

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

К сожалению, я не могу дать конкретные указания, какие значения устанавливать, но мы сами придерживаемся следующих правил:

  1. Используя инструмент нагрузочного тестирования, моделируем базовый уровень трафика и наблюдаем за использованием ресурсов пода (памяти и процессора).
  2. Устанавливаем запросы пода на произвольно низкое значение (с ограничением ресурсов примерно в 5 раз больше значения запросов) и наблюдаем. Когда запросы на слишком низком уровне, процесс не может начаться, что часто вызывает загадочные ошибки времени выполнения Go.

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

Представьте ситуацию, когда у вас легковесный веб-сервер с очень высоким ограничением ресурсов, например 4 ГБ памяти. Вероятно, этот процесс придется масштабировать горизонтально и каждый новый модуль придется планировать на узле с доступным объемом памяти не менее 4 ГБ. Если такого узла не существует, кластер должен ввести новый узел для обработки этого пода, что может занять некоторое время. Важно добиться минимальной разницы между запросами ресурсов и лимитами, чтобы обеспечить быстрое и плавное масштабирование.

Шаг второй: настройка тестов Liveness и Readiness


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

Liveness показывает, работает ли контейнер. Если она выходит из строя, kubelet убивает контейнер, и для него включается политика перезапуска. Если контейнер не оснащен Liveness-пробой, то состоянием по умолчанию будет успех так говорится в документации Kubernetes.

Пробы Liveness должны быть дешевыми, то есть не потреблять много ресурсов, потому что они запускаются часто и должны информировать Kubernetes, что приложение запущено.

Если вы установите параметр для запуска каждую секунду, то это добавит 1 запрос в секунду, так что примите во внимание, что для обработки этого трафика понадобятся дополнительные ресурсы.

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

Мы настроили в приложениях конечную точку работоспособности, которая просто возвращает код ответа 200. Это показатель того, что процесс запущен и способен обрабатывать запросы (но еще не трафик).

Проба Readiness указывает, готов ли контейнер к обслуживанию запросов. Если проба готовности выходит из строя, контроллер конечных точек удаляет IP-адрес пода из конечных точек всех служб, соответствующих поду. Это также говорится в документации Kubernetes.

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

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

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

SELECT small_item FROM table LIMIT 1

Вот пример, как мы настраиваем эти два значения в Kubernetes:

livenessProbe:  httpGet:      path: /api/liveness       port: http readinessProbe:   httpGet:       path: /api/readiness       port: http  periodSeconds: 2

Можно добавить некоторые дополнительные параметры конфигурации:

  • initialDelaySeconds сколько секунд пройдет между запуском контейнера и началом запуска проб.
  • periodSeconds интервал ожидания между запусками проб.
  • timeoutSeconds количество секунд, по истечении которых под считается аварийным. Обычный тайм-аут.
  • failureThreshold количество отказов тестов, прежде чем в под будет отправлен сигнал перезапуска.
  • successThreshold количество успешных проб, прежде чем под переходит в состояние готовности (после сбоя, когда под запускается или восстанавливается).

Шаг третий: настройка дефолтных сетевых политик пода


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

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

Например, ниже приведена простая политика, которая запрещает весь входящий трафик для конкретного пространства имен:

---apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata:   name: default-deny-ingressspec:   podSelector: {}   policyTypes:     - Ingress

Визуализация этой конфигурации:


(http://personeltest.ru/aways/miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Более подробно здесь.

Шаг четвертый: нестандартное поведение с помощью хуков и init-контейнеров


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

Особые трудности возникли с Nginx. Мы заметили, что при последовательном развертывании этих подов активные соединения прерывались до успешного завершения.

После обширных изысканий в интернете выяснилось, что Kubernetes не ждет, пока соединения Nginx исчерпают себя, прежде чем завершить работу пода. С помощью pre-stop хука мы внедрили такую функциональность и полностью избавились от даунтайма:

lifecycle:  preStop:   exec:     command: ["/usr/local/bin/nginx-killer.sh"]

А вот nginx-killer.sh:

#!/bin/bashsleep 3PID=$(cat /run/nginx.pid)nginx -s quitwhile [ -d /proc/$PID ]; do   echo "Waiting while shutting down nginx..."   sleep 10done

Еще одна чрезвычайно полезная парадигма использование init-контейнеров для обработки запуска конкретных приложений. Это особенно полезно в случае, если у вас есть ресурсоемкий процесс миграции базы данных, который следует запустить до запуска приложения. Для этого процесса вы также можете указать более высокий лимит ресурсов, не устанавливая такой лимит для основного приложения.

Другой распространенной схемой является доступ к секретам в init-контейнере, который предоставляет эти учетные данные главному модулю, что предотвращает несанкционированный доступ к секретам из самого основного модуля приложения.

Как обычно, цитата из документации: init-контейнеры безопасно запускают пользовательский код или утилиты, которые иначе снизят безопасность образа контейнера приложения. Храня отдельно ненужные инструменты, вы ограничиваете поверхность атаки образа контейнера приложения.

Шаг пятый: настройка ядра


Напоследок расскажем о более продвинутой технике.

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

Однако Kubernetes позволяет запустить привилегированный контейнер, который изменяет параметры ядра только для конкретного пода. Вот что мы использовали для изменения максимального количества открытых соединений:

initContainers:  - name: sysctl     image: alpine:3.10     securityContext:         privileged: true      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

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

В заключение


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

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

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

Всегда задавайте себе такие вопросы:

  1. Сколько ресурсов потребляют приложения и как изменится этот объем?
  2. Каковы реальные требования к масштабированию? Сколько трафика в среднем будет обрабатывать приложение? А как насчет пикового трафика?
  3. Как часто сервису потребуется горизонтальное масштабирование? Как быстро нужно вводить в строй новые поды, чтобы принимать трафик?
  4. Насколько корректно завершается работа подов? Нужно ли это вообще? Можно ли добиться развертывания без даунтайма?
  5. Как минимизировать риски для безопасности и ограничить ущерб от любых скомпрометированных подов? Есть ли у каких-то сервисов разрешения или доступы, которые им не требуются?

Kubernetes предоставляет невероятную платформу, которая позволяет использовать лучшие практики для развертывания тысяч сервисов в кластере. Тем не менее все приложения разные. Иногда внедрение требует немного больше работы.

К счастью, Kubernetes предоставляет необходимые настройки для достижения всех технических целей. Используя комбинацию запросов ресурсов и лимитов, проб Liveness и Readiness, init-контейнеров, сетевых политик и нестандартной настройки ядра, вы можете добиться высокой производительности наряду с отказоустойчивостью и быстрой масштабируемостью.

Что еще почитать:

  1. Лучшие практики и рекомендации для запуска контейнеров и Kubernetes в производственных средах.
  2. 90+ полезных инструментов для Kubernetes: развертывание, управление, мониторинг, безопасность и не только.
  3. Наш канал Вокруг Kubernetes в Телеграме.
Подробнее..

Контейнеризация понятным языком от самых азов до тонкостей работы с Kubernetes

27.11.2020 18:11:38 | Автор: admin


Чем контейнеры отличаются от виртуальных машин, почему Docker настолько популярен, что такое Kubernetes и в чём его преимущества и недостатки. В интервью АйТиБороде СТО Слёрма Марсель Ибраев и старший инженер Southbridge Николай Месропян рассказали о контейнеризации понятным языком. Мы перевели интервью в текст для тех, кому лень смотреть.
Мне не лень смотреть, мне лень читать


Разница между контейнеризацией и виртуализацией


Что такое виртуализация?


Виртуализация появилась как средство уплотнения окружений на одном и том же железе. Сначала программный продукт выполнялся на железном сервере. Потом, чтобы иметь возможность поселять в одно и то же железо больше клиентов, чтобы максимально полно утилизировать производительные мощности, придумали виртуализацию. Теперь на одном и том же железе можно держать несколько окружений. В зависимости от среды, опять же. Есть полностью проприетарные решения, такие как vmware vsphere, есть опенсорсные решения, как QEMU KVM, на основе которого Red Hat делает свой коммерческий гипервизор Red Hat Virtualization. На платформе Windows есть Hyper-V.


Благодаря виртуализации мы получаем возможность более полно утилизировать ресурсы железа. Но при этом речь идёт о полной изоляции: в виртуальной машине полностью изолированное ядро, все библиотеки, строго ограниченные ресурсы по процессору и памяти.


Ядра разделяются физически или можно виртуально разделить там одно физическое ядро на несколько при виртуализации?


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


Что такое контейнеры и контейнеризация, и чем отличаются?


Детали зависят от операционной системы, на которой выполняется контейнер, но вообще контейнер делит с хостом ядро, пространство памяти ядра, и своё у контейнера только пользовательское окружение. Первая широко распространенная технология контейнеризации в Linux это OpenVZ, которая потом превратилась в коммерческий продукт Virtuozzo. Мы много работали и работаем с OpenVZ. У нас клиентские окружения жили в контейнерах OpenVZ, пока мы не перешли на более современные технологии.


То есть контейнер от виртуальной машины отличается только тем, что в контейнере общее адресное пространство?


Нет. Виртуальная машина изолируется полностью средствами процессора (технологии Intel, AMD, VMX).


Контейнер работает на ядре хостовой операционной системы и использует для изоляции возможности не железа, а операционной системы, так называемое пространство имён. Если мы говорим о Docker, как о наиболее распространённой сейчас технологии виртуализации, используются так называемые cgroups в ядре Linux.


Контейнер это продолжение виртуализации? То есть это технология, которая является преемником виртуализации?


Нет. Они ни в коем случае не конкурируют. Они занимают совершенно разные ниши в использовании.


Тогда почему их постоянно сравнивают? И постоянно есть вопрос, что лучше виртуализация или контейнеризация?


С моей точки зрения сравнить контейнеризацию и виртуализацию нельзя. Это сравнение теплого с мягким.


Где лучше использовать виртуализацию, а где контейнеризацию? Для как разработчика нет разницы: и то, и то используется для развертывания приложений. Два-три приложения ты фигачишь контейнером. Ты можешь виртуальных машин столько создать, и в каждой из них запустить своё приложение. В чем разница для обычных девелоперов?


Для тебя виртуальная машина это обычная изолированная операционная система, целиком: своё ядро, свой init, systemd и так далее. Чем она отличается от контейнера с точки зрения потребления ресурсов? Тем, что она полностью занимает все ресурсы, под неё выделенные. То есть, есть механизмы, когда можно динамически, то есть в зависимости от потребления процессами внутри виртуальной машины, освобождать память на хосте или занимать её. Но это всё полумеры.


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


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


Возвращаясь к твоему первому вопросу, разница вот в чём. Допустим, если у тебя на хосте Linux или VMware, то виртуальная машина у тебя может быть Windows. Если у тебя в контейнере Linux, то у тебя и снаружи Linux. Потому что мы в первую очередь пользуемся для изоляции не средствами железа, не средствами гипервизора, а средствами операционной системы cgroups и namespace.


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


Виртуалка сама по себе большая, так как содержит целую операционную систему. Если нам нужно развернуть виртуальную машину, то нужно нести с собой и ядро, и всё пользовательское окружение, и какой-то запас места (потому что динамически оно с хостом шариться в общем случае не может). Я не видел линуксовую виртуальную машину весом меньше 10 Гб, и это без данных. Потом к ней еще нужно прицепить диски для данных, в зависимости от того, что будет внутри.


Если говорить о контейнерах, есть разные дистрибутивы, в том числе специально созданные для контейнеризации, тот же Alpine Linux, который в голом виде весит 20 или 50 Мб в зависимости от версии. То есть ничего не весит, собственно говоря.


Виртуалка тянет полностью всю операционку, а когда Docker создаешь, ты тянешь только какие-то небольшие пакеты?


Нет. Чтобы создать Docker-контейнер ты должен собрать образ. Ты берёшь какой-то базовый образ, тот же Alpine, CentOS или Ubuntu. В него с помощью специальных команд зашиваешь свое приложение и выгружаешь уже туда, где оно будет работать.


То есть все равно ты в контейнере используешь полноценную операционку? Вот тот же образ Alpine Linux.


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


Но потенциально ты можешь и полноценный Linux запустить в контейнере?


Потенциально да, можешь.


Но смысла в этом, наверное, нет.


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


Один контейнер? А это не слишком жирно использовать для одного приложения, ну пусть и урезанную, но операционную систему?


Когда нужна изоляция это не слишком жирно.


Понял. Есть ли какие-то еще инструменты, которые позволяют сделать что-то похожее на контейнеризацию, но не контейнеризация?


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


Почему Docker захватил весь рынок? Вот ты говорил, что было решение какое-то изначально в Linux?


Нет. Оно занимало и занимает совершенно другую нишу. Docker захватил весь рынок в первую очередь потому, что он первым начал использовать технологии namespace и cgroups для, так сказать, народа. Понизил порог вхождения в эти технологии до того, чтобы можно было выйти на широкий рынок, на широкого пользователя.


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


А как тут обстоит дело с дебагом твоего кода, логированием и всем остальным? Со стороны кажется, что это сложновато: нужно залезть внутрь какого-то контейнера, который представляет из себя урезанную операционку


Когда работаешь с контейнерами, в принципе не обязательно думать, что это операционка, не операционка. Там начинается другой мир. Да, к нему надо привыкнуть, но с дебагом, логированием проблем нет никаких, потому что хорошим тоном считается писать все логи в stdout/stderr контейнера, а не в файлики внутри него.


Docker-контейнер знаменит тем, что он одноразовый. Он запустился, а после того, как ты контейнер удаляешь, если ты специально никаких мер не предпринимал, чтобы сохранить в нём данные, у тебя всё удаляется. Поэтому все логи обычно пишут в stdout/stderr, средствами Docker или внешних утилит экспортируют их в ElasticSearch, ClickHouse или какие-то другие системы хранения логов и централизованно уже с ними работают. В первую очередь потому, что контейнеров много. Контейнеров в сетапах могут быть десятки, сотни, тысячи и десятки тысяч.


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


Что насчет контейнеризации в Windows? Насколько я помню, там если не всё очень плохо, то не всё так просто, как на Linux.


Там, действительно, очень сложно. Я ни в коем случае не Windows-админ, знаком поверхностно. Но насколько я знаю, нативная контейнеризация в Windows есть. Есть средства изоляции и по ресурсам, и по пространствам имен, сетевые пространства имен, для памяти, под файлы и так далее. То есть можно Windows запустить как контейнер Windows. Это Windows Server Containerization, если я не ошибаюсь (Windows-админы, не обессудьте).


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


Когда контейнер выполняется, он не представляет собой некий образ. Когда выполняется виртуальная машина это образ, внутри которого своя файловая система, свои разделы, где всё это нарезано и всё это варится. Когда выполняется контейнер, для операционной системы это просто набор ограничений. Когда мы смотрим на процесс виртуальной машины с хоста, мы видим один процесс. В винде это Microsoft Hyper-V, в Linux это QEMU KVM, в vSphere это тоже один процесс. Когда мы смотрим с хоста на контейнер, то видим дерево процессов.


Но почему мы образы передаем друг другу? Я приложение запаковываю в Docker, и мы девелоперы передаём друг другу образы.


Образ это то, из чего контейнер запускается. А с точки зрения хоста это дерево процессов, которые ограничены через встроенные средства ограничения, то есть через namespace и cgroups. Это я к тому, что Docker по своей сути линуксовый.


А почему нельзя было сделать универсальное решение, чтобы оно и для Linux, и для винды работала? Там нет общих API или в Linux есть что-то, чего нет


Ядро-то разное.


Архитектура разная, да?


Да, API Windows и API Linux это совершенно разные вещи. По той же причине нет нативного Docker для macOS. Потому что используются средства изоляции линуксового ядра.


Я думал, что ядра macOS и Linux очень похожи.


Нет. macOS больше UNIX-like, нежели Linux. Потому что, как известно GNU is not UNIX (рекурсивный акроним). А macOS внутри более, так сказать, близка к юниксам. И там нет таких механизмов, как в Linux. Они развиваются независимо.


Docker и для Windows, и для macOS это чужеродное тело, которое запускается в линуксовой виртуалке.


Получается, чтобы запустить контейнер, нужно запустить еще и виртуалку?


Мы запускаем линуксовую виртуалку, а уже в ней мы уже запускаем эти контейнеры. Docker Desktop скрывает от пользователя все сложные процессы, но внутри все равно остаются всякие. Ну не то, чтобы это очень неэффективно. Если вам нужно разрабатывать что-то под Docker, но у вас только Windows или только macOS, то это позволяет работать, да. Но в продакшене с нагрузками так ничего толком не запустишь.


Я понял, что ты в основном с Linux работаешь, но вдруг ты слышал про WSL (Windows Subsystem for Linux)?


Разумеется, я на OpenNET читаю об этом всём и удивляюсь.


А может ли эта штука запустить контейнеры нативно? Я просто не знаю, она тоже под виртуалкой?


Насколько я понимаю, WSL это Wine наоборот. То есть трансляция вызовов API в нативные для винды. Если у нас Wine это трансляция вызовов виндового API для ядра Linux, то WSL это наоборот. И поэтому средств изоляции там ядерных линуксовых нет. Поэтому увы, увы.


Про оркестрацию


Скажем, у нас микросервисная архитектура, и не одно приложение, а много всего: 10, 20, 40, 100 микросервисов. Руками их конфигурировать совсем не прикольно. Как с этим разбираются?


Да, это вполне типовая ситуация. Сейчас особенно, потому что стильно, модно, молодежно. Постепенно приложение обрастает логикой, микросервисов становится больше и больше. И одного Docker, и даже Docker Compose уже становится мало. Ну и плюс ко всему, наверное, еще хочется какую-то отказоустойчивость, чтобы это на нескольких серверах работало. Возможно, какой-то Service Discovery и прочее.


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



Марсель Ибраев, СТО Слёрм


Docker Compose не позволяет нам ничего делать? Ведь это тоже средство управления несколькими контейнерами.


Docker Compose, как минимум, не позволяет запускать проект на нескольких серверах. То есть это все равно все-таки история про одну ноду. Поэтому, да.


ОК. Что придумано? Что есть сейчас, чтобы это все делать?


Сразу нужно сказать, что инфраструктурный стандарт это всё-таки Kubernetes. Штука, которая в свое время была произведена в Google. И Google по зову сердца, по доброте своей решил поделиться с миром.


Есть ещё ряд решений, например, Docker Swarm, Mesos, OpenShift и другие, но всё-таки наиболее популярен и пользуется спросом Kubernetes. Компании, которые задумываются о том, что им нужен оркестратор, в первую очередь смотрят на Kubernetes.


Где обычно применяются оркестраторы, в частности Kubernetes?


Да, это очень важный вопрос. Дело в том, что Kubernetes все проблемы не решает. Компания работает, работает, у них всё плохо (как правило, плохо с процессами) они такие: Блин, Kubernetes классная штука. Давайте её себе поставим у нас всё сразу станет хорошо! Но становится сильно хуже.


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


Если вы небольшая компания или если у вас монолит, и вы такие: Сейчас мы его в куб засунем и все станет хорошо! Нет, не станет.


Kubernetes работает только с контейнерами Docker и с их оркестрацией?


Kubernetes работает с контейнерами, но не только с Docker. У Kubernetes есть такая штука, которая называется Container Runtime Interface. В принципе, все системы контейнеризации, которые сейчас есть и которые поддерживают Container Runtime Interface, могут работать с Kubernetes. Например, rkt.


Сейчас возникло движение энтузиастов, которые выкорчевывают Docker из Kubernetes и используют что-то другое. Потому что Docker тоже не без проблем. Главная проблема Docker это его демон, который имеет свойство зависать, особенно при большой нагрузке. Но зачем демон, если у нас уже есть Kubernetes, есть достаточно зрелая инфраструктура и нам надо просто какое-то место для запуска контейнеров.


Дополнительный демон по сути не нужен. В эту сторону движение сейчас активно идёт, но, я думаю, дойдёт не быстро. Устоявшееся мнение, что контейнеры равно Docker, будет держаться долго.


А что может быть использовано вместо Docker более оптимальным путем?


Оптимально пока сложно сказать, потому что у конкурентов Docker есть свои минусы. К примеру, containerd не имеет нормального средства управления им. К слову, с версии 1.11, кажется, под капотом Docker containerd и работает. По сути, сейчас Docker выполняет роль обёртки над containerd, а там containerd, а внутри ещё runC, если уж совсем углубляться.


Кто-то говорит про Podman: делайте просто алиас Podman Docker, и можно сразу работать. Но тоже есть свои нюансы, поэтому мы в том числе пока работаем с Docker.


Расскажи подробнее, как вообще Kubernetes работает? Что у него происходит под капотом? Для начала уточним, Kubernetes это сервис или это какое-то ПО, которое можно ставить на сервера, или это и то и то?


Ну это ПО, да. И при этом ПО, которое сейчас очень активно развивается и предоставляется облачными провайдерами как сервис. При этом ничто не мешает его поставить на железные серверы.


Всегда нужно держать в голове, что Kubernetes это в первую очередь оркестратор контейнеров. Когда вы это понимаете, то вы понимаете, для чего он нужен.


Kubernetes состоит из нескольких компонентов, которые выполняют каждый свою роль (подробно о них ещё поговорим). Из этого вытекает две особенности:


  1. Компоненты друг друга не аффектят. Если один упал, то все остальные могут продолжить работать.
  2. Эти компоненты работают по pull-модели. Нет никакого центрального компонента, командира, который всем раздаёт команды, а как сдох, так все не знают что делать. Каждый компонент выполняет свою часть работы: сделал, завершил. Если он умер, ну значит этот кусочек не выполнится.

Теперь по поводу самих компонентов. Основной компонент это API-сервер. Это просто апишка, REST API (разработчики понимают, о чём речь): управление с помощью http-запросов, версионирование кстати тоже. Очень важно, что там есть версия API, при обновлении мы можем на эти версии API завязываться и за счёт этого обновляться менее болезненно. Есть API, с которым работают все: и клиент (мы, как оператор кластера), и компоненты остальные в том числе.


API-сервер работает с хранилищем, которое представляет из себя просто etcd. Etcd это key-value хранилище, то есть ключ-значение. Вот и API-сервер это единственный компонент, который с этим хранилищем взаимодействует.


Это какая-то разработка команды kubernetes?


Нет, это отдельная штука, очень древняя.


А почему её у Redis нет, например?


У Redis есть проблемы с многопоточностью, есть проблемы с кластеризацией (хотя они ее постепенно решают). А etcd штука древняя, все детские болезни там уже вылечены, и она достаточно такая отказоустойчивая.


Кстати, это хороший показатель, что если разработчики Kubernetes уже начиная с первых версий используют etcd, то, наверное, у себя его тоже можно использовать как key-value в кластер-режиме.


API-сервер единственный, кто с etcd работает, он записывает, считывает информацию. А в etcd у нас хранится всё. Там наши настройки кластера, манифесты всё, что мы делаем.


Мы как клиент хотим что-то создать, запустить приложение, и мы эту информацию передаем в API-сервер. Мы непосредственно это не делаем, конечно, там есть такая утилита, которая называется kubectl. С её помощью мы управляем всем кластером, делаем все операции, в том числе и запускаем приложения. Передаем yaml-манифест, где у нас в декларативном формате описано, как должно выглядеть приложение в кластере. Вот мы это передаем. Оно сохраняется в etcd и следующие компоненты постоянно смотрят в API-сервер.


Если немного углубиться, там есть подписка на событие и они по сути watch'ат. То есть никакого DDoS'а самого себя там нет. Следующий компонент, который берёт эту историю в работу это kube-controller-manager. По сути, мозг кластера Kubernetes. В него вшиты множество контроллеров: node-controller, endpoint-controller. Практически у всех абстракций, которые есть в Kubernetes, есть контроллер, и он вшит в этот бинарь. Эти контроллеры занимаются просто контролем вот этой абстракции: смотрят, есть ли новые, нужно ли что-то удалить и так далее.


Давай на примере. Если продолжать говорить о приложении, то контроллер, который отвечает за какое-то конкретное приложение, точнее за его манифест, за его абстракцию он видит, что мы что-то хотим создать, запустить. И он выполняет соответствующую работу, а именно дописывает манифест в etcd, обновляет информацию. Тут, конечно, без некоторого углубления нормально не объяснишь. Но есть такая абстракция, которая называется ReplicaSet. Она позволяет запускать приложение в нескольких инстансах. Через нее мы можем увеличивать, уменьшать количество реплик. И все здорово.


Это балансировка нагрузки?


Это просто контроль за количеством инстансов одного и того же приложения.


А зачем?


Чтобы иметь возможность в случае чего скейлить свое приложение или скейлить обратно. То есть хотим в три инстанса реплики просто пишем три у нас три инстанса.


Ну это очень похоже на балансировку нагрузок.


Балансировкой уже занимается другая абстракция, которая уже трафик распределяет на вот эти три инстанса.


То есть они в принципе могут в паре работать?


Да. Они в паре и работают.


ReplicaSet не только создаёт реплики, она ещё и следит, чтобы их действительно было три. Причем не больше, не меньше.


Инстансы, которые запускает ReplicaSet, называются подами. В подах и работает наше приложение (про поды мы ещё поговорим).


И вот как раз, когда мы создаем, например, ReplicaSet, у нас есть такой ReplicaSet controller в этом контроллер-менеджере, который описание подов для ReplicaSet генерирует, и туда же, грубо говоря, в etcd через API-сервер скидывает.


Потом подключается следующий компонент. После того, как мы поняли, какое приложение нам нужно из скольких инстансов запускать, оно вот в etcd хранится этот манифест. Далее у нас идет такой компонент, который называется scheduler. Его роль достаточно проста. Он решает, на каких серверах это приложение надо запускать. Но делает это не просто так, у него есть свои алгоритмы принятия решения.


Ну в частности, он смотрит на ресурсы, то есть сколько ресурсов на ноде, если мы для этого приложения запрашиваем 1 ГБ ОЗУ, а на ноде только 512 свободны, он туда не отправляет.


Под приложением ты понимаешь Docker-контейнер с приложением?


Да, контейнер.


Контейнер с приложением каким-то.


Да.


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


Это сказывается на стабильности работы системы в целом? Правильно я понимаю, если у нас есть какой-то нестабильный сервак, который может там валиться очень часто, то у него будет меньше очков.


На стабильность он не смотрит. Он смотрит в первую очередь на ресурсы. Какой смысл отправлять приложение на запуск туда, где их недостаточно. Смотрит ещё на priority class это такая штука, с помощью которой мы можем задать приоритет.


Например, в кластере два окружения: продакшн и стейджинг. Конечно, продакшн более важен. И для них мы priority class выставляем высокий. Для стейджинга мы можем поставить поменьше. Когда происходит авария, Kubernetes понимает, что часть серверов отвалилась (за это будет отвечать Node Controller, который контролирует жизнь нод), он принимает решение, что надо те поды, которые там были, запустить в живых серверах. Scheduler будет запускать в первую очередь поды продакшена.


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


А если не хватит под продакшн места?


Если не хватит, ну сорян. Поды будут висеть в pending, то есть ждать, когда появятся ресурсы. И scheduler назначает Если на такой низкий уровень опуститься, то в манифесте пода есть специальное поле, которое называется nodeName имя ноды. И вот пока scheduler не принял решение оно пустое. Scheduler говорит, что вот этот под, вот это приложение нужно запускать там на Node 2, и он эту информацию передает, API-сервер это записывает в etcd и в это поле вносит это имя. А далее в работу вступает последний компонент всей этой схемы, который называется kubelet.


Kubelet это компонент своего рода "node agent", то есть агент, который запущен на всех серверах кластера. Он тоже постоянно в API-сервер смотрит. И он видит, что появился под, то есть приложение, у которого в поле имя сервера написано его имя, там, где он работает. Он такой: Ага! Значит его нужно у себя запустить! Он видит, что у него запущено, и что от него хотят. Он передает Docker API, из манифеста считывает, что там конкретно нужно запустить, и говорит Docker, какой контейнер нужно запустить.


Kubelet, получается, замена Docker демона?


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


Хелс-чек такой?


В том числе хелс-чек тоже делает Kubelet. И мы постоянно видим, какой статус у наших приложений в реальном времени. Вот то, что там сейчас пулинг образа идёт, что там сейчас она запускается, вот он на раннинг все хорошо, все запустилось. И вот только на этом этапе у нас физический запуск произошёл. То есть всё вот это это подготовка.


Ноды это всегда сервер или это может быть кластер серверов?


Ноды это место, где запущен Kubelet.


Это может быть виртуалка, как я понимаю?


Да, может быть виртуалка.


Ты сказал, что в результате этих действий мы получаем физически развёрнутое приложение. Kubelet посылает какие-то свои статусы, либо он просто stdout контейнера фигачит? К чему этот вопрос. Потому что, если у тебя приложение в stdout выдает логи, какой-то дебаг kubernetes как-то умеет это в одно место собирать и предоставлять в удобочитаемом виде, или это не его обязанность вообще?


В твоем вопросе, два вопроса скрыты. Статус самого контейнера (жив или не жив) берёт из Docker. Функционал приложения (работает ли оно) вот эти дебаг, логи, какие-то хелс-чеки это все тоже делает Kubelet, но для этого надо несколько строчек в манифест добавить и сказать, как именно проверять.


На данный момент поддерживается три возможности проверять приложение:


  1. http-get это http-запрос в контейнер на инпоинт, и мы видим, работает оно, не работает, отвечает, не отвечает. С 200 по 399 код это ок, если 301 даже редирект это ок. Если 404 это не ок. 500 тем более.
  2. exec мы внутрь контейнера делаем какой-то запрос, какую-то команду, проваливаемся. Например, select 1, проверяем, всё ли нормально с базой.
  3. tcp socket Kubelet просто проверяет доступные сокеты. Если все хорошо, то все хорошо.

Есть три типа проверки контейнеров: это liveness, readiness и startup пробы.


Liveness проба это контроль за жизнью приложения. Постоянно Kubelet смотрит, ходит и смотрит. Там гибкие настройки, можно написать, как часто ходить, как проверять и так далее.


Readiness проба проверяет, а готово ли приложение принимать трафик. Потому что разные истории могут быть, это могут быть разные инпоинты у приложения. Мы проверяем, работает ли приложение, готово ли оно принимать трафик.


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


На каком размере архитектуры может работать Kubernetes? Может ли он контролировать сразу миллион инстансов?


Насколько я помню из документации, это 5000+, что ли, нод. На одной ноде по умолчанию можно запустить 110 подов, 110 экземпляров приложения.


Под это экземпляр одного и того же приложения? Или могут быть два разных контейнера на одном серваке и это будет два разных пода?


Под это абстракция, в которой запускается приложение. Тут важно понять, что это не какая-то физическая оболочка, не какой-то процесс ещё один, это скорее именно абстракция, с которой работает Kubernetes.


Kubernetes не умеет работать с контейнерами. Мы не можем сказать: заскейль нам вот это приложение в трёх контейнерах. Мы можем только сказать: заскейль нам в трех подах. В поде может быть как один контейнер, так и несколько. То есть мы можем туда запихнуть, например, nginx и php-fpm в связке, и они будут скейлится по два контейнера в связке.


Но тут надо понимать, что хорошая практика засовывать в контейнер неделимые части приложения. Всё-таки, если 2-3 контейнера надо засовывать, то может, стоит ещё поиграться с логикой приложения. Обычно один контейнер засовывают и там есть еще второй, который сам запускается, это pause контейнер, который держит сетевой namespace, чтобы все контейнеры в поде были в одном namespace, и чтобы всё хорошо работало.


Это первая часть беседы. Во второй будет про хранение данных в Kubernetes и про Ansible. Не пропустите!


Вопросы задавал Лекс АйТиБорода iamitbeard

Подробнее..

Контейнеризация понятным языком хранение данных и безопасность в Kubernetes, зачем нужен Ansible

07.12.2020 16:06:42 | Автор: admin


В чём проблема с базами данных и как позаботиться о безопасности в Kubernetes? Как врубиться в Ansible? Ответы на эти и другие вопросы читайте в продолжении интервью Лекса АйТиБороды со старшим инженером Southbridge Николаем Месропяном и СТО Слёрма Марселем Ибраевым.


Прочесть первую часть
Посмотреть интервью целиком


На вопросы отвечает Марсель Ибраев


В чём проблема с базами данных в Kubernetes? Для меня как для разработчика проблем нет: зафигачил базу в Docker-контейнер и работаешь.


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


А может, Docker не нужен? Зачем мне Docker для базы?


И это тоже. Были даже такие разговоры, что если база данных удовлетворяет 12 факторам, то это обычное приложение, запускайте и всё, 2020 год на дворе. Зачем вы её выносите отдельно, туда-сюда.


Но 12 факторов это не совсем про базу данных, там больше про stateless говорится. У нас все-таки stateful приложение, то есть приложение, которое должно сохранять статус и результат работы. После перезапуска оно этот результат терять не должно, в отличие от stateless.


И вот именно здесь краеугольный камень. Чтобы научить Kubernetes сохранять статус приложения (базы данных, таблички вот это всё), нужно присыпать ещё несколько абстракций сверху: PersistentVolume, PersistentVolumeClaim, StorageClass не буду сильно углубляться. То есть просто нужно иметь в виду, что повышается планка работы с Kubernetes.
Нужен человек, который умеет настраивать кластер этой базы данных. Потому что Kubernetes не важно, что запускается в поде (Pod): stateless приложение или stateful приложение он с ними работает одинаково. Он не соберёт магическим образом кластер баз данных и не будет за ним следить. Вы напишете манифест с тремя инстансами базы данных, Kubernetes его запустит, и это будет три отдельных инстанса базы данных, которые будут не в кластере.


Но с приложениями он умеет это делать?


Тоже нет. По факту это как бы отдельные инстансы, отдельные реплики приложения. Они по идее самостоятельные.


То есть на уровне записи и чтения данных всё равно нужно будет абстракцию какую-то свою клепать, Kubernetes ни при чём?


Да, и поэтому здесь нужны компетенции какого-то DBA, который на монолитах это уже 1000 раз настраивал, и скажет: Да, без проблем! Настроим!.


Следующий вопрос возникает с запуском базы данных, когда нам нужно выставить request/limit. Request/limit это инструмент, который позволяет работать с ресурсами.


Request это запрос на ресурсы. Например, мы знаем, что этому приложению минимально нужно 200 МБ ОЗУ. Если будет меньше, оно просто крашнется и не запустится. Мы выставляем request 200.


limit максимальная планка. Мы говорим, что 512 максимум, а если выше, то оно явно потекло и надо с ним что-то делать.


А вот как это сделать для базы данных хрен его поймёшь! В понедельник мы работаем на типовой нагрузке, в пятницу пришла баба Люда из бухгалтерии и запустила выгрузку из 1С миллиона строк, и нагрузка в потолок.


На моей истории было дважды, когда приложение (не база данных, для базы данных это будет суперкритично) начало утилизировать очень много ресурсов, и на сервере ресурсов не хватило всё померло. Kubernetes умный, он всё понял: взял и перетащил это приложение на другой сервер, но потреблять много ресурсов оно не перестало. Новый сервер упал, и как домино всё сложилось. Может повезти, что это всё в цикл замкнётся и будет переезжать, но ничего хорошего всё равно не будет. А с базой данных это критично, потому что вам нужно сохранять свои данные.


Есть 4 способа, как это сделать:


  1. очень стабильно, но дорого,
  2. не очень производительно, но дёшево,
  3. более или менее по производительности, но без гарантии,
  4. и дёшево, и круто.

Очень стабильно, но дорого это когда мы покупаем энтерпрайз, какую-нибудь железку с поддержкой. Там стоит такая бандура, SaaS всякие, и все работает. Здорово, но не у всех есть такие возможности.


Не очень производительно, но дёшево это использование облачных сервисов, которые предоставляют диски. Но здесь нужно понимать, что у многих есть в инструкции (в том числе и в документации баз данных) такая сноска: Ребята, если вы будете использовать какие-нибудь диски Google или Digital Ocean, например, то поставьте галочку using local SSD, чтобы использовать локальные диски, а не какие-то там через сеть, и вот тогда всё с базой данных будет OK.


Более-менее по производительности, но без гарантии (этот вариант нативно поддерживает Kubernetes) это когда мы просто берём, грубо говоря, прокидываем диск сервера в контейнер. Мапим папочку прям. У нас на сервере могут быть SSD, и мы SSD внутрь контейнера и получаем. Минус в том, что мы тем самым прибиваем базу данных к этому серверу. Спрашивается, зачем мы запускаем её в Kubernetes, если что случись, она там и сдохнет. Ну и лежала бы на своём монолитовском сервере.


И дёшево, и круто это собственная СХД, которую построил и поддерживает какой-то бородатый админ. При этом он ещё должен вывести уровень производительности этой СХД на такой уровень, чтобы она сравнялась как-то с SSD. Первое, что приходит в голову, это Ceph. Но пока мы делали курс по Ceph, пообщались с теми, кто много с этой технологией работает, они сказали: Не, чувак, там столько костылей, что это всё равно придётся закрывать мощностями оборудования. То есть такого нет.


Вот такие варианты: даже не 2 стула, а 4 выбирай, куда садиться. Если у вас возникает вопрос: Могу ли я запустить базу данных в Kubernetes?, значит, конкретно вы не можете пока.


В Kubernetes есть чарты (charts) и хельм (Helm), что это такое?


Сначала расскажу, как возникла потребность в их создании. Вот у нас есть кластер, всё хорошо, но мы его поднимали не просто так, а чтобы запустить приложение.


Приложение может состоять из множества микросервисов, которые надо как-то деплоить в кластер. Конечно, мы можем каждый раз править манифесты руками. Но одно приложение может содержать от пяти и до бесконечности манифестов.


Манифесты это yaml-файлики, которые отвечают за самые разные абстракции. В том числе сам под (Pod), хотя это будет не под, а абстракция, которая называется либо Deployment (именно в ней запускают приложение), либо StatefulSet (если это stateful-приложение), либо DaemonSet, ну неважно. К нему добавляется абстракция типа Service внутренний кубернетский балансировщик. К нему же добавится абстракция Ingress внешний балансировщик. Там же ещё могут быть PV/PVC короче, много всего.


Когда мы приложение разрабатываем, нам надо сделать какой-то релиз. Что-то поменялось в приложении, что-то поменяться могло в манифестах. Например, версия image была 2, стала 3. Нам придётся каждый раз ходить в репы и менять руками 2 на 3. Таких реп может быть сотни, и осталось только не забыть везде нужное поменять. В этот момент типичный админ задумывается об инструментах, которые позволяют менять все файлы внутри папки с одного паттерна на другой. И в принципе, это будет работать.


Только проблема в том, что это решение kubectl based, то есть всё равно весь вот этот вывод, который мы в папке перелопатили, мы через пайп отправляем в kubectl. Говорим: kubectl, вот теперь всё, что у нас есть новое в папке, отправляй в кластер. И это тоже будет работать. Но если что-то пойдёт не так (циферку не ту поставили или приложение просто не поднялось), нативного способа откатиться через kubectl нет. Там есть команда kubectl rollout undo, но она работает только с deployment, а у нас могут быть и другие абстракции, с которыми она не работает. И тут возникает вопрос, а как откатываться? Потому что шаблонизация вроде более-менее понятна, да и шаблонизаторов много всяких (и Kustomize, и json.net), но как откатываться большой вопрос.


Явный ответ на это дает Helm, потому что у него из коробки есть возможность cделать rollout, то есть откат, в случае, если какие-то есть проблемы. Работает он достаточно просто. Он все манифесты из папки, что мы нагенирировали, сохраняет в артефакт. Например, в config какой-то. Прям вот как оно есть, так и сохраняет подряд. Назвал его артефакт версия 1, сохранил в кластер. Потом мы выпустили новый релиз. Он ага, артефакт версия 2. И вот так они копятся. Потом мы выкатили новый релиз, какой-нибудь версии 9, и поняли, что это не то. Он говорит: Ок, у меня есть версия 8, достает все манифесты оттуда и фигачит в кластер.


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


Ты уже не раз говорил про stateful и stateless приложения. Разработчикам эти термины знакомы. И понятно, что со stateless просто: развернул и не паришься. Есть какие-то особенности разворачивания stateful приложений? И что для этого используется внутри Kubernetes? Какие-то может быть особенные штуки для хранения состояния приложения? Как оно хранится? Где оно хранится?


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


CSI Driver это ещё один интерфейс, который позволяет унифицировать подключение внешних инструментов. Как есть Container Runtime Interface, который не только Docker может подключить, но всё, что CRI поддерживает, так и CSI может подключить всё, что его поддерживает. Технология не так давно вышла в прод, развивается и допиливается, но в целом это хороший шаг. Остаётся подключить, например, Ceph как систему хранения данных и через абстракции Kubernetes запрашивать диски или файловые системы в этой СХД и подключать себе. То есть по сути приложение будет работать с этой СХД, а Kubernetes будет в роли посредника.


А до появления этого механизма в Kubernetes нельзя было разворачивать stateful приложения?


Можно было. Есть такая штука, как storage class. Это ещё одна абстракция, и там это всё настраивается. Скажем так, объём тех решений, которые можно применять в Kubernetes, будет расти. Уже есть всякие облачные хранилища, которые можно подключать, и множество всего другого.


Как построен механизм безопасности всего процесса поднятия, слежения за твоими приложениями? Общение наверняка ведётся по https, но есть ли ещё что-нибудь? Что, например, будет, если API Kubernetes завладеет злоумышленник? Как этого избежать?


Не открывать API наружу! Достаточно просто.


А как не открывать? Выстраивать приватную сеть?


Ну да, VPN, двухфакторная авторизация что-нибудь такое. API нужно закрывать очень хорошо, потому что каждый год находят уязвимости. В 2018 году, например, обнаружили критическую уязвимость с индексом 9,8, если я не путаю. Она позволяла поднять привилегии в API, и всё делай, что хочешь. Были уязвимости в 2019 году. Мне запомнилась возможность подменять файлы на хосте через контейнер.


И всё это можно сделать, если авторизоваться в API. Естественно, API не дураки писали, и там есть аутентификация, тот же самый TLS, просто так ничего не сделаешь. Но уязвимости бывают, поэтому API надо всеми возможными способами закрывать.


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


Например?


Глобально можно посмотреть в первую очередь как раз с точки зрения уязвимостей, получения прав и так далее. Решение закрытие API. Если Kubernetes развёрнут не в облаках, а на своих виртуалках, то надо регулярно обновлять свои сервера. Про iptables и SSH наружу, я думаю, говорить не надо. Это общие и достаточно банальные требования, но их надо выполнять. Kubernetes не панацея, это дополнительный слой абстракции. Сверху намазали, и за этим теперь тоже надо как-то следить.


Второй аспект безопасности это уровень привилегий пользователей. В Kubernetes это хорошо сделано. Есть так называемый Role Based Access Control или RBAC сокращенно. Мы можем разграничить пользователей по тому, какие действия им доступны: ты можешь работать только здесь, а ты только здесь. Но там есть достаточно простые хаки, которые позволяют это всё очень легко обойти.


Например, в Kubernetes есть namespace пространство имен. Условно, у нас есть Kubernetes, есть два namespace: стейджинг и продакшн. В стейджинг мы пускаем разработчиков, они там работают, но мы им не даем что-то править на проде. В прод ходит только CI/CD. Но просто имея возможность запускать свои приложения хоть где и ничего больше, я могу за 2 минуты стать админом кластера. Я запущу под (Pod), которого отправлю на запуск на master, где у нас находятся все доступы и креды, и подмонтирую их себе в контейнер. Потом я посмотрю токены, скопирую себе на компьютер и стану админом.


Это такая штука, о которой не все задумываются. Но эту штуку тоже уже прикрыли. Есть инструмент Pod security policy, который позволяет чётко сказать, какие поды и где и как могут запускаться, какие порты могут использовать.


Ещё один аспект безопасности касается работы нескольких команд в Kubernetes. Особенно, если есть на аутсорсе команды, которые пишут свою часть приложения. По умолчанию, любой человек с доступом в namespace может постучаться во все эндпоинты (endpoints), которые есть. Прикрыть это позволяет Network policies. Это своего рода firewall внутри кластера. Когда мы внешнюю команду к себе зовём, мы спрашиваем, к каким эндпоинтам она должна иметь доступ, чтобы взаимодействовать с приложением, и только эту часть открываем. В основной кластер они зайти не могут.


Если говорить про безопасность, то в Kubernetes есть ещё одна интересная штука защита от человеческого фактора. Известно много громких факапов, произошедших по вине человека (история с GitLab, когда они положили пол своего продакшена, с Yandex.Cloud и другими компаниями). Прод базу данных снёс и оказалось, что в шести местах бэкапов нет или они неконсистентные, и случайным образом где-то там дампик лежал несколько часовой давности и типа: Ух! (перекрестился) Слава Богу! Восстановили!


Чтобы этого избежать, в Kubernetes тоже есть инструменты. Например, limit ranges, resource quotas. Они позволяют держать в узде тех, кто работает с Kubernetes. Например, не дают заскейлить свое приложение в какое-то очень большое количество. А такое один раз было. Коллега рассказывал: админ хотел поставить единицу. Клавиша залипла, интернет лагнул, вместо одного раза он нажал несколько и всё. Правда, это было не на Kubernetes, а на Nomad. Но факт такой, что он: Вам надо 111 млн приложений? Хорошо. Сейчас будет! И всё начало лагать, естественно. Чтобы этого избежать, мы можем сказать: Здесь максимум 10 реплик, и всё.


Ты упомянул Nomad. Какие ещё есть аналоги Kubernetes?


Самый яркий аналог, про который все говорят, это Docker Swarm. Docker Swarm это разработка от Docker, их нативное решение. Не требует никакой дополнительной установки: переходим в swarm-режим, и у нас появляется возможность Docker Swarm.


Docker Swarm это тоже оркестратор, поэтому многие сравнивают его с Kubernetes. Но если говорить глобально, то Kubernetes это более мощный комбайн, в нём больше возможностей. В Docker Swarm их чуть поменьше, но я бы сказал так: если у вас супертребований нет, то можно пользоваться и Docker Swarm. Но в отличие от него, Kubernetes, например, хорошо умеет автоскейлить. Причем, как ноды автоскейлить, заказывать, так и поды, то есть реплики приложений. В облаках это работает вообще шикарно. Пришла нагрузка с рекламной кампании мы не сидим, не заказываем руками, а он сам сходил и заказал. Когда нагрузка ушла, он обратно их схлопнул до нужного количества.


На каких операционных системах обычно запускается Kubernetes?


Ну обычно на Linux: CentOS, Debian.


А на винде умеет?


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


Какими навыками нужно обладать, чтобы успешно работать с Kubernetes? Программирование, может, какое-то нужно? YAML, понятно.


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


Я бы добавил, что нужны базовые знания по сетям, по Linux (если Kubernetes будет запускаться на Linux-машинах, их надо будет тюнить) и умение читать документацию.


В каких случаях Kubernetes вообще не стоит использовать?


Не стоит использовать, если рассчитываете, что поставите его, и все вопросы решатся. К внедрению надо подходить системно, и ставить Kubernetes только тогда, когда есть реальная необходимость.


Есть ещё ситуация Kubernetes ради зарплаты. Это когда технический специалист зашёл на Хедхантер, посмотрел на зарплаты, и пошёл пропихивать это решение в компании, чтобы потом попросить повышение: Я же теперь специалист по Kubernetes, платите мне больше. Это вряд ли к чему-то хорошему приведёт.


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


В идеальном мире, конечно, Kubernetes ставится сразу же Сначала приложение готовится, потом уже туда. Но чаще всего нет.


Переходя к следующему блоку, спрошу: в чём различие Kubernetes и Ansible?


Ansible это, в первую очередь, система управления конфигурацией. Kubernetes это оркестратор контейнеров.
Знание Ansible не будет лишним, когда вы начнёте работать с Kubernetes, и наоборот. Потому что Kubernetes нужно как-то управлять, и Ansible для этого хорошо подходит.
Если мы говорим, что Kubernetes это Infrastructure as code (IaC), то вот этим IaC нужно как-то управлять. Руками туда лезть? В идеальном мире не лезут, всё прогоняют через пайплайны: надо что-то сделать в Kubernetes запускаешь плейбук он пошёл сам сделал. Поэтому знание Ansible и Kubernetes, наверное, важны в равной степени.


Далее отвечает Николай Месропян


Что такое Ansible и для чего он нужен?


Это средство управления конфигурациями. Для чего нужно? Когда у вас один сервер, им можно управлять руками: просто ходить на него, набирать команды или тыкать мышкой, что-то руками править, настраивать. Если их 2, 10 это ещё реально. Но в конце концов серверов может стать и 20, и 100, и 1000. Вот тогда возникает необходимость управлять ими более автоматизированно и централизованно.



Мы говорим про управление конфигурациями операционной системы или вообще любыми? Я задеплоил 25 приложений разных на хост, и я хочу


Обычно я начинаю рассказ про Ansible и свой курс с концепции, которая называются питомцы и стадо Pets vs Cattle. Pets это питомцы. Ты за ними ухаживаешь, лечишь, даёшь им уникальные имена. Cattle это стадо. Если кто-то заболевает, ты его забиваешь и создаешь вместо него новый сервер. У тебя их очень много, и ты их в лицо не знаешь, а знаешь только по номерам. Питомцы превращаются в стадо, когда их становится много.


Но когда они заболевать начинают!


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


Jenkins вроде как тоже кроме того, что поддерживает пайплайны, умеет конфигурировать приложения. Это в чём-то похоже на Ansible?


Нет, это совершенно разные вещи. Ansible похож на Chef, Puppet, SaltStack. Jenkins больше похож на GitLab CI и TeamCity. Это совершенно разные классы программного обеспечения для разных целей, но они могут работать вместе.


Я встречался с решениями, когда через пайплайн на GitLab CI производился деплой приложений посредством приложения, использующего внутри себя Chef.
Сам я писал пайплайны для GitLab CI на Ansible. То есть они вместе работать могут. Иногда и просто должны.


Из чего состоит Ansible, и как он вообще работает? Это какое-то приложение над приложениями или отдельный сервер, что это вообще?


Cначала расскажу про ПО для управления конфигурациями. Расскажу, чем они друг от друга отличаются, и потом перейдём конкретно к Ansible.


ПО для управления конфигурациями можно разделить на два основных класса: pull и push.
Когда работает система принципа pull, на клиентских хостах, то есть на управляемых хостах, выполняется некий daemon (сервис), который подтягивает конфигурацию с центрального репозитория и на месте уже её раскатывает.


push это когда управляющий процесс выполняется на одном центральном сервере, а на конечные ноды доезжают только конечные изменения (по сети). То есть конфигурирование производится не на месте, а по сети.


По сети SSH, FTP?


Как правило, SSH. Вот Ansible относится именно к push. На центральном сервере выполняется процесс, который использует конфигурацию, чаще всего хранящуюся в репозитории.


Зачем вообще нужен pull, если push выглядит достаточно красиво и ненапряжно?


Push медленный.


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


При модели push центральный процесс каждый раз идёт к ноде по SSH, что-то там делает, возвращает результат, здесь он оценивается, и всё это повторяется.


Тогда чем push лучше pull?


На конечных нодах не хранится никакой конфигурации, что называется нулевой footprint (след).


Бывает важно, чтобы на клиентских машинах никто не мог понять, как и что конфигурируется. Расскажу про нашу компанию, Southbridge. У нас аутсорсинг, множество клиентов, и информация под клиентов хранится в репозитории. Потому что мы не хотим, чтобы клиент мог своими руками что-то подкрутить и сломать. Он может сделать это без злого умысла, просто: Программист сказал, что можно сделать так, давай-ка сделаем! А потом раз продакшн снесся, и он приходит к нам: У вас все плохо работает!


В действительности таких случаев у нас не было, когда мы использовали pull-системы. Но тем не менее. К тому же лучше подстраховаться, чтобы конфигурация одних клиентов не попала каким-то другим. Именно поэтому мы используем push.


Плюс нам понравилась сама парадигма, что всё лежит в одном месте. Сами клиентские процессы иногда написаны на тяжёлых языках (Ruby, например) и тоже подъедают системные ресурсы: и память, и процессор. Поэтому это в каких-то случаях может быть нежелательным.


А клиентские ПО, клиентские ресурсы, которые в push работают, они пишутся руками или есть готовые какие-то решения?


Вот теперь я могу рассказать, из чего Ansible состоит (без слайдов и доски сложно, но попробую объяснить на пальцах).


Для Ansible мы пишем конфигурацию, состоящую из сущности под названием плейбуки (playbook), ролей и инвентаря. Плейбуки и роли это определение конфигурации. Инвентарь это параметризация.


Плейбук это файл, состоящий из набора так называемых плеев (play).
Сразу скажу, что вся конфигурация пишется на YAML. Ansible написан на Python, поэтому для него родной язык разметки YAML.


Ansible, если что, лучше всего ставить в virtualenv, чтобы быть независимым от мейнтейнера
А, да, начнём с того, где Ansible работает: управляющие хосты поддерживаются только Linux.


А если это виртуалка?


Всё равно. Всё-таки используются системные вызовы, и чтобы не адаптироваться, в качестве управляющего хоста поддерживается только Linux. Управлять можем Linux-системами, Windows-системами и Mac OS. Но для Mac, насколько я знаю, оно пока в зачаточном состоянии.


Android, мобильные платформы?


Наверняка что-то есть, но пока оно большого распространения не получило.


У нас выполняется центральный процесс, который этот плейбук читает.


Плейбук это набор плеев. Каждый плей состоит из двух элементов: описание хостов, на которых выполняется данный участок кода, и tasks задачи, то есть именно что делать.


Если совсем примитивно, с помощью Ansible можно сделать на хостах всё то же, что мы делаем руками, только автоматизировано, массово, детерминировано и т. д. Поэтому элементарный task или элементарная задача это выполнение какой-нибудь команды, например. Но чаще всего используются специализированные модули: для шаблонизации файлов, для запуска сервисо


А команды это просто команды консоли или?


Да, есть такой модуль command, который просто выполняет консольную команду.


Он на клиенте?


На клиенте, да.


А почему нельзя просто через SSH напрямую в терминал фигачить?


Модуль command это последний шанс, если у тебя нет специализированного модуля для выполнения определённой задачи.


Чтобы запустить сервис, мы не делаем command systemctl start engine, мы используем модуль service или модуль systemd, который приводит систему, в данном случае сервис, в заданное состояние. То есть он запущен и включён либо остановлен и выключен.


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


Понятно, в не зависимости от того, сколько раз ты запускаешь


Результат всегда один и тот же.


Это прям как в REST API, разработчикам должно быть понятно.


Да!


Поэтому мы описываем в задачах желаемую конфигурацию и Ansible приводит (если это грамотно написано) систему в заданное состояние. Если система уже в заданном состоянии, то действие не производится. Потом мы в выводе команды всё это видим.


Что такое роли? Роли можно уподобить подключаемым библиотекам. То есть мы пишем роль, которая, допустим, настраивает веб-сервер nginx, и потом из разных плейбуков для разных хостов с разными параметрами мы эту роль подключаем.


Роли могут храниться как на диске вместе с плейбуками конфигурации, так и во внешних репозиториях. Есть такая штука, называется Ansible Galaxy это что-то вроде центрального хранилища ролей. Туда можно свою роль подать на рассмотрение. Это не то чтобы пакетный менеджер, а как GitHub, допустим. Только специализированный, исключительно для Ansible.


То есть роли это готовые куски конфигураций, которые ты можешь у себя встраивать и переиспользовать?


Это описание конфигурации, которому ты можешь сам задавать разные параметры из инвентаря.


Отлично. И тут включается механизм параметризации?


Да. При запуске команды ansible-playbook, которая и делает нужные изменения на хостах, мы задаём ей параметром так называемый инвентарь откуда брать параметры. В инвентаре у нас чаще всего список хостов, объединенных в группы (чтобы Ansible знал, на каких хостах выполнять те или иные задачи), и набор переменных для них, которые подставляются в роль и меняют поведение в нужную сторону.


Jinja2 это из этой темы?


Jinja2 это весьма мощный язык шаблонизации. Jinja2 используется в ролях.


Это параметризация, как я понимаю, нет?


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


Как гарантировать, что твой конфиг работоспособный, что он не устарел, не сломался вдруг на сервере что-то поменялось, и у тебя команда ansible-play не сработала. Это как-то тестируется?


Да, разумеется, это тестируется.


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


Это подход инфраструктура как код (Infrastructure-as-Code, IaC). Код в каком смысле? Код должен проходить ревью, то есть твои коллеги должны видеть, что ты написал и подтвердить, что это вменяемо.


Речь про конфигурации?


Да. То есть точно также весь инфраструктурный код хранится в репозитории GitLab, GitHub чем вы там пользуетесь.


Рядом с продуктовым кодом? Или это отдельная репа?


Разумеется, отдельная. Это много реп чаще всего. И потом перед тем как что-то изменить, ты делаешь merge request с изменениями (в зависимости от того, какая схема у вас в компании принята). В простейшем случае пишешь в телегу коллеге: Вот, я написал, посмотри и кидаешь ссылку. Он говорит: Всё нормально, идём на тестирование.


Раз инфраструктура как код, у нас код проходит пайплайн. Всегда самый первый или второй job это lint. Команда ansible-lint просматривает код на синтаксические ошибки и дает дельные советы. Если ты ставишь пакет, вызывая его как command "yum" или "apt-get" install, то тебе линтер напишет: Используй специализированный модуль yum или apt. То есть тут не только синтаксические ошибки, но и семантика.


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


Главный вопрос: не на проде же ты это будешь делать?


Да, разумеется, запускается виртуалка, в ней согласно описанной для Molecule конфигурации запускается Ansible плейбук.


А где гарантия, что виртуалка похожа на прод?


Надо самому следить.


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


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


А по ресурсам она тоже должна быть такая же, как прод, или необязательно?


Это зависит от конкретного случая. Если после запуска Molecule ты тестируешь ещё что-то с помощью каких-то внешних инструментов Условно говоря, выкатываешь сайт, тестируешь Selenium (такое тоже бывает), то, естественно, ресурсов должно быть больше. Если он у тебя просто проходит набор ролей, то ресурсов нужно ровно столько, чтобы этот Python в память влез.


Тебе как человеку, который работает с Ansible, нужно программирование? Ты говорил, язык разметки YAML, написан на Python, нужно ли дописывать какие-то модули, ещё что-то. Просто, чтобы понимали девелоперы: нужно ли владеть языком при работе с Ansible?


Можно не владеть. Но лучше владеть.


Каким и для чего?


Python. Потому что, если под конкретную задачу нет нужного модуля, конечно, можно написать это всё в виде скрипта и дёргать его из task, но это плохой тон, это называется bashsible в комьюнити, связанных с Ansible. И это прям фу-фу.


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


Нативном для Ansible, да?


Да.


Потому что можно на C попробовать это сделать.


Ну, если затраченные усилия будут оправданы, почему бы и нет.


Какие есть более или менее популярные аналоги Ansible? Или это монополист на рынке?


Нет, это, конечно, не монополист. Значительно более старые решения (в смысле прошедшие более длинный путь развития) это Chef и CFEngine. Последний сейчас уже мало используется.


Они все работают примерно одинаково?


Они работают в основном как pull: на настраиваемой ноде выполняется сервис, который тянет конфигурацию.


Ansible единственный, кто делает push из крупных?


Ansible умеет pull, но это не основной его режим работы. SaltStack тоже умеет push, но это не его основной режим работы. Ansible, можно сказать, в этом плане единственный. Ну и видимо, народу нравится, потому что я смотрю время от времени Google Trends по запросам, относящимся к Chef, к Ansible, и вижу, что Ansible набирает всё большую популярность. Более старых решения на пятилетнем промежутке её чуть-чуть теряют. Хотя есть энтузиасты, которые за них топят и их поддерживают.


Может, есть какие-то книги, что можно почитать, чтобы базово познакомиться с Ansible?


Знакомство с Ansible лучше всего начать с документации. На docs.ansible.com расписаны и самые основы, и примеры простых плейбуков, и вообще всё что нужно.
Я еще скажу про книгу Ansible: Up and Running. Книга неплохая, но Ansible достаточно быстро развивается и поэтому многие вещи в ней уже устарели. Поэтому лучше всего начинать именно с документации на сайте, она там всегда актуальна.


Вопросы задавал Лекс АйТиБорода iamitbeard

Подробнее..

Перевод Недостающее введение в контейнеризацию

08.02.2021 08:23:59 | Автор: admin

Эта статья помогла мне немного углубится в устройство и принцип работы контейнеров. Поэтому решил ее перевести. "Экосистема контейнеров иногда может сбивать с толку, этот пост может помочь вам понять некоторые запутанные концепции Docker и контейнеров. Мы также увидим, как развивалась экосистема контейнеров". Статья 2019 года.

Docker - одна из самых известных платформ контейнеризации в настоящее время, она была выпущена в 2013 году. Однако использование изоляции и контейнеризации началось раньше. Давайте вернемся в 1979 год, когда мы начали использовать Chroot Jail, и посмотрим на самые известные технологии контейнеризации, появившиеся после. Это поможет нам понять новые концепции.

Все началось с того, что Chroot Jail и системный вызов Chroot были введены во время разработки версии 7 Unix в 1979 году. Chroot jail предназначен для Change Root и считается одной из первых технологий контейнеризации. Он позволяет изолировать процесс и его дочерние элементы от остальной части операционной системы. Единственная проблема с этой изоляцией заключается в том, что корневой процесс может легко выйти из chroot. В нем никогда не задумывались механизмы безопасности. FreeBSD Jail была представлена в ОС FreeBSD в 2000 году и была предназначена для обеспечения большей безопасности простой изоляции файлов Chroot. В отличие от Chroot, реализация FreeBSD также изолирует процессы и их действия от Файловой системы.

Chroot Jail. Источник: https://linuxhill.wordpress.com/2014/08/09/014-setting-up-a-chroot-jail-in-crunchbang-11debian-wheezyChroot Jail. Источник: https://linuxhill.wordpress.com/2014/08/09/014-setting-up-a-chroot-jail-in-crunchbang-11debian-wheezy

Когда в ядро Linux были добавлены возможности виртуализации на уровне операционной системы, в 2001 году был представлен Linux VServer, который использовал chroot-подобный механизм в сочетании с security contexts (контекстами безопасности), так и виртуализацию на уровне операционной системы. Он более продвинутый, чем простой chroot, и позволяет запускать несколько дистрибутивов Linux на одном VPS.

В феврале 2004 года Sun (позже приобретенная Oracle) выпустила (Oracle) Solaris Containers, реализацию Linux-Vserver для процессоров X86 и SPARC. Контейнер Solaris - это комбинация элементов управления ресурсами системы и разделения ресурсов, обеспечиваемых zone.

Подобно контейнерам Solaris, первая версия OpenVZ была представлена в 2005 году. OpenVZ, как и Linux-VServer, использует виртуализацию на уровне ОС и был принят многими хостинговыми компаниями для изоляции и продажи VPS. Виртуализация на уровне ОС имеет некоторые ограничения, поскольку контейнеры и хост используют одну и ту же архитектуру и версию ядра, недостаток возникает в ситуациях, когда гостям требуются версии ядра, отличные от версии на хосте. Linux-VServer и OpenVZ требуют патча ядра, чтобы добавить некоторые механизмы управления, используемые для создания изолированного контейнера. Патчи OpenVZ не были интегрированы в ядро.

В 2007 году Google выпустил CGroups - механизм, который ограничивает и изолирует использование ресурсов (ЦП, память, дисковый ввод-вывод, сеть и т. д.) для набора процессов. CGroups были, в отличие от ядра OpenVZ, встроены в ядро Linux в 2007 году.

В 2008 году была выпущена первая версия LXC (Linux Containers). LXC похож на OpenVZ, Solaris Containers и Linux-VServer, однако он использует CGroups, которые уже реализованы в ядре Linux. Затем в 2013 году компания CloudFoundry создала Warden - API для управления изолированными, эфемерными средами с контролируемыми ресурсами. В своих первых версиях Warden использовал LXC.

В 2013 году была представлена первая версия Docker. Он выполняет виртуализацию на уровне операционной системы, как и контейнеры OpenVZ и Solaris.

В 2014 году Google представил LMCTFY, версию стека контейнеров Google с открытым исходным кодом, которая предоставляет контейнеры для приложений Linux. Инженеры Google сотрудничают с Docker над libcontainer и переносят основные концепции и абстракции в libcontainer. Проект активно не развивается, и в будущем ядро этого проекта, вероятно, будет заменено libcontainer.

LMCTFY запускает приложения в изолированных средах на том же ядре и без патчей, поскольку он использует CGroups, namespases и другие функции ядра Linux.

Фото Павла Червиньского для UnsplashФото Павла Червиньского для Unsplash

Google - лидер в контейнерной индустрии. Все в Google работает на контейнерах. Каждую неделю в инфраструктуре Google работает более 2 миллиардов контейнеров.

В декабре 2014 года CoreOS выпустила и начала поддерживать rkt (первоначально выпущенную как Rocket) в качестве альтернативы Docker.

Jails, VPS, Zones, контейнеры и виртуальные машины

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

До сих пор мы вкратце видели, как работает Jail, и представили, как Linux-VServer позволяет запускать изолированные пользовательские пространства, в которых программы запускаются непосредственно в ядре операционной системы хоста, но имеют доступ к ограниченному подмножеству его ресурсов.

Linux-VServer позволяет запускать VPS, и для его использования необходимо пропатчить ядро хоста.

Контейнеры Solaris называются Zones.

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

Виртуальные машины могут быть System Virtual Machines (системными виртуальными машинами) или Process Virtual Machines (процессными виртуальными машинами). В повседневном использовании под словом виртуальные машины мы обычно имеем в виду системные виртуальные машины, которые представляют собой эмуляцию оборудования хоста для эмуляции всей операционной системы. Однако Process Virtual Machines, иногда называемый Application Virtual Machine (Виртуальной машиной приложения), используется для имитации среды программирования для выполнения отдельного процесса: примером является виртуальная машина Java.

Виртуализация на уровне ОС также называется контейнеризацией. Такие технологии, как Linux-VServer и OpenVZ, могут запускать несколько операционных систем, используя одну и ту же архитектуру и версию ядра.

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

Источник: https://fntlnz.wtf/post/why-containersИсточник: https://fntlnz.wtf/post/why-containers

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

VM vs Container. Источник: Docker BlogVM vs Container. Источник: Docker Blog

Контейнеры ОС vs контейнеры приложений

Виртуализация на уровне ОС помогает нам в создании контейнеров. Такие технологии, как LXC и Docker, используют этот тип изоляции.

Здесь у нас есть два типа контейнеров:

  • Контейнеры ОС, в которые упакована операционная система со всем стеком приложений (пример LEMP).

  • Контейнеры приложений, которые обычно запускают один процесс для каждого контейнера.

В случае с контейнерами приложений у нас будет 3 контейнера для создания стека LEMP:

  • сервер PHP (или PHP FPM).

  • Веб-сервер (Nginx).

  • Mysql.

Докер: контейнер или платформа?

Коротко: и то и другое

Подробный ответ:

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

Манифест стандартного контейнера был удален.

Docker начал создавать монолитное приложение с множеством функций - от запуска облачных серверов до создания и запуска образов / контейнеров. Docker использовал libcontainer для взаимодействия с такими средствами ядра Linux, как Control Groups и Namespaces.

Давайте создадим контейнер с использованием СGroups и Namespaces

В этом примере я использую Ubuntu, но это должно работать для большинства дистрибутивов. Начните с установки CGroup Tools and утилиты stress, поскольку мы собираемся выполнить некоторые стресс-тесты.

sudo apt install cgroup-toolssudo apt install stress

Эта команда создаст новый контекст исполнения:

sudo unshare --fork --pid --mount-proc bashps aux

Команда "unshare" разъединяет части контекста исполнения процесса

Теперь, используя cgcreate, мы можем создать группы управления и определить два контроллера: один в памяти, а другой - в процессоре.

cgcreate -a $USER -g memory:mygroup -g cpu:mygroup ls /sys/fs/cgroup/{memory,cpu}/mygroup

Следующим шагом будет определение лимита памяти и его активация:

echo 3000000 > /sys/fs/cgroup/memory/mygroup/memory.kmem.limit_in_bytes cgexec -g memory:mygroup bash

Теперь давайте запустим stress для изолированного namespace, которое мы создали с ограничениями памяти.

stress --vm 1 --vm-bytes 1G --timeout 10s

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

Выполнение этих шагов поможет понять, как средства Linux, такие как CGroups и другие функции управления ресурсами, могут создавать изолированные среды в системах Linux и управлять ими.

Интерфейс libcontainer взаимодействует с этими средствами для управления контейнерами Docker и их запуска.

runC: Использование libcontainer без Docker

В 2015 году Docker анонсировал runC: легкую портативную среду выполнения контейнеров.

runC - это, по сути, небольшой инструмент командной строки для непосредственного использования libcontainer, без использования Docker Engine.

Цель runC - сделать стандартные контейнеры доступными повсюду.

Этот проект был передан в дар Open Container Initiative (OCI).

Репозиторий libcontainer сейчас заархивирован. На самом деле, libcontainer не забросили, а перенесли в репозиторий runC.

Перейдем к практической части и создадим контейнер с помощью runC. Начните с установки среды выполнения runC (прим. переводчика: если стоит docker то этого можно (нужно) не делать):

sudo apt install runc

Давайте создадим каталог (/mycontainer), в который мы собираемся экспортировать содержимое образа Busybox.

sudo sumkdir /mycontainercd /mycontainer/mkdir rootfs  docker export $(docker create busybox) | tar -C rootfs -xvf -

Используя команду runC, мы можем запустить контейнер busybox, который использует извлеченный образ и файл спецификации (config.json).

runc specrunc run mycontainerid

Команда runc spec изначально создает этот файл JSON:

{ "ociVersion": "1.0.1-dev", "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": {   "bounding": [    "CAP_AUDIT_WRITE",    "CAP_KILL",    "CAP_NET_BIND_SERVICE"   ],   "effective": [    "CAP_AUDIT_WRITE",    "CAP_KILL",    "CAP_NET_BIND_SERVICE"   ],   "inheritable": [    "CAP_AUDIT_WRITE",    "CAP_KILL",    "CAP_NET_BIND_SERVICE"   ],   "permitted": [    "CAP_AUDIT_WRITE",    "CAP_KILL",    "CAP_NET_BIND_SERVICE"   ],   "ambient": [    "CAP_AUDIT_WRITE",    "CAP_KILL",    "CAP_NET_BIND_SERVICE"   ]  },  "rlimits": [   {    "type": "RLIMIT_NOFILE",    "hard": 1024,    "soft": 1024   }  ],  "noNewPrivileges": true }, "root": {  "path": "rootfs",  "readonly": true }, "hostname": "runc", "mounts": [  {   "destination": "/proc",   "type": "proc",   "source": "proc"  },  {   "destination": "/dev",   "type": "tmpfs",   "source": "tmpfs",   "options": [    "nosuid",    "strictatime",    "mode=755",    "size=65536k"   ]  },  {   "destination": "/dev/pts",   "type": "devpts",   "source": "devpts",   "options": [    "nosuid",    "noexec",    "newinstance",    "ptmxmode=0666",    "mode=0620",    "gid=5"   ]  },  {   "destination": "/dev/shm",   "type": "tmpfs",   "source": "shm",   "options": [    "nosuid",    "noexec",    "nodev",    "mode=1777",    "size=65536k"   ]  },  {   "destination": "/dev/mqueue",   "type": "mqueue",   "source": "mqueue",   "options": [    "nosuid",    "noexec",    "nodev"   ]  },  {   "destination": "/sys",   "type": "sysfs",   "source": "sysfs",   "options": [    "nosuid",    "noexec",    "nodev",    "ro"   ]  },  {   "destination": "/sys/fs/cgroup",   "type": "cgroup",   "source": "cgroup",   "options": [    "nosuid",    "noexec",    "nodev",    "relatime",    "ro"   ]  } ], "linux": {  "resources": {   "devices": [    {     "allow": false,     "access": "rwm"    }   ]  },  "namespaces": [   {    "type": "pid"   },   {    "type": "network"   },   {    "type": "ipc"   },   {    "type": "uts"   },   {    "type": "mount"   }  ],  "maskedPaths": [   "/proc/kcore",   "/proc/latency_stats",   "/proc/timer_list",   "/proc/timer_stats",   "/proc/sched_debug",   "/sys/firmware",   "/proc/scsi"  ],  "readonlyPaths": [   "/proc/asound",   "/proc/bus",   "/proc/fs",   "/proc/irq",   "/proc/sys",   "/proc/sysrq-trigger"  ] }}

Альтернативой для создания кастомной спецификации конфигурации является использование oci-runtime-tool, подкоманда oci-runtime-tool generate имеет множество опций, которые можно использовать для выполнения разных настроек.

Для получения дополнительной информации см. Runtime-tools.

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

Давайте посмотрим, чем отличается исходный файл config.json от нового:

Давайте теперь снова запустим контейнер и заметим, как он ожидает 10 секунд, прежде чем завершится.

Стандарты сред исполнения контейнеров

С тех пор, как контейнеры стали широко распространенными, различные участники этой экосистемы работали над стандартизацией. Стандартизация - ключ к автоматизации и обобщению передового опыта. Передав проект runC OCI, Docker начал использовать containerd в 2016 году в качестве среды выполнения контейнера, взаимодействующей с базовой средой исполнения низкого уровня runC.

docker info | grep -i runtime

Containerd полностью поддерживает запуск пакетов OCI и управление их жизненным циклом. Containerd (как и другие среды выполнения, такие как cri-o) использует runC для запуска контейнеров, но реализует также другие высокоуровневые функции, такие как управление образами и высокоуровневые API.

Интеграция containerd со средами выполнения Docker и OCIИнтеграция containerd со средами выполнения Docker и OCI

Сontainerd, Shim и RunC, как все работает вместе

runC построен на libcontainer, который является той же библиотекой, которая ранее использовалась для Docker Engine.

До версии 1.11 Docker Engine использовался для управления томами, сетями, контейнерами, образами и т. д.

Теперь архитектура Docker разбита на четыре компонента:

  • Docker engine

  • containerd

  • containerd-shim

  • runC

Бинарные файлы соответственно называются docker, docker-containerd, docker-containerd-shim и docker-runc.

Давайте перечислим этапы запуска контейнера с использованием новой архитектуры docker:

  1. Docker engine создает контейнер (из образа) и передает его в containerd.

  2. Containerd вызывает containerd-shim

  3. Containerd-shim использует runC для запуска контейнера

  4. Containerd-shim позволяет среде выполнения (в данном случае runC) завершиться после запуска контейнера

Используя эту новую архитектуру, мы можем запускать контейнеры без служб (daemon-less containers), и у нас есть два преимущества:

  1. runC может завершиться после запуска контейнера, и нам не нужны запущенными все процессы исполнения.

  2. containerd-shim сохраняет открытыми файловые дескрипторы, такие как stdin, stdout и stderr, даже когда Docker и /или containerd завершаются.

Если runC и Containerd являются средами исполнения, какого черта мы используем оба для запуска одного контейнера?

Это, наверное, один из самых частых вопросов. Поняв, почему Docker разбил свою архитектуру на runC и Containerd, вы понимаете, что оба являются средами исполнения.

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

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

Среда исполнения низкого уровня (например, runC) должна быть легкой, быстрой и не конфликтовать с другими более высокими уровнями управления контейнерами. Когда вы создаете контейнер Docker, он фактически управляет двумя средами исполнения containerd и runC.

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

  • передача и хранение образов,

  • завершение и наблюдение за контейнерами,

  • низкоуровневое хранилище,

  • сетевые настройки,

  • и т.п.

Мы можем добавить новую среду исполнения с помощью Docker, выполнив:

sudo dockerd --add-runtime=<runtime-name>=<runtime-path> 

Например:

sudo apt-get install nvidia-container-runtimesudo dockerd --add-runtime=nvidia=/usr/bin/nvidia-container-runtime

Интерфейс среды исполнения контейнера (Container Runtime Interface)

Kubernetes - одна из самых популярных систем оркестровки. С ростом числа сред выполнения контейнеров Kubernetes стремится быть более расширяемым и взаимодействовать с большим количеством сред выполнения контейнеров, помимо Docker.

Первоначально Kubernetes использовал среду исполнения Docker для запуска контейнеров, и она по-прежнему остается средой исполнения по умолчанию.

Однако CoreOS хотела использовать Kubernetes со средой исполнения RKT и предлагала патчи для Kubernetes, чтобы использовать эту среду исполнения в качестве альтернативы Docker.

Вместо изменения кодовой базы kubernetes в случае добавлении новой среды исполнения контейнера создатели Kubernetes решили создать CRI (Container Runtime Interface), который представляет собой набор API-интерфейсов и библиотек, позволяющих запускать различные среды исполнения контейнеров в Kubernetes. Любое взаимодействие между ядром Kubernetes и поддерживаемой средой выполнения осуществляется через CRI API.

Вот некоторые из плагинов CRI:

CRI-O:

CRI-O - это первая среда исполнения контейнера, созданная для интерфейса CRI kubernetes. Cri-O не предназначен для замены Docker, но его можно использовать вместо среды исполнения Docker в Kubernetes.

Containerd CRI :

С cri-containerd пользователи могут запускать кластеры Kubernetes, используя containerd в качестве базовой среды исполнения без установленного Docker.

gVisor CRI:

gVisor - это проект, разработанный Google, который реализует около 200 системных вызовов Linux в пользовательском пространстве для дополнительной безопасности по сравнению с контейнерами Docker, которые работают непосредственно поверх ядра Linux и изолированы с помощью namespace.

Google Cloud App Engine использует gVisor CRI для изоляции клиентов.

Среда исполнения gVisor интегрируется с Docker и Kubernetes, что упрощает запуск изолированных контейнеров.

CRI-O Kata Containers

Kata Containers - это проект с открытым исходным кодом, создающий легкие виртуальные машины, которые подключаются к экосистеме контейнеров. CRI-O Kata Containers позволяет запускать контейнеры Kata в Kubernetes вместо среды выполнения Docker по умолчанию.

Проект Moby

От проекта создания Docker как единой монолитной платформы отказались и родился проект Moby, в котором Docker состоит из множества компонентов, таких как RunC.

Источник: Solomon Hykes TwitterИсточник: Solomon Hykes Twitter

Moby - это проект по организации и разделения на модули Docker. Это экосистема разработки. Обычные пользователи Docker не заметят никаких изменений.

Источник: Solomon Hykes TwitterИсточник: Solomon Hykes Twitter

Moby помогает в разработке и запуске Docker CE и EE (Moby - это исходный код Docker), а также в создании среды разработки для других сред исполнения и платформ.

Open Containers Initiative

Как мы видели, Docker пожертвовал RunC Open Container Initiative (OCI), но что это?

OCI - это открытая структура, запущенная в 2015 году Docker, CoreOS и другими лидерами контейнерной индустрии.

Open Container Initiative (OCI) направлена на установление общих стандартов для контейнеров, чтобы избежать потенциальной фрагментации и разделения внутри экосистемы контейнеров.

Он содержит две спецификации:

  • runtime-spec: спецификация исполнения

  • image-spec: спецификация образов

Контейнер, использующий другую среду исполнения, можно использовать с Docker API. Контейнер, созданный с помощью Docker, должен работать с любым другим движком.

На этом статья заканчивается.

Буду рад замечаниям и возможно неточностям в статье оригинального автора. Это позволит избежать заблуждений в понимании внутреннего устройства контейнеров. Если нет возможности комментирования на хабре можете обсудить тут в комментариях.

Подробнее..

Перевод Сеть контейнеров это не сложно

26.05.2021 22:23:37 | Автор: admin

Работа с контейнерами многим кажется волшебством, пришло время разобраться как работает сеть контейнеров. Мы покажем на примерах, что это совсем не сложно. Помните, что контейнеры - всего лишь изолированные процессы Linux.

В этой статье мы ответим на следующие вопросы:

  • Как виртуализировать сетевые ресурсы, чтобы контейнеры думали, что у каждого из них есть выделенный сетевой стек?

  • Как превратить контейнеры в дружелюбных соседей, не дать им мешать друг другу и научить хорошо общаться?

  • Как настроить сетевой доступ из контейнера во внешний мир (например, в Интернет)?

  • Как получить доступ к контейнерам, работающим на сервере, из внешнего мира (публикация портов)?

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

  • Network namespaces

  • Virtual Ethernet devices (veth)

  • Virtual network switches (bridge)

  • IP маршрутизация и преобразование сетевых адресов (NAT)

Нам потребуется немного сетевой магии и никакого кода ...

С чего начать?

Все примеры в статье были сделаны на виртуальной машине CentOS 8. Но вы можете выбрать тот дистрибутив, который вам нравится.

Создадим виртуальную машину с помощью Vagrant и подключимся к ней по SSH:

$ vagrant init centos/8$ vagrant up$ vagrant ssh[vagrant@localhost ~]$ uname -aLinux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64

Мы не будем использовать какое-либо популярное решение для контейнеризации (например, docker или podman). Вместо этого мы сосредоточимся на основных концепциях и воспользуемся минимальным набором инструментов для достижения наших учебных целей.

Изоляция контейнеров с помощью Network namespaces

Что составляет сетевой стек Linux? Ну, очевидно, набор сетевых устройств. Что еще? Набор правил маршрутизации. И не забываем про настройку netfilter, создадим необходимые правила iptables.

Напишем небольшой скрипт inspect-net-stack.sh:

#!/usr/bin/env bashecho "> Network devices"ip linkecho -e "\n> Route table"ip routeecho -e "\n> Iptables rules"iptables --list-rules

Но прежде, чем запускать его, давайте внесём изменения в правила iptables, чтобы идентифицировать их в дальнейшем:

$ sudo iptables -N ROOT_NS

Запускаем скрипт:

$ sudo ./inspect-net-stack.sh> Network devices1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff> Route tabledefault via 10.0.2.2 dev eth0 proto dhcp metric 10010.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100> Iptables rules-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT-N ROOT_NS

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

Мы уже упоминали об одном из Linux namespaces, используемых для изоляции контейнеров, которое называет сетевое пространство имён (Network namespace). Если заглянуть в man ip-netns, то мы прочтём, что Network namespace логически является копией сетевого стека со своими собственными маршрутами, правилами брандмауэра и сетевыми устройствами. Мы не будем затрагивать другие Linux namespaces в этой статье и ограничимся только областью видимости сетевого стека.

Для создания Network namespace нам достаточно утилиты ip, которая входим в популярный пакет iproute2. Создадим новое сетевое пространство имён:

$ sudo ip netns add netns0$ ip netnsnetns0

Новое сетевое пространство имён создано, но как начать его использовать? Воспользуемся командой Linux под названием nsenter. Она осуществляет вход в одно или несколько указанных пространств имен, а затем выполняет в нём указанную программу:

$ sudo nsenter --net=/var/run/netns/netns0 bash# The newly created bash process lives in netns0$ sudo ./inspect-net-stack.sh> Network devices1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00> Route table> Iptables rules-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT

Приведённый выше пример показывает, что процесс bash, работающий внутри пространства имён netns0, видит совершенно другой сетевой стек. Отсутствуют правила маршрутизации, и правила iptables, есть только один loopback interface. Все идет по плану...

Подключаем контейнер к хосту через virtual Ethernet devices (veth)

Выделенный сетевой стек будет бесполезен, если к нему отсутствует доступ. К счастью, Linux предоставляет подходящее средство для этого - virtual Ethernet devices (veth)! Согласно man veth, veth-device - это виртуальные устройства Ethernet. Они работают как туннели между сетевыми пространствами имён для создания моста к физическому сетевому устройству в другом пространстве имён, а также могут использоваться как автономные сетевые устройства.

Виртуальные Ethernet устройства всегда работают парами. Создадим их прямо сейчас:

$ sudo ip link add veth0 type veth peer name ceth0

С помощью этой единственной команды мы только что создали пару взаимосвязанных виртуальных Ethernet устройств. Имена veth0 и ceth0 были выбраны произвольно:

$ ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff5: ceth0@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff6: veth0@ceth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff

И veth0, и ceth0 после создания находятся в сетевом стеке хоста (также называемом Root Network namespace). Чтобы связать корневое пространство имён с пространством имён netns0, нам нужно сохранить одно из устройств в корневом пространстве имён и переместить другое в netns0:

$ sudo ip link set ceth0 netns netns0# List all the devices to make sure one of them disappeared from the root stack$ ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff6: veth0@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff link-netns netns0

Как только мы включаем устройства и назначаем правильные IP-адреса, любой пакет, происходящий на одном из них, немедленно появляется на его одноранговом устройстве, соединяющем два пространства имён. Начнем с корневого пространства имён:

$ sudo ip link set veth0 up$ sudo ip addr add 172.18.0.11/16 dev veth0

Продолжим сnetns0:

$ sudo nsenter --net=/var/run/netns/netns0$ ip link set lo up  # whoops$ ip link set ceth0 up$ ip addr add 172.18.0.10/16 dev ceth0$ ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:005: ceth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000    link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff link-netnsid 0

Проверяем подключение:

# From netns0, ping root's veth0$ ping -c 2 172.18.0.11PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.038 ms64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.040 ms--- 172.18.0.11 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 58msrtt min/avg/max/mdev = 0.038/0.039/0.040/0.001 ms# Leave netns0$ exit# From root namespace, ping ceth0$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.073 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.046 ms--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 3msrtt min/avg/max/mdev = 0.046/0.059/0.073/0.015 ms

Обратите внимание, если мы попытаемся проверить доступность любых других адресов из пространства имен netns0, у нас ничего не получится:

# Inside root namespace$ ip addr show dev eth02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute eth0       valid_lft 84057sec preferred_lft 84057sec    inet6 fe80::5054:ff:fee3:2777/64 scope link       valid_lft forever preferred_lft forever# Remember this 10.0.2.15$ sudo nsenter --net=/var/run/netns/netns0# Try host's eth0$ ping 10.0.2.15connect: Network is unreachable# Try something from the Internet$ ping 8.8.8.8connect: Network is unreachable

Для таких пакетов в таблице маршрутизации netns0 просто нет маршрута. В настоящий момент существует единственный маршрут до сети 172.18.0.0/16:

# From netns0 namespace:$ ip route172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

В Linux есть несколько способов заполнения таблицы маршрутизации. Один из них - извлечение маршрутов из подключенных напрямую сетевых интерфейсов. Помните, что таблица маршрутизации в netns0 была пустой сразу после создания пространства имен. Но затем мы добавили туда устройство ceth0 и присвоили ему IP-адрес 172.18.0.10/16. Поскольку мы использовали не простой IP-адрес, а комбинацию адреса и сетевой маски, сетевому стеку удалось извлечь из него информацию о маршрутизации. Каждый пакет, предназначенный для сети 172.18.0.0/16, будет отправлен через устройство ceth0. Но все остальные пакеты будут отброшены. Точно так же есть новый маршрут в корневом пространстве имен:

# From root namespace:$ ip route# ... omitted lines ...172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11

На этом этапе мы ответили на первый вопрос. Теперь мы знаем, как изолировать, виртуализировать и подключать сетевые стеки Linux.

Объединение контейнеров с помощью virtual network switch (bridge)

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

# From root namespace$ sudo ip netns add netns1$ sudo ip link add veth1 type veth peer name ceth1$ sudo ip link set ceth1 netns netns1$ sudo ip link set veth1 up$ sudo ip addr add 172.18.0.21/16 dev veth1$ sudo nsenter --net=/var/run/netns/netns1$ ip link set lo up$ ip link set ceth1 up$ ip addr add 172.18.0.20/16 dev ceth1

Проверим доступность:

# From netns1 we cannot reach the root namespace!$ ping -c 2 172.18.0.21PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.From 172.18.0.20 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.20 icmp_seq=2 Destination Host Unreachable--- 172.18.0.21 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 55mspipe 2# But there is a route!$ ip route172.18.0.0/16 dev ceth1 proto kernel scope link src 172.18.0.20# Leaving netns1$ exit# From root namespace we cannot reach the netns1$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 172.18.0.11 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.11 icmp_seq=2 Destination Host Unreachable--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 23mspipe 2# From netns0 we CAN reach veth1$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 172.18.0.21PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.64 bytes from 172.18.0.21: icmp_seq=1 ttl=64 time=0.037 ms64 bytes from 172.18.0.21: icmp_seq=2 ttl=64 time=0.046 ms--- 172.18.0.21 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 33msrtt min/avg/max/mdev = 0.037/0.041/0.046/0.007 ms# But we still cannot reach netns1$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 172.18.0.10 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.10 icmp_seq=2 Destination Host Unreachable--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 63mspipe 2

Что-то пошло не так... По какой-то причине мы не можем подключиться из netns1 к root namespace. А из root namespace мы не можем подключиться к netns1. Однако, поскольку оба контейнера находятся в одной IP-сети 172.18.0.0/16, есть доступ к veth1 хоста из контейнера netns0. Интересно...

Возможно, мы столкнулись с конфликтом маршрутов. Давайте проверим таблицу маршрутизации в root namespace:

$ ip route# ... omitted lines ...172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21

После добавления второй пары veth в таблице маршрутизации root namespace появился новый маршрут 172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21, но маршрут до этой подсети уже существовал! Когда второй контейнер пытается проверить связь с устройством veth1, используется первый маршрут и мы видим ошибку подключения. Если бы мы удалили первый маршрут sudo ip route delete 172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11 и перепроверили подключение, то увидели бы обратную ситуацию, то есть подключение netns1 будет восстановлено, но netns0 останется в подвешенном состоянии.

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

Рассмотрим Linux Bridge - еще один виртуализированный сетевой объект! Linux Bridge ведёт себя как коммутатор. Он пересылает пакеты между подключенными к нему интерфейсами. А поскольку это коммутатор, то он работает на уровне L2 (то есть Ethernet).

Чтобы предыдущие этапы нашего эксперимента в дальнейшем не вносили путаницы, удалим существующие сетевые пространства имён:

$ sudo ip netns delete netns0$ sudo ip netns delete netns1# But if you still have some leftovers...$ sudo ip link delete veth0$ sudo ip link delete ceth0$ sudo ip link delete veth1$ sudo ip link delete ceth1

Заново создаём два контейнера. Обратите внимание, мы не назначаем IP-адреса новым устройствам veth0 и veth1:

$ sudo ip netns add netns0$ sudo ip link add veth0 type veth peer name ceth0$ sudo ip link set veth0 up$ sudo ip link set ceth0 netns netns0$ sudo nsenter --net=/var/run/netns/netns0$ ip link set lo up$ ip link set ceth0 up$ ip addr add 172.18.0.10/16 dev ceth0$ exit$ sudo ip netns add netns1$ sudo ip link add veth1 type veth peer name ceth1$ sudo ip link set veth1 up$ sudo ip link set ceth1 netns netns1$ sudo nsenter --net=/var/run/netns/netns1$ ip link set lo up$ ip link set ceth1 up$ ip addr add 172.18.0.20/16 dev ceth1$ exit

Убедимся, что на хосте нет новых маршрутов:

$ ip routedefault via 10.0.2.2 dev eth0 proto dhcp metric 10010.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100

И, наконец, создадим bridge интерфейс:

$ sudo ip link add br0 type bridge$ sudo ip link set br0 up

Теперь подключим к нему veth0 и veth1:

$ sudo ip link set veth0 master br0$ sudo ip link set veth1 master br0

... и проверим возможность подключения между контейнерами:

$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.259 ms64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.051 ms--- 172.18.0.20 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 0.051/0.155/0.259/0.104 ms
$ sudo nsenter --net=/var/run/netns/netns1$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.037 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.089 ms--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 36msrtt min/avg/max/mdev = 0.037/0.063/0.089/0.026 ms

Прекрасно! Все отлично работает. При этом мы даже не настраивали интерфейсы veth0 и veth1. Мы назначили только два IP-адреса интерфейсам ceth0 и ceth1. Но поскольку они оба находятся в одном сегменте Ethernet (подключены к виртуальному коммутатору), существует возможность подключения на уровне L2:

$ sudo nsenter --net=/var/run/netns/netns0$ ip neigh172.18.0.20 dev ceth0 lladdr 6e:9c:ae:02:60:de STALE$ exit$ sudo nsenter --net=/var/run/netns/netns1$ ip neigh172.18.0.10 dev ceth1 lladdr 66:f3:8c:75:09:29 STALE$ exit

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

Настраиваем сетевой доступ из контейнера во внешний мир (IP routing and masquerading)

Сейчас контейнеры могут подключаться друг к другу. Но будут ли удачны подключения к хосту, то есть к корневому пространству имён?

$ sudo nsenter --net=/var/run/netns/netns0$ ping 10.0.2.15  # eth0 addressconnect: Network is unreachable

Интерфейс eth0 не доступен. Всё очевидно, в netns0 отсутствует маршрут для этого подключения:

$ ip route172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

Корневое пространство имён также не может взаимодействовать с контейнерами:

# Use exit to leave netns0 first:$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.From 213.51.1.123 icmp_seq=1 Destination Net UnreachableFrom 213.51.1.123 icmp_seq=2 Destination Net Unreachable--- 172.18.0.10 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 213.51.1.123 icmp_seq=1 Destination Net UnreachableFrom 213.51.1.123 icmp_seq=2 Destination Net Unreachable--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms

Чтобы установить связь между корневым пространством имён и пространством имён контейнера, нам нужно назначить IP-адрес сетевому интерфейсу моста:

$ sudo ip addr add 172.18.0.1/16 dev br0

Теперь после того, как мы назначили IP-адрес интерфейсу моста, мы получили маршрут в таблице маршрутизации хоста:

$ ip route# ... omitted lines ...172.18.0.0/16 dev br0 proto kernel scope link src 172.18.0.1$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.036 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.049 ms--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 11msrtt min/avg/max/mdev = 0.036/0.042/0.049/0.009 ms$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.059 ms64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.056 ms--- 172.18.0.20 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 4msrtt min/avg/max/mdev = 0.056/0.057/0.059/0.007 ms

Контейнер, вероятно, также получил возможность пинговать интерфейс моста, но они все ещё не могут связаться с хостом eth0. Нам нужно добавить маршрут по умолчанию для контейнеров:

$ sudo nsenter --net=/var/run/netns/netns0$ ip route add default via 172.18.0.1$ ping -c 2 10.0.2.15PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data.64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.036 ms64 bytes from 10.0.2.15: icmp_seq=2 ttl=64 time=0.053 ms--- 10.0.2.15 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 14msrtt min/avg/max/mdev = 0.036/0.044/0.053/0.010 ms# And repeat the change for netns1

Теперь наш хост является маршрутизатором, а интерфейс моста стал шлюзом по умолчанию для контейнеров.

Отлично, нам удалось добиться сетевой связности контейнеров с корневым пространством имён. Теперь давайте попробуем подключить их к внешнему миру. По умолчанию переадресация пакетов (ip packet forwarding), то есть функциональность маршрутизатора в Linux отключена. Нам нужно её включить

# In the root namespacesudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'

Теперь самое интересное - проверка подключения:

$ sudo nsenter --net=/var/run/netns/netns0$ ping 8.8.8.8# hangs indefinitely long for me...

Всё равно не работает. Мы что-то упустили? Если бы контейнер отправлял пакеты во внешний мир, сервер-получатель не смог бы отправлять пакеты обратно в контейнер, потому что IP-адрес контейнера является частным и правила маршрутизации для этого конкретного IP-адреса известны только в локальной сети. К тому же многие контейнеры в мире имеют один и тот же частный IP-адрес 172.18.0.10. Решение этой проблемы называется преобразованием сетевых адресов (NAT). Принцип работы, следующий - перед отправкой во внешнюю сеть пакеты, отправленные контейнерами, заменяют свои исходные IP-адреса (source IP addesses) на адрес внешнего интерфейса хоста. Хост также будет отслеживать все существующие сопоставления (mapping) и по прибытии будет восстанавливать IP-адреса перед пересылкой пакетов обратно в контейнеры. Звучит сложно, но у меня для вас хорошие новости! Нам нужна всего одна команда, чтобы добиться требуемого результата:

$ sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/16 ! -o br0 -j MASQUERADE

Команда довольно проста. Мы добавляем новое правило в таблицу nat цепочки POSTROUTING с просьбой выполнить MASQUERADE всех исходящих пакетов из сети 172.18.0.0/16, но не через интерфейс моста.

Проверьте подключение:

$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 8.8.8.8PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=43.2 ms64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms--- 8.8.8.8 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms
$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 8.8.8.8PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=43.2 ms64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms--- 8.8.8.8 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms

Помните, что политика iptables по умолчанию - ACCEPT для каждой цепочки, она может быть довольно опасной в реальных условиях:

sudo iptables -S-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT

В качестве хорошего примера Docker вместо этого ограничивает все по умолчанию, а затем разрешает только для известных маршрутов:

$ sudo iptables -t filter --list-rules-P INPUT ACCEPT-P FORWARD DROP-P OUTPUT ACCEPT-N DOCKER-N DOCKER-ISOLATION-STAGE-1-N DOCKER-ISOLATION-STAGE-2-N DOCKER-USER-A FORWARD -j DOCKER-USER-A FORWARD -j DOCKER-ISOLATION-STAGE-1-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT-A FORWARD -o docker0 -j DOCKER-A FORWARD -i docker0 ! -o docker0 -j ACCEPT-A FORWARD -i docker0 -o docker0 -j ACCEPT-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 5000 -j ACCEPT-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2-A DOCKER-ISOLATION-STAGE-1 -j RETURN-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP-A DOCKER-ISOLATION-STAGE-2 -j RETURN-A DOCKER-USER -j RETURN$ sudo iptables -t nat --list-rules-P PREROUTING ACCEPT-P INPUT ACCEPT-P POSTROUTING ACCEPT-P OUTPUT ACCEPT-N DOCKER-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 5000 -j MASQUERADE-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER-A DOCKER -i docker0 -j RETURN-A DOCKER ! -i docker0 -p tcp -m tcp --dport 5005 -j DNAT --to-destination 172.17.0.2:5000$ sudo iptables -t mangle --list-rules-P PREROUTING ACCEPT-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT-P POSTROUTING ACCEPT$ sudo iptables -t raw --list-rules-P PREROUTING ACCEPT-P OUTPUT ACCEPT

Настроим сетевой доступ из внешнего мира в контейнеры (port publishing)

Публикация портов контейнеров для некоторых (или всех) интерфейсов хоста - популярная практика. Но что на самом деле означает публикация порта?

Представьте, что у нас есть сервис, работающий внутри контейнера:

$ sudo nsenter --net=/var/run/netns/netns0$ python3 -m http.server --bind 172.18.0.10 5000

Если мы попытаемся отправить HTTP-запрос этому сервису с хоста, все будет работать (ну, есть связь между корневым пространством имён и всеми интерфейсами контейнера, почему бы и нет?):

# From root namespace$ curl 172.18.0.10:5000<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"># ... omitted lines ...

Однако, если бы мы получили доступ к этому серверу из внешнего мира, какой IP-адрес мы бы использовали? Единственный IP-адрес, который мы можем знать, - это адрес внешнего интерфейса хоста eth0:

$ curl 10.0.2.15:5000curl: (7) Failed to connect to 10.0.2.15 port 5000: Connection refused

Таким образом, нам нужно найти способ перенаправить все пакеты, поступающие на порт 5000 интерфейса eth0 хоста, на адрес172.18.0.10:5000. Или, другими словами, нам нужно опубликовать порт 5000 контейнера на интерфейсе eth0 хоста.

# External trafficsudo iptables -t nat -A PREROUTING -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000# Local traffic (since it doesn't pass the PREROUTING chain)sudo iptables -t nat -A OUTPUT -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000

Кроме того, нам нужно включить iptables intercepting traffic over bridged networks (перехватывать трафик bridged networks):

sudo modprobe br_netfilter

Время проверить!

curl 10.0.2.15:5000<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"># ... omitted lines ...

Разбираемся в работе Docker network drivers

Но что же вам сделать теперь со всеми этими бесполезными знаниями? Например, мы могли бы попытаться разобраться в некоторых сетевых режимах Docker!

Начнем с режима --network host. Попробуйте сравнить вывод следующих команд ip link и sudo docker run -it --rm --network host alpine ip link. Сюрприз, они совпадут! Таким образом host mode Docker просто не использует изоляцию сетевого пространства имён и контейнеры работают в корневом сетевом пространстве имён и совместно используют сетевой стек с хост-системой.

Следующий режим, который нужно проверить, - это --network none. Вывод команды sudo docker run -it --rm --network none alpine ip link показывает только один сетевой интерфейс обратной loopback. Это очень похоже на наши наблюдения за только что созданным сетевым пространством имен. То есть до того момента, когда мы добавляли какие-либо veth устройства.

И последнее, но не менее важное: режим --network bridge (по умолчанию), это именно то, что мы пытались воспроизвести в этой статье.

Сети и rootless контейнеры

Одной из приятных особенностей диспетчера контейнеров podman является его ориентация на rootless контейнеры. Однако, как вы, вероятно, заметили, в этой статье мы использовали много эскалаций sudo и без root-прав настроить сеть невозможно. При настройке сетей rootful контейнеров Podman очень близок к Docker. Но когда дело доходит до rootless контейнеров, Podman полагается на проект slirp4netns:

Начиная с Linux 3.8, непривилегированные пользователи могут создавать network_namespaces (7) вместе с user_namespaces (7). Однако непривилегированные сетевые пространства имен оказались не очень полезными, потому что для создания пар veth (4) в пространствах имен хоста и сети по-прежнему требуются привилегии root (иначе доступ в Интернету будет отсутствовать).

slirp4netns позволяет получить доступ из сетевое пространства имен в Интернет непривилегированным пользователям, подключая устройство TAP в сетевом пространстве имен к стеку TCP/IP usermode (slirp).

Сеть rootless контейнера весьма ограничена: технически сам контейнер не имеет IP-адреса, потому что без привилегий root невозможно настроить сетевое устройство. Более того, проверка связи (ping) из rootless контейнера не работает, поскольку в нем отсутствует функция безопасности CAP_NET_RAW, которая необходима для работы команды ping.

Заключение

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

Подробнее..

Перевод Linux-контейнеры в паре строчек кода

11.11.2020 16:12:52 | Автор: admin
В продолжение прошлой статьи о KVM публикуем новый перевод и разбираемся, как работают контейнеры на примере запуска Docker-образа busybox.

Эта статья о контейнерах является продолжением предыдущей статьи о KVM. Я бы хотел показать, как именно работают контейнеры, запустив Docker-образ busybox в нашем собственном небольшом контейнере.

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

BusyBox Docker


Нашей главной целью будет запустить обычный образ busybox для Docker, но без Docker. Docker использует btrfs в качестве файловой системы для своих образов. Давайте попробуем скачать образ и распаковать его в директорию:

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


Теперь у нас есть файловая система образа busybox, распакованная в папку rootfs. Конечно, можно запустить ./rootfs/bin/sh и получить рабочую shell-оболочку, но если мы посмотрим на список процессов, файлов, или сетевых интерфейсов, увидим, что у нас есть доступ ко всей ОС.

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

Clone


Поскольку мы хотим контролировать то, к чему имеет доступ дочерний процесс, мы будем использовать clone(2) вместо fork(2). Clone делает почти то же самое, но позволяет передавать флаги, указывая, какие ресурсы вы хотите разделять (с хостом).

Разрешены следующие флаги:

  • CLONE_NEWNET изолированные сетевые устройства
  • CLONE_NEWUTS имя хоста и домена (система разделения времени UNIX)
  • CLONE_NEWIPC объекты IPC
  • CLONE_NEWPID идентификаторы процессов (PID)
  • CLONE_NEWNS точки монтирования (файловые системы)
  • CLONE_NEWUSER пользователи и группы.

В нашем эксперименте мы попытаемся изолировать процессы, IPC, сетевые и файловые системы. Итак, начинаем:

static char child_stack[1024 * 1024];int child_main(void *arg) {  printf("Hello from child! PID=%d\n", getpid());  return 0;}int main(int argc, char *argv[]) {  int flags =      CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET;  int pid = clone(child_main, child_stack + sizeof(child_stack),                  flags | SIGCHLD, argv + 1);  if (pid < 0) {    fprintf(stderr, "clone failed: %d\n", errno);    return 1;  }  waitpid(pid, NULL, 0);  return 0;}

Код должен запускаться с привилегиями суперпользователя, иначе клонирование не удастся.

Эксперимент дает любопытный результат: дочерний PID равен 1. Нам хорошо известно, что PID 1 обычно у процесса init. Но в этом случае дочерний процесс получает свой собственный изолированный список процессов, где он стал первым процессом.

Рабочая оболочка


Чтобы упростить изучение новой среды, запустим shell-оболочку в дочернем процессе. Давайте запускать произвольные команды, наподобие docker run:

int child_main(void *arg) {  char **argv = (char **)arg;  execvp(argv[0], argv);  return 0;}

Теперь запуск нашего приложения с аргументом /bin/sh открывает настоящую оболочку, в которой мы сможем вводить команды. Такой результат доказывает, насколько мы ошибались, говоря об изолированности:

# echo $$
1
# ps
PID TTY TIME CMD
5998 pts/31 00:00:00 sudo
5999 pts/31 00:00:00 main
6001 pts/31 00:00:00 sh
6004 pts/31 00:00:00 ps


Как мы видим, сам процесс shell-оболочки имеет PID равный 1, но, на самом деле, он может видеть и получать доступ ко всем другим процессам основной ОС. Причина в том, что список процессов читается из procfs, которая все еще наследуется.

Итак, размонтируем procfs:

umount2("/proc", MNT_DETACH);

Теперь при запуске shell-оболочки ломаются команды ps, mount и другие, потому что procfs не смонтирована. Однако это все равно лучше, чем утечка родительской procfs.

Chroot


Для создания корневого каталога обычно применяется chroot, но мы воспользуемся альтернативой pivot_root. Этот системный вызов переносит текущий корень системы в подкаталог и назначает другую директорию корнем:

int child_main(void *arg) {  /* Unmount procfs */  umount2("/proc", MNT_DETACH);  /* Pivot root */  mount("./rootfs", "./rootfs", "bind", MS_BIND | MS_REC, "");  mkdir("./rootfs/oldrootfs", 0755);  syscall(SYS_pivot_root, "./rootfs", "./rootfs/oldrootfs");  chdir("/");  umount2("/oldrootfs", MNT_DETACH);  rmdir("/oldrootfs");  /* Re-mount procfs */  mount("proc", "/proc", "proc", 0, NULL);  /* Run the process */  char **argv = (char **)arg;  execvp(argv[0], argv);  return 0;}

Имеет смысл смонтировать tmpfs в /tmp, sysfs в /sys и создать действующую файловую систему /dev, но для краткости я пропущу этот шаг.

Теперь мы видим только файлы из образа busybox, как будто мы использовали chroot:

/ # ls
bin dev etc home proc root sys tmp usr var

/ # mount
/dev/sda2 on / type ext4 (rw,relatime,data=ordered)
proc on /proc type proc (rw,relatime)

/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
4 root 0:00 ps

/ # ps ax
PID USER TIME COMMAND
1 root 0:00 /bin/sh
5 root 0:00 ps ax


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

Сеть


Создать новое сетевое пространство имен было только началом! Нужно назначить ему сетевые интерфейсы и настроить их для правильной пересылки пакетов.

Если у вас нет интерфейса br0, необходимо создать вручную (brctl является частью пакета bridge-utils в Ubuntu):

brctl addbr br0
ip addr add dev br0 172.16.0.100/24
ip link set br0 up
sudo iptables -A FORWARD -i wlp3s0 -o br0 -j ACCEPT
sudo iptables -A FORWARD -o wlp3s0 -i br0 -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 172.16.0.0/16 -j MASQUERADE

В моем случае, wlp3s0 был основным сетевым интерфейсом WiFi, а 172.16.x.x сетью для контейнера.

Наша программа запуска контейнеров должна создать пару интерфейсов, veth0 и veth1, связать их с br0 и настроить маршрутизацию внутри контейнера.

В функции main() мы запустим перед клонированием эти команды:

system("ip link add veth0 type veth peer name veth1");system("ip link set veth0 up");system("brctl addif br0 veth0");

По окончании вызова clone() мы добавим veth1 в новое дочернее пространство имен:

char ip_link_set[4096];snprintf(ip_link_set, sizeof(ip_link_set) - 1, "ip link set veth1 netns %d",         pid);system(ip_link_set);

Теперь, если мы запустим ip link в оболочке контейнера, мы увидим интерфейс loopback и некоторый интерфейс veth1@xxxx. Но сеть по-прежнему не работает. Зададим уникальное имя хоста в контейнере и настроим маршруты:

int child_main(void *arg) {  ....  sethostname("example", 7);  system("ip link set veth1 up");  char ip_addr_add[4096];  snprintf(ip_addr_add, sizeof(ip_addr_add),           "ip addr add 172.16.0.101/24 dev veth1");  system(ip_addr_add);  system("route add default gw 172.16.0.100 veth1");  char **argv = (char **)arg;  execvp(argv[0], argv);  return 0;}

Посмотрим, как это выглядит:

/ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
47: veth1@if48: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
link/ether 72:0a:f0:91:d5:11 brd ff:ff:ff:ff:ff:ff

/ # hostname
example

/ # ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=57 time=27.161 ms
64 bytes from 1.1.1.1: seq=1 ttl=57 time=26.048 ms
64 bytes from 1.1.1.1: seq=2 ttl=57 time=26.980 ms
...

Работает!

Вывод


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

Безусловно, Docker способен на гораздо большее! Но удивительно, сколько подходящих API имеет ядро Linux и как легко их использовать, чтобы достичь виртуализации на уровне ОС.

Надеюсь, вам понравилась статья. Вы можете найти проекты автора на Github и подписаться на Twitter, чтобы следить за новостями, а также через rss.
Подробнее..

Перевод Шлюзы Java.Net в интеграционных продукциях InterSystems IRIS

06.10.2020 12:23:44 | Автор: admin

Шлюзы в InterSystems IRIS это механизм взаимодействия между ядром InterSystems IRIS и прикладным кодом на языках Java/.Net. С помощью шлюзов вы можете работать как с объектами Java/.NET из ObjectScript так и с объектами ObjectScript и глобалами из Java/.NET. Шлюзы могут быть запущены где угодно - локально, на удаленном сервере, в докере.

В этой статье я покажу, как можно легко разработать и контейнеризовать интеграционную продукцию с .Net/Java кодом. А для взаимодействия с кодом на языках Java/.Net будем использовать PEX, предоставляющий возможность реализовать любой элемент интеграционной продукции на языках Java/.Net.

Для нашего примера мы разработаем интеграцию с Apache Kafka.

Архитектура

Apache Kafka популярный брокер сообщений. В Kafka есть тема (topic) сообщения в которую издатели (publisher) пишут сообщения и есть подписчики (consumer) на темы, которые читают эти сообщения.

Сначала мы напишем Java бизнес-операцию которая будет пубиковать сообщения в Apache Kafka. Затем добавим бизнес-службу на языке C# которая будет сообщения читать, сохранять и передавать для дальнейшей обработки в InterSystems IRIS.

Наше решение будеть работать в докере и выглядит следующим образом:

Java Gateway

Прежде всего, разработаем Бизнес-Операцию на Java для отправки сообщений в Apache Kafka. Код может быть написан в любой Java IDE и выглядеть так:

  • Для разработки новой PEX бизнес-операции необходимо реализовать абстрактный класс com.intersystems.enslib.pex.BusinessOperation.

  • Публичные свойства класса это настройки нашего бизнес-хоста

  • Метод OnInit используется для установления соединения с Apache Kafka и получения указателя на InterSystems IRIS

  • OnTearDown используется для отключения от Apache Kafka (при остановке процесса).

  • OnMessage получает сообщение dc.KafkaRequest и отправляет его в Apache Kafka.

Теперь упакуем нашу бизнес-операцию в Docker контейнер.

Вот наш докер-файл:

FROM openjdk:8 AS builderARG APP_HOME=/tmp/appCOPY src $APP_HOME/srcCOPY --from=intersystemscommunity/jgw:latest /jgw/*.jar $APP_HOME/jgw/WORKDIR $APP_HOME/jar/ADD https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/2.5.0/kafka-clients-2.5.0.jar .ADD https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar .ADD https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar .ADD https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar .WORKDIR $APP_HOME/srcRUN javac -classpath $APP_HOME/jar/*:$APP_HOME/jgw/* dc/rmq/KafkaOperation.java &amp;&amp; \    jar -cvf $APP_HOME/jar/KafkaOperation.jar dc/rmq/KafkaOperation.classFROM intersystemscommunity/jgw:latestCOPY --from=builder /tmp/app/jar/*.jar $GWDIR/

Посмотрим, что здесь происходит (я предполагаю, что вы знакомы с многоступенчатыми докер сборками):

FROM openjdk:8 AS builder

JDK8 это базовый образ, в котором мы будем компилировать наше приложение.

ARG APP_HOME=/tmp/appCOPY src $APP_HOME/src

Копируем исходный код из папки /src в /tmp/app.

COPY --from=intersystemscommunity/jgw:latest /jgw/*.jar $APP_HOME/jgw/

Копируем библиотеки Java Gateway в папку /tmp/app/jgw.

WORKDIR $APP_HOME/jar/ADD https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/2.5.0/kafka-clients-2.5.0.jar .ADD https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar .ADD https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar .ADD https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar .WORKDIR $APP_HOME/srcRUN javac -classpath $APP_HOME/jar/*:$APP_HOME/jgw/* dc/rmq/KafkaOperation.java &amp;&amp; \    jar -cvf $APP_HOME/jar/KafkaOperation.jar dc/rmq/KafkaOperation.class

Все зависимости скачаны - вызываем javac/jar для компиляции jar файла. Для реальных проектов рекомендуется использовать полноценную систему сборки maven или gradle.

FROM intersystemscommunity/jgw:latestCOPY --from=builder /tmp/app/jar/*.jar $GWDIR/

И, наконец, jar файлы копируются в базовый образ Java шлюза, который содержит все необходимые зависимости и обеспечивает запуск Java шлюза.

.Net Gateway

Далее разработаем службу .Net, которая будет получать сообщения от Apache Kafka. Код может быть написан в любой .Net IDE и выглядеть так.

Особенности:

  • Для разработки новой PEX бизнес-службы необходимо реализовать абстрактный класс InterSystems.EnsLib.PEX.BusinessService.

  • Публичные свойства класса это настройки нашего бизнес-хоста

  • Метод OnInit используется для установления соединения с Apache Kafka, подписки на темы Apache Kafka и получения указателя на InterSystems IRIS

  • OnTearDown используется для отключения от Apache Kafka (при остановке процесса)

  • OnMessage получает сообщения из Apache Kafka и отправляет сообщение класса Ens.StringContainer в целевые бизнес-хосты продукции

Теперь упакуем нашу бизнес-операцию в Docker контейнер.

Вот наш докер-файл:

FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS buildENV ISC_PACKAGE_INSTALLDIR /usr/irissysENV GWLIBDIR libENV ISC_LIBDIR ${ISC_PACKAGE_INSTALLDIR}/dev/dotnet/bin/Core21WORKDIR /sourceCOPY --from=store/intersystems/iris-community:2020.2.0.211.0 $ISC_LIBDIR/*.nupkg $GWLIBDIR/# copy csproj and restore as distinct layersCOPY *.csproj ./RUN dotnet restore# copy and publish app and librariesCOPY . .RUN dotnet publish -c release -o /app# final stage/imageFROM mcr.microsoft.com/dotnet/core/runtime:2.1WORKDIR /appCOPY --from=build /app ./# Configs to start the Gateway ServerRUN cp KafkaConsumer.runtimeconfig.json IRISGatewayCore21.runtimeconfig.json &amp;&amp; \    cp KafkaConsumer.deps.json IRISGatewayCore21.deps.jsonENV PORT 55556CMD dotnet IRISGatewayCore21.dll $PORT 0.0.0.0

Посмотрим, что здесь происходит:

FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build

Используем образ .Net Core 2.1 SDK для сборки нашего приложения.

ENV ISC_PACKAGE_INSTALLDIR /usr/irissysENV GWLIBDIR libENV ISC_LIBDIR ${ISC_PACKAGE_INSTALLDIR}/dev/dotnet/bin/Core21WORKDIR /sourceCOPY --from=store/intersystems/iris-community:2020.2.0.211.0 $ISC_LIBDIR/*.nupkg $GWLIBDIR/

Копируем библиотеки .Net Gateway из официального образа InterSystems IRIS:

# copy csproj and restore as distinct layersCOPY *.csproj ./RUN dotnet restore# copy and publish app and librariesCOPY . .RUN dotnet publish -c release -o /app

Компилируем нашу бизнес-операцию.

FROM mcr.microsoft.com/dotnet/core/runtime:2.1WORKDIR /appCOPY --from=build /app ./

Копируем библиотеки в финальный контейнер.

RUN cp KafkaConsumer.runtimeconfig.json IRISGatewayCore21.runtimeconfig.json &amp;&amp; \    cp KafkaConsumer.deps.json IRISGatewayCore21.deps.json

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

ENV PORT 55556CMD dotnet IRISGatewayCore21.dll $PORT 0.0.0.0

Запускаем шлюз на порту 55556, слушаем все сетевые интерфейсы.

Готово!

Вот полная конфигурация docker-compose, для запуска демо целиком (в том числе и UI для Apache Kafka, для просмотра сообщений).

Запуск демо

Для запуска демо локально:

Установите:

Выполните:

git clone https://github.com/intersystems-community/pex-demo.gitcd pex-demodocker-compose pulldocker-compose up -d

Выводы

  • В интеграционных продукциях InterSystems IRIS появилась возможность создавать любые элементы продукции на языках Java/.Net

  • Код на Java/.Net возможно вызывать из InterSystems ObjectScript и наоборот, код на InterSystems ObjectScript из Java/.Net

  • Генерация прокси классов больше не требуется

  • Возможна как классическая поставка решения, так и поставка в Docker

Ссылки

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru