Pull to refresh

Comments 45

  • envsubst может работать как простой шаблонизатор по env переменным (без кучи кода с awk).
  • самый лучший (ИМХО) контейнер для статики скачивает архив со статикой с s3 перед стартом (занимает секунды), а пересобирать вообще не нужно. Можно скачивать вместе с nginx конфигом, если меняется от проекта к проекту.
    • если используется оркестратор вроде k8s или nomad, то можно вообще не собирать свой образ, а просто в манифесте переписать /etc/nginx/nginx.conf и entrypoint с wget для стандартного образа.

wget -qO- $ASSETS_URL | tar xvz -C /srv
exec nginx

Статика редко весит больше 20-50мб в архиве. Поэтому скачивать перед стартом — малая кровь за такой простой подход.

Как раз недавно изучал подобный сценарий. Обнаружилось, что в обычном образе nginx нет ни wget, ни curl, а вот в alpine-варианте есть wget. Можно пойти ещё дальше: статику предварительно сжать и раздавать её со смонтированного RAM-диска (tmpfs). И всё это со стандартным образом nginx-alpine.

Скачивание статики перед стартом рушит всю суть контейнеров. Вам придется поддерживать две сущности — версии контейнеров и версии этой самой статики. Это неудобно и приводит к ошибкам. Ну и скачивание может отвалиться в самый неудобный момент, даже с S3.

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

У меня нет проблем с тем, чтобы хранить ссылку на архив вместо ссылки на image. А вот проблемы, когда у вас 20-50-100 контейнеров со статикой и их нужно обновить все, возникает постоянно.

Я все же не пойму о каких проблемах идет речь?

Ну есть некий docker.io/nginx:1.2.3 b 100500 его реплик. Обновили версию — обновились контейнеры, все логично.

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

Если оно не скачается, то и не запустится. А rolling update откатит все обратно. Точно так же новый image может быть недоступен. Распределенные registry работают поверх s3 кстати и говорить, что registry надежнее не совсем правильно.


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


PS s3 я имею в виду s3 compatible storage, которое есть у всех приличных облаков или больших кластеров (ceph, minio, etc)

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

Точка отказа s3? С отказом s3 откажет и registry (только если они не пользуются разными s3). high available registry всегда используют внешнее хранилище, s3 самое простое.

Точка отказа в самом факте сценария «для запуска контейнера надо что-то скачать». Контейнер должен быть автономный, в идеале. Понятно что ваш пример с подключением к БД не будет автономный, но уж статические файлы можно положить в контейнер.

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

Да, уволюсь завтра, экспертиза ниже плинтуса =)

Это не экспертиза, это опыт работы с различной реализацией статики в некрупных проектах
Ну есть некий docker.io/nginx:1.2.3 b 100500 его реплик. Обновили версию — обновились контейнеры, все логично.

У вас это
myregistry.com/myproject1:v2
myregistry.com/myproject2:v2
myregistry.com/myproject3:v2
myregistry.com/myproject4:v2
Которые from nginx:1.2.3 с разной статикой.


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

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

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

Я понял, кажется, поинт:
Наши контейнеры для каждого проекта со статикой содержат
FROM nginx:1.16.1 в докерфайле в репе проекта
Выходит 1.16.2 с закрытием уязвимости. Нам надо пройтись по всем репам, по всем докерфайлам, изменить на 1.16.2, закоммитить, запушить, отдать на код-ревью, сбилдить, тегнуть новый образ, протестировать, изменить конфиги докера (ещё коммиты, пуш, ревью) и т. д. В случае с общим образом и подкачиваемым контент ом, нужно только конфиги докера изменять.

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

Я видимо просто не работал с такими неповоротливыми проектами, где обновление версии nginx будет длиться месяц.

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

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


Разница в том, что обычно нужно прогнать по всему флоу сначала изменения докерфайла, а потом изменения конфигов докера (docker-compose, swarm, k8s, ...), а в варианте с выкачкой только второе. Процессы разработки и изменения базового образа не пересекаются, не блочат друг друга

Но это же не базовый образ. Может я как-то не так объясняю, давайте на своем, живом примере.

Есть образ с nginx, в который внесены некоторые корректировки. Это отдельный «базовый» образ docker-nginx:1.0

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

В проекте (k8s через helm) есть деплоймент, который берет этот образ с указанием версии, моунтит в него конфиги из k8s configmap и запускает. Для дева там запускается прям базовый образ с локальной шареной директорией для статики.

Для прода собирается так (реальный Dockerfile):
FROM registry.gitlab.com/base/docker-nginx:1.0
ADD ./public /usr/share/nginx/html/public


Это билдится в CI/CD гитлаба для конкретного проекта.
Т.е. работает это так:
— Базовый образ не меняется почти никогда
— Для смены конфигов — меняем configmap
— Для смены статики пушим статику в репо, образ билдится, деплоится для конкретного проекта

