Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Первый шаг развертывания в Kubernetes – это размещение вашего приложения в контейнере. В этой серии мы рассмотрим, как можно создать образ небольшого и безопасного контейнера.
Благодаря Docker, создание образов контейнеров никогда еще не было настолько простым. Укажите базовый образ, добавьте свои изменения и создайте контейнер.
Несмотря на то, что данный прием отлично подходит для начала работы, использование базовых образов по умолчанию может привести к небезопасной работе с большими образами, полными уязвимостей.
Кроме того, большинство образов в Docker используют для базового образа Debian или Ubuntu, и хотя это обеспечивает отличную совместимость и легкую адаптацию (файл Docker занимает всего две строки кода), базовые образы способны добавить сотни мегабайтов дополнительной нагрузки в ваш контейнер. Например, простой файл node.js приложения Go «hello-world» занимают около 700 мегабайт, при том, что размер собственно вашего приложения составляет всего лишь несколько мегабайтов.
Таким образом, вся эта дополнительная нагрузка – пустая трата цифрового пространства и отличный тайник для уязвимостей и ошибок в системе безопасности. Поэтому давайте рассмотрим два способа уменьшения размера образа контейнера.
Первый – это использование базовых образов маленького размера, второй – использование шаблона проектирования Builder Pattern. Использование меньших базовых изображений, вероятно, самый простой способ уменьшения размера вашего контейнера. Скорее всего, ваш язык или стек, которые вы используете, обеспечивает оригинальный образ приложения гораздо меньшего размера, чем образ по умолчанию. Давайте взглянем на наш контейнер node.js.
По умолчанию в Docker размер базового образа node:8 равен 670 МБ, а размер node: 8-alpine составляет всего 65 МБ, то есть в 10 раз меньше. Используя меньший базовый образ Alpine, вы существенно сократите размер вашего контейнера. Alpine -это небольшой и легкий дистрибутив Linux, который очень популярен среди пользователей Docker, потому что он совместим со многими приложениями, сохраняя при этом небольшой размер контейнеров. В отличие от стандартного образа Docker «node», «node:alpine» удаляет множество служебных файлов и программ, оставляя только те, которых достаточно для запуска вашего приложения.
Чтобы перейти к меньшему базовому образу, просто обновите Docker-файл для начала работы с новым базовым изображением:
Теперь, в отличие от старого образа onbuild, вам нужно скопировать свой код в контейнер и установить любые зависимости. В новом Docker-файле контейнер начинается с образа node:alpine, затем создает каталог для кода, устанавливает зависимости с помощью менеджера пакетов NPM и, наконец, запускает server.js.
С помощью этого обновления получается контейнер в 10 раз меньшего размера. Если ваш язык программирования или стек не имеет функции уменьшения базового образа, используйте Alpine Linux. Он также предоставит возможность полностью управлять содержимым контейнера. Использование базовых образов маленького размера — отличный способ быстрого создания небольших контейнеров. Но можно достичь еще большего уменьшения, используя Builder Pattern.
В интерпретируемых языках исходный код сначала передается интерпретатору, а затем уже непосредственно выполняется. В компилируемых языках исходный код предварительно превращается в скомпилированный код. При этом компиляция часто использует инструменты, которые в действительности не нужны для запуска кода. Это означает, что вы можете полностью удалить эти инструменты из финального контейнера. Для этого можно использовать Builder Pattern.
Код создается в первом контейнере и компилируется. Затем скомпилированный код упаковывается в конечный контейнер без компиляторов и инструментов, необходимых для компиляции этого кода. Давайте пропустим через этот процесс приложение Go. Во-первых, мы перейдем от образа onbuild к Alpine Linux.
В новом файле Docker контейнер начинается с образа golang:alpine. Затем он создает каталог для кода, копирует его в исходный код, создает этот исходный код и запускает приложение. Этот контейнер намного меньше, чем контейнер onbuild, но он все еще содержит компилятор и другие инструменты Go, которые в действительности нам не нужны. Поэтому давайте просто извлечем скомпилированную программу и уложим ее в свой собственный контейнер.
Вы можете заметить нечто странное в этом файле Docker: он содержит две строки FROM. Первый раздел из 4-х строк выглядит точно так же, как и предыдущий файл Docker за исключением того, что он использует ключевое слово AS, чтобы дать имя этому этапу. В следующем разделе имеется новая строка FROM, позволяющая начать новый образ, при этом вместо образа golang:alpine в качестве базового образа мы будем использовать Raw alpine.
Raw Alpine Linux не имеет никаких установленных SSL сертификатов, что приведет к сбою большинства вызовов API по протоколу HTTPS, поэтому давайте установим несколько корневых сертификатов CA.
А теперь самое интересное: для копирования скомпилированного кода из первого контейнера во второй можно просто использовать команду COPY, расположенную в 5-й строке второго раздела. Она скопирует только один файл приложения и не затронет служебные инструменты Go. Новый многоступенчатый файл Docker будет содержать образ контейнера размером всего лишь 12 мегабайт при том, что исходный образ контейнера составлял 700 мегабайт, а это большая разница!
Таким образом, использование небольших базовых изображений и Builder Pattern — отличные способы создавать контейнеры гораздо меньших размеров без большого объема работы.
Возможно, что в зависимости от стека приложения, существуют дополнительные способы уменьшить размер образа и контейнера, но действительно ли маленькие контейнеры имеют измеримое преимущество? Давайте рассмотрим два аспекта, где маленькие контейнеры чрезвычайно эффективны – это производительность и безопасность.
Чтобы оценить рост производительности, рассмотрим длительность процесса создания контейнера, вставки его в реестр (push) и последующего извлечения оттуда (pull). Вы можете увидеть, что контейнер меньшего размера обладает неоспоримым преимуществом по сравнению с контейнером большего размера.
Docker будет кэшировать слои, поэтому последующие сборки будут выполняться очень быстро. Однако во многих системах CI, которые используются для сборки и тестирования контейнеров, слои не кэшируются, поэтому здесь имеется значительная экономия времени. Как видно, время построения контейнера большого размера в зависимости от мощности вашей машины составляет от 34 до 54 секунд, а при использовании контейнера, уменьшенного при помощи Builder Pattern – от 23 до 28 секунд. Для операций подобного рода прирост производительности составит 40-50%. Поэтому просто подумайте, сколько раз вы создаете и тестируете свой код.
После того, как контейнер построен, вам нужно вставить его образ (push container image) в реестр контейнеров, чтобы затем использовать в своем кластере Kubernetes. Я рекомендую использовать реестр контейнеров Google.
Используя Google Container Registry (GCR), вы платите только за «сырое» хранилище и сеть, а дополнительная плата за управление контейнерами не взимается. Это конфиденциально, безопасно и очень быстро. GCR использует много трюков, чтобы ускорить операцию pull. Как видите, вставка образа контейнера Docker Container Image при использовании go:onbuild в зависимости от производительности компьютера займет от 15 до 48с, а та же операция с контейнером меньшего размера – от 14 до 16с, причем для менее производительных машин преимущество в скорости операции увеличивается в 3 раза. Для больших машин время примерно одинаково, так как GCR использует глобальный кэш для общей базы изображений, то есть вам вообще не нужно их загружать. В компьютере малой мощности CPU является узким местом, поэтому преимущество использования малых контейнеров здесь намного ощутимей.
Если вы используете GCR, я настоятельно рекомендую применять Google Container Builder (GCB) как часть вашей системы сборки.
Как видите, его использование позволяет достичь намного лучших результатов в уменьшении длительности операции Build+Push, чем даже у производительной машины – в этом случае процесс построения и отправки контейнеров на хост ускоряется почти в 2 раза. Кроме того, каждый день вы получаете 120 минут сборки бесплатно, что в большинстве случаев удовлетворяет потребности создания контейнеров.
Далее идет самая важная метрика производительности – скорость извлечения, или скачивания контейнеров Pull. И если вас не особо заботит время, затрачиваемое на операцию push, то длительность процесса pull серьезно влияет на общую производительность системы. Предположим, что у вас есть кластер из трех узлов и с одним из них происходит сбой. Если вы используете систему управления, например Google Kubernetes Engine, то она автоматически заменит нерабочий узел новым. Однако этот новый узел будет совершенно пустым, и вам придется перетащить в него все ваши контейнеры, чтобы он начал работать. Если операция pull будет достаточно долгой, то все это время ваш кластер будет работать с меньшей производительностью.
Существует много случаев, когда может произойти подобное: это добавление нового узла в кластер, обновление узлов или даже переключение на новый контейнер для развертывания. Таким образом, минимизация времени извлечения pull становится ключевым фактором. Неоспоримо то, что маленький контейнер скачивается намного быстрее большого. Если вы используете несколько контейнеров в кластере Kubernetes, экономия времени может быть очень существенной.
Взгляните на приведенное сравнение: операция pull при работе с маленькими контейнерами занимает в 4-9 раз меньше времени в зависимости от мощности машины, чем такая же операция с использованием go:onbuild. Использование общих базовых образов контейнеров малого размера значительно ускоряет время и скорость, с которой новые узлы Kubernetes могут развертываться и выходить в интернет.
Давайте рассмотрим вопрос безопасности. Считается, что контейнеры меньшего размера намного безопаснее больших, потому что у них меньшая поверхность атаки. Так ли это на самом деле? Одна из полезнейших функций Google Container Registry заключается в возможности автоматически сканировать ваши контейнеры на наличие уязвимостей. Несколько месяцев назад я создал как onbuild, так и многоступенчатые контейнеры, так что давайте посмотрим, есть ли там какие-нибудь уязвимые места.
Результат потрясающий: в небольшом контейнере обнаружено всего 3 средних уязвимости, а в большом — 16 критических и 376 прочих уязвимостей. Если рассмотреть содержимое большого контейнера, видно, что большинство проблем безопасности не имеют ничего общего с нашим приложением, а связаны с программами, которые мы даже не используем. Поэтому когда люди говорят о большой поверхности для атак, они имеют в виду именно это.
Вывод очевиден: создавайте маленькие контейнеры, потому что они дают реальные преимущества в производительности и безопасности вашей системы.
Продолжение будет совсем скоро…
Немного рекламы :)
Спасибо, что остаётесь с нами. Вам нравятся наши статьи? Хотите видеть больше интересных материалов? Поддержите нас, оформив заказ или порекомендовав знакомым, облачные VPS для разработчиков от $4.99, уникальный аналог entry-level серверов, который был придуман нами для Вас: Вся правда о VPS (KVM) E5-2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps от $19 или как правильно делить сервер? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40GB DDR4).
Dell R730xd в 2 раза дешевле в дата-центре Equinix Tier IV в Амстердаме? Только у нас 2 х Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 ТВ от $199 в Нидерландах! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — от $99! Читайте о том Как построить инфраструктуру корп. класса c применением серверов Dell R730xd Е5-2650 v4 стоимостью 9000 евро за копейки?