Как стать автором
Обновить
81.57
Nixys
DevOps, DevSecOps, MLOps — системный IT-интегратор

Ошибочные шаблоны при построении образов контейнеров

Время на прочтение18 мин
Количество просмотров5.7K
Автор оригинала: Jérôme Petazzoni

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

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

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

Большие образы

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

Но какой размер для образа считать большим?

Для микросервисов с относительно небольшим количеством зависимостей я бы не стал переживать за образы размером менее 100 МБ. Для более сложных рабочих нагрузок (монолиты или, скажем, приложения для дата-сайенс) можно использовать образы размером до 1 ГБ. При большем размере я бы уже задумался.

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

Гигантские образы «все в одном»

Иногда в образ нужно собрать Node, PHP, Python, Ruby и несколько движков баз данных, а также сотни библиотек, потому что этот образ планируется сделать основой для платформы PAAS или CI. Это характерный пример для платформ, у которых есть только один доступный образ для запуска всех приложений и задач. Действительно, в таком случае в образе должно быть собрано все необходимое.

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

Наборы данных

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

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

В противном случае, скажем, если он превышает 1 ГБ, возникают проблемы. Конечно, если Dockerfile у вас хорошо организован, модель будет добавлена перед кодом. Кошмар случится, если добавить модель после кода. Сборки будут тормозить, занимать много места на диске, а если код нужно тестировать на удаленных машинах (а не на локальных), модель нужно будет каждый раз сжимать и извлекать, что требует много дискового пространства на удаленных машинах. И это очень плохо.

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

Когда вы запускаете его локально с помощью такого инструмента, как Compose, можно использовать монтирование привязки из локального каталога (он выступает здесь в роли кеша) и отдельный контейнер для загрузки данных. Файл Compose будет выглядеть так:

services:
 data-loader:
  image: nixery.dev/shell/curl
  volumes:
  - ./data:/data
  command: |
   if ! [ -f /data/dataset ]; then
	curl ... -o /data/dataset
	touch /data/ready
   fi
 data-worker:
  build: worker
  volumes:
  - ./data:/data
  command: |
   while ! [ -f /data/ready ]; do sleep 1; done
   exec worker   

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

В таком случае, при запуске, например, на Kubernetes, можно использовать для загрузки данных initContainer с примерно похожим Pod’ом:

spec:
 volumes:
 - name: data
 initContainers:
 - name: data-loader
  image: nixery.dev/curl
  volumeMounts:
  - name: data
   mountPath: /data
  command:
  - curl
  - ...
  - -o
  - /data/dataset
 containers:
 - name: data-worker
  image: .../worker
  volumeMounts:
  - name: data
   mountPath: /data

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

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

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

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

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

Маленькие образы

Есть вероятность, что образ будет слишком маленьким. Стоп, а что не так с образом размером всего 5 МБ?

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

Образы, созданные с помощью distroless или из FROM scratch , могут быть небольшими. Но есть ли в этом смысл, если команда регулярно страдает, потому что они не могут даже получить оболочку в образе, чтобы, например, проверить, какая сейчас версия у конкретного файла, увидеть запущенные процессы с помощью ps или сетевые соединения с помощью netstat или ss?

Все очень зависит от контекста. У кого-то совсем нет потребности добавлять оболочку в образ. Если вы используете Docker, можно проверить, что происходит, скопировав в запущенный контейнер статические инструменты (например, busybox) через docker cp . Если вы работаете с локальными образами, вы можете легко перестроить образ и добавить необходимые инструменты. В Kubernetes вы можете включить альфа-функцию эфемерных контейнеров. Но в большинстве рабочих кластеров Kubernetes у вас не будет доступа к базовому движку контейнера, и вы не сможете включить альфа-функции, поэтому…

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

FROM gcr.io/distroless/static-debian11

COPY --from=busybox /bin/busybox /busybox

SHELL ["/busybox", "sh", "-c"]

RUN /busybox --install

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

В целом, лично я предпочитаю делать сборки на образах Alpine, потому что они крошечные (Alpine - 5 МБ). В Alpine вы можете использовать apk add для всего, что захотите, и когда вам это нужно. Проблемы с сетевым трафиком? Воспользуйтесь tcpdump и ngrep. Нужно загружать и удалять файлы JSON? Вам помогут curl и jq!