Получаем образы вида project-nginx:1/2/3/4/5 в каждый из которых вшита статика его версии.

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

Я просто не понимаю что такое «прогнать по всему флоу». Если вы меняете статику — вы меняете проект. Это в любом случае коммит, пуш, ревью и все что вы там еще делаете.
А если у вас статика просто где-то валяется «во вне», то я не знаю зачем ее вообще выкачивать в докере, просто раздавайте через cdn и все.

Вот, ситуация, обновили базовый образ. Теперь нужно (могут быть нюансы):


  1. Поменять в каждой репе докерфайл на FROM registry.gitlab.com/base/docker-nginx:1.0.1
  2. Закоммитить, запушить, создать PR
  3. Отревьювить изменения
  4. Сбилдить и залить в регистри с новым тегом
  5. Протестить
  6. Изменить в деплоймент/values тег
    7...

В случае с подтягиванием файлов, шаги 1-5 не нужны, сразу начинаем с деплоймент/values

Но это же все делается при любых изменениях в проекте. Причем шаги 3 и далее делаются автоматически (ну разве что выкат на прод по кнопке).

Шаг 1 — это поменять один символ.
Шаг 2 — это Ctrl + K, Enter в IDEA.
Ну PR может быть как-то вручную создавать, хотя не все работают через PR.

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

Пусть даже мы оставляем это убийство. У вас 100 реп, в каждой запускается «базовый образ». Его обновили. Вы там делаете деплой по latest тегу, что ли? Вряд ли. Тогда точно так же придется править 100 реп.

Ссылка на выкачивание статики берется оттуда же, откуда ссылка на image. Разницы нет.


Конкретно про спор.
FROM registry.gitlab.com/base/docker-nginx:1.0
ADD ./public /usr/share/nginx/html/public
Такой вариант хороший, кроме выше описанной ситуации, когда нужно обновить в куче реп это одну строчку (в больших компаниях реал сложно). Но если маунтить конфиг, то редко нужно.


Касательно подхода с выкачиванием статики… я вспомнил зачем это ввел. Была идея написать k8s оператор, чтобы одним деплойментом nginx сервить всю статику, потому что печально наблюдать 100+ контейнеров nginx в кластере.

Была идея написать k8s оператор, чтобы деплойментом nginx сервить всю статику, потому что печально наблюдать 100+ контейнеров nginx в кластере.

Вот это действительно хорошая идея. Может не через скачивание, а через PV или типа того я бы начал смотреть. Но куча nginx c одинаковыми конфигами действительно напрягает.

PV это уже stateful + PV нужен ReadWriteMany (я редко работаю с такими, может ReadOnlyMany, но вообще не сталкивался с ним), такие хранилища обычно медленные. Со скачиванием просто получается реаликация с мастером в виде s3.

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

Ссылка на s3 это как ссылка на внешнее апи или подключение к базе. Из-за этого сервис не приобретает state. Это stateless.


Подход с другой стороны
Сможете ли вы запустить 1кк контейнеров с общим PV? -> без дикого гемора нет (так себя ведут stateful)
Сможете ли вы запускать 1кк контейнеров с выкачиванием? -> да (так себя ведут stateless)

Я об единственном кейсе сейчас: обновление базового образа, сами статические файлы не менялись, ссылка не менялась. Всё что нужно изменить таг в чарте/деплойменте. С билдом статики в контейнере нам нужно сделать 200 изменений на 100 реп: 100 в докерфайлах и 100 в чартах, со скачиванием — 100, только в чартах

По-моему, эта проблема характерна для любых приложений в Docker.


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


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

Если используем FROM scratch, то проблемы такой нет :) реально такое только с golang видел/делал


А так, да, проблема есть и вариантов для её решения много, какие-то лучше подходят в одной ситуации, какие-то в другой.

  • По умолчанию envsubst заменяет в файле всё, что начинается на $. Чтобы он заменял только реально существующие переменные окружения, ему надо передать их список, который я и получаю с awk.
  • Использование S3 мне не нравится с точки зрения атомарности релиза и версионирования. Хотя, наверняка, я просто не умею его правильно готовить.

Насчет второго пункта — там все тоже самое. Нет никакой разницы… передавать куда-то ссылку на image или ссылку на архив или ссылку на image+архив вместе.

Странный для меня подход. В чём плюсы?


Минусы, навскидку:


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

Это может иметь какой-то смысл в случае использования сторонних образов, когда свой репозиторий образов вообще не создаёшь, а инфраструкта для билда статики на S3 и её версионирования уже есть. Но в общем случае плюсы мне не понятны, хотя сам использую публичные образы nginx с примонтированными конфигами и, если надо, контентом как фронт для php-fpm. Но то монтирование, а не скачивание. Статика всё равно билдится в наши образы, вопрос лишь какие из них.