Итог: маленькие образы, как правило, хороши, а образы distroless – это вообще бомба, но при определенных обстоятельствах. В случаях, когда «У меня нет доступа в контейнер, и мне придется добавить несколько операторов print() в код и извлекать его через CI до переноса данных, потому что я не могу использовать «kubectl exec ls», возможно, это решение не для вас. Но решать, конечно, вам самим!

Zip, tar, и прочие архивы

(Добавлено 15 декабря 2021 года)

Как правило, добавлять архив (zip, tar.gz или другие) к образу контейнера – так себе идея. Это определенно плохая идея, если контейнер распаковывает этот архив при запуске, бессмысленно тратя на этот процесс время и дисковое пространство!

Оказывается, образы Docker уже сжаты, когда хранятся в реестре, помещаются в реестр или извлекаются из него. Это означает две вещи:

  • хранение сжатых файлов в образе контейнера не позволяет сэкономить дисковое пространство,

  • если хранить несжатые файлы в контейнере, он не будет занимать больше места.

Если мы включим в образ архив (например, архив tar) и распакуем его при запуске контейнера:

  • мы тратим время и циклы ЦП по сравнению с образом контейнера с уже распакованными и готовыми к использованию данными;

  • мы тратим впустую дисковое пространство, потому что в конечном итоге сохраняем как сжатые, так и несжатые данные в файловой системе контейнера;

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

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

Повторная сборка общих базовых образов

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

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

Вы спросите, почему?

Причина №1: брать образ из реестра почти всегда быстрее, чем собирать его. (Да, есть исключения, но поверьте, они довольно редки.)

Причина №2: поскольку это база, на которой строится все остальное, вероятно, вам захочется быть уверенным, что у вас есть определенный набор версий образа. В противном случае мы возвращаемся к проблемам типа «ну а у меня-то работает» - именно этого мы пытались избежать, используя контейнеры! Если каждый проводит сборку локально, нужно быть особенно осторожными, чтобы этот процесс был детерминированным и воспроизводимым: закрепить все версии; проверить хеш всех загрузок; используя && или set -e, где требуется, чтобы немедленно прервать выполнение, если что-то в списке команд в процессе сборки пойдет не так. Либо мы можем просто сохранить базовый образ в реестре и быть уверены, что все используют один и тот же образ. Готово.

А если нужно скорректировать этот базовый образ? Есть ли простой способ сделать это, не отправляя новую версию базового образа (это и не нужно, если образ используется только локально), или не редактируя файлы Docker?

Если вы используете Compose, вот пример шаблона основного образа. Это очень простой шаблон (не думаю, что он станет для вас каким-то открытием), но я часто вижу, как его внедряют с помощью скриптов оболочки, файлов Makefile и других инструментов, , поэтому я подумал, что будет полезным показать, что Compose вполне хватит для этой задачи. При сборке приложения он откроет базовый образ. Но если вам нужен настраиваемый базовый образ, можно собрать его отдельно docker-compose build.

Сборка единого репозитория из корневой директории

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

Например:

monorepo
├── app1
│  └── source...
└── app2
  └── source...

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

monorepo
├── app1
│  └── source...
├── app2
│  └── source...
├── Dockerfile.app1
└── Dockerfile.app2

Далее можно собрать сервисы с помощью docker build . -f Dockerfile.app1. Проблема с этим подходом заключается в том, что если мы используем «старый» конструктор Docker (а не BuildKit), первое, что он делает - это загружает весь репозиторий в Docker Engine. А если репозиторий у вас на 5 ГБ, то Docker будет копировать 5 ГБ перед каждой сборкой, даже в остальном ваш файл Docker прекрасно спроектирован и отлично использует кеширование.

Я предпочитаю иметь файлы Docker в каждом подкаталоге, чтобы их сборка была независимой, в небольшом и изолированном контексте:

monorepo
├── app1
│  ├── Dockerfile
│  └── source...
└── app2
  ├── Dockerfile
  └── source...

Затем мы можем перейти в директории app1 или app2 и запустить docker build. Ему понадобится только содержимое этого подкаталога.