Я выше писал часть из этого...


  • s3 я имею в виду s3 compatible storage, которое есть у всех приличных облаков или больших кластеров (ceph, minio, etc на своих серверах)
  • ваш личный HA registry (3 копии) скорее всего будет использовать этот самый s3 как внешнее хранилище https://docs.docker.com/registry/storage-drivers/s3/
  • версией image рулят DevOps парни, версией статики фронтендщики, разделение ролей и тп

У нас все, что касается разворачивание лежит в отдельном репозитории. Туда не могут лезть все. От команды конкретного проекта требуется только отдать артефакт. Будет это image или архив не так важно (фронтенщикам проще s3, мало кто хорошо умеет собрать образ).


PS Вместо s3 можно использовать артефакты CI/CD у gitlab или еще кого (они так же лежат обычно на s3 https://docs.gitlab.com/ee/administration/job_artifacts.html#s3-compatible-connection-settings). Тогда думать вообще про хранилище не нужно. Но у нас исторически напрямую.

2 вопроса:


  1. Почему не собрать модуль Brothli в оригинальном образе и просто не скопировать его в финальный, опять же на основе оригинального?
  2. Почему envsubst, а не dockerize?
  1. Так можно сделать там, где нужен полноценный nginx, хотя может возникнуть вопрос бинарной совместимости модуля и nginx. В данном же случае задача была сделать nginx как можно меньше, поэтому пришлось его пересобрать убрав всё, что мне показалось ненужным.
  2. Про dockerize я просто не знал. Спасибо за наводку!

После прочтения лично у меня остался один вопрос: зачем там brotli?
Ведь для девелопмента он не нужен точно, а на production статика раздается с CDN.

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

Так если не надо CDN, так как все в одном городе, то и сжимать не сильно нужно.
А сам CDN может быть абсолютно бесплатным. Главное захотеть. Подсказка: cf

Я недавно знакомому отключил его для сайта Самары. Результат -100мс пинга. Бесплатно того не стоит.

Какого размера получился результирующий образ?

Если нас интересует компактность образа именно в проде, то вижу возможность для оптимизации. node-gyp и инструменты сборки нативного кода, как правило, нужны на этапе установки но не нужны после этого. Если использовать multi-stage build, то можно убрать эти зависимости из финального образа. Недавно удалось выиграть за счет multistage более 300MB размера образа (~1.6GB -> ~1.25GB)

Можно ещё со squash поиграться

  1. непонятно, стоит ли овчинка выделки. Время потрачено, размер образа уменьшен, но задачу можно было решить добавлением 10МБ RAM на каждый контейнер, я же правильно понимаю?
  2. find умеет фильтровать по имени и выполнять команды, то есть для того же compress не надо городить цикл и надеяться на то, что будут имена файлов без пробелов, например.
  3. Перевод с buildtime configuration на runtime configuration ̶н̶а̶д̶о̶ по моему мнению, лучше делать на уровне приложения, а не костылить баш скрипты. Тогда в образе будет исключительно nginx и сборка SPA, а конфиг подмонтировать в виде файла. Пример для angular: https://juristr.com/blog/2018/01/ng-app-runtime-config/. Сжатие при этом тоже можно перенести в этап сборки, то есть бинарник бротли тоже не понадобится, да и вообще отдельный entrypoint не будет нужен
  1. Всё верно. Но время уже потрачено и результат есть, поэтому теперь я его просто переиспользую.
  2. Когда появится возможность, я попробую сделать через один find.
  3. В статье речь скорее про delivery-time configuration, а не runtime configuration.
… в составе Kubernetes-кластера

# Флаг нужен, чтобы выполнить скрипт только при самом первом запуске

В кубере нет второго запуска контейнера. И каждый перезапуск будет выполнятся сжатие файлов, а если заданы лимиты на pod, тогда запуск может длиться достаточно долго. Вполне возможно, что и не завершится из-за лимитов на память и случится ООМ.
Вообще непонятно зачем выполнять сжатие файлов в готовом!!! контейнере. Выполняйте это в процессе сборки образа.
На продовых кластерах очень часто осуществляется запуск контейнеров от non-root пользователя. При запуске pod свалится в CrashloopBackOff из-за прав на директории/файлы и конфигурации nginx.conf.
А иногда встречается политика readOnlyRootFilesystem: true, запрещающая создавать rw-слой и в этом случае тоже получаем CrashloopBackOff.
А что если понадобится подправить nginx.conf? Пересобирать базовый образ?

Так что в случае с запуском в кубере это будет или дев-кластер или локальный minikube.
Ну вот по поводу изменения настройки RESTfull API в самой статике я бы не согласился. Аякс запросы не должны идти прямо на бекенд, они должны проксироваться самим нджинксом. Для этого существует reverse-proxy. Таким образом и статику не нужно настраивать и не будет проблем с CORS на бекенде(выключать CORS на продакшене запрещено).

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

Замена значений в скомпилированных .js файлах не будет, например, работать, если используется SRI (механизм, позволяющий браузерам по подписи проверить что файл не изменили по пути).

Sign up to leave a comment.

Articles