Однако иногда для процесса сборки требуются зависимости, которые находятся вне директории для приложения; например, общий код в подкаталоге lib ниже:

monorepo
├── app1
│  └── source...
├── app2
│  └── source...
└── lib
  └── source...

Что же делать в этом случае?

Решение №1: запаковать зависимости в отдельные образы. При создании образов для app1 и app2 вместо копирования директории lib из репозитория скопируйте его из образа lib или общего базового образа. Возможно, это про вас, а может, это вас не касается: одним из основных преимуществ единого репозитория является то, что конкретный коммит может точно описать, какую версию кода и его зависимости мы используем; а такое решение может это поломать.

Решение №2: использовать BuildKit. Для BuildKit не надо копировать весь контекст сборки, и в таком скрипте это будет гораздо более эффективным решением.

Давайте взглянем на BuildKit в этом контексте!

Если не использовать BuildKit

BuildKit - это новый бэкэнд для docker build. Это полная переработка с множеством новых функций, включая параллельные сборки, кросс-арочные сборки (например, создание образов ARM на Intel и наоборот), создание образов в Kubernetes Pods и многое другое. При этом, он все еще полностью совместим с существующим синтаксисом файлов Docker. Это похоже на переход на электромобиль: у нас все еще есть руль и две педали, но начинка совершенно другая.

Если вы работаете в последней версии Docker Desktop, вероятно, вы уже используете BuildKit, так что это здорово. В противном случае (в частности, если вы работаете в Linux) установите переменную среды DOCKER_BUILDKIT=1 и запустите команду docker build или docker-compose; например:

DOCKER_BUILDKIT=1 docker build . --tag test

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

«Как мне понять, что я использую BuildKit?»

Выходные данные сборки без BuildKit:

Sending build context to Docker daemon 529.9kB

Step 1/92 : FROM golang:alpine AS builder

 ---> cfd0f4793b46

...

Step 90/92 : RUN (   ab -V ...

 ---> Running in 645af9563c4d

Removing intermediate container 645af9563c4d

 ---> 0972a40bd5bb

Step 91/92 : CMD  if tty >/dev/null; then ...

 ---> Running in 50226973af9f

Removing intermediate container 50226973af9f

 ---> 2e963346566b

Step 92/92 : EXPOSE 22/tcp

 ---> Running in e06a628465b3

Removing intermediate container e06a628465b3

 ---> 37d860630477

Successfully built 37d860630477

  • начинается с «Sending build context…» (в данном случае более 500 КБ);

  • нужно передавать весь контекст сборки при каждой сборке;

  • текстовый вывод в основном черно-белый, за исключением стандартного вывода ошибок этапов сборки, который отображается красным;

  • каждая строка файла Docker соответствует «шагу»;

  • каждая строка файла Docker создает промежуточный образ (тот самый ---> xxx на выходе);

  • линейное исполнение (92 шага для этого образа и всех его этапов);

  • время сборки для этого образа: 3 минуты 40 секунд.

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

 => [internal] load build definition from Dockerfile                 0.0s

 => => transferring dockerfile: 8.91kB                             0.0s

 => [internal] load .dockerignore                               0.0s

 => => transferring context: 2B                            0.0s

 => [internal] load metadata for docker.io/library/golang:alpine           0.0s

...

 => [stage-19 27/28] COPY setup-tailhist.sh /usr/local/bin              0.0s

 => [stage-19 28/28] RUN (   ab -V | head -n1 ;  bash --version | head -n1 ;  curl --ve 0.7s

 => exporting to image                                2.0s

 => => exporting layers                                 2.0s

 => => writing image sha256:9bd0149e04b9828f9e0ab2b09222376464ee3ca00a2de0564f973e2f90e0cfdb  0.0s

  • начинается с нескольких строчек [internal] и переносит только необходимое из контекста сборки;

  • может кэшировать части контекста между различными сборками;

  • вывод текста в основном темно-синий;

  • команды файла Docker, например, RUN и COPY создают новые шаги, тогда как другие команды (например, EXPOSE и CMD в конце) нет;

  • каждый шаг создает слой, но без промежуточных образов;

  • по возможности, параллельное исполнение с использованием диаграммы зависимостей (итоговый образ – на 28 шаге 19 стадии файла Docker);

  • время сборки для этого образа: 1 минута, 30 секунд.

Поэтому постарайтесь использовать BuildKit: я просто не могу придумать для него недостатков. Он никогда не замедлит работу и во многих случаях значительно ускорит сборки.

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

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

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

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

(Кстати, если вы попробуете очень быстро задеплоить изменения в кластере разработки Kubernetes, обязательно посмотрите статью Эллен Кёрбс «В поисках самого быстрого деплоя» (видео и презентация). Спойлер: мне хватит пальцев одной руки, чтобы посчитать секунды между «Сохранить мой код Go в редакторе» и «теперь этот код запущен на удаленных кластерах Kubernetes». 💯)

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

Использование собственных скриптов вместо существующих инструментов

Мы все это делали: старый добрый ./build.sh (или build.bat). Более двух десятков лет назад, когда я получал степень бакалавра информатики, большинство домашних заданий на C я делал, используя не Makefile, а дерьмовый скрипт оболочки. И это не потому, что я не знал о Makefile, просто мы работали как под Linux, так и под HP/UX, и я творчески подходил к задаче вставлять себе палки в колеса и учитывать небольшие различия между соответствующими реализациями make. (Может быть именно поэтому я сейчас стараюсь держаться подальше от башизмов).

Существует множество инструментов, помогающих в разработке: Compose, Skaffold, Tilt – и это только несколько примеров. У них есть отличная документация и руководства, их используют тысячи разработчиков. Кто-то из ваших коллег уже знает о них и знает, как поддерживать файлы Compose или Tilt.

Если наш самодельный скрипт для деплоя состоит всего из 10 строк, он не делает ничего сложного и его можно заменить файлом Compose или Tilt. (Имейте в виду, что если в скрипте используется внешний инструмент, например, Terraform или облачный интерфейс командной строки, мы должны убедиться, что он установлен, и это в любом случае потребует не меньше усилий, чем набрать “git clone ; docker-compose up”.)

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

Если самодельный скрипт деплоя содержит тысячу строк или больше, вероятно, в нем много настраиваемой логики и он обрабатывает множество ситуаций. Круто! Но также это означает, что теперь вам нужно писать документацию, тесты и, возможно, даже провести внутреннее обучение для новых сотрудников. К сожалению, по моему опыту, такие скрипты как минимум в 10 раз (а чаще в 100 раз) больше, чем эквивалентный файл Compose или Tilt. В них больше ошибок, меньше функций, и никто за пределами вашей команды или организации не знает, как их использовать.

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

«Но мы хотим скрыть сложность контейнеров / Docker / Kubernetes от наших разработчиков!»

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

Принудительный запуск в контейнерах

Мне нравится запускать все в контейнерах, но я считаю, что заставлять запускать все подряд в контейнерах - очень плохая идея.

Допустим, у нас есть скрипт, который использует интерфейс командной строки gcloud, Terraform и прочие инструменты, например crane и jq.

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

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

Поначалу кажется, что это просто замена yadda-deploy.sh на docker run yadda-image. На деле же нужно открыть некоторые переменные среды, связать несколько томов для учетных данных и кода. В конечном итоге мы можем написать новый скрипт yadda-deploy.sh (который запустит docker run за кулисами). И вот здесь мы можем столкнуться с проблемой.

Сравните два варианта:

Способ №1: для выполнения этой задачи запустите скрипт yadda-deploy.sh. Для него нужно установить инструменты X, Y и Z. Если вы не хотите устанавливать их локально, можно запустить скрипт в контейнере через образ yadda/deploy (билд с помощью файла Docker в этом подкаталоге) и команду docker или docker-compose

Способ №2: для выполнения этой задачи запустите скрипт yadda-deploy.sh. Этот скрипт требует установки Docker.

Поначалу кажется, что способ №2 лучше, и поэтому так много команд идут по этому пути, ведь кажется, что он короче и требований меньше! Вот только многих деталей не хватает. Способ №1 позволяет подробно рассказать о требованиях всего в нескольких строках. В способе №2 вам нужно открыть скрипт, чтобы увидеть, что он делает. Вполне простая задача, если речь идет о небольшом скрипте на 10 строк. Гораздо сложнее, если это гигантский скрипт, как мы обсуждали в предыдущем разделе.

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

И все еще хуже, когда мы запускаем скрипт в удаленной среде, например, в CI или в Kubernetes!

Действительно, если скрипт должен вызывать Docker (или Compose), что произойдет, когда мы попытаемся запустить его в среде, которая уже в контейнере? Иногда можно использовать Docker-in-Docker в CI, но далеко не всегда. Поэтому, если скрипт полагается на вызов Docker или Compose, у нас проблемы.

С другой стороны, если мы придерживаемся принципа «запусти yadda-deploy.sh в среде с пакетами X, Y и Z», это намного проще, потому что мы уже знаем, какие пакеты нам нужны и в каком образе они есть.

Использование слишком сложных инструментов

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

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

Если в шаблоне много $, можно не пытаться от них избавиться, а использовать [envsubst] из пакета gettext.

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

Если необходимо преобразовать некоторые переменные, например установить строчные буквы, удалить специальные символы, пробелы, кодировать или декодировать base64, вычислить хэш… Мы можем установить дополнительные инструменты для выполнения всех этих преобразований перед вызовом envsubst.

А может, нужно поддерживать петли? Здесь можно задуматься о вложении в механизм обработки шаблонов. И тут-то мы подошли к чему-то по-настоящему интересному!

Если в стеке есть такие языки, как Node, Python или Ruby, существует большая вероятность найти небольшой пакет, который сделает то, что нам нужно. (Например, в Python пакет Jinja2 предоставляет инструмент интерфейса командной строки j2.) С другой стороны, если в стеке не используется Python, добавлять его только для того, чтобы установить Jinja2, кажется излишним.

Если мы уже используем Terraform, в нем есть мощный механизм шаблонов, который может генерировать локальные или дистанционные файлы. Круто же! Но добавление Terraform только ради его движка шаблонов – это чересчур.

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

Стоит также с осторожностью использовать инструменты, которые трудно освоить и/или которые знакомы очень немногим. Возможно, Bazel - это один из наиболее эффективных способов создания артефактов и запуска CI на огромных базах кода, но многие ли из ваших коллег знают его в достаточной степени, чтобы поддерживать правила сборки? А что вы будете делать, когда этот человек уйдет? 😬

Противоречивые названия скриптов и образов

Еще одно воспоминание из моих первых дней в IT: в первый год использования UNIX я регулярно сам себя наказывал, когда давал своим тестовым скриптам и программам имя test.

И что с того?

Само по себе это не является большой проблемой; но раньше я использовал DOS. А в DOS, если вы хотите запустить программу HELLO.COM или HELLO.EXE, расположенную в текущем каталоге, можно запустить hello напрямую; вам не нужно набирать ./hello как в UNIX. Поэтому я настроил скрипты входа в систему так, чтобы в $PATH была . .

Вы поняли, к чему это приведет? Вместо запуска ./test я запускал test и в итоге вызвал /usr/bin/test (также известный как /usr/bin/[) и удивился, почему ничего не произошло (потому что без аргументов /usr/bin/test ничего не отображает и просто закрывается).

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

Это особенно верно для двухбуквенных команд, потому что в UNIX их очень много! Например:

  • bc и dc («собрать контейнер» и «задеплоить контейнер» для некоторых, но также некоторые относительно распространенные калькуляторы текстового режима в UNIX);

  • cc («создать контейнер», а еще стандартный компилятор С в UNIX);

  • go (конфликтует с набором инструментов Go);

  • Сборка с файлами Docker.

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

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

И ещё

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

Когда я обучаю работе с контейнерами, я посвящаю целый раздел советам и приемам по созданию «лучших образов» и написанию «лучших файлов Docker». Я обычно завершаю этот раздел таким выводом:

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

Спасибо за чтение!

P.S.: больше полезностей для DevOps'ов - в телеграм-канале DevOps FM.

Теги:
Хабы:
+5
Комментарии1

Публикации

Информация

Сайт
nixys.ru
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия
Представитель
Vlada Grishkina-Makareva